Skip to content

Commit 000b2ae

Browse files
icholykuhe
andauthored
fix(pagination): allow operation input to specify pagination token (#1500)
* fix(pagination): only use startingToken if it exists * add unit tests * add changeset * undo types change * default return value of withCommand in paginator --------- Co-authored-by: George Fu <[email protected]>
1 parent 2aff9df commit 000b2ae

File tree

4 files changed

+177
-7
lines changed

4 files changed

+177
-7
lines changed

.changeset/lazy-geese-brush.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smithy/types": minor
3+
"@smithy/core": minor
4+
---
5+
6+
allow paginator token fallback to be specified by operation input

packages/core/src/pagination/createPaginator.spec.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ describe(createPaginator.name, () => {
3636
}
3737
}
3838

39+
class ClientStringToken {
40+
private pages = 5;
41+
async send(command: any) {
42+
if (--this.pages > 0) {
43+
return {
44+
outToken: command.input.inToken,
45+
};
46+
}
47+
return {};
48+
}
49+
}
3950
class CommandStringToken {
4051
public middlewareStack = {
4152
add: vi.fn(),
@@ -91,6 +102,158 @@ describe(createPaginator.name, () => {
91102
expect(pages).toEqual(5);
92103
});
93104

105+
it("should prioritize token set in paginator config, fallback to token set in input parameters", async () => {
106+
class CommandExpectPaginatorConfigToken {
107+
public constructor(public input: any) {
108+
expect(input).toMatchObject({
109+
inToken: "abc",
110+
});
111+
}
112+
}
113+
class CommandExpectOperationInputToken {
114+
public constructor(public input: any) {
115+
expect(input).toMatchObject({
116+
inToken: "xyz",
117+
});
118+
}
119+
}
120+
{
121+
const paginate = createPaginator<
122+
PaginationConfiguration,
123+
{ inToken?: string; sizeToken?: number },
124+
{ outToken: string }
125+
>(ClientStringToken, CommandExpectPaginatorConfigToken, "inToken", "outToken", "sizeToken");
126+
127+
let pages = 0;
128+
const client = new ClientStringToken() as any;
129+
130+
for await (const page of paginate(
131+
{
132+
client,
133+
startingToken: "abc",
134+
},
135+
{
136+
inToken: "xyz",
137+
}
138+
)) {
139+
pages += 1;
140+
expect(page).toBeDefined();
141+
}
142+
143+
expect(pages).toEqual(5);
144+
}
145+
{
146+
const paginate = createPaginator<
147+
PaginationConfiguration,
148+
{ inToken?: string; sizeToken?: number },
149+
{ outToken: string }
150+
>(ClientStringToken, CommandExpectOperationInputToken, "inToken", "outToken", "sizeToken");
151+
152+
let pages = 0;
153+
const client = new ClientStringToken() as any;
154+
155+
for await (const page of paginate(
156+
{
157+
client,
158+
},
159+
{
160+
inToken: "xyz",
161+
}
162+
)) {
163+
pages += 1;
164+
expect(page).toBeDefined();
165+
}
166+
167+
expect(pages).toEqual(5);
168+
}
169+
});
170+
171+
it("should prioritize page size set in operation input, fallback to page size set in paginator config (inverted from token priority)", async () => {
172+
class CommandExpectPaginatorPageSize {
173+
public constructor(public input: any) {
174+
expect(input).toMatchObject({
175+
sizeToken: 100,
176+
});
177+
}
178+
}
179+
class CommandExpectOperationInputPageSize {
180+
public constructor(public input: any) {
181+
expect(input).toMatchObject({
182+
sizeToken: 99,
183+
});
184+
}
185+
}
186+
{
187+
const paginate = createPaginator<
188+
PaginationConfiguration,
189+
{ inToken?: string; sizeToken?: number },
190+
{ outToken: string }
191+
>(ClientStringToken, CommandExpectPaginatorPageSize, "inToken", "outToken", "sizeToken");
192+
193+
let pages = 0;
194+
const client = new ClientStringToken() as any;
195+
196+
for await (const page of paginate(
197+
{
198+
client,
199+
pageSize: 100,
200+
},
201+
{
202+
inToken: "abc",
203+
}
204+
)) {
205+
pages += 1;
206+
expect(page).toBeDefined();
207+
}
208+
209+
expect(pages).toEqual(5);
210+
}
211+
{
212+
const paginate = createPaginator<
213+
PaginationConfiguration,
214+
{ inToken?: string; sizeToken?: number },
215+
{ outToken: string }
216+
>(ClientStringToken, CommandExpectOperationInputPageSize, "inToken", "outToken", "sizeToken");
217+
218+
let pages = 0;
219+
const client = new ClientStringToken() as any;
220+
221+
for await (const page of paginate(
222+
{
223+
client,
224+
pageSize: 100,
225+
},
226+
{
227+
sizeToken: 99,
228+
inToken: "abc",
229+
}
230+
)) {
231+
pages += 1;
232+
expect(page).toBeDefined();
233+
}
234+
235+
expect(pages).toEqual(5);
236+
}
237+
});
238+
239+
it("should have the correct AsyncGenerator.TNext type", async () => {
240+
const paginate = createPaginator<
241+
PaginationConfiguration,
242+
{ inToken?: string; sizeToken: number },
243+
{
244+
outToken: string;
245+
}
246+
>(ClientStringToken, CommandStringToken, "inToken", "outToken.outToken2.outToken3", "sizeToken");
247+
const asyncGenerator = paginate(
248+
{ client: new ClientStringToken() as any },
249+
{ inToken: "TOKEN_VALUE", sizeToken: 100 }
250+
);
251+
252+
const { value, done } = await asyncGenerator.next();
253+
expect(value?.outToken).toBeTypeOf("string");
254+
expect(done).toBe(false);
255+
});
256+
94257
it("should handle deep paths", async () => {
95258
const paginate = createPaginator<
96259
PaginationConfiguration,

packages/core/src/pagination/createPaginator.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const makePagedClientRequest = async <ClientType extends Client<any, any, any>,
1111
...args: any[]
1212
): Promise<OutputType> => {
1313
let command = new CommandCtor(input);
14-
command = withCommand(command);
14+
command = withCommand(command) ?? command;
1515
return await client.send(command, ...args);
1616
};
1717

@@ -36,14 +36,16 @@ export function createPaginator<
3636
input: InputType,
3737
...additionalArguments: any[]
3838
): Paginator<OutputType> {
39-
let token: any = config.startingToken || undefined;
39+
const _input = input as any;
40+
// for legacy reasons this coalescing order is inverted from that of pageSize.
41+
let token: any = config.startingToken ?? _input[inputTokenName];
4042
let hasNext = true;
4143
let page: OutputType;
4244

4345
while (hasNext) {
44-
(input as any)[inputTokenName] = token;
46+
_input[inputTokenName] = token;
4547
if (pageSizeTokenName) {
46-
(input as any)[pageSizeTokenName] = (input as any)[pageSizeTokenName] ?? config.pageSize;
48+
_input[pageSizeTokenName] = _input[pageSizeTokenName] ?? config.pageSize;
4749
}
4850
if (config.client instanceof ClientCtor) {
4951
page = await makePagedClientRequest(
@@ -61,7 +63,6 @@ export function createPaginator<
6163
token = get(page, outputTokenName);
6264
hasNext = !!(token && (!config.stopOnSameToken || token !== prevToken));
6365
}
64-
// @ts-ignore
6566
return undefined;
6667
};
6768
}

packages/types/src/pagination.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export interface PaginationConfiguration {
2929
/**
3030
* @param command - reference to the instantiated command. This callback is executed
3131
* prior to sending the command with the paginator's client.
32-
* @returns the original command or a replacement.
32+
* @returns the original command or a replacement, defaulting to the original command object.
3333
*/
34-
withCommand?: (command: Command<any, any, any, any, any>) => typeof command;
34+
withCommand?: (command: Command<any, any, any, any, any>) => typeof command | undefined;
3535
}

0 commit comments

Comments
 (0)