Skip to content

Commit 615df04

Browse files
jtomaszewskiclaude
andcommitted
feat: support nested pagination arguments in relay pagination
- Add extractPaginationArgs function to handle nested pagination args - Update compareArgs to areNonPaginationArgsEqual for better arg filtering - Support common nested patterns: paging, pagination, page - Add comprehensive test for nested pagination functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b31d5cb commit 615df04

2 files changed

Lines changed: 180 additions & 32 deletions

File tree

exchanges/graphcache/src/extras/relayPagination.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,4 +1613,85 @@ describe('as directive', () => {
16131613
},
16141614
});
16151615
});
1616+
1617+
it('works with nested pagination args', () => {
1618+
const Pagination = gql`
1619+
query ($cursor: String) {
1620+
__typename
1621+
items(pagination: { first: 1, after: $cursor }) {
1622+
__typename
1623+
edges {
1624+
__typename
1625+
node {
1626+
__typename
1627+
id
1628+
}
1629+
}
1630+
nodes {
1631+
__typename
1632+
id
1633+
}
1634+
pageInfo {
1635+
__typename
1636+
hasNextPage
1637+
endCursor
1638+
}
1639+
}
1640+
}
1641+
`;
1642+
1643+
const store = new Store({
1644+
resolvers: {
1645+
Query: {
1646+
items: relayPagination(),
1647+
},
1648+
},
1649+
});
1650+
1651+
const pageOne = {
1652+
__typename: 'Query',
1653+
items: {
1654+
__typename: 'ItemsConnection',
1655+
edges: [itemEdge(1)],
1656+
nodes: [itemNode(1)],
1657+
pageInfo: {
1658+
__typename: 'PageInfo',
1659+
hasNextPage: true,
1660+
endCursor: '1',
1661+
},
1662+
},
1663+
};
1664+
1665+
const pageTwo = {
1666+
__typename: 'Query',
1667+
items: {
1668+
__typename: 'ItemsConnection',
1669+
edges: [itemEdge(2)],
1670+
nodes: [itemNode(2)],
1671+
pageInfo: {
1672+
__typename: 'PageInfo',
1673+
hasNextPage: false,
1674+
endCursor: null,
1675+
},
1676+
},
1677+
};
1678+
1679+
write(store, { query: Pagination, variables: { cursor: null } }, pageOne);
1680+
write(store, { query: Pagination, variables: { cursor: '1' } }, pageTwo);
1681+
1682+
const res = query(store, {
1683+
query: Pagination,
1684+
variables: { cursor: null },
1685+
});
1686+
1687+
expect(res.partial).toBe(false);
1688+
expect(res.data).toEqual({
1689+
...pageTwo,
1690+
items: {
1691+
...pageTwo.items,
1692+
edges: [pageOne.items.edges[0], pageTwo.items.edges[0]],
1693+
nodes: [pageOne.items.nodes[0], pageTwo.items.nodes[0]],
1694+
},
1695+
});
1696+
});
16161697
});

exchanges/graphcache/src/extras/relayPagination.ts

