diff --git a/src/domain-services/flows/flow-search-service.ts b/src/domain-services/flows/flow-search-service.ts index c48d3ade..956048bf 100644 --- a/src/domain-services/flows/flow-search-service.ts +++ b/src/domain-services/flows/flow-search-service.ts @@ -129,7 +129,7 @@ export class FlowSearchService { ); return { - items, + flows: items, hasNextPage: first <= flows.length, hasPreviousPage: afterCursor !== undefined, startCursor: flows.length ? flows[0].id.valueOf() : 0, diff --git a/src/domain-services/flows/graphql/types.ts b/src/domain-services/flows/graphql/types.ts index 5efe1be2..5a2c89c3 100644 --- a/src/domain-services/flows/graphql/types.ts +++ b/src/domain-services/flows/graphql/types.ts @@ -85,7 +85,7 @@ export default class Flow implements ItemPaged { @ObjectType() export class FlowSearchResult extends PageInfo { @Field(() => [Flow], { nullable: false }) - items: Flow[]; + flows: Flow[]; } export type FlowSortField = diff --git a/src/utils/graphql/pagination.ts b/src/utils/graphql/pagination.ts new file mode 100644 index 00000000..bdc8008c --- /dev/null +++ b/src/utils/graphql/pagination.ts @@ -0,0 +1,64 @@ +import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; +import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; +import { ObjectType, Field } from 'type-graphql'; + +export interface ItemPaged { + cursor: number; +} + +@ObjectType() +export class PageInfo { + @Field({ nullable: false }) + hasNextPage: boolean; + + @Field({ nullable: false }) + hasPreviousPage: boolean; + + @Field({ nullable: false }) + startCursor: number; + + @Field({ nullable: false }) + endCursor: number; + + @Field({ nullable: false }) + pageSize: number; + + @Field(() => String, { nullable: false }) + sortField: TSortFields; + + @Field({ nullable: false }) + sortOrder: string; + + @Field({ nullable: false }) + total: number; +} + +export function prepareConditionFromCursor( + sortCondition: { column: string; order: 'asc' | 'desc' }, + afterCursor?: number, + beforeCursor?: number +): any { + if (afterCursor && beforeCursor) { + throw new Error('Cannot use before and after cursor at the same time'); + } + + if (afterCursor || beforeCursor) { + const isAscending = sortCondition.order === 'asc'; + const cursorValue = afterCursor || beforeCursor; + + let op; + if (isAscending) { + op = afterCursor ? Op.GT : Op.LT; + } else { + op = beforeCursor ? Op.GT : Op.LT; + } + + return { + id: { + [op]: createBrandedValue(cursorValue), + }, + }; + } + + return {}; +} diff --git a/tests/unit/pagination.spec.ts b/tests/unit/pagination.spec.ts new file mode 100644 index 00000000..8d889a08 --- /dev/null +++ b/tests/unit/pagination.spec.ts @@ -0,0 +1,72 @@ +import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; +import { prepareConditionFromCursor } from '../../src/utils/graphql/pagination'; + +describe('Based on cursor and order for pagination', () => { + describe('Order is asc', () => { + const sortCondition = { column: 'id', order: 'asc' as const }; + + it("Should return 'GT' when afterCursor is defined", () => { + const afterCursor = 1; + const beforeCursor = undefined; + const result = prepareConditionFromCursor( + sortCondition, + afterCursor, + beforeCursor + ); + expect(result.id).toEqual({ [Op.GT]: afterCursor }); + }); + + it("Should return 'LT' when beforeCursor is defined", () => { + const afterCursor = undefined; + const beforeCursor = 1; + const result = prepareConditionFromCursor( + sortCondition, + afterCursor, + beforeCursor + ); + expect(result.id).toEqual({ [Op.LT]: beforeCursor }); + }); + + it('Should throw an error when both afterCursor and beforeCursor are defined', () => { + const afterCursor = 1; + const beforeCursor = 2; + expect(() => + prepareConditionFromCursor(sortCondition, afterCursor, beforeCursor) + ).toThrowError('Cannot use before and after cursor at the same time'); + }); + }); + + describe("Order is 'desc'", () => { + const sortCondition = { column: 'id', order: 'desc' as const }; + + it("Should return 'LT' when afterCursor is defined", () => { + const afterCursor = 1; + const beforeCursor = undefined; + const result = prepareConditionFromCursor( + sortCondition, + afterCursor, + beforeCursor + ); + expect(result.id).toEqual({ [Op.LT]: afterCursor }); + }); + + it("Should return 'GT' when beforeCursor is defined", () => { + const afterCursor = undefined; + const beforeCursor = 1; + const result = prepareConditionFromCursor( + sortCondition, + afterCursor, + beforeCursor + ); + expect(result.id).toEqual({ [Op.GT]: beforeCursor }); + }); + + it('Should throw an error when both afterCursor and beforeCursor are defined', () => { + const afterCursor = 1; + const beforeCursor = 2; + expect(() => + prepareConditionFromCursor(sortCondition, afterCursor, beforeCursor) + ).toThrowError('Cannot use before and after cursor at the same time'); + }); + }); +});