Apollo Client 3: how to cache a mutation result into a nested collection? (collection within a collection) - graphql

In my Apollo Client 3 app, I am doing a mutation and want to cache the result into a collection which is nested within an item of a collection.
Specifically, I am creating a comment within a list of comments, each list within a post, each post within a list of posts. My app's data hierarchy looks like:
user 1
profile 1
post 1
comment 1.1
comment 1.2
post 2
comment 2.1
comment 2.2
< write mutation result here >
post 3
comment 3.1
comment 3.2
comment 3.3
...
In this situation, how would I best cache a created comment into its parent post's comment-collection? I am looking at the useMutation hook's update or modify config, but am not too sure.
For additional context, here is query that corresponds to the above data hierarchy:
query getUserPosts($userParams: GetUserParams!$postsPaginationParams: CursorPaginationParams!) {
user(params: $userParams) {
id
profile {
id
# ...
ownedPosts(pagination: $postsPaginationParams) {
items {
id
# ...
featuredComments {
id
primaryText
creationTimestamp
owner {
id
name
}
}
}
pagination {
# ...
}
}
}
}
}
And here is my mutation:
input CreateCommentParams {
ownerId: String!
postId: String!
primaryText: String!
}
mutation createComment($params: CreateCommentParams!) {
createComment(params: $params) {
id
owner {
id
name
}
primaryText
creationTimestamp
}
}
And here is what the useMutation is so far:
useMutation(CREATE_COMMENT_MUTATION, {
// ...
update: (cache, { data }) => {
if (data) {
const cacheId = cache.identify(data.createComment);
cache.modify({
fields: {
// ...how to update the comments array of the specific post?
}
})
}
},
})

You need to find the Post you are updating and update its featuredComments field like so:
useMutation(CREATE_COMMENT_MUTATION, {
// ...
update: (cache, { data }) => {
cache.modify({
id: cache.identify({
__typename: 'Post', // Assuming your this is the _typename of your Post type in your schema
id: postId,
}),
fields: {
featuredComments: (previous, { toReference }) => [...previous, toReference(data.createComment)]
},
}),
}),
})

Related

How to paste array of objects into GraphQL Apollo cache?

I have an array of countries received from Apollo backend without an ID field.
export const QUERY_GET_DELIVERY_COUNTRIES = gql`
query getDeliveryCountries {
deliveryCountries {
order
name
daysToDelivery
zoneId
iso
customsInfo
}
}
`
Schema of these objects:
{
customsInfo: null
daysToDelivery: 6
iso: "UA"
name: "Ukraine"
order: 70
zoneId: 8
__typename: "DeliveryCountry"
}
In nested components I read these objects from client.readQuery.
What I want is to insert it to localStorage, read it initially and write this data to Apollo Client Cache.
What I've already tried to do:
useEffect(() => {
const deliveryCountries = JSON.parse(localStorage.getItem('deliveryCountries') || '[]')
if(!deliveryCountries || !deliveryCountries.length) {
getCountriesLazy()
} else {
deliveryCountries.map((c: DeliveryCountry) => {
client.writeQuery({
query: QUERY_GET_DELIVERY_COUNTRIES,
data: {
deliveryCountries: {
__typename: "DeliveryCountry",
order: c.order,
name: c.name,
daysToDelivery: c.daysToDelivery,
zoneId: c.zoneId,
iso: c.iso,
customsInfo: c.customsInfo
}
}
})
})
}
}, [])
But after execution the code above I have only one object in countries cache. How to write all objects without having an explicit ID, how can I do it? Or maybe I'm doing something wrong?
Lol. I just had to put the array into necessary field without iterating. writeQuery replaces all the data and not add any "to the end".
client.writeQuery({
query: QUERY_GET_DELIVERY_COUNTRIES,
data: {
deliveryCountries: deliveryCountries
}
})

graphql - Refer to other fields in mutation

I want to create 2 related objects, e.g. 1 Location and 1 Place where Place has a reference to Location like so:
type Location {
id: String
name: String
}
type Place {
id: String
locationId: String
}
Is it possible to do this with 1 mutation request? Currently I'm doing this with 2 separate mutation requests like below:
mutation ($locationName: String!) {
insert_Location(objects: {name: $locationName}) {
returning {
id
}
}
}
//in another request, use the id returned from the request above
mutation ($locationId: String!) {
insert_Place(objects: {locationId: $locationId}) {
returning {
id
}
}
}
I'm aware it's possible to have multiple fields in a mutation so I could create 2 Locations in 1 mutation request like below.
mutation ($locationName: String!) {
location1: insert_Location(objects: {name: $locationName}) {
returning {
id
}
}
location2: insert_Location(objects: {name: $locationName}) {
returning {
id
}
}
}
However if I wanted to do this to create 1 Location and 1 Place, is there a way to retrieve the created Location Id and pass it to the 2nd field to create the Place?
For future reference:
As #Xetera pointed out, because the 2 types have a foreign key relationship you can do a nested insert mutation where hasura would handle setting the foreign key value. In my case it would look something like:
mutation ($locationName: String!) {
insert_Place(
objects: {
Location: {data: {name: $locationName}}, //hasura will create Location and assign the id to Place.locationId
}
) {
returning {
id
}
}
}
Docs here for further reading: https://hasura.io/docs/1.0/graphql/manual/mutations/insert.html#insert-an-object-along-with-its-related-objects-through-relationships

Enumerating all fields from a GraphQL query

