Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

feat(graphcache): local directives (#3306)

Co-authored-by: Phil Pluckthun <phil@kitten.sh>

+1 -1
docs/graphcache/cache-updates.md
···
---
title: Cache Updates
-
order: 3
---
# Cache Updates
···
---
title: Cache Updates
+
order: 4
---
# Cache Updates
+6 -1
docs/graphcache/errors.md
···
---
title: Errors
-
order: 7
---
# Help!
···
Please make sure that you're not calling `cache.updateQuery`,
`cache.writeFragment`, or `cache.link` inside `resolvers`.
···
---
title: Errors
+
order: 8
---
# Help!
···
Please make sure that you're not calling `cache.updateQuery`,
`cache.writeFragment`, or `cache.link` inside `resolvers`.
+
+
## (28) Resolver and directive match the same field
+
+
When you have a resolver defined on a field you shouln't be combining it with a directive as the directive
+
will apply and the resolver will be void.
+35
docs/graphcache/local-directives.md
···
···
+
---
+
title: Local Directives
+
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: directiveArguments => () => {
+
/* Resolver */
+
},
+
},
+
});
+
```
+
+
The function signature of a directive is a function which receives the arguments the directive is called with in the document.
+
That function should returns a [Resolver](./local-directives.md).
+
+
### Reading on
+
+
[On the next page we'll learn about "Cache Updates".](./cache-updates.md)
+1 -1
docs/graphcache/local-resolvers.md
···
### Reading on
-
[On the next page we'll learn about "Cache Updates".](./cache-updates.md)
···
### Reading on
+
[On the next page we'll learn about "Cache Directives".](./local-directives.md)
+1 -1
docs/graphcache/offline.md
···
---
title: Offline Support
-
order: 6
---
# Offline Support
···
---
title: Offline Support
+
order: 7
---
# Offline Support
+4 -4
docs/graphcache/schema-awareness.md
···
---
title: Schema Awareness
-
order: 4
---
# Schema Awareness
···
```js
const introspectedSchema = {
__schema: {
-
queryType: { name: 'Query', },
-
mutationType: { name: 'Mutation', },
-
subscriptionType: { name: 'Subscription', },
},
};
···
---
title: Schema Awareness
+
order: 5
---
# Schema Awareness
···
```js
const introspectedSchema = {
__schema: {
+
queryType: { name: 'Query' },
+
mutationType: { name: 'Mutation' },
+
subscriptionType: { name: 'Subscription' },
},
};
+2 -1
exchanges/graphcache/src/ast/variables.ts
···
import {
FieldNode,
OperationDefinitionNode,
valueFromASTUntyped,
} from '@0no-co/graphql.web';
···
/** Evaluates a fields arguments taking vars into account */
export const getFieldArguments = (
-
node: FieldNode,
vars: Variables
): null | Variables => {
let args: null | Variables = null;
···
import {
FieldNode,
+
DirectiveNode,
OperationDefinitionNode,
valueFromASTUntyped,
} from '@0no-co/graphql.web';
···
/** Evaluates a fields arguments taking vars into account */
export const getFieldArguments = (
+
node: FieldNode | DirectiveNode,
vars: Variables
): null | Variables => {
let args: null | Variables = null;
+142
exchanges/graphcache/src/cacheExchange.test.ts
···
OperationResult,
CombinedError,
} from '@urql/core';
import { vi, expect, it, describe } from 'vitest';
···
expect(result).toHaveBeenCalledTimes(1);
expect(reexecuteOperation).toHaveBeenCalledTimes(0);
expect(result.mock.calls[0][0]).toHaveProperty('data.author', null);
});
});
···
OperationResult,
CombinedError,
} from '@urql/core';
+
import { print, stripIgnoredCharacters } from 'graphql';
import { vi, expect, it, describe } from 'vitest';
···
expect(result).toHaveBeenCalledTimes(1);
expect(reexecuteOperation).toHaveBeenCalledTimes(0);
expect(result.mock.calls[0][0]).toHaveProperty('data.author', null);
+
});
+
});
+
+
describe('directives', () => {
+
it('returns optional fields as partial', () => {
+
const client = createClient({
+
url: 'http://0.0.0.0',
+
exchanges: [],
+
});
+
const { source: ops$, next } = makeSubject<Operation>();
+
+
const query = gql`
+
{
+
todos {
+
id
+
text
+
completed @_optional
+
}
+
}
+
`;
+
+
const operation = client.createRequestOperation('query', {
+
key: 1,
+
query,
+
variables: undefined,
+
});
+
+
const queryResult: OperationResult = {
+
...queryResponse,
+
operation,
+
data: {
+
__typename: 'Query',
+
todos: [
+
{
+
id: '1',
+
text: 'learn urql',
+
__typename: 'Todo',
+
},
+
],
+
},
+
};
+
+
const reexecuteOperation = vi
+
.spyOn(client, 'reexecuteOperation')
+
.mockImplementation(next);
+
+
const response = vi.fn((forwardOp: Operation): OperationResult => {
+
if (forwardOp.key === 1) return queryResult;
+
return undefined as any;
+
});
+
+
const result = vi.fn();
+
const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
+
+
pipe(
+
cacheExchange({})({ forward, client, dispatchDebug })(ops$),
+
tap(result),
+
publish
+
);
+
+
next(operation);
+
+
expect(response).toHaveBeenCalledTimes(1);
+
expect(result).toHaveBeenCalledTimes(1);
+
expect(reexecuteOperation).toHaveBeenCalledTimes(0);
+
expect(result.mock.calls[0][0].data).toEqual({
+
todos: [
+
{
+
completed: null,
+
id: '1',
+
text: 'learn urql',
+
},
+
],
+
});
+
});
+
+
it('does not return missing required fields', () => {
+
const client = createClient({
+
url: 'http://0.0.0.0',
+
exchanges: [],
+
});
+
const { source: ops$, next } = makeSubject<Operation>();
+
+
const query = gql`
+
{
+
todos {
+
id
+
text
+
completed @_required
+
}
+
}
+
`;
+
+
const operation = client.createRequestOperation('query', {
+
key: 1,
+
query,
+
variables: undefined,
+
});
+
+
const queryResult: OperationResult = {
+
...queryResponse,
+
operation,
+
data: {
+
__typename: 'Query',
+
todos: [
+
{
+
id: '1',
+
text: 'learn urql',
+
__typename: 'Todo',
+
},
+
],
+
},
+
};
+
+
const reexecuteOperation = vi
+
.spyOn(client, 'reexecuteOperation')
+
.mockImplementation(next);
+
+
const response = vi.fn((forwardOp: Operation): OperationResult => {
+
if (forwardOp.key === 1) return queryResult;
+
return undefined as any;
+
});
+
+
const result = vi.fn();
+
const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share);
+
+
pipe(
+
cacheExchange({})({ forward, client, dispatchDebug })(ops$),
+
tap(result),
+
publish
+
);
+
+
next(operation);
+
+
expect(response).toHaveBeenCalledTimes(1);
+
expect(result).toHaveBeenCalledTimes(1);
+
expect(
+
stripIgnoredCharacters(print(response.mock.calls[0][0].query))
+
).toEqual('{todos{id text completed __typename}}');
+
expect(reexecuteOperation).toHaveBeenCalledTimes(0);
+
expect(result.mock.calls[0][0].data).toEqual(null);
});
});
+1290 -1150
exchanges/graphcache/src/extras/relayPagination.test.ts
···
import { gql } from '@urql/core';
-
import { it, expect } from 'vitest';
import { __initAnd_query as query } from '../operations/query';
import { __initAnd_write as write } from '../operations/write';
import { Store } from '../store/store';
···
};
}
-
it('works with forward pagination', () => {
-
const Pagination = gql`
-
query ($cursor: String) {
-
__typename
-
items(first: 1, after: $cursor) {
__typename
-
edges {
__typename
-
node {
__typename
id
}
-
}
-
nodes {
-
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
hasNextPage
-
endCursor
}
}
-
}
-
`;
-
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 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({
-
...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) {
__typename
-
edges {
__typename
-
node {
__typename
id
}
-
}
-
nodes {
-
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
hasPreviousPage
-
startCursor
}
}
-
}
-
`;
-
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 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);
-
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]],
-
},
});
-
});
-
it('handles duplicate edges', () => {
-
const Pagination = gql`
-
query ($cursor: String) {
-
__typename
-
items(first: 2, after: $cursor) {
__typename
-
edges {
__typename
-
node {
__typename
id
}
-
}
-
nodes {
-
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
hasNextPage
-
endCursor
}
}
-
}
-
`;
-
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 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 });
-
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) {
__typename
-
edges {
__typename
-
node {
__typename
id
}
-
}
-
nodes {
-
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
hasPreviousPage
-
hasNextPage
-
startCursor
-
endCursor
}
}
-
}
-
`;
-
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 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,
},
-
},
-
};
-
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) {
__typename
-
edges {
__typename
-
node {
__typename
id
}
-
}
-
nodes {
-
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
hasPreviousPage
-
hasNextPage
-
startCursor
-
endCursor
}
}
-
}
-
`;
-
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 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,
},
-
},
-
};
-
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 {
__typename
-
node {
__typename
id
}
-
}
-
nodes {
-
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
hasNextPage
-
endCursor
}
}
-
}
-
`;
-
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,
},
-
},
-
});
-
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);
-
});
-
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(),
},
-
},
-
});
-
write(
-
store,
-
{ query: Pagination },
-
{
-
__typename: 'Query',
-
items: {
-
__typename: 'ItemsConnection',
-
edges: [],
-
nodes: [],
-
},
-
}
-
);
-
const res = query(store, {
-
query: Pagination,
-
});
-
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) {
__typename
-
totalCount
}
-
}
-
`;
-
const store = new Store({
-
resolvers: {
-
Query: {
-
items: relayPagination(),
},
-
},
-
});
-
write(
-
store,
-
{ query: Pagination },
-
{
-
__typename: 'Query',
-
items: {
-
__typename: 'ItemsConnection',
-
totalCount: 2,
-
},
-
}
-
);
-
const resOne = query(store, {
-
query: Pagination,
-
});
-
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) {
__typename
-
edges {
__typename
-
node {
__typename
id
}
-
}
-
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 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);
-
const res = query(store, {
-
query: Pagination,
-
variables: { first: 2 },
});
-
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) {
__typename
-
edges {
__typename
-
node {
__typename
id
}
-
}
-
nodes {
-
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
hasPreviousPage
-
hasNextPage
-
startCursor
-
endCursor
}
}
-
}
-
`;
-
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',
},
-
},
-
};
-
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 },
});
-
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) {
__typename
-
edges {
__typename
-
node {
__typename
id
}
-
}
-
nodes {
-
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
hasPreviousPage
-
hasNextPage
-
startCursor
-
endCursor
}
}
-
}
-
`;
-
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',
},
-
},
-
};
-
write(store, { query: Pagination, variables: { first: 5 } }, results);
-
const res = query(store, {
-
query: Pagination,
-
variables: { after: '3', first: 2, last: 2 },
});
-
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) {
__typename
-
edges {
__typename
-
node {
__typename
id
}
-
}
-
nodes {
-
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
hasPreviousPage
-
hasNextPage
-
startCursor
-
endCursor
}
}
-
}
-
`;
-
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',
},
-
},
-
};
-
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 },
-
});
-
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) {
__typename
-
edges {
__typename
-
node {
__typename
id
}
-
}
-
nodes {
-
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
hasPreviousPage
-
hasNextPage
-
startCursor
-
endCursor
}
}
-
}
-
`;
-
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 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, filter: 'b' } },
-
results2
-
);
-
const res = query(store, {
-
query: Pagination,
-
variables: { first: 2, filter: 'b' },
});
-
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) {
__typename
-
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 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);
-
const res = query(store, {
-
query: Pagination,
-
variables: { first: 2 },
});
-
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) {
__typename
-
edges {
__typename
-
node {
__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 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);
-
const res = query(store, {
-
query: Pagination,
-
variables: { first: 2 },
});
-
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) {
__typename
-
edges {
__typename
-
node {
__typename
id
}
-
}
-
nodes {
-
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
hasPreviousPage
-
startCursor
}
}
-
}
-
`;
-
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 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',
},
-
},
-
};
-
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 } });
-
expect(res.partial).toBe(false);
-
expect(res.data).toEqual(pageTwo);
-
write(store, { query: Pagination, variables: { last: 3 } }, pageThree);
-
res = query(store, { query: Pagination, variables: { last: 3 } });
-
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) {
__typename
-
edges {
__typename
-
node {
__typename
id
}
-
}
-
nodes {
-
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
hasNextPage
-
endCursor
}
}
-
}
-
`;
-
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 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);
-
const res = query(store, { query: Pagination, variables: { first: 2 } });
-
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) {
__typename
-
nodes {
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
startCursor
-
endCursor
}
}
-
}
-
`;
-
const PaginationBackward = gql`
-
query ($last: Int!, $before: String) {
-
__typename
-
items(last: $last, before: $before) {
__typename
-
nodes {
__typename
-
id
-
}
-
pageInfo {
-
__typename
-
startCursor
-
endCursor
}
}
-
}
-
`;
-
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 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,
},
-
},
-
};
-
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);
});
-
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
}
}
-
}
-
`;
-
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);
});
···
import { gql } from '@urql/core';
+
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';
···
};
}
+
describe('as resolver', () => {
+
it('works with forward pagination', () => {
+
const Pagination = gql`
+
query ($cursor: String) {
__typename
+
items(first: 1, after: $cursor) {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
__typename
id
}
+
pageInfo {
+
__typename
+
hasNextPage
+
endCursor
+
}
}
}
+
`;
+
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 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({
+
...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) {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
__typename
id
}
+
pageInfo {
+
__typename
+
hasPreviousPage
+
startCursor
+
}
}
}
+
`;
+
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 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);
+
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]],
+
},
+
});
});
+
it('handles duplicate edges', () => {
+
const Pagination = gql`
+
query ($cursor: String) {
__typename
+
items(first: 2, after: $cursor) {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
__typename
id
}
+
pageInfo {
+
__typename
+
hasNextPage
+
endCursor
+
}
}
}
+
`;
+
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 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 });
+
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) {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
__typename
id
}
+
pageInfo {
+
__typename
+
hasPreviousPage
+
hasNextPage
+
startCursor
+
endCursor
+
}
}
}
+
`;
+
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 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,
+
},
},
+
};
+
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) {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
__typename
id
}
+
pageInfo {
+
__typename
+
hasPreviousPage
+
hasNextPage
+
startCursor
+
endCursor
+
}
}
}
+
`;
+
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 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,
+
},
},
+
};
+
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 {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
__typename
id
}
+
pageInfo {
+
__typename
+
hasNextPage
+
endCursor
+
}
}
}
+
`;
+
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,
+
},
},
+
});
+
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);
+
});
+
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(),
+
},
},
+
});
+
write(
+
store,
+
{ query: Pagination },
+
{
+
__typename: 'Query',
+
items: {
+
__typename: 'ItemsConnection',
+
edges: [],
+
nodes: [],
+
},
+
}
+
);
+
const res = query(store, {
+
query: Pagination,
+
});
+
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) {
+
__typename
+
totalCount
+
}
}
+
`;
+
const store = new Store({
+
resolvers: {
+
Query: {
+
items: relayPagination(),
+
},
},
+
});
+
write(
+
store,
+
{ query: Pagination },
+
{
+
__typename: 'Query',
+
items: {
+
__typename: 'ItemsConnection',
+
totalCount: 2,
+
},
+
}
+
);
+
const resOne = query(store, {
+
query: Pagination,
+
});
+
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) {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
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 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);
+
const res = query(store, {
+
query: Pagination,
+
variables: { first: 2 },
+
});
+
+
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) {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
__typename
id
}
+
pageInfo {
+
__typename
+
hasPreviousPage
+
hasNextPage
+
startCursor
+
endCursor
+
}
}
}
+
`;
+
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',
+
},
},
+
};
+
+
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 },
+
});
+
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) {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
__typename
id
}
+
pageInfo {
+
__typename
+
hasPreviousPage
+
hasNextPage
+
startCursor
+
endCursor
+
}
}
}
+
`;
+
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',
+
},
},
+
};
+
write(store, { query: Pagination, variables: { first: 5 } }, results);
+
const res = query(store, {
+
query: Pagination,
+
variables: { after: '3', first: 2, last: 2 },
+
});
+
+
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) {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
__typename
id
}
+
pageInfo {
+
__typename
+
hasPreviousPage
+
hasNextPage
+
startCursor
+
endCursor
+
}
}
}
+
`;
+
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',
+
},
},
+
};
+
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 },
+
});
+
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) {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
__typename
id
}
+
pageInfo {
+
__typename
+
hasPreviousPage
+
hasNextPage
+
startCursor
+
endCursor
+
}
}
}
+
`;
+
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 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, filter: 'b' } },
+
results2
+
);
+
const res = query(store, {
+
query: Pagination,
+
variables: { first: 2, filter: 'b' },
+
});
+
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) {
__typename
+
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 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);
+
const res = query(store, {
+
query: Pagination,
+
variables: { first: 2 },
+
});
+
+
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) {
__typename
+
edges {
__typename
+
node {
+
__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 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);
+
+
const res = query(store, {
+
query: Pagination,
+
variables: { first: 2 },
+
});
+
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) {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
__typename
id
}
+
pageInfo {
+
__typename
+
hasPreviousPage
+
startCursor
+
}
}
}
+
`;
+
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 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',
+
},
},
+
};
+
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 } });
+
expect(res.partial).toBe(false);
+
expect(res.data).toEqual(pageTwo);
+
write(store, { query: Pagination, variables: { last: 3 } }, pageThree);
+
res = query(store, { query: Pagination, variables: { last: 3 } });
+
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) {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
__typename
id
}
+
pageInfo {
+
__typename
+
hasNextPage
+
endCursor
+
}
}
}
+
`;
+
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 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);
+
const res = query(store, { query: Pagination, variables: { first: 2 } });
+
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) {
__typename
+
nodes {
+
__typename
+
id
+
}
+
pageInfo {
+
__typename
+
startCursor
+
endCursor
+
}
}
}
+
`;
+
const PaginationBackward = gql`
+
query ($last: Int!, $before: String) {
__typename
+
items(last: $last, before: $before) {
__typename
+
nodes {
+
__typename
+
id
+
}
+
pageInfo {
+
__typename
+
startCursor
+
endCursor
+
}
}
}
+
`;
+
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 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,
+
},
},
+
};
+
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);
});
+
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
+
}
+
}
+
}
+
`;
+
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);
+
});
});
+
describe('as directive', () => {
+
it('works with forward pagination', () => {
+
const Pagination = gql`
+
query ($cursor: String) {
__typename
+
items(first: 1, after: $cursor) @_relayPagination {
__typename
+
edges {
+
__typename
+
node {
+
__typename
+
id
+
}
+
}
+
nodes {
+
__typename
+
id
+
}
+
pageInfo {
+
__typename
+
hasNextPage
+
endCursor
+
}
}
}
+
`;
+
const store = new Store({
+
directives: {
+
relayPagination: () => relayPagination(),
},
+
});
+
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,
+
},
+
},
+
};
+
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]],
+
nodes: [pageOne.items.nodes[0], pageTwo.items.nodes[0]],
+
},
+
});
+
});
});
+499 -352
exchanges/graphcache/src/extras/simplePagination.test.ts
···
import { gql } from '@urql/core';
-
import { it, expect } 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) {
__typename
-
id
-
name
}
-
}
-
`;
-
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);
-
});
-
it('works with backwards pagination', () => {
-
const Pagination = gql`
-
query ($skip: Number, $limit: Number) {
-
__typename
-
persons(skip: $skip, limit: $limit) {
__typename
-
id
-
name
}
-
}
-
`;
-
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);
-
});
-
it('handles duplicates', () => {
-
const Pagination = gql`
-
query ($skip: Number, $limit: Number) {
-
__typename
-
persons(skip: $skip, limit: $limit) {
__typename
-
id
-
name
}
-
}
-
`;
-
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]],
-
});
-
});
-
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) {
__typename
-
id
-
name
}
-
}
-
`;
-
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' },
});
-
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) {
__typename
-
id
-
name
}
-
}
-
`;
-
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],
});
-
});
-
it('should preserve the correct order in backward pagination', () => {
-
const Pagination = gql`
-
query ($skip: Number, $limit: Number) {
-
__typename
-
persons(skip: $skip, limit: $limit) {
__typename
-
id
-
name
}
-
}
-
`;
-
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],
});
});
-
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
}
-
}
-
`;
-
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);
});
···
import { gql } from '@urql/core';
+
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 { MergeMode, simplePagination } from './simplePagination';
+
describe('as resolver', () => {
+
it('works with forward pagination', () => {
+
const Pagination = gql`
+
query ($skip: Number, $limit: Number) {
__typename
+
persons(skip: $skip, limit: $limit) {
+
__typename
+
id
+
name
+
}
}
+
`;
+
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);
});
+
it('works with backwards pagination', () => {
+
const Pagination = gql`
+
query ($skip: Number, $limit: Number) {
__typename
+
persons(skip: $skip, limit: $limit) {
+
__typename
+
id
+
name
+
}
}
+
`;
+
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);
});
+
+
it('handles duplicates', () => {
+
const Pagination = gql`
+
query ($skip: Number, $limit: Number) {
+
__typename
+
persons(skip: $skip, limit: $limit) {
+
__typename
+
id
+
name
+
}
+
}
+
`;
+
+
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]],
+
});
});
+
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) {
+
__typename
+
id
+
name
+
}
}
+
`;
+
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' },
+
});
+
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) {
+
__typename
+
id
+
name
+
}
}
+
`;
+
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],
+
});
});
+
it('should preserve the correct order in backward pagination', () => {
+
const Pagination = gql`
+
query ($skip: Number, $limit: Number) {
__typename
+
persons(skip: $skip, limit: $limit) {
+
__typename
+
id
+
name
+
}
}
+
`;
+
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],
+
});
});
+
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
+
}
}
+
`;
+
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);
});
});
+
describe('as directive', () => {
+
it('works with forward pagination', () => {
+
const Pagination = gql`
+
query ($skip: Number, $limit: Number) {
__typename
+
persons(skip: $skip, limit: $limit) @_simplePagination {
+
__typename
+
id
+
name
+
}
}
+
`;
+
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);
});
+
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);
+
});
});
+2 -1
exchanges/graphcache/src/helpers/help.ts
···
| 24
| 25
| 26
-
| 27;
type DebugNode = ExecutableDefinitionNode | InlineFragmentNode;
···
| 24
| 25
| 26
+
| 27
+
| 28;
type DebugNode = ExecutableDefinitionNode | InlineFragmentNode;
+43 -8
exchanges/graphcache/src/operations/query.ts
···
getMainOperation,
normalizeVariables,
getFieldArguments,
} from '../ast';
import {
···
Link,
OperationRequest,
Dependencies,
} from '../types';
import { joinKeys, keyOfField } from '../store/keys';
···
return result;
};
const readSelection = (
ctx: Context,
key: string,
···
return;
}
-
const resolvers = store.resolvers[typename];
const iterate = makeSelectionIterator(
typename,
entityKey,
···
const fieldArgs = getFieldArguments(node, ctx.variables);
const fieldAlias = getFieldAlias(node);
const fieldKey = keyOfField(fieldName, fieldArgs);
const key = joinKeys(entityKey, fieldKey);
const fieldValue = InMemoryData.readRecord(entityKey, fieldKey);
const resultValue = result ? result[fieldName] : undefined;
···
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;
if (fieldName === '__typename') {
// We directly assign the typename as it's already available
···
} 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]
-
) {
// We have to update the information in context to reflect the info
// that the resolver will receive
updateContext(ctx, output, typename, entityKey, key, fieldName);
···
output[fieldAlias] = fieldValue;
}
-
dataFieldValue = resolvers[fieldName]!(
output,
fieldArgs || ({} as Variables),
store,
···
getMainOperation,
normalizeVariables,
getFieldArguments,
+
getDirectives,
} from '../ast';
import {
···
Link,
OperationRequest,
Dependencies,
+
Resolver,
} from '../types';
import { joinKeys, keyOfField } from '../store/keys';
···
return result;
};
+
function getFieldResolver(
+
ctx: Context,
+
node: FormattedNode<FieldNode>,
+
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) {
+
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 directiveResolver || fieldResolver;
+
}
+
const readSelection = (
ctx: Context,
key: string,
···
return;
}
const iterate = makeSelectionIterator(
typename,
entityKey,
···
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;
···
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;
if (fieldName === '__typename') {
// We directly assign the typename as it's already available
···
} 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' && 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);
···
output[fieldAlias] = fieldValue;
}
+
dataFieldValue = resolver(
output,
fieldArgs || ({} as Variables),
store,
+15
exchanges/graphcache/src/store/store.ts
···
KeyingConfig,
Entity,
CacheExchangeOpts,
} from '../types';
import { invariant } from '../helpers/help';
···
type DocumentNode = TypedDocumentNode<any, any>;
type RootField = 'query' | 'mutation' | 'subscription';
/** Implementation of the {@link Cache} interface as created internally by the {@link cacheExchange}.
* @internal
*/
···
{
data: InMemoryData.InMemoryData;
resolvers: ResolverConfig;
updates: UpdatesConfig;
optimisticMutations: OptimisticMutationConfig;
···
if (!opts) opts = {} as C;
this.resolvers = opts.resolvers || {};
this.optimisticMutations = opts.optimistic || {};
this.keys = opts.keys || {};
···
KeyingConfig,
Entity,
CacheExchangeOpts,
+
DirectivesConfig,
} from '../types';
import { invariant } from '../helpers/help';
···
type DocumentNode = TypedDocumentNode<any, any>;
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
*/
···
{
data: InMemoryData.InMemoryData;
+
directives: DirectivesConfig;
resolvers: ResolverConfig;
updates: UpdatesConfig;
optimisticMutations: OptimisticMutationConfig;
···
if (!opts) opts = {} as C;
this.resolvers = opts.resolvers || {};
+
this.directives =
+
{ ...defaultDirectives, ...opts.directives } || defaultDirectives;
this.optimisticMutations = opts.optimistic || {};
this.keys = opts.keys || {};
+16
exchanges/graphcache/src/types.ts
···
* @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers} for the full resolvers docs.
*/
resolvers?: ResolverConfig;
/** Configures optimistic updates to react to mutations instantly before an API response.
*
* @remarks
···
[typeName: string]: {
[fieldName: string]: Resolver | void;
} | void;
};
/** Cache Updater, which defines additional cache updates after cache writes.
···
* @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers} for the full resolvers docs.
*/
resolvers?: ResolverConfig;
+
/** Configures directives which can perform custom logic on fields.
+
*
+
* @remarks
+
* 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.
+
*/
+
directives?: DirectivesConfig;
/** Configures optimistic updates to react to mutations instantly before an API response.
*
* @remarks
···
[typeName: string]: {
[fieldName: string]: Resolver | void;
} | void;
+
};
+
+
export type Directive = (
+
directiveArguments: Record<string, unknown> | null
+
) => Resolver;
+
+
export type DirectivesConfig = {
+
[directiveName: string]: Directive;
};
/** Cache Updater, which defines additional cache updates after cache writes.