Lines changed: 99 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,39 @@ const defaultPageInfo: PageInfo = {
4242

4343
const ensureKey = (x: any): string | null => (typeof x === 'string' ? x : null);
4444

45+
const extractPaginationArgs = (args: Variables) => {
46+
const result = {
47+
first: undefined as number | undefined,
48+
last: undefined as number | undefined,
49+
after: null as string | null,
50+
before: null as string | null,
51+
};
52+
53+
// Check for nested args in common patterns
54+
const nestedPatterns = ['paging', 'pagination', 'page'];
55+
for (const pattern of nestedPatterns) {
56+
const nested = args[pattern];
57+
if (nested && typeof nested === 'object') {
58+
if ('first' in nested && typeof nested.first === 'number')
59+
result.first = nested.first;
60+
if ('last' in nested && typeof nested.last === 'number')
61+
result.last = nested.last;
62+
if ('after' in nested && typeof nested.after === 'string')
63+
result.after = nested.after;
64+
if ('before' in nested && typeof nested.before === 'string')
65+
result.before = nested.before;
66+
}
67+
}
68+
69+
// Check for direct args
70+
if (typeof args.first === 'number') result.first = args.first;
71+
if (typeof args.last === 'number') result.last = args.last;
72+
if (typeof args.after === 'string') result.after = args.after;
73+
if (typeof args.before === 'string') result.before = args.before;
74+
75+
return result;
76+
};
77+
4578
const concatEdges = (
4679
cache: Cache,
4780
leftEdges: NullArray<string>,
@@ -89,24 +122,65 @@ const concatNodes = (
89122
return newNodes;
90123
};
91124

92-
const compareArgs = (
125+
const isPaginationArg = (key: string, value: any): boolean => {
126+
// Direct pagination args
127+
if (
128+
key === 'first' ||
129+
key === 'last' ||
130+
key === 'after' ||
131+
key === 'before'
132+
) {
133+
return true;
134+
}
135+
136+
// Nested pagination args - check common patterns
137+
const nestedPatterns = ['paging', 'pagination', 'page'];
138+
if (
139+
nestedPatterns.includes(key) &&
140+
typeof value === 'object' &&
141+
value !== null &&
142+
!Array.isArray(value)
143+
) {
144+
const nested = value as Variables;
145+
return (
146+
nested.first !== undefined ||
147+
nested.last !== undefined ||
148+
nested.after !== undefined ||
149+
nested.before !== undefined
150+
);
151+
}
152+
153+
return false;
154+
};
155+
156+
const areNonPaginationArgsEqual = (
93157
fieldArgs: Variables,
94158
connectionArgs: Variables
95159
): boolean => {
160+
// Create versions of args without pagination args for comparison
161+
const filteredFieldArgs: Variables = {};
162+
const filteredConnectionArgs: Variables = {};
163+
164+
for (const key in fieldArgs) {
165+
if (!isPaginationArg(key, fieldArgs[key])) {
166+
filteredFieldArgs[key] = fieldArgs[key];
167+
}
168+
}
169+
96170
for (const key in connectionArgs) {
97-
if (
98-
key === 'first' ||
99-
key === 'last' ||
100-
key === 'after' ||
101-
key === 'before'
102-
) {
103-
continue;
104-
} else if (!(key in fieldArgs)) {
171+
if (!isPaginationArg(key, connectionArgs[key])) {
172+
filteredConnectionArgs[key] = connectionArgs[key];
173+
}
174+
}
175+
176+
// Compare non-pagination args
177+
for (const key in filteredConnectionArgs) {
178+
if (!(key in filteredFieldArgs)) {
105179
return false;
106180
}
107181

108-
const argA = fieldArgs[key];
109-
const argB = connectionArgs[key];
182+
const argA = filteredFieldArgs[key];
183+
const argB = filteredConnectionArgs[key];
110184

111185
if (
112186
typeof argA !== typeof argB || typeof argA !== 'object'
@@ -117,17 +191,8 @@ const compareArgs = (
117191
}
118192
}
119193

120-
for (const key in fieldArgs) {
121-
if (
122-
key === 'first' ||
123-
key === 'last' ||
124-
key === 'after' ||
125-
key === 'before'
126-
) {
127-
continue;
128-
}
129-
130-
if (!(key in connectionArgs)) return false;
194+
for (const key in filteredFieldArgs) {
195+
if (!(key in filteredConnectionArgs)) return false;
131196
}
132197

133198
return true;
@@ -235,7 +300,7 @@ export const relayPagination = (
235300

236301
for (let i = 0; i < size; i++) {
237302
const { fieldKey, arguments: args } = fieldInfos[i];
238-
if (args === null || !compareArgs(fieldArgs, args)) {
303+
if (args === null || !areNonPaginationArgsEqual(fieldArgs, args)) {
239304
continue;
240305
}
241306

@@ -247,33 +312,35 @@ export const relayPagination = (
247312
continue;
248313
}
249314

315+
const paginationArgs = extractPaginationArgs(args);
316+
250317
if (
251318
mergeMode === 'inwards' &&
252-
typeof args.last === 'number' &&
253-
typeof args.first === 'number'
319+
typeof paginationArgs.last === 'number' &&
320+
typeof paginationArgs.first === 'number'
254321
) {
255-
const firstEdges = page.edges.slice(0, args.first + 1);
256-
const lastEdges = page.edges.slice(-args.last);
257-
const firstNodes = page.nodes.slice(0, args.first + 1);
258-
const lastNodes = page.nodes.slice(-args.last);
322+
const firstEdges = page.edges.slice(0, paginationArgs.first + 1);
323+
const lastEdges = page.edges.slice(-paginationArgs.last);
324+
const firstNodes = page.nodes.slice(0, paginationArgs.first + 1);
325+
const lastNodes = page.nodes.slice(-paginationArgs.last);
259326

260327
startEdges = concatEdges(cache, startEdges, firstEdges);
261328
endEdges = concatEdges(cache, lastEdges, endEdges);
262329
startNodes = concatNodes(startNodes, firstNodes);
263330
endNodes = concatNodes(lastNodes, endNodes);
264331

265332
pageInfo = page.pageInfo;
266-
} else if (args.after) {
333+
} else if (paginationArgs.after) {
267334
startEdges = concatEdges(cache, startEdges, page.edges);
268335
startNodes = concatNodes(startNodes, page.nodes);
269336
pageInfo.endCursor = page.pageInfo.endCursor;
270337
pageInfo.hasNextPage = page.pageInfo.hasNextPage;
271-
} else if (args.before) {
338+
} else if (paginationArgs.before) {
272339
endEdges = concatEdges(cache, page.edges, endEdges);
273340
endNodes = concatNodes(page.nodes, endNodes);
274341
pageInfo.startCursor = page.pageInfo.startCursor;
275342
pageInfo.hasPreviousPage = page.pageInfo.hasPreviousPage;
276-
} else if (typeof args.last === 'number') {
343+
} else if (typeof paginationArgs.last === 'number') {
277344
endEdges = concatEdges(cache, page.edges, endEdges);
278345
endNodes = concatNodes(page.nodes, endNodes);
279346
pageInfo = page.pageInfo;

0 commit comments

Comments
 (0)