Apollo Client - Cache Redirects for sub-types - graphql

I am using Apollo in production for about a year and I am trying to optimize my cache management.
Let's imagine the following fictive simple schema:
type Query {
allBooks: [Book]
allCups: [Cup]
allColors: [Color]
}
type Book {
id: Int
name: String
cover_color_id: Int
CoverColor: Color
}
type Cup {
id: Int
name: String
cover_color_id: Int
CoverColor: Color
}
type Color {
id: Int
name: String
hex_code: String
}
I would like to configure cacheRedirects so that when I demand the Book.CoverColor or Cup.CoverColor (via allBooks for instance ); it will first look for the Color with matching ID in the cache, before asking the server for it.
Is that possible ?
Thanks in advance!
PS: I tried this, which doesn't seem to work:
cacheRedirects: {
Query: {
// Stuff that perfectly works
},
Book: {
// this is not even executed :(
CoverColor: (book, args, { getCacheKey }) => {
return getCacheKey({
__typename: 'Color',
id: book.cover_color_id
})
}
}
}

It depends on what your queries look like.
if your allBooks redirect returns list of ids and your query has returnPartialData enabled it should work.
if you querying allBooks without hitting redirect, there is no reason for apollo to use cached fields on each element of [Book] if it already has all data
Make a query for a single book and it should work as you expect.
type Query {
allBooks: [Book]
allCups: [Cup]
allColors: [Color]
book(id: Int): Book
}
const cacheRedirects: CacheResolverMap = {
Query: {
book: (_, args, { getCacheKey }) =>
getCacheKey({ __typename: 'Book', id: `${args.id}` }),
},
Book: {
CoverColor: (book, args, { getCacheKey }) => {
return getCacheKey({
__typename: 'Color',
id: book.cover_color_id,
})
},
},
}

Related

Are fields with list types forbidden in GraphQL schema stitching selection sets?

