Querying child node fields in Gatsby - javascript

I have the following GraphQL schema, which defines 3 types: a CondaPackage which hasmany CondaVersion, which hasmany CondaExecutable. I want to be able to query a CondaVersion and ask "how many CondaExecutables do you own which succeeded my analysis". Currently I've written a succeededExeCount and allExeCount which resolve this field by loading all children and manually counting the number of children that succeeded.
exports.createSchemaCustomization = ({ actions: { createTypes }, schema }) => {
createTypes([
schema.buildObjectType({
name: "CondaPackage",
fields: {
succeededExeCount: {
type: "Int!",
resolve(source, args, context){
// TODO
}
},
allExeCount: {
type: "Int!",
resolve(source, args, context){
// TODO
}
}
},
interfaces: ["Node"]
}),
schema.buildObjectType({
name: "CondaVersion",
fields: {
succeededExeCount: {
type: "Float!",
resolve(source, args, context){
const children = context.nodeModel.getNodesByIds({
ids: source.children,
type: "CondaExecutable"
})
return children.reduce((acc, curr) => acc + curr.fields.succeeded, 0)
}
},
allExeCount: {
type: "Int!",
resolve(source, args, context){
return source.children.length;
}
}
},
interfaces: ["Node"]
}),
schema.buildObjectType({
name: "CondaExecutable",
fields: {
succeeded: {
type: "Boolean!",
resolve(source, args, context, info) {
return source.fields.succeeded || false;
}
},
},
interfaces: ["Node"]
})
])
}
My first problem is that this seems incredibly inefficient. For each CondaVersion I'm running a separate query for its children, which is a classic N+1 query problem. Is there a way to tell Gatsby/GraphQL to simply "join" the two tables like I would using SQL to avoid this?
My second problem is that I now need to count the number of succeeding children from the top level type: CondaPackage. I want to ask "how many CondaExecutables do your child CondaVersions own which succeeded my analysis". Again, in SQL this would be easy because I would just JOIN the 3 types. However, the only way I can currently do this is by using getNodesByIds for each child, and then for each child's child, which is n*m*o runtime, which is terrifying. I would like to run a GraphQL query as part of the field resolution which lets me grab the succeededExeCount from each child. However, Gatsby's runQuery seems to return nodes without including derived fields, and it won't let me select additional fields to return. How can I access fields on a node's child's child in Gatsby?

