Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(graphcache) - optimistic support for mutations without selection #657

Merged
merged 5 commits into from
Mar 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/twelve-trainers-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@urql/exchange-graphcache': minor
---

Support optimistic values for mutations without a selectionset
129 changes: 128 additions & 1 deletion exchanges/graphcache/src/cacheExchange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ describe('data dependencies', () => {
expect(result).toHaveBeenCalledTimes(2);
});

it('writes optimistic mutations to the cache', () => {
it('does not reach updater when mutation has no selectionset in optimistic phase', () => {
jest.useFakeTimers();

const mutation = gql`
Expand Down Expand Up @@ -452,6 +452,133 @@ describe('data dependencies', () => {
jest.runAllTimers();
expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(1);
});

it('does reach updater when mutation has no selectionset in optimistic phase with optimistic update', () => {
jest.useFakeTimers();

const mutation = gql`
mutation {
concealAuthor
}
`;

const mutationData = {
__typename: 'Mutation',
concealAuthor: true,
};

const client = createClient({ url: 'http://0.0.0.0' });
const { source: ops$, next } = makeSubject<Operation>();

jest.spyOn(client, 'reexecuteOperation').mockImplementation(next);

const opMutation = client.createRequestOperation('mutation', {
key: 1,
query: mutation,
});

const response = jest.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
return { operation: opMutation, data: mutationData };
}

return undefined as any;
}
);

const result = jest.fn();
const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));

const updates = {
Mutation: {
concealAuthor: jest.fn(),
},
};

const optimistic = {
concealAuthor: jest.fn(() => true) as any,
};

pipe(
cacheExchange({ updates, optimistic })({ forward, client })(ops$),
tap(result),
publish
);

next(opMutation);
expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1);
expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(1);

jest.runAllTimers();
expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(2);
});

it('respects aliases in the optimistic update data that is written', () => {
jest.useFakeTimers();

const mutation = gql`
mutation {
concealed: concealAuthor
}
`;

const mutationData = {
__typename: 'Mutation',
concealed: true,
};

const client = createClient({ url: 'http://0.0.0.0' });
const { source: ops$, next } = makeSubject<Operation>();

jest.spyOn(client, 'reexecuteOperation').mockImplementation(next);

const opMutation = client.createRequestOperation('mutation', {
key: 1,
query: mutation,
});

const response = jest.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
return { operation: opMutation, data: mutationData };
}

return undefined as any;
}
);

const result = jest.fn();
const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response));

const updates = {
Mutation: {
concealAuthor: jest.fn(),
},
};

const optimistic = {
concealAuthor: jest.fn(() => true) as any,
};

pipe(
cacheExchange({ updates, optimistic })({ forward, client })(ops$),
tap(result),
publish
);

next(opMutation);
expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1);
expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(1);

const data = updates.Mutation.concealAuthor.mock.calls[0][0];
// Expect both fields to exist
expect(data.concealed).toBe(true);
expect(data.concealAuthor).toBe(true);

jest.runAllTimers();
expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(2);
});
});

describe('optimistic updates', () => {
Expand Down
41 changes: 21 additions & 20 deletions exchanges/graphcache/src/operations/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ const writeSelection = (
const fieldName = getName(node);
const fieldArgs = getFieldArguments(node, ctx.variables);
const fieldKey = keyOfField(fieldName, fieldArgs);
const fieldValue = data[getFieldAlias(node)];
const fieldAlias = getFieldAlias(node);
let fieldValue = data[fieldAlias];

if (process.env.NODE_ENV !== 'production') {
if (!isRoot && fieldValue === undefined) {
Expand Down Expand Up @@ -219,35 +220,34 @@ const writeSelection = (
}
}

if (node.selectionSet) {
let fieldData: Data | NullArray<Data> | null;
// Process optimistic updates, if this is a `writeOptimistic` operation
// otherwise read the field value from data and write it
if (ctx.optimistic && isRoot) {
const resolver = ctx.store.optimisticMutations[fieldName];
if (!resolver) continue;
// We have to update the context to reflect up-to-date ResolveInfo
updateContext(ctx, typename, typename, fieldKey, fieldName);
fieldData = ensureData(
resolver(fieldArgs || makeDict(), ctx.store, ctx)
);
data[fieldName] = fieldData;
} else {
fieldData = ensureData(fieldValue);
}
if (ctx.optimistic && isRoot) {
const resolver = ctx.store.optimisticMutations[fieldName];
if (!resolver) continue;
// We have to update the context to reflect up-to-date ResolveInfo
updateContext(ctx, typename, typename, fieldKey, fieldName);
fieldValue = data[fieldAlias] = ensureData(
resolver(fieldArgs || makeDict(), ctx.store, ctx)
);
}

if (node.selectionSet) {
// Process the field and write links for the child entities that have been written
if (entityKey && !isRoot) {
const key = joinKeys(entityKey, fieldKey);
const link = writeField(ctx, getSelectionSet(node), fieldData, key);
const link = writeField(
ctx,
getSelectionSet(node),
ensureData(fieldValue),
key
);
InMemoryData.writeLink(entityKey || typename, fieldKey, link);
} else {
writeField(ctx, getSelectionSet(node), fieldData);
writeField(ctx, getSelectionSet(node), ensureData(fieldValue));
}
} else if (entityKey && !isRoot) {
// This is a leaf node, so we're setting the field's value directly
InMemoryData.writeRecord(entityKey || typename, fieldKey, fieldValue);
} else if (ctx.optimistic && isRoot) continue;
}

if (isRoot) {
// We have to update the context to reflect up-to-date ResolveInfo
Expand All @@ -263,6 +263,7 @@ const writeSelection = (
// so that the data is already available in-store if necessary
const updater = ctx.store.updates[typename][fieldName];
if (updater) {
data[fieldName] = fieldValue;
updater(data, fieldArgs || makeDict(), ctx.store, ctx);
}
}
Expand Down