How to delete cache record from Apollo GraphQL client with readQuery and writeQuery - apollo

The Apollo GraphQL team says that readQuery and writeQuery are good for 95% of the use cases. I am using useMutation and update and want to remove an item from a cache without having to call refetchQueries. My code is as follows:
const [deleteSpeaker] = useMutation(DELETE_SPEAKER, {
update(cache, { data: {deleteSpeaker}}) {
const { speakers} = cache.readQuery({query: GET_SPEAKERS});
cache.writeQuery({
query: GET_SPEAKERS,
data: { speakers: speakers.filter(speaker => speaker.id !== deleteSpeaker.id) }
});
},
});
What gets returned from readQuery leads me to think I should be filtering for speakers.datalist but when I do that, the cache does not update.
What is the correct way to update cache to reflect a removed record from the GET_SPEAKERS query.
export const DELETE_SPEAKER = gql`
mutation DeleteSpeaker($speakerId: Int!) {
deleteSpeaker(speakerId: $speakerId) {
id
first
last
favorite
}
}
`;
and GET_SPEAKERS
export const GET_SPEAKERS = gql`
query {
speakers {
datalist {
id
first
last
favorite
company
}
}
}
`;

reading apollo docs, this should be something lke:
const [deleteSpeaker] = useMutation(DELETE_SPEAKER, {
update(cache, { data: {deleteSpeaker}}) {
cache.modify({
id: cache.identify(deleteSpeaker.id),
fields: {
comments(existingSpeakerRefs, { readField }) {
return existingSpeakerRefs.filter(
speaker => deleteSpeaker.id !== readField('id', speakerRef)
);
},
},
});
},
});

Related

How can I mock a paginated GraphQL query?

