Skip to content

fix(pagination): allow operation input to specify pagination token #1500

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/lazy-geese-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@smithy/types": minor
"@smithy/core": minor
---

allow paginator token fallback to be specified by operation input
163 changes: 163 additions & 0 deletions packages/core/src/pagination/createPaginator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ describe(createPaginator.name, () => {
}
}

class ClientStringToken {
private pages = 5;
async send(command: any) {
if (--this.pages > 0) {
return {
outToken: command.input.inToken,
};
}
return {};
}
}
class CommandStringToken {
public middlewareStack = {
add: vi.fn(),
Expand Down Expand Up @@ -91,6 +102,158 @@ describe(createPaginator.name, () => {
expect(pages).toEqual(5);
});

it("should prioritize token set in paginator config, fallback to token set in input parameters", async () => {
class CommandExpectPaginatorConfigToken {
public constructor(public input: any) {
expect(input).toMatchObject({
inToken: "abc",
});
}
}
class CommandExpectOperationInputToken {
public constructor(public input: any) {
expect(input).toMatchObject({
inToken: "xyz",
});
}
}
{
const paginate = createPaginator<
PaginationConfiguration,
{ inToken?: string; sizeToken?: number },
{ outToken: string }
>(ClientStringToken, CommandExpectPaginatorConfigToken, "inToken", "outToken", "sizeToken");

let pages = 0;
const client = new ClientStringToken() as any;

for await (const page of paginate(
{
client,
startingToken: "abc",
},
{
inToken: "xyz",
}
)) {
pages += 1;
expect(page).toBeDefined();
}

expect(pages).toEqual(5);
}
{
const paginate = createPaginator<
PaginationConfiguration,
{ inToken?: string; sizeToken?: number },
{ outToken: string }
>(ClientStringToken, CommandExpectOperationInputToken, "inToken", "outToken", "sizeToken");

let pages = 0;
const client = new ClientStringToken() as any;

for await (const page of paginate(
{
client,
},
{
inToken: "xyz",
}
)) {
pages += 1;
expect(page).toBeDefined();
}

expect(pages).toEqual(5);
}
});

it("should prioritize page size set in operation input, fallback to page size set in paginator config (inverted from token priority)", async () => {
class CommandExpectPaginatorPageSize {
public constructor(public input: any) {
expect(input).toMatchObject({
sizeToken: 100,
});
}
}
class CommandExpectOperationInputPageSize {
public constructor(public input: any) {
expect(input).toMatchObject({
sizeToken: 99,
});
}
}
{
const paginate = createPaginator<
PaginationConfiguration,
{ inToken?: string; sizeToken?: number },
{ outToken: string }
>(ClientStringToken, CommandExpectPaginatorPageSize, "inToken", "outToken", "sizeToken");

let pages = 0;
const client = new ClientStringToken() as any;

for await (const page of paginate(
{
client,
pageSize: 100,
},
{
inToken: "abc",
}
)) {
pages += 1;
expect(page).toBeDefined();
}

expect(pages).toEqual(5);
}
{
const paginate = createPaginator<
PaginationConfiguration,
{ inToken?: string; sizeToken?: number },
{ outToken: string }
>(ClientStringToken, CommandExpectOperationInputPageSize, "inToken", "outToken", "sizeToken");

let pages = 0;
const client = new ClientStringToken() as any;

for await (const page of paginate(
{
client,
pageSize: 100,
},
{
sizeToken: 99,
inToken: "abc",
}
)) {
pages += 1;
expect(page).toBeDefined();
}

expect(pages).toEqual(5);
}
});

it("should have the correct AsyncGenerator.TNext type", async () => {
const paginate = createPaginator<
PaginationConfiguration,
{ inToken?: string; sizeToken: number },
{
outToken: string;
}
>(ClientStringToken, CommandStringToken, "inToken", "outToken.outToken2.outToken3", "sizeToken");
const asyncGenerator = paginate(
{ client: new ClientStringToken() as any },
{ inToken: "TOKEN_VALUE", sizeToken: 100 }
);

const { value, done } = await asyncGenerator.next();
expect(value?.outToken).toBeTypeOf("string");
expect(done).toBe(false);
});

it("should handle deep paths", async () => {
const paginate = createPaginator<
PaginationConfiguration,
Expand Down
11 changes: 6 additions & 5 deletions packages/core/src/pagination/createPaginator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const makePagedClientRequest = async <ClientType extends Client<any, any, any>,
...args: any[]
): Promise<OutputType> => {
let command = new CommandCtor(input);
command = withCommand(command);
command = withCommand(command) ?? command;
return await client.send(command, ...args);
};

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

while (hasNext) {
(input as any)[inputTokenName] = token;
_input[inputTokenName] = token;
if (pageSizeTokenName) {
(input as any)[pageSizeTokenName] = (input as any)[pageSizeTokenName] ?? config.pageSize;
_input[pageSizeTokenName] = _input[pageSizeTokenName] ?? config.pageSize;
}
if (config.client instanceof ClientCtor) {
page = await makePagedClientRequest(
Expand All @@ -61,7 +63,6 @@ export function createPaginator<
token = get(page, outputTokenName);
hasNext = !!(token && (!config.stopOnSameToken || token !== prevToken));
}
// @ts-ignore
return undefined;
};
}
Expand Down
4 changes: 2 additions & 2 deletions packages/types/src/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export interface PaginationConfiguration {
/**
* @param command - reference to the instantiated command. This callback is executed
* prior to sending the command with the paginator's client.
* @returns the original command or a replacement.
* @returns the original command or a replacement, defaulting to the original command object.
*/
withCommand?: (command: Command<any, any, any, any, any>) => typeof command;
withCommand?: (command: Command<any, any, any, any, any>) => typeof command | undefined;
}
Loading