fix(server): pagination input parser (#10245)

This commit is contained in:
forehalo
2025-02-18 07:15:51 +00:00
parent da67c78152
commit 51e842000a
5 changed files with 209 additions and 22 deletions

View File

@@ -0,0 +1,81 @@
# Snapshot report for `src/base/graphql/__tests__/pagination.spec.ts`
The actual snapshot is saved in `pagination.spec.ts.snap`.
Generated by [AVA](https://avajs.dev).
## should return encode pageInfo
> Snapshot 1
{
edges: [
{
cursor: 'MTE=',
node: {
id: 11,
},
},
{
cursor: 'MTI=',
node: {
id: 12,
},
},
{
cursor: 'MTM=',
node: {
id: 13,
},
},
{
cursor: 'MTQ=',
node: {
id: 14,
},
},
{
cursor: 'MTU=',
node: {
id: 15,
},
},
{
cursor: 'MTY=',
node: {
id: 16,
},
},
{
cursor: 'MTc=',
node: {
id: 17,
},
},
{
cursor: 'MTg=',
node: {
id: 18,
},
},
{
cursor: 'MTk=',
node: {
id: 19,
},
},
{
cursor: 'MjA=',
node: {
id: 20,
},
},
],
pageInfo: {
endCursor: 'MjA=',
hasNextPage: true,
hasPreviousPage: true,
startCursor: 'MTE=',
},
totalCount: 105,
}

View File

@@ -0,0 +1,106 @@
import { Args, Field, ObjectType, Query, Resolver } from '@nestjs/graphql';
import test from 'ava';
import Sinon from 'sinon';
import { createTestingApp } from '../../../__tests__/utils';
import { Public } from '../../../core/auth';
import { paginate, Paginated, PaginationInput } from '../pagination';
const TOTAL_COUNT = 105;
const ITEMS = Array.from({ length: TOTAL_COUNT }, (_, i) => ({ id: i + 1 }));
const paginationStub = Sinon.stub().callsFake(input => {
const start = input.offset + (input.after ? parseInt(input.after) : 0);
return paginate(
ITEMS.slice(start, start + input.first),
'id',
input,
TOTAL_COUNT
);
});
const query = `query pagination($input: PaginationInput) {
pagination(paginationInput: $input) {
totalCount
edges {
cursor
node {
id
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}`;
@ObjectType()
class TestType {
@Field()
id!: number;
}
@ObjectType()
class PaginatedTestType extends Paginated(TestType) {}
@Public()
@Resolver(() => TestType)
class TestResolver {
@Query(() => PaginatedTestType)
async pagination(
@Args(
'paginationInput',
{
type: () => PaginationInput,
defaultValue: { first: 10, offset: 0 },
},
PaginationInput.decode
)
input: PaginationInput
) {
return paginationStub(input);
}
}
const app = await createTestingApp({
providers: [TestResolver],
});
test.after.always(async () => {
await app.close();
});
test('should decode pagination input', async t => {
await app.gql(query, {
input: {
first: 5,
offset: 1,
after: Buffer.from('4').toString('base64'),
},
});
t.true(
paginationStub.calledOnceWithExactly({
first: 5,
offset: 1,
after: '4',
})
);
});
test('should return encode pageInfo', async t => {
const result = paginate(
ITEMS.slice(10, 20),
'id',
{
first: 10,
offset: 0,
after: '9',
},
TOTAL_COUNT
);
t.snapshot(result);
});

View File

@@ -1,24 +1,26 @@
import { Type } from '@nestjs/common';
import {
Field,
FieldMiddleware,
InputType,
Int,
MiddlewareContext,
NextFn,
ObjectType,
} from '@nestjs/graphql';
const parseCursorMiddleware: FieldMiddleware = async (
_ctx: MiddlewareContext,
next: NextFn
) => {
const value = await next();
return value === undefined || value === null ? null : decode(value);
};
import { PipeTransform, Type } from '@nestjs/common';
import { Field, InputType, Int, ObjectType } from '@nestjs/graphql';
@InputType()
export class PaginationInput {
/**
* Because there is no resolver for GraphQL's InputTypes, we can't automatically decode the cursor input from base64 values.
* Use this helper as `PipeTransform` to transform input args
*
* @example
*
* paginate(@Args('input', PaginationInput.decode) PaginationInput) {}
*/
static decode: PipeTransform<PaginationInput, PaginationInput> = {
transform: value => {
return {
...value,
after: value.after ? decode(value.after) : null,
// before: value.before ? decode(value.before) : null,
};
},
};
@Field(() => Int, {
nullable: true,
description: 'returns the first n elements from the list.',
@@ -37,7 +39,6 @@ export class PaginationInput {
nullable: true,
description:
'returns the elements in the list that come after the specified cursor.',
middleware: [parseCursorMiddleware],
})
after?: string | null;
@@ -46,7 +47,6 @@ export class PaginationInput {
// nullable: true,
// description:
// 'returns the elements in the list that come before the specified cursor.',
// middleware: [parseCursorMiddleware],
// })
// before?: string | null;
}
@@ -71,7 +71,7 @@ export function paginate<T>(
edges,
pageInfo: {
hasNextPage: edges.length >= paginationInput.first,
hasPreviousPage: paginationInput.offset > 0,
hasPreviousPage: !!paginationInput.after || paginationInput.offset > 0,
endCursor: edges.length ? edges[edges.length - 1].cursor : null,
startCursor: edges.length ? edges[0].cursor : null,
},