Optimistic update for a deletion mutation - graphql

I'm writing a deletion mutation. The mutation should delete a Key node and update the viewer's keys collection (I'm using Relay-style collections: viewer { keys(first: 3) { edges { node { ... }}}}.
Following the advice here, I'm using the FIELDS_CHANGE config for simplicity, and it's actually working:
export class DeleteKeyMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`
fragment on Viewer { id }
`,
};
getMutation() { return Relay.QL`mutation {deleteKey}`; }
getVariables() {
return {
id: this.props.id,
};
}
getFatQuery() {
return Relay.QL`
fragment on DeleteKeyPayload {
viewer { keys }
}
`;
}
getConfigs() {
return [
{
type: 'FIELDS_CHANGE',
fieldIDs: {
viewer: this.props.viewer.id,
},
},
];
}
}
Now, how should I write an optimistic mutation for this? I've tried different approaches but none worked.

Optimistic update in Relay is just a simulation of what the server will return if operation succeeds. In your case you are removing one key, meaning the result would be an object without that key.
getOptimisticUpdate() {
return {
viewer: {
id: this.props.viewer.id,
keys: {
edges: this.props.viewer.keys.edges.filter((keyEdge) => key.node.id !== this.props.id)
}
}
};
}
You will also need to include the keys to your fragments so they are available in the mutation.
static fragments = {
viewer: () => Relay.QL`
fragment on Viewer { id, keys { edges(first: 3) { node { id } }}
`,
};
The problem with this approach is that it relies on your mutation to know what's your current keys pagination. If you are operating on the whole Connection at once, it is fine, but if you are using Relay pagination you should consider using other mutation operations.
There is NODE_DELETE, which can delete all occurrences of your key from Relay store or you can use RANGE_DELETE to only delete it from your current connection.

Related

Apollo cache update is not reflected on paginated query

I have two components, one of the components creates new items, the other one displays them using an "infinite scroll list". These two components do not have a parent/child relationship and they're not rendered at the same time (they're on different "pages").
I've followed these docs and included the modified object in the mutation of my first component. And I can see the new object in the Apollo cache using dev tools. (Car:<some UUID> gets added in the cache after the mutation runs)
My paginated component is configured with relay style pagination, and the pagination works fine, but when I add a new item it doesn't appear in the list until I refresh the page.
My InMemoryCache looks like this:
...
typePolicies: {
// paginated results
Query: {
fields: {
cars: relayStylePagination()
}
},
CarsResult: {
fields: {
edges: {
// Concatenate the incoming list items with
// the existing list items.
merge(existing = [], incoming) {
return [...existing, ...incoming]
}
}
}
},
PageInfo: {
fields: {
endCursor: {
merge(existing, incoming) {
return incoming
}
}
}
}
}
The mutation looks like this:
${CAR_SUMMARY_FRAGMENT}
mutation CreateCar($name: String!) {
createCar(
input: {
name: $name
}
) {
...CarSummary
}
}
The CreateCar return type is Car
Then my paginated query:
query CarsPaginated($after: Cursor) {
cars(
page: { first: 25, after: $after }
orderBy: { field: CREATE_TIME, direction: DESC }
) {
edges {
node {
...CarSummary
}
}
totalCount
pageInfo {
hasNextPage
endCursor
}
}
}
The CarsPaginated return type is CarsResult:
type CarsResult {
edges: [CarEdge]
pageInfo: PageInfo!
totalCount: Int!
}
type CarEdge {
node: Car
cursor: Cursor!
}
Ideally, I'd like the new item to show up at the top of my items list on the other component.
I've tried to use the "refetchQueries" attribute but the paginated query is not active since the list component is not rendered at that time.
Maybe there's something I need to do in the typePolicies?

Object still frozen when using Immer and NGXS

I am implementing ngxs into an existing codebase.
I have imported Immer and then ngxs bridge in hopes to handle side effects easier.
I've followed every example that I can find through google, I always get:
core.js:6014 ERROR TypeError: Cannot assign to read only property 'savedPrograms' of object '[object Object]'
I've tried using the #ImmutableContext() decorator to accont for this, but I get the exact same error. I also tried using the produce method, but when i give draft.savedPrograms a new value, it throws the error above.
#Action(UserActions.AddProgram)
#ImmutableContext()
public addProgram(ctx: StateContext<UserStateModel>, action) {
ctx.setState(
produce((draft: UserStateModel) => {
draft.user.savedPrograms = action.payload;
})
);
}
The only way i can get this to work is if i use JSON parse/stringify to create a copy of the user and then update the user object.
#Action(UserActions.AddProgram)
public addProgram(ctx: StateContext<UserStateModel>, action) {
const state = produce(draft => {
const copy = JSON.parse(JSON.stringify(draft));
copy.user.savedPrograms.push(action.payload);
draft = copy;
});
ctx.setState(state);
}
I'm not quite sure why ImmutableContext doesn't work out of the box
From immer-adapter documentation:
import { ImmutableContext, ImmutableSelector } from '#ngxs-labs/immer-adapter';
#State<AnimalsStateModel>({
name: 'animals',
defaults: {
zebra: {
food: [],
name: 'zebra'
},
panda: {
food: [],
name: 'panda'
}
}
})
export class AnimalState {
#Selector()
#ImmutableSelector()
public static zebraFood(state: AnimalsStateModel): string[] {
return state.zebra.food.reverse();
}
#Action(Add)
#ImmutableContext()
public add({ setState }: StateContext<AnimalsStateModel>, { payload }: Add): void {
setState((state: AnimalsStateModel) => ({
state.zebra.food.push(payload);
return state;
}));
}
}
As I understand you need to remove produce from your code:
#Action(UserActions.AddProgram)
#ImmutableContext()
public addProgram(ctx: StateContext<UserStateModel>, action) {
ctx.setState(
(draft: UserStateModel) => {
draft.user.savedPrograms = action.payload;
return draft;
}
);
}