I am using apollo/client and graphql-tools/mock to auto mock graphql queries and test React Native components that use them. My schema is generated from an introspection query created by graphql-codegen. For the most part, my queries are getting mocked by addMocksToSchema just fine. However I have a query that is not returning any mock data.
The query is paginated and doesn't follow the same structure of the examples in the docs (https://www.graphql-tools.com/docs/mocking). Instead of having a query with a node that has a field that is a connection type, the connection is returned from the query. This means I can't use relayStylePaginationMock to mock my function because the resolver argument of addMocksToSchema expects the nodes to be objects not functions(function is the return type of relayStylePaginationMock).
In the below code I have tried overriding the newsPost query with a resolver, but I can't figure out how to get the NewsPostEdges from the store and put them in my mock. Everything I have tried has broken the mock and caused it to return undefined for the whole mocked query.
Why does a paginated mock not work by default?
How can I mock this query?
Schema:
type Query {
newsPost: NewsPostConnection
}
type NewsPostConnection {
totalCount: Int
edges: [NewsPostEdge]!
pageInfo: PageInfo!
}
type NewsPostEdge {
node: NewsPostNode
cursor: String!
}
type NewsPostNode {
newsPostId: Int!
isPinned: Boolean!
label: String
title: String
content: String
postType: NewsPostType!
createdDate: DateTime
createdDateTime: String
creator: UserNode!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
endCursor: String
startCursor: String
}
News Posts query:
query NewsPosts(
$after: String
$first: Int
$newsPostId: Filter_ID
$sort: [NewsPostSortEnum]
$isPinned: Filter_Boolean
) {
newsPosts(
after: $after
first: $first
newsPostId: $newsPostId
sort: $sort
isPinned: $isPinned
) {
pageInfo {
hasNextPage
endCursor
}
edges {
post: node {
newsPostId
postType
isPinned
label
createdDateTime
creator {
initials
avatarUrl
displayName
}
content
}
}
}
}
newsPostsContent.test.tsx
import React from 'react';
import { waitFor } from '#testing-library/react-native';
import { PartialDeep } from 'type-fest';
import { faker } from '#faker-js/faker';
import { createFakeUser, render } from '#root/unit-tests/#util';
import { NewsPostNode, NewsPostType } from '#root/src/generated';
import NewsPostContent from '../NewsPostContent';
const mocks = {
NewsPostNode: (): PartialDeep<NewsPostNode> => {
const postId = faker.random.numeric(4);
const createdDate = faker.date.recent(10);
return {
postId,
isPinned: true,
label: 'test',
content: `<div><p>${faker.random.words(10)}</p></div>`,
postType: NewsPostType.Announcement,
createdDate: createdDate.toISOString(),
createdDateTime: createdDate.toISOString(),
};
},
UserNode: createUserPerson(),
};
describe('Dashboard News', () => {
it('renders dashboard news', async () => {
const { getByTestId, debug } = render(
<NewsPostContent />,
mocks,
);
await waitFor(() => [debug(), expect(getByTestId('newsPostContent:Card')).toBeDefined()]);
});
});
NewsPostsContetnt.tsx
const NewsPostContent = () => {
const [newsPostList, setNewsPostList] = useState<PartialDeep<NewsPostNode>[]>([])
const {
data,
loading,
refetch: refetchPosts,
} = useNewsPostsQuery({
variables: { first: MAX_POSTS, isPinned: true, sort: [PostSortEnum.CreatedDateDesc] },
});
console.log(data); // <-- returns undefined when mock breaks
useEffect(() => {
const newsPostEdges = data?.newsPosts?.edges ?? [];
const newsPostNodes = newsPostEdges.reduce((posts, newsPostNode) => {
if (newsPostNode?.post) {
posts.push(newsPostNode.post);
}
return posts;
}, [] as PartialDeep<NewsPostNode>[]);
setNewsPostList(newsPostNodes);
}, [data]);
return (
{<View>
// Component UI to render posts
</View>}
)
}
AutoMockedProvider.tsx
import React from 'react';
import { ApolloProvider, ApolloClient, InMemoryCache } from '#apollo/client';
import { buildClientSchema } from 'graphql';
import {
addMocksToSchema,
createMockStore,
IMocks,
IMockStore,
relayStylePaginationMock,
} from '#graphql-tools/mock';
import { SchemaLink } from '#apollo/client/link/schema';
import { faker } from '#faker-js/faker';
const introspectionResult = require('../../src/generated/introspection.json');
const defaultMocks = {
Date: () => faker.date.recent().toISOString(),
DateTime: () => faker.date.recent().toISOString(),
};
const resolvers = (store: IMockStore) => ({
Query: {
newsPosts: (root, { isPinned, after, first, postId, sort }) => {
return {
edges: (ref) => {
const connectionsRef = store.get('NewsPostConnection');
const edgesRef = store.get(connectionsRef, 'edges');
return edgesRef; // <-- this breaks the mock
},
pageInfo: {
endCursor: null,
hasNextPage: false,
},
};
},
},
});
const AutoMockedProvider = ({
mocks = {},
children,
}: React.PropsWithChildren<{ mocks?: IMocks }>) => {
const schema = buildClientSchema(introspectionResult);
const store = createMockStore({ mocks: { ...defaultMocks, ...mocks }, schema });
const schemaWithMocks = addMocksToSchema({
schema,
mocks: {
...defaultMocks,
...mocks,
},
resolvers,
preserveResolvers: false,
store,
});
const client = new ApolloClient({
link: new SchemaLink({ schema: schemaWithMocks }),
cache: new InMemoryCache(),
});
return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
export default AutoMockedProvider;

How can i work with GraphQL Mutation?

how can i work with resolvers for mutations after i create type Mutations in graphql-yoga?
i've tried to create resolvers for mutations, but when i run in graph playground, i the code return error.
and here's my code:
const { GraphQLServer } = require('graphql-yoga')
// 1
const typeDefs = `
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(name: String!): User!
}
type User {
id: ID!
name: String!
}
`
// 2
const resolvers = {
Query: {
users: () => User,
},
Mutation: {
// WHAT SHOULD I WRITE IN HERE?
}
}
// 3
const server = new GraphQLServer({
typeDefs,
resolvers,
})
server.start(() => console.log(`Server is running on http://localhost:4000`))
if someone know how can i do for resolvers mutation, can shared with me?
thanks
Resolver for createUser can be defined as follows:
const resolvers = {
Query: {
// Query resolvers
},
Mutation: {
createUser: (parent, args) => {
// Business logic. Maybe save record in database
// Return created user. I am returning dummy data for now, so that you can test it in playground
return {id: 1, name: "John}
}
}
}
Finally it works for me.
i used this:
const resolvers = {
Query: {
users: () => User
},
Mutation: {
createUser: (source, {input}) => {
let newUser = [];
newUser.id = id;
newUser.name = input.name;
User.push(newUser);
return newUser;
}
}
}

Apollo link state only works when redundant query is defined?

I have Apollo link state working:
import React from 'react';
import ReactDOM from 'react-dom';
import { HttpLink, InMemoryCache, ApolloClient } from 'apollo-client-preset';
import { WebSocketLink } from 'apollo-link-ws';
import { ApolloLink, split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import { AUTH_TOKEN } from './constant';
import RootContainer from './components/RootContainer';
import { ApolloProvider } from 'react-apollo';
import { withClientState } from 'apollo-link-state';
import { gql } from 'apollo-boost';
const httpLink = new HttpLink({ uri: 'http://localhost:4000' });
const middlewareLink = new ApolloLink((operation, forward) => {
const tokenValue = localStorage.getItem(AUTH_TOKEN);
operation.setContext({
headers: {
Authorization: tokenValue ? `Bearer ${tokenValue}` : '',
},
});
return forward(operation);
});
const httpLinkAuth = middlewareLink.concat(httpLink);
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000`,
options: {
reconnect: true,
connectionParams: {
Authorization: `Bearer ${localStorage.getItem(AUTH_TOKEN)}`,
},
},
});
const link = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLinkAuth,
);
const cache = new InMemoryCache();
const stateLink = withClientState({
cache,
defaults: {
groupMenuStatus: {
__typename: 'GroupMenuStatus',
isOpen: false,
},
},
resolvers: {
Mutation: {
updateGroupMenuStatus: (_, { isOpen }, { cache }) => {
const data = {
groupMenuStatus: {
__typename: 'GroupMenuStatus',
isOpen,
},
};
cache.writeData({ data });
return null;
},
},
Query: {
groupMenuStatus: async (_, args, { cache }) => {
const query = gql`
query groupMenuStatus {
groupMenuStatus #client {
isOpen
}
}
`;
const res = cache.readQuery({ query });
return res.groupMenuStatus;
},
},
},
});
const client = new ApolloClient({
link: ApolloLink.from([stateLink, link]),
cache,
connectToDevTools: true,
});
const token = localStorage.getItem(AUTH_TOKEN);
ReactDOM.render(
<ApolloProvider client={client}>
<RootContainer token={token} />
</ApolloProvider>,
document.getElementById('root'),
);
However is most of the examples online they havn't needed to define a query resolver. If I remove the code below then the query from the front-end will always return the default state, the mutation seems to have no effect:
Query: {
groupMenuStatus: async (_, args, { cache }) => {
const query = gql`
query groupMenuStatus {
groupMenuStatus #client {
isOpen
}
}
`;
const res = cache.readQuery({ query });
return res.groupMenuStatus;
},
},
According to the official docs on https://www.apollographql.com/docs/link/links/state.html
Query resolvers are only called on a cache miss. Since the first time you call the query will be a cache miss, you should return any default state from your resolver function.
So, if you define a default, your query resolver will never be called. (You got the definition right, it is called Query indeed)
If you do not declare a default, you might use the query resolver to write something on cache (and then the query resolver will not be called anymore), or you can just return some value, and the resolver will be called every time.
I use it, for example to get user geolocation on the first call, that's my default value now, and the resolver is never called again.
Check my use case:
Query: {
async smePosition(_: any, {}: any, { cache }: IContext): Bluebird<any> {
return new Bluebird((resolve: any, reject: any): void => {
window.navigator.geolocation.getCurrentPosition(
({coords: {latitude: lat, longitude: lng}}) => {
const data = {
smePosition: {
__typename: 'SMe',
position: {lat, lng , __typename: 'IPosition'},
},
}
cache.writeData({ data })
resolve()
},
)
})
},
},
In this case, I don't define a defaults value for 'smePosition'

Apollo: Update React Props on Subscription Update?

Looking at the Apollo docs example code for subscriptions, I am not yet seeing how to update the React props with the subscription results.
From http://dev.apollodata.com/react/subscriptions.html:
Here is a regular query:
import { CommentsPage } from './comments-page.js';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
const COMMENT_QUERY = gql`
query Comment($repoName: String!) {
entry(repoFullName: $repoName) {
comments {
id
content
}
}
}
`;
const withData = graphql(COMMENT_QUERY, {
name: 'comments',
options: ({ params }) => ({
variables: {
repoName: `${params.org}/${params.repoName}`
},
})
});
export const CommentsPageWithData = withData(CommentsPage);
Now, let’s add the subscription.
Note that this sample code appears to leave out this part of the props code for usual queries - from http://dev.apollodata.com/react/queries.html:
props: ({ ownProps, data: { loading, currentUser, refetch } }) => ({
userLoading: loading,
user: currentUser,
refetchUser: refetch,
}),
...which AFAIK is the correct way to update the data props on my React component and trigger a page refresh.
Here is the complete subscription code sample from http://dev.apollodata.com/react/subscriptions.html:
const withData = graphql(COMMENT_QUERY, {
name: 'comments',
options: ({ params }) => ({
variables: {
repoName: `${params.org}/${params.repoName}`
},
}),
props: props => {
return {
subscribeToNewComments: params => {
return props.comments.subscribeToMore({
document: COMMENTS_SUBSCRIPTION,
variables: {
repoName: params.repoFullName,
},
updateQuery: (prev, {subscriptionData}) => {
if (!subscriptionData.data) {
return prev;
}
const newFeedItem = subscriptionData.data.commentAdded;
return Object.assign({}, prev, {
entry: {
comments: [newFeedItem, ...prev.entry.comments]
}
});
}
});
}
};
},
});
How do I get the code shown here, to update the data props on my React component and trigger a page refresh, when the results come in from the non-subscription query COMMENT_QUERY?
Thanks to #neophi on the Apollo Slack for this answer!
const withDataAndSubscription = graphql(GETIMS_QUERY, {
options({toID}) {
console.log(GETIMS_QUERY);
const fromID = Meteor.userId();
return {
fetchPolicy: 'cache-and-network',
variables: {fromID: `${fromID}`, toID: `${toID}`}
};
}
,
props: props => {
return {
loading: props.data.loading,
instant_message: props.data.instant_message,
subscribeToMore: props.data.subscribeToMore,
subscribeToNewIMs: params => {
const fromID = Meteor.userId();
const toID = params.toID;
return props.data.subscribeToMore({
document: IM_SUBSCRIPTION_QUERY,
variables: {fromID: `${fromID}`, toID: `${toID}`},
updateQuery: (previousResult, {subscriptionData}) => {
if (!subscriptionData.data) {
return previousResult;
}
const newMsg = subscriptionData.data.createIM;
return update(previousResult, {
instant_message: {
$push: [newMsg],
},
});
}
});
}
};
},
})
;

Optimistic UI Not Updating - Apollo

After making a mutation the UI does not update with a newly added item until the page is refreshed. I suspect the problem is in the update section of the mutation but I'm not sure how to troubleshoot further. Any advice is much appreciated.
Query (separate file)
//List.js
export const AllItemsQuery = gql`
query AllItemsQuery {
allItems {
id,
name,
type,
room
}
}
`;
Mutation
import {AllItemsQuery} from './List'
const AddItemWithMutation = graphql(createItemMutation, {
props: ({ownProps, mutate}) => ({
createItem: ({name, type, room}) =>
mutate({
variables: {name, type, room},
optimisticResponse: {
__typename: 'Mutation',
createItem: {
__typename: 'Item',
name,
type,
room
},
},
update: (store, { data: { submitItem } }) => {
// Read the data from the cache for this query.
const data = store.readQuery({ query: AllItemsQuery });
// Add the item from the mutation to the end.
data.allItems.push(submitItem);
// Write the data back to the cache.
store.writeQuery({ query: AllItemsQuery, data });
}
}),
}),
})(AddItem);
Looks promising, one thing that is wrong is the name of the result of the mutation data: { submitItem }. Because in the optimistic Response you declare it as createItem. Did you console.log and how does the mutation look like?
update: (store, {
data: {
submitItem // should be createItem
}
}) => {
// Read the data from our cache for this query.
const data = store.readQuery({
query: AllItemsQuery
});
// Add our comment from the mutation to the end.
data.allItems.push(submitItem); // also here
// Write our data back to the cache.
store.writeQuery({
query: AllItemsQuery,
data
});
}
I'm not entirely sure that the problem is with the optimisticResponse function you have above (that is the right approach), but I would guess that you're using the wrong return value. For example, here is a response that we're using:
optimisticResponse: {
__typename: 'Mutation',
updateThing: {
__typename: 'Thing',
thing: result,
},
},
So if I had to take a wild guess, I would say that you might want to try using the type within your return value:
optimisticResponse: {
__typename: 'Mutation',
createItem: {
__typename: 'Item',
item: { // This was updated
name,
type,
room
}
},
},
As an alternative, you can just refetch. There have been a few times in our codebase where things just don't update the way we want them to and we can't figure out why so we punt and just refetch after the mutation resolves (mutations return a promise!). For example:
this.props.createItem({
... // variables go here
}).then(() => this.props.data.refetch())
The second approach should work every time. It's not exactly optimistic, but it will cause your data to update.