1import { stringifyVariables } from '@urql/core';
2import type { Resolver, Variables, NullArray } from '../types';
3
4export type MergeMode = 'before' | 'after';
5
6/** Input parameters for the {@link simplePagination} factory. */
7export interface PaginationParams {
8 /** The name of the field argument used to define the page’s offset. */
9 offsetArgument?: string;
10 /** The name of the field argument used to define the page’s length. */
11 limitArgument?: string;
12 /** Flip between forward and backwards pagination.
13 *
14 * @remarks
15 * When set to `'after'`, its default, pages are merged forwards and in order.
16 * When set to `'before'`, pages are merged in reverse, putting later pages
17 * in front of earlier ones.
18 */
19 mergeMode?: MergeMode;
20}
21
22/** Creates a {@link Resolver} that combines pages of a primitive pagination field.
23 *
24 * @param options - A {@link PaginationParams} configuration object.
25 * @returns the created pagination {@link Resolver}.
26 *
27 * @remarks
28 * `simplePagination` is a factory that creates a {@link Resolver} that can combine
29 * multiple lists on a paginated field into a single, combined list for infinite
30 * scrolling.
31 *
32 * Hint: It's not recommended to use this when you can handle infinite scrolling
33 * in your UI code instead.
34 *
35 * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers#simple-pagination} for more information.
36 * @see {@link https://urql.dev/goto/docs/basics/ui-patterns/#infinite-scrolling} for an alternate approach.
37 */
38export const simplePagination = ({
39 offsetArgument = 'skip',
40 limitArgument = 'limit',
41 mergeMode = 'after',
42}: PaginationParams = {}): Resolver<any, any, any> => {
43 const compareArgs = (
44 fieldArgs: Variables,
45 connectionArgs: Variables
46 ): boolean => {
47 for (const key in connectionArgs) {
48 if (key === offsetArgument || key === limitArgument) {
49 continue;
50 } else if (!(key in fieldArgs)) {
51 return false;
52 }
53
54 const argA = fieldArgs[key];
55 const argB = connectionArgs[key];
56
57 if (
58 typeof argA !== typeof argB || typeof argA !== 'object'
59 ? argA !== argB
60 : stringifyVariables(argA) !== stringifyVariables(argB)
61 ) {
62 return false;
63 }
64 }
65
66 for (const key in fieldArgs) {
67 if (key === offsetArgument || key === limitArgument) {
68 continue;
69 }
70 if (!(key in connectionArgs)) return false;
71 }
72
73 return true;
74 };
75
76 return (_parent, fieldArgs, cache, info) => {
77 const { parentKey: entityKey, fieldName } = info;
78
79 const allFields = cache.inspectFields(entityKey);
80 const fieldInfos = allFields.filter(info => info.fieldName === fieldName);
81 const size = fieldInfos.length;
82 if (size === 0) {
83 return undefined;
84 }
85
86 const visited = new Set();
87 let result: NullArray<string> = [];
88 let prevOffset: number | null = null;
89
90 for (let i = 0; i < size; i++) {
91 const { fieldKey, arguments: args } = fieldInfos[i];
92 if (args === null || !compareArgs(fieldArgs, args)) {
93 continue;
94 }
95
96 const links = cache.resolve(entityKey, fieldKey) as string[];
97 const currentOffset = args[offsetArgument];
98
99 if (
100 links === null ||
101 links.length === 0 ||
102 typeof currentOffset !== 'number'
103 ) {
104 continue;
105 }
106
107 const tempResult: NullArray<string> = [];
108
109 for (let j = 0; j < links.length; j++) {
110 const link = links[j];
111 if (visited.has(link)) continue;
112 tempResult.push(link);
113 visited.add(link);
114 }
115
116 if (
117 (!prevOffset || currentOffset > prevOffset) ===
118 (mergeMode === 'after')
119 ) {
120 result = [...result, ...tempResult];
121 } else {
122 result = [...tempResult, ...result];
123 }
124
125 prevOffset = currentOffset;
126 }
127
128 const hasCurrentPage = cache.resolve(entityKey, fieldName, fieldArgs);
129 if (hasCurrentPage) {
130 return result;
131 } else if (!(info as any).store.schema) {
132 return undefined;
133 } else {
134 info.partial = true;
135 return result;
136 }
137 };
138};