How to normalize state in apollo-link-state?

Given a query that returns a response with many levels, for example this query on Github's GraphQL API:
query {
viewer {
starredRepositories(first: 100) {
edges {
node {
repositoryTopics(first: 100) {
edges {
node {
id
topic {
id
name
}
}
}
}
}
}
}
}
}
How can you normalize the topics and store it into a store using apollo-link-state?
{
topics: [Topic]
}
Currently my store is set up as follows:
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { withClientState } from 'apollo-link-state';
const cache = new InMemoryCache();
const store = withClientState({
cache,
defaults: {
topics: [],
},
resolvers: {},
typeDefs: `
type Topic {
id: String!
name: String!
}
type Query {
topics: [Topic]
}
`,
});
const client = new ApolloClient({
cache,
links: ApolloLink.from([
// Other links ... ,
store,
// Other links ... ,
]),
});
Inspecting my cache shows the ROOT_QUERY:
{
topics: { ... },
viewer: User
starredRepositories({"first":100}): StarredRepositoryConnection
...
}
As well as all the entities normalized by apollo-cache-inmemory.
To my understanding normalizing data is completely outside of the scope of Apollo queries or cache. You're going to want to create some sort of helper function to flatten the object as needed once it has been fetched from the cache. Unlike Redux there isn't middleware where an action can be processed on it and stored in cache. At least to my knowledge. I did find graphql_normalizr which might give you what you want though. For me I would stick with simply wrapping the Query in a component with a helper function to run the fetched object through a normalized schema via normalizr https://github.com/paularmstrong/normalizr/issues/108 before being returned.

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

Relay: Optimistic update not causing a component rerender

I'm using PostgraphQL (https://github.com/calebmer/postgraphql) with Relay and wired a UpdateQuestionMutation into my app. However, I do not get optimistic updating to work.(When I enable network throttling in chrome I can see that the the optimistic update gets handled but the component still shows the old title).Do I miss something? I have following pieces :
class QuestionClass extends Component<IQuestion, void> {
save = (item) => {
this.props.relay.commitUpdate(
new UpdateQuestionMutation({store: this.props.store, patch: item})
);
this.isEditing = false;
};
public render(): JSX.Element {
const item = this.props.store;
console.log(item);
...
const Question = Relay.createContainer(QuestionClass, {
fragments: {
// The property name here reflects what is added to `this.props` above.
// This template string will be parsed by babel-relay-plugin.
store: () => Relay.QL`
fragment on Question {
${UpdateQuestionMutation.getFragment('store')}
title
description
userByAuthor {
${User.getFragment('store')}
}
}`,
},
});
...
export default class UpdateQuestionMutation extends Relay.Mutation<any, any> {
getMutation() {
return Relay.QL `mutation { updateQuestion }`
}
getVariables() {
console.log(this.props);
return {
id: this.props.store.id,
questionPatch: this.props.patch
}
}
getFatQuery() {
return Relay.QL `fragment on UpdateQuestionPayload { question }`
}
getConfigs() {
return [{
type: "FIELDS_CHANGE",
fieldIDs: {
question: this.props.store.id
}
}]
}
getOptimisticResponse() {
return {
store: this.props.patch
}
}
// This mutation has a hard dependency on the question's ID. We specify this
// dependency declaratively here as a GraphQL query fragment. Relay will
// use this fragment to ensure that the question's ID is available wherever
// this mutation is used.
static fragments = {
store: () => Relay.QL`
fragment on Question {
id
}
`,
};
}
Edit: That's what I see in the postgraphql logs:
mutation UpdateQuestion($input_0: UpdateQuestionInput!) { updateQuestion(input: $input_0) { clientMutationId ...F1 } } fragment F0 on Question { id rowId title description userByAuthor { id rowId username } } fragment F1 on UpdateQuestionPayload { question { id ...F0 } }

Resources