I have an array of entities that look like this:
const aEntities = [
{
id: 1,
name: 'Test',
oneToManyRelation: [
{
id: 2
},
{
id: 3
}
],
oneToOneRelation: {
id: 1
}
}
];
The entities are represented by the type AType. I want to make an extension of this type in a separate subschema and prove that it is possible to add fields that derive their values from the contents of oneToOneRelation and oneToManyRelation respectively.
The following schema, implementing a derived field based on oneToOneRelation, works fine:
const aSchema = makeExecutableSchema({
resolvers: {
Query: {
aEntities: () => aEntities
}
},
schemaTransforms: [stitchingDirectivesValidator],
typeDefs: gql`
${allStitchingDirectivesTypeDefs}
type AType {
id: ID!
name: String!
oneToOneRelation: AEmbeddedType!
}
type AEmbeddedType {
id: ID!
}
type Query {
aEntities: [AType!]!
}
`
});
const bSchema = makeExecutableSchema({
resolvers: {
AType: {
oneToOneId: ({ oneToOneRelation }) => oneToOneRelation.id
},
Query: {
aEntities_fromBSchema: (_, { keys }) => keys,
}
},
schemaTransforms: [stitchingDirectivesValidator],
typeDefs: gql`
${allStitchingDirectivesTypeDefs}
type AType #key(selectionSet: "{ oneToOneRelation { id } }") {
oneToOneId: String!
}
scalar Key
type Query {
aEntities_fromBSchema(keys: [Key!]!): [AType!]! #merge
}
`
})
const schema = stitchSchemas({
subschemaConfigTransforms: [stitchingDirectivesTransformer],
subschemas: [
{
schema: aSchema
},
{
schema: bSchema,
}
]
})
But once I add oneToManyRelation { id } to the selectionSet i run into problems:
const aSchema = makeExecutableSchema({
resolvers: {
Query: {
aEntities: () => aEntities
}
},
schemaTransforms: [stitchingDirectivesValidator],
typeDefs: gql`
${allStitchingDirectivesTypeDefs}
type AType {
id: ID!
name: String!
oneToManyRelation: [AEmbeddedType!]!
oneToOneRelation: AEmbeddedType!
}
type AEmbeddedType {
id: ID!
}
type Query {
aEntities: [AType!]!
}
`
});
const bSchema = makeExecutableSchema({
resolvers: {
AType: {
oneToManyIds: ({ oneToManyRelation }) => oneToManyRelation.map(({ id }) => id),
oneToOneId: ({ oneToOneRelation }) => oneToOneRelation.id
},
Query: {
aEntities_fromBSchema: (_, { keys }) => keys,
}
},
schemaTransforms: [stitchingDirectivesValidator],
typeDefs: gql`
${allStitchingDirectivesTypeDefs}
type AType #key(selectionSet: "{ oneToOneRelation { id }, oneToManyRelation { id } }") {
oneToOneId: String!
oneToManyIds: [String!]!
}
scalar Key
type Query {
aEntities_fromBSchema(keys: [Key!]!): [AType!]! #merge
}
`
})
I get the following error:
oneToManyRelation.map is not a function
And when I log the keys parameter in the aEntities_fromBSchema resolver it seems that oneToManyRelation haven't been resolved to be an array at all, but rather an (empty) object:
[
{
oneToOneRelation: [Object: null prototype] { id: '1' },
oneToManyRelation: [Object: null prototype] { id: undefined },
__typename: 'AType'
}
]
Is referencing list types in key selection sets simply forbidden as of graphql-tools v 7.0.2? It looks like I actually can circumvent the issue by using a subschema merge config defined outside of the SDL (without batching, instead using the args and selectionSet config parameters), but for validation/gateway reasons I'd prefer to have all my subschemas contain all of their type merging instructions as SDL directives.
Nb. This is a simplified representation of a real world problem.
Nb2. In the real world application one of my subschemas is a remote GraphQL application that I don't control, hence the need for some advanced tailoring in the stitching layer.
Edit: Simply adding the following to the merge options on the subschema config seems to solve the problem. Someone know of a good reason why this doesn't seem to be reproducible with SDL directives? (Or a good way to do so?)
// AType
{
argsFromKeys: (keys) => ({ keys }),
fieldName: 'aEntities_fromBSchema',
key: ({ oneToOneRelation, oneToManyRelation }) => ({ oneToManyRelation, oneToOneRelation }),
selectionSet: '{ oneToOneRelation { id }, oneToManyRelation { id } }'
}
You have likely found a bug! Please open an issue on the GitHub repo so we can track it. :)

Pattern for multiple types from GraphQL Union