Given a GraphQL schema and resolvers for Apollo Server, and a GraphQL query, is there a way to create a collection of all requested fields (in an Object or a Map) in the resolver function?
For a simple query, it's easy to recreate this collection from the info argument of the resolver.
Given a schema:
type User {
id: Int!
username: String!
roles: [Role!]!
}
type Role {
id: Int!
name: String!
description: String
}
schema {
query: Query
}
type Query {
getUser(id: Int!): User!
}
and a resolver:
Query: {
getUser: (root, args, context, info) => {
console.log(infoParser(info))
return db.Users.findOne({ id: args.id })
}
}
with a simple recursive infoParser function like this:
function infoParser (info) {
const fields = {}
info.fieldNodes.forEach(node => {
parseSelectionSet(node.selectionSet.selections, fields)
})
return fields
}
function parseSelectionSet (selections, fields) {
selections.forEach(selection => {
const name = selection.name.value
fields[name] = selection.selectionSet
? parseSelectionSet(selection.selectionSet.selections, {})
: true
})
return fields
}
The following query results in this log:
{
getUser(id: 1) {
id
username
roles {
name
}
}
}
=> { id: true, username: true, roles: { name: true } }
Things get pretty ugly pretty soon, for example when you use fragments in the query:
fragment UserInfo on User {
id
username
roles {
name
}
}
{
getUser(id: 1) {
...UserInfo
username
roles {
description
}
}
}
GraphQL engine correctly ignores duplicates, (deeply) merges etc. queried fields on execution, but it is not reflected in the info argument. When you add unions and inline fragments it just gets hairier.
Is there a way to construct a collection of all fields requested in a query, taking in account advanced querying capabilities of GraphQL?
Info about the info argument can be found on the Apollo docs site and in the graphql-js Github repo.
I know it has been a while but in case anyone ends up here, there is an npm package called graphql-list-fields by Jake Pusareti that does this. It handles fragments and skip and include directives.
you can also check the code here.

How to update apollo cache after mutation (query with filter)

I am pretty new to GraphQL. I am using graph.cool in a Vue.js project with apollo.
I am using right now the in-memory cache.
I had previously a simple 'allPosts' query.
And after creating a new one, I used the update() hook and readQuery() + writeQuery()
However I want that logged in users can only see their posts. So I modified the query with a filter.
query userStreams ($ownerId: ID!) {
allStreams(filter: {
owner: {
id: $ownerId
}
}) {
id
name
url
progress
duration
watched
owner {
id
}
}
}
My thought was, that I only need to pass in the userid variable. However this is not working. I am always getting
Error: Can't find field allStreams({"filter":{"owner":{}}}) on object (ROOT_QUERY) undefined.
this.$apollo.mutate({
mutation: CREATE_STREAM,
variables: {
name,
url,
ownerId
},
update: (store, { data: { createStream } }) => {
const data = store.readQuery({
query: USERSTREAMS,
variables: {
id: ownerId
}
})
data.allStreams.push(createStream)
store.writeQuery({
query: USER_STREAMS,
variables: {
id: ownerId
},
data
})
}
})
When you use readQuery or writeQuery, you should use the same variable name. So replace
variables: { id: ownerId }
With
variables: { ownerId }
Also, the reason you are getting an exception is that readQuery throws an exception if the data is not in the store. That happens before the first time you use writeQuery (or get the data with some other query).
You could write some default values to the store before calling this mutation.
You could also use readFragment that returns null instead of throwing an exception. But that would require more changes to your code.

Relay mutation fragments intersection

I don't use Relay container, because I'd like to have more control over components. Instead of it I use HOC + Relay.Store.forceFetch, that fetches any given query with variables. So I have the following query:
query {
root {
search(filter: $filter) {
selectors {
_id,
data {
title,
status
}
},
selectorGroups {
_id,
data {
title,
}
}
}
}
}
Then I have to do some mutation on selector type.
export default class ChangeStatusMutation extends Relay.Mutation {
getMutation() {
return Relay.QL`mutation {selectors_status_mutation}`;
}
getVariables() {
return {
id: this.props.id,
status: this.props.status
};
}
getFatQuery() {
return Relay.QL`
fragment on selectors_status_mutationPayload{
result {
data {
status
}
}
}
`;
}
static fragments = {
result: () => Relay.QL`
fragment on selector {
_id,
data {
title,
status
}
}`,
};
getOptimisticResponse() {
return {
result: {
_id: this.props.id,
data: {
status: this.props.status
}
}
};
}
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {
result: this.props.id
},
}];
}
}
Call mutation in component:
const mutation = new ChangeStatusMutation({id, status, result: selector});
Relay.Store.commitUpdate(mutation);
After mutation commitment selector in Relay storage is not changed. I guess that's because of empty Tracked Fragment Query and mutation performs without any fields:
ChangeStatusMutation($input_0:selectors_statusInput!) {
selectors_status_mutation(input:$input_0) {
clientMutationId
}
}
But the modifying selector was already fetched by Relay, and I pass it to the mutation with props. So Relay knows the type, that should be changed, how to find the item and which fields should be replaced. But can not intersect. What's wrong?
So, you're definitely a bit "off the ranch" here by avoiding Relay container, but I think this should still work...
Relay performs the query intersection by looking up the node indicated by your FIELDS_CHANGE config. In this case, your fieldIDs points it at the result node with ID this.props.id.
Are you sure you have a node with that ID in your store? I'm noticing that in your forceFetch query you fetch some kind of alternative _id but not actually fetching id. Relay requires an id field to be present on anything that you later want to refetch or use the declarative mutation API on...
I'd start by checking the query you're sending to fetch whatever this result type is. I don't see you fetching that anywhere in your question description, so I'm just assuming that maybe you aren't fetching that right now?

Resources