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

feat(graphcache): local directives #3306

Merged
merged 16 commits into from
Jul 21, 2023
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
2 changes: 1 addition & 1 deletion docs/graphcache/cache-updates.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Cache Updates
order: 3
order: 4
---

# Cache Updates
Expand Down
7 changes: 6 additions & 1 deletion docs/graphcache/errors.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Errors
order: 7
order: 8
---

# Help!
Expand Down Expand Up @@ -411,3 +411,8 @@ write to the cache when it's being queried.

Please make sure that you're not calling `cache.updateQuery`,
`cache.writeFragment`, or `cache.link` inside `resolvers`.

## (28) Resolver and directive match the same field

When you have a resolver defined on a field you shouln't be combining it with a directive as the directive
will apply and the resolver will be void.
35 changes: 35 additions & 0 deletions docs/graphcache/local-directives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Local Directives
order: 3
---
JoviDeCroock marked this conversation as resolved.
Show resolved Hide resolved

# Local Directives

Graphcache supports adding directives to GraphQL Documents, when we prefix a
directive with an underscore (`_`) it will be stripped from the document and stored
on the `_directives` property on the AST-node.

> Ensure you prefix directives with `_` if you only want to alter local behavior.

By default graphcache will add two directives `@_optional` and `@_required` which
allow you to mark fields as being optional or mandatory.

If you want to add directives yourself you can do so by performing

```js
cacheExchange({
directives: {
// If you now add `@_pagination` to your document we will execute this
pagination: directiveArguments => () => {
/* Resolver */
},
},
});
```

The function signature of a directive is a function which receives the arguments the directive is called with in the document.
That function should returns a [Resolver](./local-directives.md).

### Reading on

[On the next page we'll learn about "Cache Updates".](./cache-updates.md)
2 changes: 1 addition & 1 deletion docs/graphcache/local-resolvers.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,4 +565,4 @@ cannot be stiched together.

### Reading on

[On the next page we'll learn about "Cache Updates".](./cache-updates.md)
[On the next page we'll learn about "Cache Directives".](./local-directives.md)
2 changes: 1 addition & 1 deletion docs/graphcache/offline.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Offline Support
order: 6
order: 7
---

# Offline Support
Expand Down
8 changes: 4 additions & 4 deletions docs/graphcache/schema-awareness.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Schema Awareness
order: 4
order: 5
---

# Schema Awareness
Expand All @@ -19,9 +19,9 @@ on `cacheExchange` allows us to pass an introspected schema to Graphcache:
```js
const introspectedSchema = {
__schema: {
queryType: { name: 'Query', },
mutationType: { name: 'Mutation', },
subscriptionType: { name: 'Subscription', },
queryType: { name: 'Query' },
mutationType: { name: 'Mutation' },
subscriptionType: { name: 'Subscription' },
},
};

Expand Down
3 changes: 2 additions & 1 deletion exchanges/graphcache/src/ast/variables.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
FieldNode,
DirectiveNode,
OperationDefinitionNode,
valueFromASTUntyped,
} from '@0no-co/graphql.web';
Expand All @@ -10,7 +11,7 @@ import { Variables } from '../types';

/** Evaluates a fields arguments taking vars into account */
export const getFieldArguments = (
node: FieldNode,
node: FieldNode | DirectiveNode,
vars: Variables
): null | Variables => {
let args: null | Variables = null;
Expand Down
142 changes: 142 additions & 0 deletions exchanges/graphcache/src/cacheExchange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OperationResult,
CombinedError,
} from '@urql/core';
import { print, stripIgnoredCharacters } from 'graphql';

import { vi, expect, it, describe } from 'vitest';

Expand Down Expand Up @@ -677,6 +678,147 @@ describe('data dependencies', () => {
});
});

describe('directives', () => {
it('returns optional fields as partial', () => {
const client = createClient({
url: 'http://0.0.0.0',
exchanges: [],
});
const { source: ops$, next } = makeSubject<Operation>();

const query = gql`
{
todos {
id
text
completed @_optional
}
}
`;

const operation = client.createRequestOperation('query', {
key: 1,
query,
variables: undefined,
});

const queryResult: OperationResult = {
...queryResponse,
operation,
data: {
__typename: 'Query',
todos: [
{
id: '1',
text: 'learn urql',
__typename: 'Todo',
},
],
},
};

const reexecuteOperation = vi
.spyOn(client, 'reexecuteOperation')
.mockImplementation(next);

const response = vi.fn((forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) return queryResult;
return undefined as any;
});

const result = vi.fn();
const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);

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

next(operation);

expect(response).toHaveBeenCalledTimes(1);
expect(result).toHaveBeenCalledTimes(1);
expect(reexecuteOperation).toHaveBeenCalledTimes(0);
expect(result.mock.calls[0][0].data).toEqual({
todos: [
{
completed: null,
id: '1',
text: 'learn urql',
},
],
});
});

it('does not return missing required fields', () => {
const client = createClient({
url: 'http://0.0.0.0',
exchanges: [],
});
const { source: ops$, next } = makeSubject<Operation>();

const query = gql`
{
todos {
id
text
completed @_required
}
}
`;

const operation = client.createRequestOperation('query', {
key: 1,
query,
variables: undefined,
});

const queryResult: OperationResult = {
...queryResponse,
operation,
data: {
__typename: 'Query',
todos: [
{
id: '1',
text: 'learn urql',
__typename: 'Todo',
},
],
},
};

const reexecuteOperation = vi
.spyOn(client, 'reexecuteOperation')
.mockImplementation(next);

const response = vi.fn((forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) return queryResult;
return undefined as any;
});

const result = vi.fn();
const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);

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

next(operation);

expect(response).toHaveBeenCalledTimes(1);
expect(result).toHaveBeenCalledTimes(1);
expect(
stripIgnoredCharacters(print(response.mock.calls[0][0].query))
).toEqual('{todos{id text completed __typename}}');
expect(reexecuteOperation).toHaveBeenCalledTimes(0);
expect(result.mock.calls[0][0].data).toEqual(null);
});
});

describe('optimistic updates', () => {
it('writes optimistic mutations to the cache', () => {
vi.useFakeTimers();
Expand Down
Loading