Edit
Here's the response from a Gatsby maintainer regarding the workaround:
Gatsby has an internal mechanism to filter/sort by fields with custom resolvers. We call it materialization. [...] The problem is that this is not a public API. This is a sort of implementation detail that may change someday and that's why it is not documented.
See the full thread here.
Original Answer
Here's a little 'secret' (not mentioned anywhere in the docs at the time of writing):
When you use runQuery, Gatsby will try to resolve derived fields... but only if that field is passed to the query's options (filter, sort, group, distinct).
For example, in CondaVersion, instead of accessing children nodes and look up fields.succeeded, you can do this:
const succeededNodes = await context.nodeModel.runQuery({
type: "CondaExecutable",
query: { filter: { succeeded: { eq: true } } }
})
Same thing for CondaPackage. You might try to do this
const versionNodes = await context.nodeModel.runQuery({
type: "CondaVersion",
query: {}
})
return versionNodes.reduce((acc, nodes) => acc + node.succeededExeCount, 0) // Error
You'll probably find that succeededExeCount is undefined.
The trick is to do this:
const versionNodes = await context.nodeModel.runQuery({
type: "CondaVersion",
- query: {}
+ query: { filter: { succeededExeCount: { gte: 0 } } }
})
It's counter intuitive, because you'd think Gatsby would just resolve all resolvable fields on a type. Instead it only resolves fields that is 'used'. So to get around this, we add a filter that supposedly does nothing.
But that's not all yet, node.succeededExeCount is still undefined.
The resolved data (succeededExeCount) is not directly stored on the node itself, but in node.__gatsby_resolved source. We'll have to access it there instead.
const versionNodes = await context.nodeModel.runQuery({
type: "CondaVersion",
query: { filter: { succeededExeCount: { gte: 0 } } }
})
return versionNodes.reduce((acc, node) => acc + node.__gatsby_resolved.succeededExeCount, 0)
Give it a try & let me know if that works.
PS: I notice that you probably use createNodeField (in CondaExec's node.fields.succeeded?) createTypes is also accessible in exports.sourceNodes, so you might be able to add this succeeded field directly.

Related

Custom merge function is not being called after updating field with cache.modify

I have written a custom merge function for the field products on type Session. It seems the merge function is only being called when I initialise the object Session:1 with its products, and not when I update products later using cache.modify.
My merge function:
const client = new ApolloClient({
uri: 'http://localhost:8081/graphql',
cache: new InMemoryCache({
typePolicies: {
Session: {
fields: {
products: {
merge (existing, incoming) {
// this is only being called on useQuery(HydrateSession), not useMutation(UpsertProduct)
console.log('existing', JSON.stringify(existing, null, 2))
console.log('incoming', JSON.stringify(incoming, null, 2))
// remove duplicates when latestProduct has the same id as an existing product — [..., latestProduct]
if (incoming.filter(p => p.id === incoming[incoming.length - 1].id).length > 1) return existing
return incoming
}
}
}
}
}
})
})
Initialisation of Session:
const HydrateSession = gql`
query {
session {
id
products {
id
}
}
}
`
...
useQuery(HydrateSession)
Updating products later using cache.modify:
const UpsertProduct = gql`
mutation UpsertProduct($product: ProductInput!) {
upsertProduct(product: $product) {
id
}
}
`
...
const [upsertProductMutation] = useMutation(UpsertProduct)
const onClick = async () => {
await upsertProductMutation({
variables: {
product: {
id: 2
}
},
update: (cache, mutationResult) => {
cache.modify({
id: 'Session:1',
fields: {
products: previous => [...previous, mutationResult.data.upsertProduct]
}
})
}
})
}
I have a full working example here https://github.com/jsindos/apollo-play, run npm i and then start two separate processes with npm start and npm serve. After clicking the button triggering the mutation, the merge function is not run (as seen by the absence of console.log statements in the console).
modify circumvents any merge functions you've defined, which means that fields are always overwritten with exactly the values you specify.
https://www.apollographql.com/docs/react/caching/cache-interaction/#using-cachemodify
Reading documentation is a good thing.

Apollo GraphQL merge cached data

I have a page that consists of 2 components and each of them has its own request for data
for example
<MovieInfo movieId={queryParamsId}/>
const GET_MOVIE_INFO = `gql
query($id: String!){
movie(id: $id){
name
description
}
}`
Next component
<MovieActors movieId={queryParamsId}/>
const GET_MOVIE_ACTORS = `gql
query($id: String!){
movie(id: $id){
actors
}
}`
For each of these queries I use apollo hook
const { data, loading, error } = useQuery(GET_DATA, {variable: {id: queryParamsId}}))
Everything is fine, but I got a warning message:
Cache data may be lost when replacing the movie field of a Query object.
To address this problem (which is not a bug in Apollo Client), either ensure all objects of type Movie have IDs, or define a custom merge function for the Query.movie field, so InMemoryCache can safely merge these objects: { ... }
It's works ok with google chrome, but this error affects Safari browser. Everything is crushing. I'm 100% sure it's because of this warning message. On the first request, I set Movie data in the cache, on the second request to the same query I just replace old data with new, so previous cached data is undefined. How can I resolve this problem?
Here is the same solution mentioned by Thomas but a bit shorter
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
YOUR_FIELD: {
// shorthand
merge: true,
},
},
},
},
});
This is same as the following
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
YOUR_FIELD: {
merge(existing, incoming, { mergeObjects }) {
return mergeObjects(existing, incoming);
},
},
},
},
},
});
Solved!
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
YOUR_FIELD: {
merge(existing = [], incoming: any) {
return { ...existing, ...incoming };
// this part of code is depends what you actually need to do, in my
case i had to save my incoming data as single object in cache
}
}
}
}
}
})
});
The other answers still work, but as of Apollo Client >= 3.3 there's an easier option that doesn't require specifying specific fields or a custom merge function. Instead, you only have to specify the type and it will merge all fields for that type:
const cache = new InMemoryCache({
typePolicies: {
YOUR_TYPE_NAME: {
merge: true,
}
}
});
From your example query, I'd guess that an id field should be available though? Try requesting the ID in your query, that should solve the problem in a much more ideal way.
Had same issue with inconsistency of data values vs. our schema. A value type within an entity was missing the id value. Caused by an incomplete data migration.
Temporary solution:
const typePolicies = {
PROBLEM_TYPE: {
keyFields: false as false,
},
PARENT_TYPE: {
fields: {
PROBLEM_FIELD: {
merge: true
}
}
}
}

