From 262801a5eaf1b66dd9b68e89d26dc320864aea91 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Wed, 5 Jul 2023 14:40:46 +0200 Subject: [PATCH 01/16] graphcache directives --- exchanges/graphcache/src/operations/query.ts | 31 +++++++++++++++----- exchanges/graphcache/src/store/store.ts | 3 ++ exchanges/graphcache/src/types.ts | 7 +++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 96ca957cd7..6155ed7c83 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -356,6 +356,10 @@ const readSelection = ( let node: FormattedNode | void; const output = InMemoryData.makeData(input); while ((node = iterate()) !== undefined) { + const fieldDirectives = node.directives?.map(x => x.name.value); + + // TODO: strip out the directive so we don't send it to origin + const storeDirective = fieldDirectives?.find(x => store.directives[x]); // Derive the needed data from our node. const fieldName = getName(node); const fieldArgs = getFieldArguments(node, ctx.variables); @@ -383,8 +387,7 @@ const readSelection = ( dataFieldValue = resultValue; } else if ( InMemoryData.currentOperation === 'read' && - resolvers && - resolvers[fieldName] + ((resolvers && resolvers[fieldName]) || storeDirective) ) { // We have to update the information in context to reflect the info // that the resolver will receive @@ -396,12 +399,23 @@ const readSelection = ( output[fieldAlias] = fieldValue; } - dataFieldValue = resolvers[fieldName]!( - output, - fieldArgs || ({} as Variables), - store, - ctx - ); + if (resolvers[fieldName]) { + dataFieldValue = resolvers[fieldName]!( + output, + fieldArgs || ({} as Variables), + store, + ctx + ); + } + + if (storeDirective) { + dataFieldValue = store.directives[storeDirective]!( + output, + fieldArgs || ({} as Variables), + store, + ctx + ); + } if (node.selectionSet) { // When it has a selection set we are resolving an entity with a @@ -423,6 +437,7 @@ const readSelection = ( if ( store.schema && dataFieldValue === null && + // TODO: how would we inform this that we are indeed dealing with a nullable field !isFieldNullable(store.schema, typename, fieldName) ) { // Special case for when null is not a valid value for the diff --git a/exchanges/graphcache/src/store/store.ts b/exchanges/graphcache/src/store/store.ts index ccf9a66784..9698cd79a4 100644 --- a/exchanges/graphcache/src/store/store.ts +++ b/exchanges/graphcache/src/store/store.ts @@ -15,6 +15,7 @@ import { KeyingConfig, Entity, CacheExchangeOpts, + DirectivesConfig, } from '../types'; import { invariant } from '../helpers/help'; @@ -46,6 +47,7 @@ export class Store< { data: InMemoryData.InMemoryData; + directives: DirectivesConfig; resolvers: ResolverConfig; updates: UpdatesConfig; optimisticMutations: OptimisticMutationConfig; @@ -60,6 +62,7 @@ export class Store< if (!opts) opts = {} as C; this.resolvers = opts.resolvers || {}; + this.directives = opts.directives || {}; this.optimisticMutations = opts.optimistic || {}; this.keys = opts.keys || {}; diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts index 9446da1923..865b0d4cdf 100644 --- a/exchanges/graphcache/src/types.ts +++ b/exchanges/graphcache/src/types.ts @@ -555,6 +555,8 @@ export type CacheExchangeOpts = { * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers} for the full resolvers docs. */ resolvers?: ResolverConfig; + // TODO: docs + directives?: DirectivesConfig; /** Configures optimistic updates to react to mutations instantly before an API response. * * @remarks @@ -693,6 +695,11 @@ export type ResolverConfig = { } | void; }; +// TODO: docs +export type DirectivesConfig = { + [directiveName: string]: Resolver | void; +}; + /** Cache Updater, which defines additional cache updates after cache writes. * * @param parent - The GraphQL object that is currently being written to the cache. From 9db26aec19b4d3e353ec9558c44c5cd9d7f4c45f Mon Sep 17 00:00:00 2001 From: jdecroock Date: Wed, 5 Jul 2023 15:16:48 +0200 Subject: [PATCH 02/16] defaults --- exchanges/graphcache/src/ast/variables.ts | 3 ++- exchanges/graphcache/src/cacheExchange.ts | 1 + exchanges/graphcache/src/operations/query.ts | 10 +++++++--- exchanges/graphcache/src/store/store.ts | 14 +++++++++++++- exchanges/graphcache/src/types.ts | 2 +- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/exchanges/graphcache/src/ast/variables.ts b/exchanges/graphcache/src/ast/variables.ts index 28b72486ab..bac920c00c 100644 --- a/exchanges/graphcache/src/ast/variables.ts +++ b/exchanges/graphcache/src/ast/variables.ts @@ -1,5 +1,6 @@ import { FieldNode, + DirectiveNode, OperationDefinitionNode, valueFromASTUntyped, } from '@0no-co/graphql.web'; @@ -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; diff --git a/exchanges/graphcache/src/cacheExchange.ts b/exchanges/graphcache/src/cacheExchange.ts index 7d0594e135..b17fbffb88 100644 --- a/exchanges/graphcache/src/cacheExchange.ts +++ b/exchanges/graphcache/src/cacheExchange.ts @@ -237,6 +237,7 @@ export const cacheExchange = operations.set(operation.key, operation); updateDependencies(operation, result.dependencies); + // TODO: remove directives before sending it on return { outcome: cacheOutcome, operation, diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 6155ed7c83..c7908c70fb 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -357,9 +357,8 @@ const readSelection = ( const output = InMemoryData.makeData(input); while ((node = iterate()) !== undefined) { const fieldDirectives = node.directives?.map(x => x.name.value); - - // TODO: strip out the directive so we don't send it to origin const storeDirective = fieldDirectives?.find(x => store.directives[x]); + // Derive the needed data from our node. const fieldName = getName(node); const fieldArgs = getFieldArguments(node, ctx.variables); @@ -409,9 +408,14 @@ const readSelection = ( } if (storeDirective) { + const fieldDirective = node.directives!.find( + x => x.name.value === storeDirective + )!; + const directiveArguments = + getFieldArguments(fieldDirective, ctx.variables) || {}; dataFieldValue = store.directives[storeDirective]!( output, - fieldArgs || ({} as Variables), + { ...(fieldArgs || ({} as Variables)), ...directiveArguments }, store, ctx ); diff --git a/exchanges/graphcache/src/store/store.ts b/exchanges/graphcache/src/store/store.ts index 9698cd79a4..db27dfe774 100644 --- a/exchanges/graphcache/src/store/store.ts +++ b/exchanges/graphcache/src/store/store.ts @@ -38,6 +38,17 @@ import { type DocumentNode = TypedDocumentNode; type RootField = 'query' | 'mutation' | 'subscription'; +const defaultDirectives: DirectivesConfig = { + optional: (_parent, args, cache, info) => { + const result = cache.resolve(info.parentFieldKey, info.fieldName, args); + return result === undefined ? null : result; + }, + required: (_parent, args, cache, info) => { + const result = cache.resolve(info.parentFieldKey, info.fieldName, args); + return result === null ? undefined : result; + }, +}; + /** Implementation of the {@link Cache} interface as created internally by the {@link cacheExchange}. * @internal */ @@ -62,7 +73,8 @@ export class Store< if (!opts) opts = {} as C; this.resolvers = opts.resolvers || {}; - this.directives = opts.directives || {}; + this.directives = + { ...defaultDirectives, ...opts.directives } || defaultDirectives; this.optimisticMutations = opts.optimistic || {}; this.keys = opts.keys || {}; diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts index 865b0d4cdf..86e3649fc7 100644 --- a/exchanges/graphcache/src/types.ts +++ b/exchanges/graphcache/src/types.ts @@ -697,7 +697,7 @@ export type ResolverConfig = { // TODO: docs export type DirectivesConfig = { - [directiveName: string]: Resolver | void; + [directiveName: string]: Resolver; }; /** Cache Updater, which defines additional cache updates after cache writes. From 74ca95b72a57cd3469a8829723dc1a1ea046c4be Mon Sep 17 00:00:00 2001 From: jdecroock Date: Thu, 6 Jul 2023 08:24:35 +0200 Subject: [PATCH 03/16] add test --- .../graphcache/src/cacheExchange.test.ts | 138 ++++++++++++++++++ exchanges/graphcache/src/operations/query.ts | 2 +- 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/exchanges/graphcache/src/cacheExchange.test.ts b/exchanges/graphcache/src/cacheExchange.test.ts index e2d16a5463..e0953991d7 100644 --- a/exchanges/graphcache/src/cacheExchange.test.ts +++ b/exchanges/graphcache/src/cacheExchange.test.ts @@ -677,6 +677,144 @@ 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(); + + 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(); + + 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(reexecuteOperation).toHaveBeenCalledTimes(0); + expect(result.mock.calls[0][0].data).toEqual(null); + }); +}); + describe('optimistic updates', () => { it('writes optimistic mutations to the cache', () => { vi.useFakeTimers(); diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index c7908c70fb..74eeca7b6d 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -398,7 +398,7 @@ const readSelection = ( output[fieldAlias] = fieldValue; } - if (resolvers[fieldName]) { + if (resolvers && resolvers[fieldName]) { dataFieldValue = resolvers[fieldName]!( output, fieldArgs || ({} as Variables), From bccf764aafb5cfa93491489b30225e956a38e9b1 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Thu, 6 Jul 2023 08:28:58 +0200 Subject: [PATCH 04/16] add warning --- exchanges/graphcache/src/helpers/help.ts | 3 ++- exchanges/graphcache/src/operations/query.ts | 11 +++++++++-- exchanges/graphcache/src/types.ts | 15 ++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/exchanges/graphcache/src/helpers/help.ts b/exchanges/graphcache/src/helpers/help.ts index 763665369d..17a02f2376 100644 --- a/exchanges/graphcache/src/helpers/help.ts +++ b/exchanges/graphcache/src/helpers/help.ts @@ -36,7 +36,8 @@ export type ErrorCode = | 24 | 25 | 26 - | 27; + | 27 + | 28; type DebugNode = ExecutableDefinitionNode | InlineFragmentNode; diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 74eeca7b6d..6c30f19d79 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -398,6 +398,13 @@ const readSelection = ( output[fieldAlias] = fieldValue; } + if (resolvers && resolvers[fieldName] && storeDirective) { + warn( + `A resolver and directive is being used at "${typename}.${fieldName}", only the directive will apply.`, + 28 + ); + } + if (resolvers && resolvers[fieldName]) { dataFieldValue = resolvers[fieldName]!( output, @@ -415,9 +422,9 @@ const readSelection = ( getFieldArguments(fieldDirective, ctx.variables) || {}; dataFieldValue = store.directives[storeDirective]!( output, - { ...(fieldArgs || ({} as Variables)), ...directiveArguments }, + fieldArgs || ({} as Variables), store, - ctx + { ...ctx, directiveArguments } ); } diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts index 86e3649fc7..f528a8af15 100644 --- a/exchanges/graphcache/src/types.ts +++ b/exchanges/graphcache/src/types.ts @@ -678,6 +678,19 @@ export type Resolver< ): Result; }['bivarianceHack']; +export type Directive< + ParentData = DataFields, + Args = Variables, + Result = ResolverResult +> = { + bivarianceHack( + parent: ParentData, + args: Args, + cache: Cache, + info: ResolveInfo & { directiveArguments: Record } + ): Result; +}['bivarianceHack']; + /** Configures resolvers which replace cached reuslts with custom values. * * @remarks @@ -697,7 +710,7 @@ export type ResolverConfig = { // TODO: docs export type DirectivesConfig = { - [directiveName: string]: Resolver; + [directiveName: string]: Directive; }; /** Cache Updater, which defines additional cache updates after cache writes. From 20e326c83dac34f51f24793841556faa27aa8f43 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 7 Jul 2023 09:33:57 +0200 Subject: [PATCH 05/16] try removing directives in formatDocuemnt --- exchanges/graphcache/src/cacheExchange.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/exchanges/graphcache/src/cacheExchange.test.ts b/exchanges/graphcache/src/cacheExchange.test.ts index e0953991d7..82777f224b 100644 --- a/exchanges/graphcache/src/cacheExchange.test.ts +++ b/exchanges/graphcache/src/cacheExchange.test.ts @@ -6,6 +6,7 @@ import { OperationResult, CombinedError, } from '@urql/core'; +import { print, stripIgnoredCharacters } from 'graphql'; import { vi, expect, it, describe } from 'vitest'; @@ -810,6 +811,9 @@ describe('directives', () => { 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); }); From 8a4396e48199c48a4536881d824f53dfaa511414 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 7 Jul 2023 15:12:59 +0200 Subject: [PATCH 06/16] add tests --- .../src/extras/relayPagination.test.ts | 2484 +++++++++-------- .../src/extras/simplePagination.test.ts | 844 +++--- 2 files changed, 1771 insertions(+), 1557 deletions(-) diff --git a/exchanges/graphcache/src/extras/relayPagination.test.ts b/exchanges/graphcache/src/extras/relayPagination.test.ts index f6f4fdaa64..29a1486f42 100644 --- a/exchanges/graphcache/src/extras/relayPagination.test.ts +++ b/exchanges/graphcache/src/extras/relayPagination.test.ts @@ -1,5 +1,5 @@ import { gql } from '@urql/core'; -import { it, expect } from 'vitest'; +import { it, expect, describe } from 'vitest'; import { __initAnd_query as query } from '../operations/query'; import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store/store'; @@ -19,1458 +19,1598 @@ function itemEdge(numItem: number) { }; } -it('works with forward pagination', () => { - const Pagination = gql` - query ($cursor: String) { - __typename - items(first: 1, after: $cursor) { +describe('as resolver', () => { + it('works with forward pagination', () => { + const Pagination = gql` + query ($cursor: String) { __typename - edges { + items(first: 1, after: $cursor) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasNextPage - endCursor + pageInfo { + __typename + hasNextPage + endCursor + } } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination(), + const store = new Store({ + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); + }); - const pageOne = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1)], - nodes: [itemNode(1)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - endCursor: '1', + const pageOne = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(1)], + nodes: [itemNode(1)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + endCursor: '1', + }, }, - }, - }; + }; - const pageTwo = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(2)], - nodes: [itemNode(2)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: false, - endCursor: null, + const pageTwo = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(2)], + nodes: [itemNode(2)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + endCursor: null, + }, }, - }, - }; + }; - write(store, { query: Pagination, variables: { cursor: null } }, pageOne); - write(store, { query: Pagination, variables: { cursor: '1' } }, pageTwo); + write(store, { query: Pagination, variables: { cursor: null } }, pageOne); + write(store, { query: Pagination, variables: { cursor: '1' } }, pageTwo); - const res = query(store, { query: Pagination }); + const res = query(store, { query: Pagination }); - expect(res.partial).toBe(false); - expect(res.data).toEqual({ - ...pageTwo, - items: { - ...pageTwo.items, - edges: [pageOne.items.edges[0], pageTwo.items.edges[0]], - nodes: [pageOne.items.nodes[0], pageTwo.items.nodes[0]], - }, + expect(res.partial).toBe(false); + expect(res.data).toEqual({ + ...pageTwo, + items: { + ...pageTwo.items, + edges: [pageOne.items.edges[0], pageTwo.items.edges[0]], + nodes: [pageOne.items.nodes[0], pageTwo.items.nodes[0]], + }, + }); }); -}); -it('works with backwards pagination', () => { - const Pagination = gql` - query ($cursor: String) { - __typename - items(last: 1, before: $cursor) { + it('works with backwards pagination', () => { + const Pagination = gql` + query ($cursor: String) { __typename - edges { + items(last: 1, before: $cursor) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasPreviousPage - startCursor + pageInfo { + __typename + hasPreviousPage + startCursor + } } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination(), + const store = new Store({ + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); + }); - const pageOne = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(2)], - nodes: [itemNode(2)], - pageInfo: { - __typename: 'PageInfo', - hasPreviousPage: true, - startCursor: '2', + const pageOne = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(2)], + nodes: [itemNode(2)], + pageInfo: { + __typename: 'PageInfo', + hasPreviousPage: true, + startCursor: '2', + }, }, - }, - }; + }; - const pageTwo = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1)], - nodes: [itemNode(1)], - pageInfo: { - __typename: 'PageInfo', - hasPreviousPage: false, - startCursor: null, + const pageTwo = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(1)], + nodes: [itemNode(1)], + pageInfo: { + __typename: 'PageInfo', + hasPreviousPage: false, + startCursor: null, + }, }, - }, - }; + }; - write(store, { query: Pagination, variables: { cursor: null } }, pageOne); - write(store, { query: Pagination, variables: { cursor: '2' } }, pageTwo); + write(store, { query: Pagination, variables: { cursor: null } }, pageOne); + write(store, { query: Pagination, variables: { cursor: '2' } }, pageTwo); - const res = query(store, { query: Pagination }); + const res = query(store, { query: Pagination }); - expect(res.partial).toBe(false); - expect(res.data).toEqual({ - ...pageTwo, - items: { - ...pageTwo.items, - edges: [pageTwo.items.edges[0], pageOne.items.edges[0]], - nodes: [pageTwo.items.nodes[0], pageOne.items.nodes[0]], - }, + expect(res.partial).toBe(false); + expect(res.data).toEqual({ + ...pageTwo, + items: { + ...pageTwo.items, + edges: [pageTwo.items.edges[0], pageOne.items.edges[0]], + nodes: [pageTwo.items.nodes[0], pageOne.items.nodes[0]], + }, + }); }); -}); -it('handles duplicate edges', () => { - const Pagination = gql` - query ($cursor: String) { - __typename - items(first: 2, after: $cursor) { + it('handles duplicate edges', () => { + const Pagination = gql` + query ($cursor: String) { __typename - edges { + items(first: 2, after: $cursor) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasNextPage - endCursor + pageInfo { + __typename + hasNextPage + endCursor + } } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination(), + const store = new Store({ + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); + }); - const pageOne = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1), itemEdge(2)], - nodes: [itemNode(1), itemNode(2)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - endCursor: '2', + const pageOne = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(1), itemEdge(2)], + nodes: [itemNode(1), itemNode(2)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + endCursor: '2', + }, }, - }, - }; + }; - const pageTwo = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(2), itemEdge(3)], - nodes: [itemNode(2), itemNode(3)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: false, - endCursor: null, + const pageTwo = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(2), itemEdge(3)], + nodes: [itemNode(2), itemNode(3)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + endCursor: null, + }, }, - }, - }; + }; + + write(store, { query: Pagination, variables: { cursor: null } }, pageOne); + write(store, { query: Pagination, variables: { cursor: '1' } }, pageTwo); + + const res = query(store, { query: Pagination }); - write(store, { query: Pagination, variables: { cursor: null } }, pageOne); - write(store, { query: Pagination, variables: { cursor: '1' } }, pageTwo); - - const res = query(store, { query: Pagination }); - - expect(res.partial).toBe(false); - expect(res.data).toEqual({ - ...pageTwo, - items: { - ...pageTwo.items, - edges: [ - pageOne.items.edges[0], - pageTwo.items.edges[0], - pageTwo.items.edges[1], - ], - nodes: [ - pageOne.items.nodes[0], - pageTwo.items.nodes[0], - pageTwo.items.nodes[1], - ], - }, + expect(res.partial).toBe(false); + expect(res.data).toEqual({ + ...pageTwo, + items: { + ...pageTwo.items, + edges: [ + pageOne.items.edges[0], + pageTwo.items.edges[0], + pageTwo.items.edges[1], + ], + nodes: [ + pageOne.items.nodes[0], + pageTwo.items.nodes[0], + pageTwo.items.nodes[1], + ], + }, + }); }); -}); -it('works with simultaneous forward and backward pagination (outwards merging)', () => { - const Pagination = gql` - query ($first: Int, $last: Int, $before: String, $after: String) { - __typename - items(first: $first, last: $last, before: $before, after: $after) { + it('works with simultaneous forward and backward pagination (outwards merging)', () => { + const Pagination = gql` + query ($first: Int, $last: Int, $before: String, $after: String) { __typename - edges { + items(first: $first, last: $last, before: $before, after: $after) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasPreviousPage - hasNextPage - startCursor - endCursor + pageInfo { + __typename + hasPreviousPage + hasNextPage + startCursor + endCursor + } } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination({ mergeMode: 'outwards' }), + const store = new Store({ + resolvers: { + Query: { + items: relayPagination({ mergeMode: 'outwards' }), + }, }, - }, - }); + }); - const pageOne = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1)], - nodes: [itemNode(1)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - hasPreviousPage: false, - startCursor: null, - endCursor: '1', + const pageOne = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(1)], + nodes: [itemNode(1)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: false, + startCursor: null, + endCursor: '1', + }, }, - }, - }; + }; - const pageTwo = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(2)], - nodes: [itemNode(2)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - hasPreviousPage: true, - startCursor: '2', - endCursor: '2', + const pageTwo = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(2)], + nodes: [itemNode(2)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: true, + startCursor: '2', + endCursor: '2', + }, }, - }, - }; + }; - const pageThree = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(-1)], - nodes: [itemNode(-1)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: false, - hasPreviousPage: true, - startCursor: '-1', - endCursor: null, + const pageThree = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(-1)], + nodes: [itemNode(-1)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + hasPreviousPage: true, + startCursor: '-1', + endCursor: null, + }, }, - }, - }; - - write( - store, - { query: Pagination, variables: { after: '1', first: 1 } }, - pageOne - ); - write( - store, - { query: Pagination, variables: { after: '2', first: 1 } }, - pageTwo - ); - write( - store, - { query: Pagination, variables: { before: '1', last: 1 } }, - pageThree - ); - - const res = query(store, { - query: Pagination, - variables: { before: '1', last: 1 }, - }); - - expect(res.partial).toBe(false); - expect(res.data).toEqual({ - ...pageThree, - items: { - ...pageThree.items, - edges: [ - pageThree.items.edges[0], - pageOne.items.edges[0], - pageTwo.items.edges[0], - ], - nodes: [ - pageThree.items.nodes[0], - pageOne.items.nodes[0], - pageTwo.items.nodes[0], - ], - pageInfo: { - ...pageThree.items.pageInfo, - hasPreviousPage: true, - hasNextPage: true, - startCursor: '-1', - endCursor: '2', + }; + + write( + store, + { query: Pagination, variables: { after: '1', first: 1 } }, + pageOne + ); + write( + store, + { query: Pagination, variables: { after: '2', first: 1 } }, + pageTwo + ); + write( + store, + { query: Pagination, variables: { before: '1', last: 1 } }, + pageThree + ); + + const res = query(store, { + query: Pagination, + variables: { before: '1', last: 1 }, + }); + + expect(res.partial).toBe(false); + expect(res.data).toEqual({ + ...pageThree, + items: { + ...pageThree.items, + edges: [ + pageThree.items.edges[0], + pageOne.items.edges[0], + pageTwo.items.edges[0], + ], + nodes: [ + pageThree.items.nodes[0], + pageOne.items.nodes[0], + pageTwo.items.nodes[0], + ], + pageInfo: { + ...pageThree.items.pageInfo, + hasPreviousPage: true, + hasNextPage: true, + startCursor: '-1', + endCursor: '2', + }, }, - }, + }); }); -}); -it('works with simultaneous forward and backward pagination (inwards merging)', () => { - const Pagination = gql` - query ($first: Int, $last: Int, $before: String, $after: String) { - __typename - items(first: $first, last: $last, before: $before, after: $after) { + it('works with simultaneous forward and backward pagination (inwards merging)', () => { + const Pagination = gql` + query ($first: Int, $last: Int, $before: String, $after: String) { __typename - edges { + items(first: $first, last: $last, before: $before, after: $after) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasPreviousPage - hasNextPage - startCursor - endCursor + pageInfo { + __typename + hasPreviousPage + hasNextPage + startCursor + endCursor + } } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination({ mergeMode: 'inwards' }), + const store = new Store({ + resolvers: { + Query: { + items: relayPagination({ mergeMode: 'inwards' }), + }, }, - }, - }); + }); - const pageOne = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1)], - nodes: [itemNode(1)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - hasPreviousPage: false, - startCursor: null, - endCursor: '1', + const pageOne = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(1)], + nodes: [itemNode(1)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: false, + startCursor: null, + endCursor: '1', + }, }, - }, - }; + }; - const pageTwo = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(2)], - nodes: [itemNode(2)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - hasPreviousPage: true, - startCursor: '2', - endCursor: '2', + const pageTwo = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(2)], + nodes: [itemNode(2)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: true, + startCursor: '2', + endCursor: '2', + }, }, - }, - }; + }; - const pageThree = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(-1)], - nodes: [itemNode(-1)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: false, - hasPreviousPage: true, - startCursor: '-1', - endCursor: null, + const pageThree = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(-1)], + nodes: [itemNode(-1)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + hasPreviousPage: true, + startCursor: '-1', + endCursor: null, + }, }, - }, - }; - - write( - store, - { query: Pagination, variables: { after: '1', first: 1 } }, - pageOne - ); - write( - store, - { query: Pagination, variables: { after: '2', first: 1 } }, - pageTwo - ); - write( - store, - { query: Pagination, variables: { before: '1', last: 1 } }, - pageThree - ); - - const res = query(store, { - query: Pagination, - variables: { before: '1', last: 1 }, - }); - - expect(res.partial).toBe(false); - expect(res.data).toEqual({ - ...pageThree, - items: { - ...pageThree.items, - edges: [ - pageOne.items.edges[0], - pageTwo.items.edges[0], - pageThree.items.edges[0], - ], - nodes: [ - pageOne.items.nodes[0], - pageTwo.items.nodes[0], - pageThree.items.nodes[0], - ], - pageInfo: { - ...pageThree.items.pageInfo, - hasPreviousPage: true, - hasNextPage: true, - startCursor: '-1', - endCursor: '2', + }; + + write( + store, + { query: Pagination, variables: { after: '1', first: 1 } }, + pageOne + ); + write( + store, + { query: Pagination, variables: { after: '2', first: 1 } }, + pageTwo + ); + write( + store, + { query: Pagination, variables: { before: '1', last: 1 } }, + pageThree + ); + + const res = query(store, { + query: Pagination, + variables: { before: '1', last: 1 }, + }); + + expect(res.partial).toBe(false); + expect(res.data).toEqual({ + ...pageThree, + items: { + ...pageThree.items, + edges: [ + pageOne.items.edges[0], + pageTwo.items.edges[0], + pageThree.items.edges[0], + ], + nodes: [ + pageOne.items.nodes[0], + pageTwo.items.nodes[0], + pageThree.items.nodes[0], + ], + pageInfo: { + ...pageThree.items.pageInfo, + hasPreviousPage: true, + hasNextPage: true, + startCursor: '-1', + endCursor: '2', + }, }, - }, + }); }); -}); -it('prevents overlapping of pagination on different arguments', () => { - const Pagination = gql` - query ($filter: String) { - items(first: 1, filter: $filter) { - __typename - edges { + it('prevents overlapping of pagination on different arguments', () => { + const Pagination = gql` + query ($filter: String) { + items(first: 1, filter: $filter) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasNextPage - endCursor + pageInfo { + __typename + hasNextPage + endCursor + } } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination(), + const store = new Store({ + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); + }); - const page = withId => ({ - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(withId)], - nodes: [itemNode(withId)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: false, - endCursor: null, + const page = withId => ({ + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(withId)], + nodes: [itemNode(withId)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + endCursor: null, + }, }, - }, - }); - - write( - store, - { query: Pagination, variables: { filter: 'one' } }, - page('one') - ); - write( - store, - { query: Pagination, variables: { filter: 'two' } }, - page('two') - ); - - const resOne = query(store, { - query: Pagination, - variables: { filter: 'one' }, + }); + + write( + store, + { query: Pagination, variables: { filter: 'one' } }, + page('one') + ); + write( + store, + { query: Pagination, variables: { filter: 'two' } }, + page('two') + ); + + const resOne = query(store, { + query: Pagination, + variables: { filter: 'one' }, + }); + const resTwo = query(store, { + query: Pagination, + variables: { filter: 'two' }, + }); + const resThree = query(store, { + query: Pagination, + variables: { filter: 'three' }, + }); + + expect(resOne.data).toHaveProperty('items.edges[0].node.id', 'one'); + expect(resOne.data).toHaveProperty('items.edges.length', 1); + + expect(resTwo.data).toHaveProperty('items.edges[0].node.id', 'two'); + expect(resTwo.data).toHaveProperty('items.edges.length', 1); + + expect(resThree.data).toEqual(null); }); - const resTwo = query(store, { - query: Pagination, - variables: { filter: 'two' }, - }); - const resThree = query(store, { - query: Pagination, - variables: { filter: 'three' }, - }); - - expect(resOne.data).toHaveProperty('items.edges[0].node.id', 'one'); - expect(resOne.data).toHaveProperty('items.edges.length', 1); - - expect(resTwo.data).toHaveProperty('items.edges[0].node.id', 'two'); - expect(resTwo.data).toHaveProperty('items.edges.length', 1); - expect(resThree.data).toEqual(null); -}); - -it('returns an empty array of edges when the cache has zero edges stored', () => { - const Pagination = gql` - query { - items(first: 1) { - __typename - edges { - __typename - } - nodes { + it('returns an empty array of edges when the cache has zero edges stored', () => { + const Pagination = gql` + query { + items(first: 1) { __typename + edges { + __typename + } + nodes { + __typename + } } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination(), + const store = new Store({ + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); - - write( - store, - { query: Pagination }, - { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [], - nodes: [], - }, - } - ); + }); + + write( + store, + { query: Pagination }, + { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [], + nodes: [], + }, + } + ); - const res = query(store, { - query: Pagination, - }); + const res = query(store, { + query: Pagination, + }); - expect(res.data).toHaveProperty('items', { - __typename: 'ItemsConnection', - edges: [], - nodes: [], + expect(res.data).toHaveProperty('items', { + __typename: 'ItemsConnection', + edges: [], + nodes: [], + }); }); -}); -it('returns other fields on the same level as the edges', () => { - const Pagination = gql` - query { - __typename - items(first: 1) { + it('returns other fields on the same level as the edges', () => { + const Pagination = gql` + query { __typename - totalCount + items(first: 1) { + __typename + totalCount + } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination(), + const store = new Store({ + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); - - write( - store, - { query: Pagination }, - { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - totalCount: 2, - }, - } - ); + }); + + write( + store, + { query: Pagination }, + { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + totalCount: 2, + }, + } + ); - const resOne = query(store, { - query: Pagination, - }); + const resOne = query(store, { + query: Pagination, + }); - expect(resOne.data).toHaveProperty('items', { - __typename: 'ItemsConnection', - totalCount: 2, + expect(resOne.data).toHaveProperty('items', { + __typename: 'ItemsConnection', + totalCount: 2, + }); }); -}); -it('returns a subset of the cached items if the query requests less items than the cached ones', () => { - const Pagination = gql` - query ($first: Int, $last: Int, $before: String, $after: String) { - __typename - items(first: $first, last: $last, before: $before, after: $after) { + it('returns a subset of the cached items if the query requests less items than the cached ones', () => { + const Pagination = gql` + query ($first: Int, $last: Int, $before: String, $after: String) { __typename - edges { + items(first: $first, last: $last, before: $before, after: $after) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasPreviousPage - hasNextPage - startCursor - endCursor + pageInfo { + __typename + hasPreviousPage + hasNextPage + startCursor + endCursor + } } } - } - `; - - const store = new Store({ - schema: require('../test-utils/relayPagination_schema.json'), - resolvers: { - Query: { - items: relayPagination({ mergeMode: 'outwards' }), + `; + + const store = new Store({ + schema: require('../test-utils/relayPagination_schema.json'), + resolvers: { + Query: { + items: relayPagination({ mergeMode: 'outwards' }), + }, }, - }, - }); + }); - const results = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1), itemEdge(2), itemEdge(3), itemEdge(4), itemEdge(5)], - nodes: [itemNode(1), itemNode(2), itemNode(3), itemNode(4), itemNode(5)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - hasPreviousPage: false, - startCursor: '1', - endCursor: '5', + const results = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [ + itemEdge(1), + itemEdge(2), + itemEdge(3), + itemEdge(4), + itemEdge(5), + ], + nodes: [ + itemNode(1), + itemNode(2), + itemNode(3), + itemNode(4), + itemNode(5), + ], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: false, + startCursor: '1', + endCursor: '5', + }, }, - }, - }; + }; - write(store, { query: Pagination, variables: { first: 2 } }, results); + write(store, { query: Pagination, variables: { first: 2 } }, results); - const res = query(store, { - query: Pagination, - variables: { first: 2 }, - }); + const res = query(store, { + query: Pagination, + variables: { first: 2 }, + }); - expect(res.partial).toBe(false); - expect(res.data).toEqual(results); -}); + expect(res.partial).toBe(false); + expect(res.data).toEqual(results); + }); -it("returns the cached items even if they don't fullfil the query", () => { - const Pagination = gql` - query ($first: Int, $last: Int, $before: String, $after: String) { - __typename - items(first: $first, last: $last, before: $before, after: $after) { + it("returns the cached items even if they don't fullfil the query", () => { + const Pagination = gql` + query ($first: Int, $last: Int, $before: String, $after: String) { __typename - edges { + items(first: $first, last: $last, before: $before, after: $after) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasPreviousPage - hasNextPage - startCursor - endCursor + pageInfo { + __typename + hasPreviousPage + hasNextPage + startCursor + endCursor + } } } - } - `; - - const store = new Store({ - schema: require('../test-utils/relayPagination_schema.json'), - resolvers: { - Query: { - items: relayPagination(), + `; + + const store = new Store({ + schema: require('../test-utils/relayPagination_schema.json'), + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); + }); - const results = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1), itemEdge(2), itemEdge(3), itemEdge(4), itemEdge(5)], - nodes: [itemNode(1), itemNode(2), itemNode(3), itemNode(4), itemNode(5)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - hasPreviousPage: false, - startCursor: '1', - endCursor: '5', + const results = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [ + itemEdge(1), + itemEdge(2), + itemEdge(3), + itemEdge(4), + itemEdge(5), + ], + nodes: [ + itemNode(1), + itemNode(2), + itemNode(3), + itemNode(4), + itemNode(5), + ], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: false, + startCursor: '1', + endCursor: '5', + }, }, - }, - }; + }; - write( - store, - { query: Pagination, variables: { after: '3', first: 3, last: 3 } }, - results - ); + write( + store, + { query: Pagination, variables: { after: '3', first: 3, last: 3 } }, + results + ); - const res = query(store, { - query: Pagination, - variables: { after: '3', first: 3, last: 3 }, - }); + const res = query(store, { + query: Pagination, + variables: { after: '3', first: 3, last: 3 }, + }); - expect(res.partial).toBe(false); - expect(res.data).toEqual(results); -}); + expect(res.partial).toBe(false); + expect(res.data).toEqual(results); + }); -it('returns the cached items even when they come from a different query', () => { - const Pagination = gql` - query ($first: Int, $last: Int, $before: String, $after: String) { - __typename - items(first: $first, last: $last, before: $before, after: $after) { + it('returns the cached items even when they come from a different query', () => { + const Pagination = gql` + query ($first: Int, $last: Int, $before: String, $after: String) { __typename - edges { + items(first: $first, last: $last, before: $before, after: $after) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasPreviousPage - hasNextPage - startCursor - endCursor + pageInfo { + __typename + hasPreviousPage + hasNextPage + startCursor + endCursor + } } } - } - `; - - const store = new Store({ - schema: require('../test-utils/relayPagination_schema.json'), - resolvers: { - Query: { - items: relayPagination(), + `; + + const store = new Store({ + schema: require('../test-utils/relayPagination_schema.json'), + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); + }); - const results = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1), itemEdge(2), itemEdge(3), itemEdge(4), itemEdge(5)], - nodes: [itemNode(1), itemNode(2), itemNode(3), itemNode(4), itemNode(5)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - hasPreviousPage: false, - startCursor: '1', - endCursor: '5', + const results = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [ + itemEdge(1), + itemEdge(2), + itemEdge(3), + itemEdge(4), + itemEdge(5), + ], + nodes: [ + itemNode(1), + itemNode(2), + itemNode(3), + itemNode(4), + itemNode(5), + ], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: false, + startCursor: '1', + endCursor: '5', + }, }, - }, - }; + }; - write(store, { query: Pagination, variables: { first: 5 } }, results); + write(store, { query: Pagination, variables: { first: 5 } }, results); - const res = query(store, { - query: Pagination, - variables: { after: '3', first: 2, last: 2 }, - }); + const res = query(store, { + query: Pagination, + variables: { after: '3', first: 2, last: 2 }, + }); - expect(res.partial).toBe(true); - expect(res.data).toEqual(results); -}); + expect(res.partial).toBe(true); + expect(res.data).toEqual(results); + }); -it('caches and retrieves correctly queries with inwards pagination', () => { - const Pagination = gql` - query ($first: Int, $last: Int, $before: String, $after: String) { - __typename - items(first: $first, last: $last, before: $before, after: $after) { + it('caches and retrieves correctly queries with inwards pagination', () => { + const Pagination = gql` + query ($first: Int, $last: Int, $before: String, $after: String) { __typename - edges { + items(first: $first, last: $last, before: $before, after: $after) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasPreviousPage - hasNextPage - startCursor - endCursor + pageInfo { + __typename + hasPreviousPage + hasNextPage + startCursor + endCursor + } } } - } - `; - - const store = new Store({ - schema: require('../test-utils/relayPagination_schema.json'), - resolvers: { - Query: { - items: relayPagination(), + `; + + const store = new Store({ + schema: require('../test-utils/relayPagination_schema.json'), + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); + }); - const results = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1), itemEdge(2), itemEdge(3), itemEdge(4), itemEdge(5)], - nodes: [itemNode(1), itemNode(2), itemNode(3), itemNode(4), itemNode(5)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - hasPreviousPage: false, - startCursor: '1', - endCursor: '5', + const results = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [ + itemEdge(1), + itemEdge(2), + itemEdge(3), + itemEdge(4), + itemEdge(5), + ], + nodes: [ + itemNode(1), + itemNode(2), + itemNode(3), + itemNode(4), + itemNode(5), + ], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: false, + startCursor: '1', + endCursor: '5', + }, }, - }, - }; + }; - write( - store, - { query: Pagination, variables: { after: '2', first: 2, last: 2 } }, - results - ); + write( + store, + { query: Pagination, variables: { after: '2', first: 2, last: 2 } }, + results + ); - const res = query(store, { - query: Pagination, - variables: { after: '2', first: 2, last: 2 }, - }); + const res = query(store, { + query: Pagination, + variables: { after: '2', first: 2, last: 2 }, + }); - expect(res.partial).toBe(false); - expect(res.data).toEqual(results); -}); + expect(res.partial).toBe(false); + expect(res.data).toEqual(results); + }); -it('does not include a previous result when adding parameters', () => { - const Pagination = gql` - query ($first: Int, $filter: String) { - __typename - items(first: $first, filter: $filter) { + it('does not include a previous result when adding parameters', () => { + const Pagination = gql` + query ($first: Int, $filter: String) { __typename - edges { + items(first: $first, filter: $filter) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasPreviousPage - hasNextPage - startCursor - endCursor + pageInfo { + __typename + hasPreviousPage + hasNextPage + startCursor + endCursor + } } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination(), + const store = new Store({ + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); + }); - const results = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1), itemEdge(2)], - nodes: [itemNode(1), itemNode(2)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - hasPreviousPage: false, - startCursor: '1', - endCursor: '2', + const results = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(1), itemEdge(2)], + nodes: [itemNode(1), itemNode(2)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: false, + startCursor: '1', + endCursor: '2', + }, }, - }, - }; + }; - const results2 = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [], - nodes: [], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: false, - hasPreviousPage: false, - startCursor: '1', - endCursor: '2', + const results2 = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [], + nodes: [], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + hasPreviousPage: false, + startCursor: '1', + endCursor: '2', + }, }, - }, - }; + }; - write(store, { query: Pagination, variables: { first: 2 } }, results); + write(store, { query: Pagination, variables: { first: 2 } }, results); - write( - store, - { query: Pagination, variables: { first: 2, filter: 'b' } }, - results2 - ); + write( + store, + { query: Pagination, variables: { first: 2, filter: 'b' } }, + results2 + ); - const res = query(store, { - query: Pagination, - variables: { first: 2, filter: 'b' }, + const res = query(store, { + query: Pagination, + variables: { first: 2, filter: 'b' }, + }); + expect(res.data).toEqual(results2); }); - expect(res.data).toEqual(results2); -}); -it('Works with edges absent from query', () => { - const Pagination = gql` - query ($first: Int, $last: Int, $before: String, $after: String) { - __typename - items(first: $first, last: $last, before: $before, after: $after) { + it('Works with edges absent from query', () => { + const Pagination = gql` + query ($first: Int, $last: Int, $before: String, $after: String) { __typename - nodes { + items(first: $first, last: $last, before: $before, after: $after) { __typename - id - } - pageInfo { - __typename - hasPreviousPage - hasNextPage - startCursor - endCursor + nodes { + __typename + id + } + pageInfo { + __typename + hasPreviousPage + hasNextPage + startCursor + endCursor + } } } - } - `; - - const store = new Store({ - schema: require('../test-utils/relayPagination_schema.json'), - resolvers: { - Query: { - items: relayPagination({ mergeMode: 'outwards' }), + `; + + const store = new Store({ + schema: require('../test-utils/relayPagination_schema.json'), + resolvers: { + Query: { + items: relayPagination({ mergeMode: 'outwards' }), + }, }, - }, - }); + }); - const results = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - nodes: [itemNode(1), itemNode(2), itemNode(3), itemNode(4), itemNode(5)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - hasPreviousPage: false, - startCursor: '1', - endCursor: '5', + const results = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + nodes: [ + itemNode(1), + itemNode(2), + itemNode(3), + itemNode(4), + itemNode(5), + ], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: false, + startCursor: '1', + endCursor: '5', + }, }, - }, - }; + }; - write(store, { query: Pagination, variables: { first: 2 } }, results); + write(store, { query: Pagination, variables: { first: 2 } }, results); - const res = query(store, { - query: Pagination, - variables: { first: 2 }, - }); + const res = query(store, { + query: Pagination, + variables: { first: 2 }, + }); - expect(res.partial).toBe(false); - expect(res.data).toEqual(results); -}); + expect(res.partial).toBe(false); + expect(res.data).toEqual(results); + }); -it('Works with nodes absent from query', () => { - const Pagination = gql` - query ($first: Int, $last: Int, $before: String, $after: String) { - __typename - items(first: $first, last: $last, before: $before, after: $after) { + it('Works with nodes absent from query', () => { + const Pagination = gql` + query ($first: Int, $last: Int, $before: String, $after: String) { __typename - edges { + items(first: $first, last: $last, before: $before, after: $after) { __typename - node { + edges { __typename - id + node { + __typename + id + } + } + pageInfo { + __typename + hasPreviousPage + hasNextPage + startCursor + endCursor } - } - pageInfo { - __typename - hasPreviousPage - hasNextPage - startCursor - endCursor } } - } - `; - - const store = new Store({ - schema: require('../test-utils/relayPagination_schema.json'), - resolvers: { - Query: { - items: relayPagination({ mergeMode: 'outwards' }), + `; + + const store = new Store({ + schema: require('../test-utils/relayPagination_schema.json'), + resolvers: { + Query: { + items: relayPagination({ mergeMode: 'outwards' }), + }, }, - }, - }); + }); - const results = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1), itemEdge(2), itemEdge(3), itemEdge(4), itemEdge(5)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - hasPreviousPage: false, - startCursor: '1', - endCursor: '5', + const results = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [ + itemEdge(1), + itemEdge(2), + itemEdge(3), + itemEdge(4), + itemEdge(5), + ], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + hasPreviousPage: false, + startCursor: '1', + endCursor: '5', + }, }, - }, - }; + }; - write(store, { query: Pagination, variables: { first: 2 } }, results); + write(store, { query: Pagination, variables: { first: 2 } }, results); - const res = query(store, { - query: Pagination, - variables: { first: 2 }, - }); + const res = query(store, { + query: Pagination, + variables: { first: 2 }, + }); - expect(res.partial).toBe(false); - expect(res.data).toEqual(results); -}); + expect(res.partial).toBe(false); + expect(res.data).toEqual(results); + }); -it('handles subsequent queries with larger last values', () => { - const Pagination = gql` - query ($last: Int!) { - __typename - items(last: $last) { + it('handles subsequent queries with larger last values', () => { + const Pagination = gql` + query ($last: Int!) { __typename - edges { + items(last: $last) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasPreviousPage - startCursor + pageInfo { + __typename + hasPreviousPage + startCursor + } } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination(), + const store = new Store({ + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); + }); - const pageOne = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(2)], - nodes: [itemNode(2)], - pageInfo: { - __typename: 'PageInfo', - hasPreviousPage: true, - startCursor: '2', + const pageOne = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(2)], + nodes: [itemNode(2)], + pageInfo: { + __typename: 'PageInfo', + hasPreviousPage: true, + startCursor: '2', + }, }, - }, - }; + }; - const pageTwo = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1), itemEdge(2)], - nodes: [itemNode(1), itemNode(2)], - pageInfo: { - __typename: 'PageInfo', - hasPreviousPage: false, - startCursor: '1', + const pageTwo = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(1), itemEdge(2)], + nodes: [itemNode(1), itemNode(2)], + pageInfo: { + __typename: 'PageInfo', + hasPreviousPage: false, + startCursor: '1', + }, }, - }, - }; + }; - const pageThree = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(0), itemEdge(1), itemEdge(2)], - nodes: [itemNode(0), itemNode(1), itemNode(2)], - pageInfo: { - __typename: 'PageInfo', - hasPreviousPage: false, - startCursor: '0', + const pageThree = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(0), itemEdge(1), itemEdge(2)], + nodes: [itemNode(0), itemNode(1), itemNode(2)], + pageInfo: { + __typename: 'PageInfo', + hasPreviousPage: false, + startCursor: '0', + }, }, - }, - }; + }; - write(store, { query: Pagination, variables: { last: 1 } }, pageOne); - write(store, { query: Pagination, variables: { last: 2 } }, pageTwo); + write(store, { query: Pagination, variables: { last: 1 } }, pageOne); + write(store, { query: Pagination, variables: { last: 2 } }, pageTwo); - let res = query(store, { query: Pagination, variables: { last: 2 } }); + let res = query(store, { query: Pagination, variables: { last: 2 } }); - expect(res.partial).toBe(false); - expect(res.data).toEqual(pageTwo); + expect(res.partial).toBe(false); + expect(res.data).toEqual(pageTwo); - write(store, { query: Pagination, variables: { last: 3 } }, pageThree); + write(store, { query: Pagination, variables: { last: 3 } }, pageThree); - res = query(store, { query: Pagination, variables: { last: 3 } }); + res = query(store, { query: Pagination, variables: { last: 3 } }); - expect(res.partial).toBe(false); - expect(res.data).toEqual(pageThree); -}); + expect(res.partial).toBe(false); + expect(res.data).toEqual(pageThree); + }); -it('handles subsequent queries with larger first values', () => { - const Pagination = gql` - query ($first: Int!) { - __typename - items(first: $first) { + it('handles subsequent queries with larger first values', () => { + const Pagination = gql` + query ($first: Int!) { __typename - edges { + items(first: $first) { __typename - node { + edges { + __typename + node { + __typename + id + } + } + nodes { __typename id } - } - nodes { - __typename - id - } - pageInfo { - __typename - hasNextPage - endCursor + pageInfo { + __typename + hasNextPage + endCursor + } } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination(), + const store = new Store({ + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); + }); - const pageOne = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1)], - nodes: [itemNode(1)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - endCursor: '1', + const pageOne = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(1)], + nodes: [itemNode(1)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + endCursor: '1', + }, }, - }, - }; + }; - const pageTwo = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - edges: [itemEdge(1), itemEdge(2)], - nodes: [itemNode(1), itemNode(2)], - pageInfo: { - __typename: 'PageInfo', - hasNextPage: false, - endCursor: '2', + const pageTwo = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(1), itemEdge(2)], + nodes: [itemNode(1), itemNode(2)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + endCursor: '2', + }, }, - }, - }; + }; - write(store, { query: Pagination, variables: { first: 1 } }, pageOne); - write(store, { query: Pagination, variables: { first: 2 } }, pageTwo); + write(store, { query: Pagination, variables: { first: 1 } }, pageOne); + write(store, { query: Pagination, variables: { first: 2 } }, pageTwo); - const res = query(store, { query: Pagination, variables: { first: 2 } }); + const res = query(store, { query: Pagination, variables: { first: 2 } }); - expect(res.partial).toBe(false); - expect(res.data).toEqual(pageTwo); -}); + expect(res.partial).toBe(false); + expect(res.data).toEqual(pageTwo); + }); -it('ignores empty pages when paginating', () => { - const PaginationForward = gql` - query ($first: Int!, $after: String) { - __typename - items(first: $first, after: $after) { + it('ignores empty pages when paginating', () => { + const PaginationForward = gql` + query ($first: Int!, $after: String) { __typename - nodes { + items(first: $first, after: $after) { __typename - id - } - pageInfo { - __typename - startCursor - endCursor + nodes { + __typename + id + } + pageInfo { + __typename + startCursor + endCursor + } } } - } - `; - const PaginationBackward = gql` - query ($last: Int!, $before: String) { - __typename - items(last: $last, before: $before) { + `; + const PaginationBackward = gql` + query ($last: Int!, $before: String) { __typename - nodes { + items(last: $last, before: $before) { __typename - id - } - pageInfo { - __typename - startCursor - endCursor + nodes { + __typename + id + } + pageInfo { + __typename + startCursor + endCursor + } } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination(), + const store = new Store({ + resolvers: { + Query: { + items: relayPagination(), + }, }, - }, - }); + }); - const forwardOne = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - nodes: [itemNode(1), itemNode(2)], - pageInfo: { - __typename: 'PageInfo', - startCursor: '1', - endCursor: '2', + const forwardOne = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + nodes: [itemNode(1), itemNode(2)], + pageInfo: { + __typename: 'PageInfo', + startCursor: '1', + endCursor: '2', + }, }, - }, - }; - const forwardAfter = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - nodes: [], - pageInfo: { - __typename: 'PageInfo', - startCursor: null, - endCursor: null, + }; + const forwardAfter = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + nodes: [], + pageInfo: { + __typename: 'PageInfo', + startCursor: null, + endCursor: null, + }, }, - }, - }; - const backwardBefore = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - nodes: [], - pageInfo: { - __typename: 'PageInfo', - startCursor: null, - endCursor: null, + }; + const backwardBefore = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + nodes: [], + pageInfo: { + __typename: 'PageInfo', + startCursor: null, + endCursor: null, + }, }, - }, - }; - - write( - store, - { query: PaginationForward, variables: { first: 2 } }, - forwardOne - ); - write( - store, - { query: PaginationBackward, variables: { last: 1, before: '1' } }, - backwardBefore - ); - - const res = query(store, { - query: PaginationForward, - variables: { first: 2 }, + }; + + write( + store, + { query: PaginationForward, variables: { first: 2 } }, + forwardOne + ); + write( + store, + { query: PaginationBackward, variables: { last: 1, before: '1' } }, + backwardBefore + ); + + const res = query(store, { + query: PaginationForward, + variables: { first: 2 }, + }); + + expect(res.partial).toBe(false); + expect(res.data).toEqual(forwardOne); + write( + store, + { query: PaginationForward, variables: { first: 1, after: '2' } }, + forwardAfter + ); + + expect(res.partial).toBe(false); + expect(res.data).toEqual(forwardOne); }); - expect(res.partial).toBe(false); - expect(res.data).toEqual(forwardOne); - write( - store, - { query: PaginationForward, variables: { first: 1, after: '2' } }, - forwardAfter - ); + it('allows for an empty page when this is the only result', () => { + const Pagination = gql` + query ($first: Int!, $after: String) { + __typename + items(first: $first, after: $after) { + __typename + nodes { + __typename + id + } + pageInfo { + __typename + startCursor + endCursor + } + } + } + `; - expect(res.partial).toBe(false); - expect(res.data).toEqual(forwardOne); + const store = new Store({ + resolvers: { + Query: { + items: relayPagination(), + }, + }, + }); + + const pageOne = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + nodes: [], + pageInfo: { + __typename: 'PageInfo', + startCursor: null, + endCursor: null, + }, + }, + }; + + write(store, { query: Pagination, variables: { first: 2 } }, pageOne); + const res = query(store, { + query: Pagination, + variables: { first: 2 }, + }); + + expect(res.partial).toBe(false); + expect(res.data).toEqual(pageOne); + }); }); -it('allows for an empty page when this is the only result', () => { - const Pagination = gql` - query ($first: Int!, $after: String) { - __typename - items(first: $first, after: $after) { +describe('as directive', () => { + it('works with forward pagination', () => { + const Pagination = gql` + query ($cursor: String) { __typename - nodes { + items(first: 1, after: $cursor) @relayPagination { __typename - id - } - pageInfo { - __typename - startCursor - endCursor + edges { + __typename + node { + __typename + id + } + } + nodes { + __typename + id + } + pageInfo { + __typename + hasNextPage + endCursor + } } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - items: relayPagination(), + const store = new Store({ + directives: { + relayPagination: relayPagination(), }, - }, - }); + }); - const pageOne = { - __typename: 'Query', - items: { - __typename: 'ItemsConnection', - nodes: [], - pageInfo: { - __typename: 'PageInfo', - startCursor: null, - endCursor: null, + const pageOne = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(1)], + nodes: [itemNode(1)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: true, + endCursor: '1', + }, }, - }, - }; + }; - write(store, { query: Pagination, variables: { first: 2 } }, pageOne); - const res = query(store, { - query: Pagination, - variables: { first: 2 }, - }); + const pageTwo = { + __typename: 'Query', + items: { + __typename: 'ItemsConnection', + edges: [itemEdge(2)], + nodes: [itemNode(2)], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + endCursor: null, + }, + }, + }; + + write(store, { query: Pagination, variables: { cursor: null } }, pageOne); + write(store, { query: Pagination, variables: { cursor: '1' } }, pageTwo); + + const res = query(store, { query: Pagination }); - expect(res.partial).toBe(false); - expect(res.data).toEqual(pageOne); + expect(res.partial).toBe(false); + expect(res.data).toEqual({ + ...pageTwo, + items: { + ...pageTwo.items, + edges: [pageOne.items.edges[0], pageTwo.items.edges[0]], + nodes: [pageOne.items.nodes[0], pageTwo.items.nodes[0]], + }, + }); + }); }); diff --git a/exchanges/graphcache/src/extras/simplePagination.test.ts b/exchanges/graphcache/src/extras/simplePagination.test.ts index a79103c901..d1f1e6242b 100644 --- a/exchanges/graphcache/src/extras/simplePagination.test.ts +++ b/exchanges/graphcache/src/extras/simplePagination.test.ts @@ -1,437 +1,511 @@ import { gql } from '@urql/core'; -import { it, expect } from 'vitest'; +import { it, expect, describe } from 'vitest'; import { __initAnd_query as query } from '../operations/query'; import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store/store'; import { simplePagination } from './simplePagination'; -it('works with forward pagination', () => { - const Pagination = gql` - query ($skip: Number, $limit: Number) { - __typename - persons(skip: $skip, limit: $limit) { +describe('as resolver', () => { + it('works with forward pagination', () => { + const Pagination = gql` + query ($skip: Number, $limit: Number) { __typename - id - name + persons(skip: $skip, limit: $limit) { + __typename + id + name + } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - persons: simplePagination(), + const store = new Store({ + resolvers: { + Query: { + persons: simplePagination(), + }, }, - }, + }); + + const pageOne = { + __typename: 'Query', + persons: [ + { id: 1, name: 'Jovi', __typename: 'Person' }, + { id: 2, name: 'Phil', __typename: 'Person' }, + { id: 3, name: 'Andy', __typename: 'Person' }, + ], + }; + + const pageTwo = { + __typename: 'Query', + persons: [ + { id: 4, name: 'Kadi', __typename: 'Person' }, + { id: 5, name: 'Dom', __typename: 'Person' }, + { id: 6, name: 'Sofia', __typename: 'Person' }, + ], + }; + + write( + store, + { query: Pagination, variables: { skip: 0, limit: 3 } }, + pageOne + ); + const pageOneResult = query(store, { + query: Pagination, + variables: { skip: 0, limit: 3 }, + }); + expect(pageOneResult.data).toEqual(pageOne); + + write( + store, + { query: Pagination, variables: { skip: 3, limit: 3 } }, + pageTwo + ); + + const pageTwoResult = query(store, { + query: Pagination, + variables: { skip: 3, limit: 3 }, + }); + expect((pageTwoResult.data as any).persons).toEqual([ + ...pageOne.persons, + ...pageTwo.persons, + ]); + + const pageThreeResult = query(store, { + query: Pagination, + variables: { skip: 6, limit: 3 }, + }); + expect(pageThreeResult.data).toEqual(null); }); - const pageOne = { - __typename: 'Query', - persons: [ - { id: 1, name: 'Jovi', __typename: 'Person' }, - { id: 2, name: 'Phil', __typename: 'Person' }, - { id: 3, name: 'Andy', __typename: 'Person' }, - ], - }; - - const pageTwo = { - __typename: 'Query', - persons: [ - { id: 4, name: 'Kadi', __typename: 'Person' }, - { id: 5, name: 'Dom', __typename: 'Person' }, - { id: 6, name: 'Sofia', __typename: 'Person' }, - ], - }; - - write( - store, - { query: Pagination, variables: { skip: 0, limit: 3 } }, - pageOne - ); - const pageOneResult = query(store, { - query: Pagination, - variables: { skip: 0, limit: 3 }, - }); - expect(pageOneResult.data).toEqual(pageOne); - - write( - store, - { query: Pagination, variables: { skip: 3, limit: 3 } }, - pageTwo - ); - - const pageTwoResult = query(store, { - query: Pagination, - variables: { skip: 3, limit: 3 }, - }); - expect((pageTwoResult.data as any).persons).toEqual([ - ...pageOne.persons, - ...pageTwo.persons, - ]); - - const pageThreeResult = query(store, { - query: Pagination, - variables: { skip: 6, limit: 3 }, - }); - expect(pageThreeResult.data).toEqual(null); -}); - -it('works with backwards pagination', () => { - const Pagination = gql` - query ($skip: Number, $limit: Number) { - __typename - persons(skip: $skip, limit: $limit) { + it('works with backwards pagination', () => { + const Pagination = gql` + query ($skip: Number, $limit: Number) { __typename - id - name + persons(skip: $skip, limit: $limit) { + __typename + id + name + } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - persons: simplePagination({ mergeMode: 'before' }), + const store = new Store({ + resolvers: { + Query: { + persons: simplePagination({ mergeMode: 'before' }), + }, }, - }, + }); + + const pageOne = { + __typename: 'Query', + persons: [ + { id: 7, name: 'Jovi', __typename: 'Person' }, + { id: 8, name: 'Phil', __typename: 'Person' }, + { id: 9, name: 'Andy', __typename: 'Person' }, + ], + }; + + const pageTwo = { + __typename: 'Query', + persons: [ + { id: 4, name: 'Kadi', __typename: 'Person' }, + { id: 5, name: 'Dom', __typename: 'Person' }, + { id: 6, name: 'Sofia', __typename: 'Person' }, + ], + }; + + write( + store, + { query: Pagination, variables: { skip: 0, limit: 3 } }, + pageOne + ); + const pageOneResult = query(store, { + query: Pagination, + variables: { skip: 0, limit: 3 }, + }); + expect(pageOneResult.data).toEqual(pageOne); + + write( + store, + { query: Pagination, variables: { skip: 3, limit: 3 } }, + pageTwo + ); + + const pageTwoResult = query(store, { + query: Pagination, + variables: { skip: 3, limit: 3 }, + }); + expect((pageTwoResult.data as any).persons).toEqual([ + ...pageTwo.persons, + ...pageOne.persons, + ]); + + const pageThreeResult = query(store, { + query: Pagination, + variables: { skip: 6, limit: 3 }, + }); + expect(pageThreeResult.data).toEqual(null); }); - const pageOne = { - __typename: 'Query', - persons: [ - { id: 7, name: 'Jovi', __typename: 'Person' }, - { id: 8, name: 'Phil', __typename: 'Person' }, - { id: 9, name: 'Andy', __typename: 'Person' }, - ], - }; - - const pageTwo = { - __typename: 'Query', - persons: [ - { id: 4, name: 'Kadi', __typename: 'Person' }, - { id: 5, name: 'Dom', __typename: 'Person' }, - { id: 6, name: 'Sofia', __typename: 'Person' }, - ], - }; - - write( - store, - { query: Pagination, variables: { skip: 0, limit: 3 } }, - pageOne - ); - const pageOneResult = query(store, { - query: Pagination, - variables: { skip: 0, limit: 3 }, - }); - expect(pageOneResult.data).toEqual(pageOne); - - write( - store, - { query: Pagination, variables: { skip: 3, limit: 3 } }, - pageTwo - ); - - const pageTwoResult = query(store, { - query: Pagination, - variables: { skip: 3, limit: 3 }, - }); - expect((pageTwoResult.data as any).persons).toEqual([ - ...pageTwo.persons, - ...pageOne.persons, - ]); - - const pageThreeResult = query(store, { - query: Pagination, - variables: { skip: 6, limit: 3 }, - }); - expect(pageThreeResult.data).toEqual(null); -}); - -it('handles duplicates', () => { - const Pagination = gql` - query ($skip: Number, $limit: Number) { - __typename - persons(skip: $skip, limit: $limit) { + it('handles duplicates', () => { + const Pagination = gql` + query ($skip: Number, $limit: Number) { __typename - id - name + persons(skip: $skip, limit: $limit) { + __typename + id + name + } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - persons: simplePagination(), + const store = new Store({ + resolvers: { + Query: { + persons: simplePagination(), + }, }, - }, + }); + + const pageOne = { + __typename: 'Query', + persons: [ + { id: 1, name: 'Jovi', __typename: 'Person' }, + { id: 2, name: 'Phil', __typename: 'Person' }, + { id: 3, name: 'Andy', __typename: 'Person' }, + ], + }; + + const pageTwo = { + __typename: 'Query', + persons: [ + { id: 3, name: 'Andy', __typename: 'Person' }, + { id: 4, name: 'Kadi', __typename: 'Person' }, + { id: 5, name: 'Dom', __typename: 'Person' }, + ], + }; + + write( + store, + { query: Pagination, variables: { skip: 0, limit: 3 } }, + pageOne + ); + write( + store, + { query: Pagination, variables: { skip: 2, limit: 3 } }, + pageTwo + ); + + const result = query(store, { + query: Pagination, + variables: { skip: 2, limit: 3 }, + }); + expect(result.data).toEqual({ + __typename: 'Query', + persons: [...pageOne.persons, pageTwo.persons[1], pageTwo.persons[2]], + }); }); - const pageOne = { - __typename: 'Query', - persons: [ - { id: 1, name: 'Jovi', __typename: 'Person' }, - { id: 2, name: 'Phil', __typename: 'Person' }, - { id: 3, name: 'Andy', __typename: 'Person' }, - ], - }; - - const pageTwo = { - __typename: 'Query', - persons: [ - { id: 3, name: 'Andy', __typename: 'Person' }, - { id: 4, name: 'Kadi', __typename: 'Person' }, - { id: 5, name: 'Dom', __typename: 'Person' }, - ], - }; - - write( - store, - { query: Pagination, variables: { skip: 0, limit: 3 } }, - pageOne - ); - write( - store, - { query: Pagination, variables: { skip: 2, limit: 3 } }, - pageTwo - ); - - const result = query(store, { - query: Pagination, - variables: { skip: 2, limit: 3 }, - }); - expect(result.data).toEqual({ - __typename: 'Query', - persons: [...pageOne.persons, pageTwo.persons[1], pageTwo.persons[2]], - }); -}); - -it('should not return previous result when adding a parameter', () => { - const Pagination = gql` - query ($skip: Number, $limit: Number, $filter: String) { - __typename - persons(skip: $skip, limit: $limit, filter: $filter) { + it('should not return previous result when adding a parameter', () => { + const Pagination = gql` + query ($skip: Number, $limit: Number, $filter: String) { __typename - id - name + persons(skip: $skip, limit: $limit, filter: $filter) { + __typename + id + name + } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - persons: simplePagination(), + const store = new Store({ + resolvers: { + Query: { + persons: simplePagination(), + }, }, - }, - }); - - const pageOne = { - __typename: 'Query', - persons: [ - { id: 1, name: 'Jovi', __typename: 'Person' }, - { id: 2, name: 'Phil', __typename: 'Person' }, - { id: 3, name: 'Andy', __typename: 'Person' }, - ], - }; - - const emptyPage = { - __typename: 'Query', - persons: [], - }; - - write( - store, - { query: Pagination, variables: { skip: 0, limit: 3 } }, - pageOne - ); - write( - store, - { query: Pagination, variables: { skip: 0, limit: 3, filter: 'b' } }, - emptyPage - ); - - const res = query(store, { - query: Pagination, - variables: { skip: 0, limit: 3, filter: 'b' }, + }); + + const pageOne = { + __typename: 'Query', + persons: [ + { id: 1, name: 'Jovi', __typename: 'Person' }, + { id: 2, name: 'Phil', __typename: 'Person' }, + { id: 3, name: 'Andy', __typename: 'Person' }, + ], + }; + + const emptyPage = { + __typename: 'Query', + persons: [], + }; + + write( + store, + { query: Pagination, variables: { skip: 0, limit: 3 } }, + pageOne + ); + write( + store, + { query: Pagination, variables: { skip: 0, limit: 3, filter: 'b' } }, + emptyPage + ); + + const res = query(store, { + query: Pagination, + variables: { skip: 0, limit: 3, filter: 'b' }, + }); + expect(res.data).toEqual({ __typename: 'Query', persons: [] }); }); - expect(res.data).toEqual({ __typename: 'Query', persons: [] }); -}); -it('should preserve the correct order in forward pagination', () => { - const Pagination = gql` - query ($skip: Number, $limit: Number) { - __typename - persons(skip: $skip, limit: $limit) { + it('should preserve the correct order in forward pagination', () => { + const Pagination = gql` + query ($skip: Number, $limit: Number) { __typename - id - name + persons(skip: $skip, limit: $limit) { + __typename + id + name + } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - persons: simplePagination({ mergeMode: 'after' }), + const store = new Store({ + resolvers: { + Query: { + persons: simplePagination({ mergeMode: 'after' }), + }, }, - }, - }); - - const pageOne = { - __typename: 'Query', - persons: [ - { id: 1, name: 'Jovi', __typename: 'Person' }, - { id: 2, name: 'Phil', __typename: 'Person' }, - { id: 3, name: 'Andy', __typename: 'Person' }, - ], - }; - - const pageTwo = { - __typename: 'Query', - persons: [ - { id: 4, name: 'Kadi', __typename: 'Person' }, - { id: 5, name: 'Dom', __typename: 'Person' }, - { id: 6, name: 'Sofia', __typename: 'Person' }, - ], - }; - - write( - store, - { query: Pagination, variables: { skip: 3, limit: 3 } }, - pageTwo - ); - write( - store, - { query: Pagination, variables: { skip: 0, limit: 3 } }, - pageOne - ); - - const result = query(store, { - query: Pagination, - variables: { skip: 3, limit: 3 }, - }); - expect(result.data).toEqual({ - __typename: 'Query', - persons: [...pageOne.persons, ...pageTwo.persons], + }); + + const pageOne = { + __typename: 'Query', + persons: [ + { id: 1, name: 'Jovi', __typename: 'Person' }, + { id: 2, name: 'Phil', __typename: 'Person' }, + { id: 3, name: 'Andy', __typename: 'Person' }, + ], + }; + + const pageTwo = { + __typename: 'Query', + persons: [ + { id: 4, name: 'Kadi', __typename: 'Person' }, + { id: 5, name: 'Dom', __typename: 'Person' }, + { id: 6, name: 'Sofia', __typename: 'Person' }, + ], + }; + + write( + store, + { query: Pagination, variables: { skip: 3, limit: 3 } }, + pageTwo + ); + write( + store, + { query: Pagination, variables: { skip: 0, limit: 3 } }, + pageOne + ); + + const result = query(store, { + query: Pagination, + variables: { skip: 3, limit: 3 }, + }); + expect(result.data).toEqual({ + __typename: 'Query', + persons: [...pageOne.persons, ...pageTwo.persons], + }); }); -}); -it('should preserve the correct order in backward pagination', () => { - const Pagination = gql` - query ($skip: Number, $limit: Number) { - __typename - persons(skip: $skip, limit: $limit) { + it('should preserve the correct order in backward pagination', () => { + const Pagination = gql` + query ($skip: Number, $limit: Number) { __typename - id - name + persons(skip: $skip, limit: $limit) { + __typename + id + name + } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - persons: simplePagination({ mergeMode: 'before' }), + const store = new Store({ + resolvers: { + Query: { + persons: simplePagination({ mergeMode: 'before' }), + }, }, - }, + }); + + const pageOne = { + __typename: 'Query', + persons: [ + { id: 7, name: 'Jovi', __typename: 'Person' }, + { id: 8, name: 'Phil', __typename: 'Person' }, + { id: 9, name: 'Andy', __typename: 'Person' }, + ], + }; + + const pageTwo = { + __typename: 'Query', + persons: [ + { id: 4, name: 'Kadi', __typename: 'Person' }, + { id: 5, name: 'Dom', __typename: 'Person' }, + { id: 6, name: 'Sofia', __typename: 'Person' }, + ], + }; + + write( + store, + { query: Pagination, variables: { skip: 3, limit: 3 } }, + pageTwo + ); + write( + store, + { query: Pagination, variables: { skip: 0, limit: 3 } }, + pageOne + ); + + const result = query(store, { + query: Pagination, + variables: { skip: 3, limit: 3 }, + }); + + expect(result.data).toEqual({ + __typename: 'Query', + persons: [...pageTwo.persons, ...pageOne.persons], + }); }); - const pageOne = { - __typename: 'Query', - persons: [ - { id: 7, name: 'Jovi', __typename: 'Person' }, - { id: 8, name: 'Phil', __typename: 'Person' }, - { id: 9, name: 'Andy', __typename: 'Person' }, - ], - }; - - const pageTwo = { - __typename: 'Query', - persons: [ - { id: 4, name: 'Kadi', __typename: 'Person' }, - { id: 5, name: 'Dom', __typename: 'Person' }, - { id: 6, name: 'Sofia', __typename: 'Person' }, - ], - }; - - write( - store, - { query: Pagination, variables: { skip: 3, limit: 3 } }, - pageTwo - ); - write( - store, - { query: Pagination, variables: { skip: 0, limit: 3 } }, - pageOne - ); - - const result = query(store, { - query: Pagination, - variables: { skip: 3, limit: 3 }, - }); + it('prevents overlapping of pagination on different arguments', () => { + const Pagination = gql` + query ($skip: Number, $limit: Number, $filter: string) { + __typename + persons(skip: $skip, limit: $limit, filter: $filter) { + __typename + id + name + } + } + `; - expect(result.data).toEqual({ - __typename: 'Query', - persons: [...pageTwo.persons, ...pageOne.persons], + const store = new Store({ + resolvers: { + Query: { + persons: simplePagination(), + }, + }, + }); + + const page = withId => ({ + __typename: 'Query', + persons: [{ id: withId, name: withId, __typename: 'Person' }], + }); + + write( + store, + { query: Pagination, variables: { filter: 'one', skip: 0, limit: 1 } }, + page('one') + ); + + write( + store, + { query: Pagination, variables: { filter: 'two', skip: 1, limit: 1 } }, + page('two') + ); + + const resOne = query(store, { + query: Pagination, + variables: { filter: 'one', skip: 0, limit: 1 }, + }); + const resTwo = query(store, { + query: Pagination, + variables: { filter: 'two', skip: 1, limit: 1 }, + }); + const resThree = query(store, { + query: Pagination, + variables: { filter: 'three', skip: 2, limit: 1 }, + }); + + expect(resOne.data).toHaveProperty('persons[0].id', 'one'); + expect(resOne.data).toHaveProperty('persons.length', 1); + + expect(resTwo.data).toHaveProperty('persons[0].id', 'two'); + expect(resTwo.data).toHaveProperty('persons.length', 1); + + expect(resThree.data).toEqual(null); }); }); -it('prevents overlapping of pagination on different arguments', () => { - const Pagination = gql` - query ($skip: Number, $limit: Number, $filter: string) { - __typename - persons(skip: $skip, limit: $limit, filter: $filter) { +describe('as directive', () => { + it('works with forward pagination', () => { + const Pagination = gql` + query ($skip: Number, $limit: Number) { __typename - id - name + persons(skip: $skip, limit: $limit) @simplePagination { + __typename + id + name + } } - } - `; + `; - const store = new Store({ - resolvers: { - Query: { - persons: simplePagination(), + const store = new Store({ + directives: { + simplePagination: simplePagination(), }, - }, + }); + + const pageOne = { + __typename: 'Query', + persons: [ + { id: 1, name: 'Jovi', __typename: 'Person' }, + { id: 2, name: 'Phil', __typename: 'Person' }, + { id: 3, name: 'Andy', __typename: 'Person' }, + ], + }; + + const pageTwo = { + __typename: 'Query', + persons: [ + { id: 4, name: 'Kadi', __typename: 'Person' }, + { id: 5, name: 'Dom', __typename: 'Person' }, + { id: 6, name: 'Sofia', __typename: 'Person' }, + ], + }; + + write( + store, + { query: Pagination, variables: { skip: 0, limit: 3 } }, + pageOne + ); + const pageOneResult = query(store, { + query: Pagination, + variables: { skip: 0, limit: 3 }, + }); + expect(pageOneResult.data).toEqual(pageOne); + + write( + store, + { query: Pagination, variables: { skip: 3, limit: 3 } }, + pageTwo + ); + + const pageTwoResult = query(store, { + query: Pagination, + variables: { skip: 3, limit: 3 }, + }); + + expect((pageTwoResult.data as any).persons).toEqual([ + ...pageOne.persons, + ...pageTwo.persons, + ]); + + const pageThreeResult = query(store, { + query: Pagination, + variables: { skip: 6, limit: 3 }, + }); + expect(pageThreeResult.data).toEqual(null); }); - - const page = withId => ({ - __typename: 'Query', - persons: [{ id: withId, name: withId, __typename: 'Person' }], - }); - - write( - store, - { query: Pagination, variables: { filter: 'one', skip: 0, limit: 1 } }, - page('one') - ); - - write( - store, - { query: Pagination, variables: { filter: 'two', skip: 1, limit: 1 } }, - page('two') - ); - - const resOne = query(store, { - query: Pagination, - variables: { filter: 'one', skip: 0, limit: 1 }, - }); - const resTwo = query(store, { - query: Pagination, - variables: { filter: 'two', skip: 1, limit: 1 }, - }); - const resThree = query(store, { - query: Pagination, - variables: { filter: 'three', skip: 2, limit: 1 }, - }); - - expect(resOne.data).toHaveProperty('persons[0].id', 'one'); - expect(resOne.data).toHaveProperty('persons.length', 1); - - expect(resTwo.data).toHaveProperty('persons[0].id', 'two'); - expect(resTwo.data).toHaveProperty('persons.length', 1); - - expect(resThree.data).toEqual(null); }); From 0c45c9a478b2d698d534d24b3a73c3a373ee6b1e Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 7 Jul 2023 15:15:19 +0200 Subject: [PATCH 07/16] docs changes --- docs/graphcache/cache-updates.md | 2 +- docs/graphcache/errors.md | 7 ++++++- docs/graphcache/local-directives.md | 4 ++++ docs/graphcache/offline.md | 2 +- docs/graphcache/schema-awareness.md | 8 ++++---- 5 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 docs/graphcache/local-directives.md diff --git a/docs/graphcache/cache-updates.md b/docs/graphcache/cache-updates.md index b98aa7e69e..41c33cea38 100644 --- a/docs/graphcache/cache-updates.md +++ b/docs/graphcache/cache-updates.md @@ -1,6 +1,6 @@ --- title: Cache Updates -order: 3 +order: 4 --- # Cache Updates diff --git a/docs/graphcache/errors.md b/docs/graphcache/errors.md index 6aa6e0278a..ed293069b3 100644 --- a/docs/graphcache/errors.md +++ b/docs/graphcache/errors.md @@ -1,6 +1,6 @@ --- title: Errors -order: 7 +order: 8 --- # Help! @@ -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. diff --git a/docs/graphcache/local-directives.md b/docs/graphcache/local-directives.md new file mode 100644 index 0000000000..568df7a69d --- /dev/null +++ b/docs/graphcache/local-directives.md @@ -0,0 +1,4 @@ +--- +title: Local Directives +order: 2 +--- diff --git a/docs/graphcache/offline.md b/docs/graphcache/offline.md index 6a326dce9e..868bd83e7c 100644 --- a/docs/graphcache/offline.md +++ b/docs/graphcache/offline.md @@ -1,6 +1,6 @@ --- title: Offline Support -order: 6 +order: 7 --- # Offline Support diff --git a/docs/graphcache/schema-awareness.md b/docs/graphcache/schema-awareness.md index a8b81eacb0..445e669903 100644 --- a/docs/graphcache/schema-awareness.md +++ b/docs/graphcache/schema-awareness.md @@ -1,6 +1,6 @@ --- title: Schema Awareness -order: 4 +order: 5 --- # Schema Awareness @@ -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' }, }, }; From 7148e5f5bf83c252065d12535e66ac2f5fe6282e Mon Sep 17 00:00:00 2001 From: jdecroock Date: Wed, 19 Jul 2023 19:49:18 +0200 Subject: [PATCH 08/16] use underscored props --- docs/graphcache/local-directives.md | 31 ++++++++++++++++++- docs/graphcache/local-resolvers.md | 2 +- .../graphcache/src/cacheExchange.test.ts | 4 +-- exchanges/graphcache/src/cacheExchange.ts | 1 - .../src/extras/relayPagination.test.ts | 2 +- .../src/extras/simplePagination.test.ts | 2 +- exchanges/graphcache/src/operations/query.ts | 9 ++---- exchanges/graphcache/src/types.ts | 20 ++++++++++-- 8 files changed, 56 insertions(+), 15 deletions(-) diff --git a/docs/graphcache/local-directives.md b/docs/graphcache/local-directives.md index 568df7a69d..a92dacc5b6 100644 --- a/docs/graphcache/local-directives.md +++ b/docs/graphcache/local-directives.md @@ -1,4 +1,33 @@ --- title: Local Directives -order: 2 +order: 3 --- + +# 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: () => {}, + }, +}); +``` + +The function signature of a directive is the same as the one of a [Resolver](./local-directives.md). In +case you need to access the arguments you have passed to a directive you can do so by checking `info.directiveArguments`. + +### Reading on + +[On the next page we'll learn about "Cache Updates".](./cache-updates.md) diff --git a/docs/graphcache/local-resolvers.md b/docs/graphcache/local-resolvers.md index 0c004d13d9..fdf19a4497 100644 --- a/docs/graphcache/local-resolvers.md +++ b/docs/graphcache/local-resolvers.md @@ -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) diff --git a/exchanges/graphcache/src/cacheExchange.test.ts b/exchanges/graphcache/src/cacheExchange.test.ts index 82777f224b..2dd212beba 100644 --- a/exchanges/graphcache/src/cacheExchange.test.ts +++ b/exchanges/graphcache/src/cacheExchange.test.ts @@ -691,7 +691,7 @@ describe('directives', () => { todos { id text - completed @optional + completed @_optional } } `; @@ -763,7 +763,7 @@ describe('directives', () => { todos { id text - completed @required + completed @_required } } `; diff --git a/exchanges/graphcache/src/cacheExchange.ts b/exchanges/graphcache/src/cacheExchange.ts index b17fbffb88..7d0594e135 100644 --- a/exchanges/graphcache/src/cacheExchange.ts +++ b/exchanges/graphcache/src/cacheExchange.ts @@ -237,7 +237,6 @@ export const cacheExchange = operations.set(operation.key, operation); updateDependencies(operation, result.dependencies); - // TODO: remove directives before sending it on return { outcome: cacheOutcome, operation, diff --git a/exchanges/graphcache/src/extras/relayPagination.test.ts b/exchanges/graphcache/src/extras/relayPagination.test.ts index 29a1486f42..8f45be42b6 100644 --- a/exchanges/graphcache/src/extras/relayPagination.test.ts +++ b/exchanges/graphcache/src/extras/relayPagination.test.ts @@ -1542,7 +1542,7 @@ describe('as directive', () => { const Pagination = gql` query ($cursor: String) { __typename - items(first: 1, after: $cursor) @relayPagination { + items(first: 1, after: $cursor) @_relayPagination { __typename edges { __typename diff --git a/exchanges/graphcache/src/extras/simplePagination.test.ts b/exchanges/graphcache/src/extras/simplePagination.test.ts index d1f1e6242b..a675f1115f 100644 --- a/exchanges/graphcache/src/extras/simplePagination.test.ts +++ b/exchanges/graphcache/src/extras/simplePagination.test.ts @@ -443,7 +443,7 @@ describe('as directive', () => { const Pagination = gql` query ($skip: Number, $limit: Number) { __typename - persons(skip: $skip, limit: $limit) @simplePagination { + persons(skip: $skip, limit: $limit) @_simplePagination { __typename id name diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 6c30f19d79..d4d1d280e1 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -356,8 +356,8 @@ const readSelection = ( let node: FormattedNode | void; const output = InMemoryData.makeData(input); while ((node = iterate()) !== undefined) { - const fieldDirectives = node.directives?.map(x => x.name.value); - const storeDirective = fieldDirectives?.find(x => store.directives[x]); + const fieldDirectives = Object.keys(node._directives || {}).map(x => x); + const storeDirective = fieldDirectives.find(x => store.directives[x]); // Derive the needed data from our node. const fieldName = getName(node); @@ -415,9 +415,7 @@ const readSelection = ( } if (storeDirective) { - const fieldDirective = node.directives!.find( - x => x.name.value === storeDirective - )!; + const fieldDirective = node._directives![storeDirective]; const directiveArguments = getFieldArguments(fieldDirective, ctx.variables) || {}; dataFieldValue = store.directives[storeDirective]!( @@ -448,7 +446,6 @@ const readSelection = ( if ( store.schema && dataFieldValue === null && - // TODO: how would we inform this that we are indeed dealing with a nullable field !isFieldNullable(store.schema, typename, fieldName) ) { // Special case for when null is not a valid value for the diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts index f528a8af15..4dfb126e53 100644 --- a/exchanges/graphcache/src/types.ts +++ b/exchanges/graphcache/src/types.ts @@ -555,7 +555,14 @@ export type CacheExchangeOpts = { * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers} for the full resolvers docs. */ resolvers?: ResolverConfig; - // TODO: docs + /** Configures directives which can perform custom logic on fields. + * + * @remarks + * `directives` is a map of a directive-name to a function which will be executed + * when graphcache encounters this directive. + * + * @see {@link https://urql.dev/goto/docs/graphcache/local-directives} for the full directives docs. + */ directives?: DirectivesConfig; /** Configures optimistic updates to react to mutations instantly before an API response. * @@ -678,6 +685,16 @@ export type Resolver< ): Result; }['bivarianceHack']; +/** Cache Directive, which may resolve or replace data during cache reads. + * + * @param parent - The GraphQL object that is currently being constructed from cache data. + * @param args - This field’s arguments. + * @param cache - {@link Cache} interface. + * @param info - {@link ResolveInfo} interface. + * @returns a {@link ResolverResult}, which is an updated value, partial entity, or entity key + * + * @see {@link https://urql.dev/goto/docs/graphcache/local-directives} for the full directives docs. + */ export type Directive< ParentData = DataFields, Args = Variables, @@ -708,7 +725,6 @@ export type ResolverConfig = { } | void; }; -// TODO: docs export type DirectivesConfig = { [directiveName: string]: Directive; }; From 89e4f8fd1afff6c04d58add4a5214c5901be94a3 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Wed, 19 Jul 2023 22:03:50 +0200 Subject: [PATCH 09/16] Update exchanges/graphcache/src/operations/query.ts Co-authored-by: Phil Pluckthun --- exchanges/graphcache/src/operations/query.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index d4d1d280e1..b2a21e5267 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -412,9 +412,7 @@ const readSelection = ( store, ctx ); - } - - if (storeDirective) { + } else if (storeDirective) { const fieldDirective = node._directives![storeDirective]; const directiveArguments = getFieldArguments(fieldDirective, ctx.variables) || {}; From e70e5d8f5304b7d3b23b5d805649e3e720a3ff71 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Wed, 19 Jul 2023 22:05:00 +0200 Subject: [PATCH 10/16] Update exchanges/graphcache/src/types.ts Co-authored-by: Phil Pluckthun --- exchanges/graphcache/src/types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts index 4dfb126e53..fcb8818d90 100644 --- a/exchanges/graphcache/src/types.ts +++ b/exchanges/graphcache/src/types.ts @@ -558,8 +558,7 @@ export type CacheExchangeOpts = { /** Configures directives which can perform custom logic on fields. * * @remarks - * `directives` is a map of a directive-name to a function which will be executed - * when graphcache encounters this directive. + * A {@link DirectivesConfig} may be passed to allow local directives to be used. For example, when `@_custom` is placed on a field and the configuration contains `custom` then this directive is executed by Graphcache. * * @see {@link https://urql.dev/goto/docs/graphcache/local-directives} for the full directives docs. */ From b559c9d4123a39a14b66ecfda4cc1ac851b3a317 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Wed, 19 Jul 2023 22:19:12 +0200 Subject: [PATCH 11/16] Update exchanges/graphcache/src/operations/query.ts Co-authored-by: Phil Pluckthun --- exchanges/graphcache/src/operations/query.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index b2a21e5267..4eb97ca97e 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -416,11 +416,11 @@ const readSelection = ( const fieldDirective = node._directives![storeDirective]; const directiveArguments = getFieldArguments(fieldDirective, ctx.variables) || {}; - dataFieldValue = store.directives[storeDirective]!( + dataFieldValue = store.directives[storeDirective]!(directiveArguments)( output, fieldArgs || ({} as Variables), store, - { ...ctx, directiveArguments } + ctx ); } From eefd8064f225a1415df5443d246c836630377108 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Wed, 19 Jul 2023 22:22:37 +0200 Subject: [PATCH 12/16] adust tests --- docs/graphcache/local-directives.md | 8 +- .../src/extras/relayPagination.test.ts | 2 +- .../src/extras/simplePagination.test.ts | 77 ++++++++++++++++++- exchanges/graphcache/src/operations/query.ts | 37 ++++++--- exchanges/graphcache/src/store/store.ts | 4 +- exchanges/graphcache/src/types.ts | 27 +------ 6 files changed, 114 insertions(+), 41 deletions(-) diff --git a/docs/graphcache/local-directives.md b/docs/graphcache/local-directives.md index a92dacc5b6..923153b8e9 100644 --- a/docs/graphcache/local-directives.md +++ b/docs/graphcache/local-directives.md @@ -20,13 +20,15 @@ If you want to add directives yourself you can do so by performing cacheExchange({ directives: { // If you now add `@_pagination` to your document we will execute this - pagination: () => {}, + pagination: directiveArguments => () => { + /* Resolver */ + }, }, }); ``` -The function signature of a directive is the same as the one of a [Resolver](./local-directives.md). In -case you need to access the arguments you have passed to a directive you can do so by checking `info.directiveArguments`. +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 diff --git a/exchanges/graphcache/src/extras/relayPagination.test.ts b/exchanges/graphcache/src/extras/relayPagination.test.ts index 8f45be42b6..3b668f7321 100644 --- a/exchanges/graphcache/src/extras/relayPagination.test.ts +++ b/exchanges/graphcache/src/extras/relayPagination.test.ts @@ -1566,7 +1566,7 @@ describe('as directive', () => { const store = new Store({ directives: { - relayPagination: relayPagination(), + relayPagination: () => relayPagination(), }, }); diff --git a/exchanges/graphcache/src/extras/simplePagination.test.ts b/exchanges/graphcache/src/extras/simplePagination.test.ts index a675f1115f..dbe14b0684 100644 --- a/exchanges/graphcache/src/extras/simplePagination.test.ts +++ b/exchanges/graphcache/src/extras/simplePagination.test.ts @@ -3,7 +3,7 @@ import { it, expect, describe } from 'vitest'; import { __initAnd_query as query } from '../operations/query'; import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store/store'; -import { simplePagination } from './simplePagination'; +import { MergeMode, simplePagination } from './simplePagination'; describe('as resolver', () => { it('works with forward pagination', () => { @@ -453,7 +453,7 @@ describe('as directive', () => { const store = new Store({ directives: { - simplePagination: simplePagination(), + simplePagination: () => simplePagination(), }, }); @@ -508,4 +508,77 @@ describe('as directive', () => { }); expect(pageThreeResult.data).toEqual(null); }); + + it('works with backwards pagination', () => { + const Pagination = gql` + query ($skip: Number, $limit: Number) { + __typename + persons(skip: $skip, limit: $limit) + @_simplePagination(mergeMode: "before") { + __typename + id + name + } + } + `; + + const store = new Store({ + directives: { + simplePagination: directiveArguments => + simplePagination({ + mergeMode: directiveArguments!.mergeMode as MergeMode, + }), + }, + }); + + const pageOne = { + __typename: 'Query', + persons: [ + { id: 7, name: 'Jovi', __typename: 'Person' }, + { id: 8, name: 'Phil', __typename: 'Person' }, + { id: 9, name: 'Andy', __typename: 'Person' }, + ], + }; + + const pageTwo = { + __typename: 'Query', + persons: [ + { id: 4, name: 'Kadi', __typename: 'Person' }, + { id: 5, name: 'Dom', __typename: 'Person' }, + { id: 6, name: 'Sofia', __typename: 'Person' }, + ], + }; + + write( + store, + { query: Pagination, variables: { skip: 0, limit: 3 } }, + pageOne + ); + const pageOneResult = query(store, { + query: Pagination, + variables: { skip: 0, limit: 3 }, + }); + expect(pageOneResult.data).toEqual(pageOne); + + write( + store, + { query: Pagination, variables: { skip: 3, limit: 3 } }, + pageTwo + ); + + const pageTwoResult = query(store, { + query: Pagination, + variables: { skip: 3, limit: 3 }, + }); + expect((pageTwoResult.data as any).persons).toEqual([ + ...pageTwo.persons, + ...pageOne.persons, + ]); + + const pageThreeResult = query(store, { + query: Pagination, + variables: { skip: 6, limit: 3 }, + }); + expect(pageThreeResult.data).toEqual(null); + }); }); diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 4eb97ca97e..77ae94b08a 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -4,6 +4,7 @@ import { FieldNode, DocumentNode, FragmentDefinitionNode, + DirectiveNode, } from '@0no-co/graphql.web'; import { @@ -16,6 +17,7 @@ import { getMainOperation, normalizeVariables, getFieldArguments, + getDirectives, } from '../ast'; import { @@ -25,6 +27,7 @@ import { Link, OperationRequest, Dependencies, + Directive, } from '../types'; import { joinKeys, keyOfField } from '../store/keys'; @@ -295,6 +298,20 @@ export const _queryFragment = ( return result; }; +function getFieldDirective( + node: FormattedNode, + store: Store +): { storeDirective: Directive; fieldDirective: DirectiveNode } | undefined { + const directives = getDirectives(node); + for (const name in directives) { + if (name !== 'include' && name !== 'skip' && store.directives[name]) + return { + storeDirective: store.directives[name], + fieldDirective: directives[name]!, + }; + } +} + const readSelection = ( ctx: Context, key: string, @@ -356,8 +373,7 @@ const readSelection = ( let node: FormattedNode | void; const output = InMemoryData.makeData(input); while ((node = iterate()) !== undefined) { - const fieldDirectives = Object.keys(node._directives || {}).map(x => x); - const storeDirective = fieldDirectives.find(x => store.directives[x]); + const foundDirective = getFieldDirective(node, store); // Derive the needed data from our node. const fieldName = getName(node); @@ -376,7 +392,7 @@ const readSelection = ( ctx.__internal.path.push(fieldAlias); // We temporarily store the data field in here, but undefined // means that the value is missing from the cache - let dataFieldValue: void | DataField; + let dataFieldValue: void | DataField = undefined; if (fieldName === '__typename') { // We directly assign the typename as it's already available @@ -386,7 +402,7 @@ const readSelection = ( dataFieldValue = resultValue; } else if ( InMemoryData.currentOperation === 'read' && - ((resolvers && resolvers[fieldName]) || storeDirective) + ((resolvers && resolvers[fieldName]) || foundDirective) ) { // We have to update the information in context to reflect the info // that the resolver will receive @@ -398,7 +414,7 @@ const readSelection = ( output[fieldAlias] = fieldValue; } - if (resolvers && resolvers[fieldName] && storeDirective) { + if (resolvers && resolvers[fieldName] && foundDirective) { warn( `A resolver and directive is being used at "${typename}.${fieldName}", only the directive will apply.`, 28 @@ -412,11 +428,12 @@ const readSelection = ( store, ctx ); - } else if (storeDirective) { - const fieldDirective = node._directives![storeDirective]; - const directiveArguments = - getFieldArguments(fieldDirective, ctx.variables) || {}; - dataFieldValue = store.directives[storeDirective]!(directiveArguments)( + } else { + const directiveArguments = getFieldArguments( + foundDirective!.fieldDirective, + ctx.variables + ); + dataFieldValue = foundDirective!.storeDirective(directiveArguments)( output, fieldArgs || ({} as Variables), store, diff --git a/exchanges/graphcache/src/store/store.ts b/exchanges/graphcache/src/store/store.ts index db27dfe774..9c6fbfb00a 100644 --- a/exchanges/graphcache/src/store/store.ts +++ b/exchanges/graphcache/src/store/store.ts @@ -39,11 +39,11 @@ type DocumentNode = TypedDocumentNode; type RootField = 'query' | 'mutation' | 'subscription'; const defaultDirectives: DirectivesConfig = { - optional: (_parent, args, cache, info) => { + optional: () => (_parent, args, cache, info) => { const result = cache.resolve(info.parentFieldKey, info.fieldName, args); return result === undefined ? null : result; }, - required: (_parent, args, cache, info) => { + required: () => (_parent, args, cache, info) => { const result = cache.resolve(info.parentFieldKey, info.fieldName, args); return result === null ? undefined : result; }, diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts index fcb8818d90..dfeb6aab85 100644 --- a/exchanges/graphcache/src/types.ts +++ b/exchanges/graphcache/src/types.ts @@ -684,29 +684,6 @@ export type Resolver< ): Result; }['bivarianceHack']; -/** Cache Directive, which may resolve or replace data during cache reads. - * - * @param parent - The GraphQL object that is currently being constructed from cache data. - * @param args - This field’s arguments. - * @param cache - {@link Cache} interface. - * @param info - {@link ResolveInfo} interface. - * @returns a {@link ResolverResult}, which is an updated value, partial entity, or entity key - * - * @see {@link https://urql.dev/goto/docs/graphcache/local-directives} for the full directives docs. - */ -export type Directive< - ParentData = DataFields, - Args = Variables, - Result = ResolverResult -> = { - bivarianceHack( - parent: ParentData, - args: Args, - cache: Cache, - info: ResolveInfo & { directiveArguments: Record } - ): Result; -}['bivarianceHack']; - /** Configures resolvers which replace cached reuslts with custom values. * * @remarks @@ -724,6 +701,10 @@ export type ResolverConfig = { } | void; }; +export type Directive = ( + directiveArguments: Record | null +) => Resolver; + export type DirectivesConfig = { [directiveName: string]: Directive; }; From 703176101883814f71885f87ba965ff268a42982 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 20 Jul 2023 08:02:52 +0200 Subject: [PATCH 13/16] Update exchanges/graphcache/src/operations/query.ts Co-authored-by: Phil Pluckthun --- exchanges/graphcache/src/operations/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 77ae94b08a..1698367ab0 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -392,7 +392,7 @@ const readSelection = ( ctx.__internal.path.push(fieldAlias); // We temporarily store the data field in here, but undefined // means that the value is missing from the cache - let dataFieldValue: void | DataField = undefined; + let dataFieldValue: undefined | DataField; if (fieldName === '__typename') { // We directly assign the typename as it's already available From 823fe3e9d43cfb64c3887a8b40fcd6b0cc3a041e Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 20 Jul 2023 08:21:39 +0200 Subject: [PATCH 14/16] undo as this would be a breaking change for graphcache codegen --- exchanges/graphcache/src/operations/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 1698367ab0..77ae94b08a 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -392,7 +392,7 @@ const readSelection = ( ctx.__internal.path.push(fieldAlias); // We temporarily store the data field in here, but undefined // means that the value is missing from the cache - let dataFieldValue: undefined | DataField; + let dataFieldValue: void | DataField = undefined; if (fieldName === '__typename') { // We directly assign the typename as it's already available From 5d7172ea48f4526e854d151d42192851b3f89183 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Fri, 21 Jul 2023 12:30:31 +0100 Subject: [PATCH 15/16] Refactor field resolver compute --- exchanges/graphcache/src/operations/query.ts | 83 ++++++++++---------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 77ae94b08a..36334dec33 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -4,7 +4,6 @@ import { FieldNode, DocumentNode, FragmentDefinitionNode, - DirectiveNode, } from '@0no-co/graphql.web'; import { @@ -27,7 +26,7 @@ import { Link, OperationRequest, Dependencies, - Directive, + Resolver, } from '../types'; import { joinKeys, keyOfField } from '../store/keys'; @@ -298,18 +297,41 @@ export const _queryFragment = ( return result; }; -function getFieldDirective( +function getFieldResolver( + ctx: Context, node: FormattedNode, - store: Store -): { storeDirective: Directive; fieldDirective: DirectiveNode } | undefined { + typename: string, + fieldName: string +): Resolver | void { + const resolvers = ctx.store.resolvers[typename]; + const fieldResolver = resolvers && resolvers[fieldName]; const directives = getDirectives(node); + + let directiveResolver: Resolver | undefined; for (const name in directives) { - if (name !== 'include' && name !== 'skip' && store.directives[name]) - return { - storeDirective: store.directives[name], - fieldDirective: directives[name]!, - }; + const directiveNode = directives[name]; + if ( + directiveNode && + name !== 'include' && + name !== 'skip' && + ctx.store.directives[name] + ) { + directiveResolver = ctx.store.directives[name]( + getFieldArguments(directiveNode, ctx.variables) + ); + if (process.env.NODE_ENV === 'production') return directiveResolver; + break; + } } + + if (fieldResolver && directiveResolver) { + warn( + `A resolver and directive is being used at "${typename}.${fieldName}" simultaneously. Only the directive will apply.`, + 28 + ); + } + + return fieldResolver; } const readSelection = ( @@ -357,7 +379,6 @@ const readSelection = ( return; } - const resolvers = store.resolvers[typename]; const iterate = makeSelectionIterator( typename, entityKey, @@ -373,13 +394,12 @@ const readSelection = ( let node: FormattedNode | void; const output = InMemoryData.makeData(input); while ((node = iterate()) !== undefined) { - const foundDirective = getFieldDirective(node, store); - // Derive the needed data from our node. const fieldName = getName(node); const fieldArgs = getFieldArguments(node, ctx.variables); const fieldAlias = getFieldAlias(node); const fieldKey = keyOfField(fieldName, fieldArgs); + const resolver = getFieldResolver(ctx, node, typename, fieldName); const key = joinKeys(entityKey, fieldKey); const fieldValue = InMemoryData.readRecord(entityKey, fieldKey); const resultValue = result ? result[fieldName] : undefined; @@ -400,10 +420,7 @@ const readSelection = ( } else if (resultValue !== undefined && node.selectionSet === undefined) { // The field is a scalar and can be retrieved directly from the result dataFieldValue = resultValue; - } else if ( - InMemoryData.currentOperation === 'read' && - ((resolvers && resolvers[fieldName]) || foundDirective) - ) { + } else if (InMemoryData.currentOperation === 'read' && resolver) { // We have to update the information in context to reflect the info // that the resolver will receive updateContext(ctx, output, typename, entityKey, key, fieldName); @@ -414,32 +431,12 @@ const readSelection = ( output[fieldAlias] = fieldValue; } - if (resolvers && resolvers[fieldName] && foundDirective) { - warn( - `A resolver and directive is being used at "${typename}.${fieldName}", only the directive will apply.`, - 28 - ); - } - - if (resolvers && resolvers[fieldName]) { - dataFieldValue = resolvers[fieldName]!( - output, - fieldArgs || ({} as Variables), - store, - ctx - ); - } else { - const directiveArguments = getFieldArguments( - foundDirective!.fieldDirective, - ctx.variables - ); - dataFieldValue = foundDirective!.storeDirective(directiveArguments)( - output, - fieldArgs || ({} as Variables), - store, - ctx - ); - } + dataFieldValue = resolver( + output, + fieldArgs || ({} as Variables), + store, + ctx + ); if (node.selectionSet) { // When it has a selection set we are resolving an entity with a From cf193919d3a8dd34fde2e6c3ccec94a0e5e002ea Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Fri, 21 Jul 2023 14:00:58 +0100 Subject: [PATCH 16/16] Fix typo in getFieldResolver --- exchanges/graphcache/src/operations/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 36334dec33..e87f1045e4 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -331,7 +331,7 @@ function getFieldResolver( ); } - return fieldResolver; + return directiveResolver || fieldResolver; } const readSelection = (