I am learning about Interfaces and Unions in GraphQL (using Apollo Server) and am wondering about something. Using documentation examples, https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/#union-type, how would I return a result which could return authors and books?
My understanding is that you can only return one object type. If a search result contains and array of both books and authors, how is such a result returned? Can things be structured for this case? I have noticed that __resolveType does not work on an array and can only return a single result (it would return the type for all the objects in the array, not each object in array).
GraphQL TypeDef
const { gql } = require('apollo-server');
const typeDefs = gql`
union Result = Book | Author
type Book {
title: String
}
type Author {
name: String
}
type Query {
search: [Result]
}
`;
Resolver
const resolvers = {
Result: {
__resolveType(obj, context, info){
console.log(obj);
if(obj.name){
return 'Author';
}
if(obj.title){
return 'Book';
}
return null;
},
},
Query: {
search: () => { ... }
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
The actual GraphQL query may look something like this and consider the search result is both books and authors:
{
search(contains: "") {
... on Book {
title
}
... on Author {
name
}
}
}
When run, __resolveType(obj, context, info){, obj is:
[{ title: 'A' }, { title: 'B' }, { name: 'C' }]
There's only two ways that would happen:
The search field's type is not actually a list (i.e. it's Result instead of [Result] as shown in the code above.
Your resolver for the search field is returning an array of an array of objects: return [[{ title: 'A' }, { title: 'B' }, { name: 'C' }]]

How can GraphQL enable an ID based query at sub fields level?

If an existing service supporting the following GraphQL queries respectively:
query to a person's bank account:
query {
balance(id: "1") {
checking
saving
}
}
result
{
"data": {
"balance": {
"checking": "800",
"saving": "3000"
}
}
}
query to a person's pending order:
query {
pending_order(id: "1") {
books
tickets
}
}
result
{
"data": {
"pending_order": {
"books": "5",
"tickets": "2"
}
}
}
The source code achieving the above functionality is something like this:
module.exports = new GraphQLObjectType({
name: 'Query',
description: 'Queries individual fields by ID',
fields: () => ({
balance: {
type: BalanceType,
description: 'Get balance',
args: {
id: {
description: 'id of the person',
type: GraphQLString
}
},
resolve: (root, { id }) => getBalance(id)
},
pending_order: {
type: OrderType,
description: 'Get the pending orders',
args: {
id: {
description: 'id of the person',
type: GraphQLString
}
},
resolve: (root, { id }) => getPendingOrders(id)
}
})
});
Now, I want to make my GraphQL service schema support person level schema, i.e.,
query {
person (id: "1") {
balance
pending_order
}
}
and get the following results:
{
"data": {
"balance": {
"checking": "800",
"saving": "3000"
}
"pending_order": {
"books": "5",
"tickets": "2"
}
}
}
How can I re-structure the schema, and how can I reuse the existing query service?
EDIT (after reading Daniel Rearden's answer):
Can we optimize the GraphQL service so that we make service call based upon the query? i.e., if the incoming query is
query {
person (id: "1") {
pending_order
}
}
my actually query becomes
person: {
...
resolve: (root, { id }) => Promise.all([
getBalance(id)
]) => ({ balance})
}
You're going to have to define a separate Person type to wrap the balance and pending_order fields.
module.exports = new GraphQLObjectType({
name: 'Person',
fields: () => ({
balance: {
type: BalanceType,
resolve: ({ id }) => getBalance(id)
},
pending_order: {
type: OrderType,
resolve: ({ id }) => getPendingOrders(id)
}
})
});
And you're going to need to add a new field to your Query type:
person: {
type: PersonType,
args: {
id: {
type: GraphQLString
}
},
// We just need to return an object with the id, the resolvers for
// our Person type fields will do the result
resolve: (root, { id }) => ({ id })
}
There's not much you can do to keep things more DRY and reuse your existing code. If you're looking for a way to reduce boilerplate, I would suggest using graphql-tools.

Why is my graphql nested query returning null?

I'm new to graphql running into issue with nested queries and need help passing an id to identify a relationship.
Queries
Above you can see the PERFORMED_FOR_Affiliation is null, though it is defined in the schema as an Affiliation type.
type Query {
affiliations(affn_id: ID!): [Affiliation]
performances(pfrm_id: ID!): [Performance]
PERFORMED_FOR_Affiliation(affn_id: ID!): Affiliation
Performance_PERFORMED_FOR(pfrm_id: ID!): [Performance]
}
PERFORMED_FOR_Affiliation query is similar to affiliations query only the relationship should return only 1 affiliation (with a matching uid).
I assume affn_id is not being passed down correctly and not sure how to do that properly. Does the PERFORMED_FOR_Affiliation need its own schema?
Schema
type Performance {
pfrm_id: ID!
mark: Int
affn_id: ID!
PERFORMED_FOR_Affiliation: Affiliation
}
type Affiliation {
affn_id: ID!
name: String
Performance_PERFORMED_FOR: [Performance]
}
I've seen some schemas that use 'nodes' and 'edge' types. As I have many other relationships would that be a better way to define the graph?
Resolvers
import performances from './mockData/performances.js';
import affiliations from './mockData/affiliations.js';
export const resolvers = {
Query: {
affiliations: (root, args) => {
return affiliations;
},
performances: (root, args) => {
return performances;
},
PERFORMED_FOR_Affiliation: (root, args) => {
return affiliations;
},
Performance_PERFORMED_FOR: (root, args) => {
return performances;
},
},
};
MockData
//affiliations.js
module.exports = [
{
"affn_id": "43700F3BE17145399924AC176EACBEF4",
"name": "Richmond Senior"
},
{
"affn_id": "8BDE709AC757416082950B1BEED0CE0A",
"name": "Cedar City"
},
{
"affn_id": "123D201BB17545E3B6ECCCCB5FC61FA3",
"name": "Delta"
}
]
and
// performances.js
module.exports = [
{
pfrm_id: "6BD41C6B1C4B43D199DE42A4A408DF1A",
mark: 1270000,
affn_id: "43700F3BE17145399924AC176EACBEF4",
},
{
pfrm_id: "EA2FBC6AB891460EA557F5B60984AD8A",
mark: 1422400,
affn_id: "8BDE709AC757416082950B1BEED0CE0A",
},
{
pfrm_id: "54A6EEB9552C49AC9F7A87E68AC272A2",
mark: 1422400,
affn_id: "123D201BB17545E3B6ECCCCB5FC61FA3",
},
]
Yes you should implement that in your resolver. I guess you will have a database in the background, e.g. mongoose:
You query for the performances and therein you populate the affiliations
performances: (root, args) => {
return new Promise((resolve, reject) => {
performanceModel.find() // find all performances
.populate("PERFORMED_FOR_Affiliation") // add values for affiliates
.exec((error, performances) => {
if (!error) {
resolve(performances); // resolve correct object
} else {
reject(error);
}
});
});
},

I need help understanding Relay OutputFields, getFatQuery

This is the code from official docs of relay, This is for GraphQLAddTodoMutation
const GraphQLAddTodoMutation = mutationWithClientMutationId({
name: 'AddTodo',
inputFields: {
text: { type: new GraphQLNonNull(GraphQLString) },
},
outputFields: {
todoEdge: {
type: GraphQLTodoEdge,
resolve: ({localTodoId}) => {
const todo = getTodo(localTodoId);
return {
cursor: cursorForObjectInConnection(getTodos(), todo),
node: todo,
};
},
},
viewer: {
type: GraphQLUser,
resolve: () => getViewer(),
},
},
mutateAndGetPayload: ({text}) => {
const localTodoId = addTodo(text);
return {localTodoId};
},
});
I think mutateAndGetPayload executes first then outputFields? since it used localTodoId object as parameter, I see localTodoId object returned from mutateAndGetPayload.
and this is the code for relay mutation.please look at the getFatQuery
export default class AddTodoMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`
fragment on User {
id,
totalCount,
}
`,
};
getMutation() {
return Relay.QL`mutation{addTodo}`;
}
getFatQuery() {
return Relay.QL`
fragment on AddTodoPayload #relay(pattern: true) {
todoEdge,
viewer {
todos,
totalCount,
},
}
`;
}
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'viewer',
parentID: this.props.viewer.id,
connectionName: 'todos',
edgeName: 'todoEdge',
rangeBehaviors: ({status}) => {
if (status === 'completed') {
return 'ignore';
} else {
return 'append';
}
},
}];
}
getVariables() {
return {
text: this.props.text,
};
}
getOptimisticResponse() {
return {
// FIXME: totalCount gets updated optimistically, but this edge does not
// get added until the server responds
todoEdge: {
node: {
complete: false,
text: this.props.text,
},
},
viewer: {
id: this.props.viewer.id,
totalCount: this.props.viewer.totalCount + 1,
},
};
}
}
I think the todoEdge is from the outputFields from GraphQL? I see a viewer query on it, why does it need to query the viewer? How do I create a getFatQuery? I would really appreciate if someone help me understand this more and about Relay mutation.
mutateAndGetPayload executes then returns the payload to the outputFields
mutationWithClientMutationId
Source-Code
starWarsSchema example
mutationWithClientMutationId
inputFields: defines the input structures for mutation, where the input fields will be wraped with the input values
outputFields: defines the ouptput structure of the fields after the mutation is done which we can view and read
mutateAndGetPayload: this function is the core one to relay mutations, which performs the mutaion logic (such as database operations) and will return the payload to be exposed to output fields of the mutation.
mutateAndGetPayload maps from the input fields to the output fields using the mutation
operation. The first argument it receives is the list of the input parameters, which we can read to perform the mutation action
The object we return from mutateAndGetPayload can be accessed within the output fields
resolve() functions as the first argument.
getFatQuery() is where we represent, using a GraphQL fragment, everything
in our data model that could change as a result of this mutation

Resources