graphql passing dynamic data to mutation

haven't used graphql or mongodb previously. What is the proper way to pass objects for the update mutation?
Since the only other way i see to pass multiple dynamically appearing parameters is to use input type which is appears to be a bit ineffective to me (in terms of how it looks in the code, especially with bigger objects), i just pass the possible values themselves. however in this case i need to dynamically construct updateObject, which again, going to get messy for the bigger models.
for example now i did:
Mutation: {
updateHub: async (_, { id, url, ports, enabled }) => {
const query = {'_id': id};
const updateFields = {
...(url? {url: url} : null),
...(ports? {ports: ports} : null),
...(enabled? {enabled: enabled} : null)
};
const result = await HubStore.findByIdAndUpdate(query, updateFields);
return {
success: !result ? false : true,
message: 'updated',
hub: result
};
}
}
any advise on the better way to handle this?
thanks!
It appears your code could benefit from using ES6 spread syntax -- it would permit you to deal with an arbitrary number of properties from your args object without the need for serial tertiary statements.
Mutation: {
updateHub: async (_, { id, ...restArgs } ) => {
const query = {'_id': id};
const updateFields = { ...restArgs };
const result = await HubStore.findByIdAndUpdate(query, updateFields);
return {
success: !result ? false : true,
message: 'updated',
hub: result
};
}
}
If for some reason you need to explicitly set the undefined properties to null in your object, you could possibly use some a config obj and method like defaults from the lodash library as shown below:
import { defaults } from 'lodash';
const nullFill = { url: null, ports: null, enabled: null }; // include any other properties that may be needed
Mutation: {
updateHub: async (_, { id, ...restArgs } ) => {
const query = {'_id': id};
const updateFields = defaults(restArgs, nullFill);
const result = await HubStore.findByIdAndUpdate(query, updateFields);
return {
success: !result ? false : true,
message: 'updated',
hub: result
};
}
}
Also, FWIW, I would consider placing the dynamic arguments that could be potentially be updated on its own input type, such as HubInput in this case, as suggested in the graphql docs. Below I've shown how this might work with your mutation. Note that because nothing on HubInput is flagged as requird (!) you are able to pass a dynamic collection of properties to update. Also note that if you take this appraoch you will need to properly destructure your args object initially in your mutation, something like { id, input }.
input HubInput {
url: String
ports: // whatever this type is, like [String]
enabled: Boolean
// ...Anything else that might need updating
}
type UpdateHubPayload {
success: Boolean
message: String
hub: Hub // assumes you have defined a type Hub
}
updateHub(id: Int, input: HubInput!): UpdateHubPayload

Resolve Custom Types at the root in GraphQL

