mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-22 20:41:50 +08:00
fix(server): pagination input parser (#10245)
This commit is contained in:
+81
@@ -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,
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
@@ -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);
|
||||||
|
});
|
||||||
@@ -1,24 +1,26 @@
|
|||||||
import { Type } from '@nestjs/common';
|
import { PipeTransform, Type } from '@nestjs/common';
|
||||||
import {
|
import { Field, InputType, Int, ObjectType } from '@nestjs/graphql';
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class PaginationInput {
|
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, {
|
@Field(() => Int, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: 'returns the first n elements from the list.',
|
description: 'returns the first n elements from the list.',
|
||||||
@@ -37,7 +39,6 @@ export class PaginationInput {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
description:
|
description:
|
||||||
'returns the elements in the list that come after the specified cursor.',
|
'returns the elements in the list that come after the specified cursor.',
|
||||||
middleware: [parseCursorMiddleware],
|
|
||||||
})
|
})
|
||||||
after?: string | null;
|
after?: string | null;
|
||||||
|
|
||||||
@@ -46,7 +47,6 @@ export class PaginationInput {
|
|||||||
// nullable: true,
|
// nullable: true,
|
||||||
// description:
|
// description:
|
||||||
// 'returns the elements in the list that come before the specified cursor.',
|
// 'returns the elements in the list that come before the specified cursor.',
|
||||||
// middleware: [parseCursorMiddleware],
|
|
||||||
// })
|
// })
|
||||||
// before?: string | null;
|
// before?: string | null;
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ export function paginate<T>(
|
|||||||
edges,
|
edges,
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
hasNextPage: edges.length >= paginationInput.first,
|
hasNextPage: edges.length >= paginationInput.first,
|
||||||
hasPreviousPage: paginationInput.offset > 0,
|
hasPreviousPage: !!paginationInput.after || paginationInput.offset > 0,
|
||||||
endCursor: edges.length ? edges[edges.length - 1].cursor : null,
|
endCursor: edges.length ? edges[edges.length - 1].cursor : null,
|
||||||
startCursor: edges.length ? edges[0].cursor : null,
|
startCursor: edges.length ? edges[0].cursor : null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ export class DocResolver {
|
|||||||
async grantedUsersList(
|
async grantedUsersList(
|
||||||
@CurrentUser() user: CurrentUser,
|
@CurrentUser() user: CurrentUser,
|
||||||
@Parent() doc: DocType,
|
@Parent() doc: DocType,
|
||||||
@Args('pagination') pagination: PaginationInput
|
@Args('pagination', PaginationInput.decode) pagination: PaginationInput
|
||||||
): Promise<PaginatedGrantedDocUserType> {
|
): Promise<PaginatedGrantedDocUserType> {
|
||||||
await this.permission.checkPagePermission(
|
await this.permission.checkPagePermission(
|
||||||
doc.workspaceId,
|
doc.workspaceId,
|
||||||
|
|||||||
Reference in New Issue
Block a user