I feel like I'm missing something obvious. I have IDs stored as [String] that I want to be able to resolve to the full objects they represent.
Background
This is what I want to enable. The missing ingredient is the resolvers:
const bookstore = `
type Author {
id: ID!
books: [Book]
}
type Book {
id: ID!
title: String
}
type Query {
getAuthor(id: ID!): Author
}
`;
const my_query = `
query {
getAuthor(id: 1) {
books { /* <-- should resolve bookIds to actual books I can query */
title
}
}
}
`;
const REAL_AUTHOR_DATA = [
{
id: 1,
books: ['a', 'b'],
},
];
const REAL_BOOK_DATA = [
{
id: 'a',
title: 'First Book',
},
{
id: 'b',
title: 'Second Book',
},
];
Desired result
I want to be able to drop a [Book] in the SCHEMA anywhere a [String] exists in the DATA and have Books load themselves from those Strings. Something like this:
const resolve = {
Book: id => fetchToJson(`/some/external/api/${id}`),
};
What I've Tried
This resolver does nothing, the console.log doesn't even get called
const resolve = {
Book(...args) {
console.log(args);
}
}
HOWEVER, this does get some results...
const resolve = {
Book: {
id(id) {
console.log(id)
return id;
}
}
}
Where the console.log does emit 'a' and 'b'. But I obviously can't scale that up to X number of fields and that'd be ridiculous.
What my team currently does is tackle it from the parent:
const resolve = {
Author: {
books: ({ books }) => books.map(id => fetchBookById(id)),
}
}
This isn't ideal because maybe I have a type Publisher { books: [Book]} or a type User { favoriteBooks: [Book] } or a type Bookstore { newBooks: [Book] }. In each of these cases, the data under the hood is actually [String] and I do not want to have to repeat this code:
const resolve = {
X: {
books: ({ books }) => books.map(id => fetchBookById(id)),
}
};
The fact that defining the Book.id resolver lead to console.log actually firing is making me think this should be possible, but I'm not finding my answer anywhere online and this seems like it'd be a pretty common use case, but I'm not finding implementation details anywhere.
What I've Investigated
Schema Directives seems like overkill to get what I want, and I just want to be able to plug [Books] anywhere a [String] actually exists in the data without having to do [Books] #rest('/external/api') in every single place.
Schema Delegation. In my use case, making Books publicly queryable isn't really appropriate and just clutters my Public schema with unused Queries.
Thanks for reading this far. Hopefully there's a simple solution I'm overlooking. If not, then GQL why are you like this...
If it helps, you can think of this way: types describe the kind of data returned in the response, while fields describe the actual value of the data. With this in mind, only a field can have a resolver (i.e. a function to tell it what kind of value to resolve to). A resolver for a type doesn't make sense in GraphQL.
So, you can either:
1. Deal with the repetition. Even if you have ten different types that all have a books field that needs to be resolved the same way, it doesn't have to be a big deal. Obviously in a production app, you wouldn't be storing your data in a variable and your code would be potentially more complex. However, the common logic can easily be extracted into a function that can be reused across multiple resolvers:
const mapIdsToBooks = ({ books }) => books.map(id => fetchBookById(id))
const resolvers = {
Author: {
books: mapIdsToBooks,
},
Library: {
books: mapIdsToBooks,
}
}
2. Fetch all the data at the root level instead. Rather than writing a separate resolver for the books field, you can return the author along with their books inside the getAuthor resolver:
function resolve(root, args) {
const author = REAL_AUTHOR_DATA.find(row => row.id === args.id)
if (!author) {
return null
}
return {
...author,
books: author.books.map(id => fetchBookById(id)),
}
}
When dealing with databases, this is often the better approach anyway because it reduces the number of requests you make to the database. However, if you're wrapping an existing API (which is what it sounds like you're doing), you won't really gain anything by going this route.

Promise on addAssociation is resolved with a stale version of the source

I have a versioning schema that versions object entries in a object_version table
const Object = sequelize.define('object', {})
const ObjectVersion = sequelize.define('object_version', {
title: {
allowNull: false,
type: DataTypes.STRING
}
})
Object.hasMany(ObjectVersion, { as: 'versions' })
Using express, i have a put route where an existing object entry can be updated, in this simple example by setting a new title, e.g. by sending a PUT request with a body of {"title":"new title"}
app.put('object/:id', (req, res) => {
const id = parseInt(req.params.id, 10)
Object.findOne({
where: { id },
include: [ Object.associations.versions ]
}).then(object => {
const newVersion = ObjectVersion.build(req.body, { objectId: id })
Object.addVersion(newVersion).then(object => {
// shouldn't this instance have all versions, including the new one?
// do i need to `findOne` again to get them?
console.log(object.toJSON().versions)
res.send(object)
})
}
})
})
When using the addVersion method that is created on my Object model by associating it to the ObjectVersion model via hasMany, the returned promise is resolved with an object instance.
The problem is that i want to return the objects JSON in the response, but the object instance the promise is resolved with does not contain the version that was just added.
To me, this looks like an oversight. It should not be necessary to perform another query. When the promise for the addXyz operation is resolved, the new association is already saved in the database so it should be possible to resolve the promise with an updated version of the object.
Maybe i'm going at this the wrong way or missed an easier way to perform an update operation that adds new associations. Any help is welcome!
After an interesting talk on Slack i realised that the addAssociation, addAssociations and setAssociations methods on model instances connect existing rows in the db with the target.
In my example, the addVersion method connects an existing object_version row with the current version instance and can not be used to insert a new row into the database.
I've opened a PR to sequelize trying to clarify this in the docs as i skipped right over this. An example that works might look like this:
app.put('object/:id', (req, res) => {
const id = parseInt(req.params.id, 10)
Object.findOne({
where: { id },
include: [ Object.associations.versions ]
}).then(object => {
object.createVersion(req.body).then(version => {
object.versions.push(version)
res.send(object)
})
})
})

Categories

Resources