From 29defaa421be9464015de921bf2a332a05232535 Mon Sep 17 00:00:00 2001 From: Manuel Ruck Date: Sun, 22 Sep 2024 18:34:20 +0200 Subject: [PATCH] =?UTF-8?q?feat(procedure):=20=F0=9F=9A=80=20add=20paginat?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Manuel Ruck --- .vscode/settings.json | 3 +- bundestag.io/admin/src/app/list/past/page.tsx | 4 +- .../admin/src/app/list/upcoming/page.tsx | 4 +- .../bruno/environments/localhost.bru | 2 +- services/procedures/bruno/findAll.bru | 2 +- services/procedures/bruno/pastProcedures.bru | 8 +- .../procedures/bruno/upcomingProcedures.bru | 9 +- services/procedures/garden.yml | 2 +- .../procedures/src/decorators/pagination.ts | 14 ++ .../procedures/procedures.controller.spec.ts | 27 +++- .../src/procedures/procedures.controller.ts | 24 +++- .../src/procedures/procedures.service.spec.ts | 124 +++++++++++------- .../src/procedures/procedures.service.ts | 80 +++++++---- 13 files changed, 207 insertions(+), 96 deletions(-) create mode 100644 services/procedures/src/decorators/pagination.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index a6ef5626a..ad58dc51f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,5 +25,6 @@ "[ignore]": { "editor.defaultFormatter": "foxundermoon.shell-format" }, - "conventionalCommits.scopes": ["push-send-queued"] + "conventionalCommits.scopes": ["push-send-queued"], + "CodeGPT.apiKey": "OpenAI" } diff --git a/bundestag.io/admin/src/app/list/past/page.tsx b/bundestag.io/admin/src/app/list/past/page.tsx index 61d7240cd..f866bcf2e 100644 --- a/bundestag.io/admin/src/app/list/past/page.tsx +++ b/bundestag.io/admin/src/app/list/past/page.tsx @@ -10,7 +10,7 @@ export default async function Page() { return ( <>

Past procedures

- {data.map((procedure) => ( + {data.procedures.map((procedure) => ( { +async function getData(): Promise<{ procedures: IProcedure[]; count: number }> { const res = await fetch(`${process.env.PROCEDURES_SERVER_URL}/procedures/list/past`, { headers: { 'Cache-Control': 'no-cache', diff --git a/bundestag.io/admin/src/app/list/upcoming/page.tsx b/bundestag.io/admin/src/app/list/upcoming/page.tsx index f2869bc99..c5528949a 100644 --- a/bundestag.io/admin/src/app/list/upcoming/page.tsx +++ b/bundestag.io/admin/src/app/list/upcoming/page.tsx @@ -10,7 +10,7 @@ export default async function Page() { return ( <>

Upcoming procedures

- {data.map((procedure) => ( + {data.procedures.map((procedure) => ( { +async function getData(): Promise<{ procedures: IProcedure[]; count: number }> { const res = await fetch(`${process.env.PROCEDURES_SERVER_URL}/procedures/list/upcoming`, { headers: { 'Cache-Control': 'no-cache', diff --git a/services/procedures/bruno/environments/localhost.bru b/services/procedures/bruno/environments/localhost.bru index 5c0f373dc..694678daa 100644 --- a/services/procedures/bruno/environments/localhost.bru +++ b/services/procedures/bruno/environments/localhost.bru @@ -1,3 +1,3 @@ vars { - url: http://localhost:3000 + url: http://localhost:3006 } diff --git a/services/procedures/bruno/findAll.bru b/services/procedures/bruno/findAll.bru index 30a9f3e1d..a3920c85b 100644 --- a/services/procedures/bruno/findAll.bru +++ b/services/procedures/bruno/findAll.bru @@ -11,6 +11,6 @@ get { } assert { - res.body.procedureId: isString + res.body.procedures[0].procedureId: isString res.status: eq 200 } diff --git a/services/procedures/bruno/pastProcedures.bru b/services/procedures/bruno/pastProcedures.bru index a072d6aac..a19f38d61 100644 --- a/services/procedures/bruno/pastProcedures.bru +++ b/services/procedures/bruno/pastProcedures.bru @@ -5,12 +5,16 @@ meta { } get { - url: {{url}}/procedures/list/past + url: {{url}}/procedures/list/past?limit=2 body: none auth: none } +params:query { + limit: 2 +} + assert { - res.body[0].procedureId: isString + res.body.count: gt 0 res.status: eq 200 } diff --git a/services/procedures/bruno/upcomingProcedures.bru b/services/procedures/bruno/upcomingProcedures.bru index bb9b5d4cf..011126374 100644 --- a/services/procedures/bruno/upcomingProcedures.bru +++ b/services/procedures/bruno/upcomingProcedures.bru @@ -5,12 +5,17 @@ meta { } get { - url: {{url}}/procedures/list/upcoming + url: {{url}}/procedures/list/upcoming?limit=1&page=2 body: none auth: none } +params:query { + limit: 1 + page: 2 +} + assert { - res.body[0].procedureId: isString + res.body.procedures[0].procedureId: isString res.status: eq 200 } diff --git a/services/procedures/garden.yml b/services/procedures/garden.yml index 3743a6bc6..a9efc4470 100644 --- a/services/procedures/garden.yml +++ b/services/procedures/garden.yml @@ -32,7 +32,7 @@ spec: sourcePath: src mode: one-way overrides: - - command: [pnpm, --filter, procedures, run, garden:dev] + - command: [pnpm, --filter, procedures, run, dev] patchResources: - name: procedures kind: Deployment diff --git a/services/procedures/src/decorators/pagination.ts b/services/procedures/src/decorators/pagination.ts new file mode 100644 index 000000000..c2582f702 --- /dev/null +++ b/services/procedures/src/decorators/pagination.ts @@ -0,0 +1,14 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const Pagination = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + const { page = 1, limit = 10 } = request.query; + + // Ensure valid pagination values + const validPage = Math.max(1, parseInt(page)); + const validLimit = Math.min(Math.max(1, parseInt(limit)), 100); // Max limit of 100 + + return { page: validPage, limit: validLimit }; + }, +); diff --git a/services/procedures/src/procedures/procedures.controller.spec.ts b/services/procedures/src/procedures/procedures.controller.spec.ts index e6faa0cbd..c4c3c3ba3 100644 --- a/services/procedures/src/procedures/procedures.controller.spec.ts +++ b/services/procedures/src/procedures/procedures.controller.spec.ts @@ -10,7 +10,7 @@ describe('ProceduresController', () => { jest.Mocked> > = { findAll: jest.fn( - () => + ({}: { page: number; limit: number }) => ({ title: 'test', id: 1, @@ -21,7 +21,7 @@ describe('ProceduresController', () => { }) as unknown as Promise, ), fetchUpcomingProcedures: jest.fn( - () => + ({}: { page: number; limit: number }) => ({ title: 'test', id: 1, @@ -32,7 +32,7 @@ describe('ProceduresController', () => { }) as any, ), fetchPastProcedures: jest.fn( - () => + ({}: { page: number; limit: number }) => ({ title: 'test', id: 1, @@ -60,7 +60,12 @@ describe('ProceduresController', () => { }); it('should return an array of procedures', async () => { - expect(controller.findAll()).toStrictEqual({ + expect( + controller.findAll({ + page: 1, + limit: 1, + }), + ).toStrictEqual({ title: 'test', id: 1, procedureId: '123456', @@ -71,7 +76,12 @@ describe('ProceduresController', () => { }); it('should return an array of upcoming procedures', async () => { - expect(controller.upcomingProcedures()).toStrictEqual({ + expect( + controller.upcomingProcedures({ + page: 1, + limit: 1, + }), + ).toStrictEqual({ title: 'test', id: 1, procedureId: '123456', @@ -82,7 +92,12 @@ describe('ProceduresController', () => { }); it('should return an array of past procedures', async () => { - expect(controller.pastProcedures()).toStrictEqual({ + expect( + controller.pastProcedures({ + page: 1, + limit: 1, + }), + ).toStrictEqual({ title: 'test', id: 1, procedureId: '123456', diff --git a/services/procedures/src/procedures/procedures.controller.ts b/services/procedures/src/procedures/procedures.controller.ts index 51669b80a..8d9a5721e 100644 --- a/services/procedures/src/procedures/procedures.controller.ts +++ b/services/procedures/src/procedures/procedures.controller.ts @@ -1,21 +1,33 @@ import { Controller, Get } from '@nestjs/common'; import { ProceduresService } from './procedures.service'; +import { Pagination } from '../decorators/pagination'; @Controller('procedures') export class ProceduresController { constructor(private readonly proceduresService: ProceduresService) {} @Get() - findAll() { - return this.proceduresService.findAll(); + findAll(@Pagination() pagination: { page: number; limit: number }) { + return this.proceduresService.findAll({ + page: pagination.page, + limit: pagination.limit, + }); } @Get('list/upcoming') - upcomingProcedures() { - return this.proceduresService.fetchUpcomingProcedures(); + upcomingProcedures( + @Pagination() pagination: { page: number; limit: number }, + ) { + return this.proceduresService.fetchUpcomingProcedures({ + page: pagination.page, + limit: pagination.limit, + }); } @Get('list/past') - pastProcedures() { - return this.proceduresService.fetchPastProcedures(); + pastProcedures(@Pagination() pagination: { page: number; limit: number }) { + return this.proceduresService.fetchPastProcedures({ + page: pagination.page, + limit: pagination.limit, + }); } } diff --git a/services/procedures/src/procedures/procedures.service.spec.ts b/services/procedures/src/procedures/procedures.service.spec.ts index f0daba1ae..57d7039fc 100644 --- a/services/procedures/src/procedures/procedures.service.spec.ts +++ b/services/procedures/src/procedures/procedures.service.spec.ts @@ -8,17 +8,22 @@ describe('ProceduresService', () => { beforeEach(async () => { procedureModel = { - find: jest.fn().mockReturnValue([ - [ - { - title: 'test', - id: 1, - type: '', - period: '', - importantDocuments: [], - }, - ], - ]), + find: jest.fn().mockReturnValue({ + skip: jest.fn().mockReturnValue({ + limit: jest.fn().mockResolvedValue([ + [ + { + title: 'test', + id: 1, + type: '', + period: '', + importantDocuments: [], + }, + ], + ]), + }), + }), + countDocuments: jest.fn().mockResolvedValue(1), }; const module: TestingModule = await Test.createTestingModule({ @@ -29,7 +34,7 @@ describe('ProceduresService', () => { }).compile(); service = module.get(ProceduresService); - // jest.clearAllMocks(); + jest.clearAllMocks(); }); it('should be defined', () => { @@ -37,7 +42,12 @@ describe('ProceduresService', () => { }); it('should return an array of procedures', () => { - expect(service.findAll()).resolves.toStrictEqual([ + expect( + service.findAll({ + page: 1, + limit: 1, + }), + ).resolves.toStrictEqual([ { title: 'test', id: 1, @@ -49,21 +59,7 @@ describe('ProceduresService', () => { }); it('should return an array of upcoming procedures', () => { - (procedureModel.find as jest.Mock).mockReturnValueOnce({ - sort: jest.fn().mockReturnValueOnce({ - limit: jest.fn().mockResolvedValue([ - { - title: 'test', - id: 1, - type: '', - period: '', - importantDocuments: [], - }, - ]), - }), - }); - - expect(service.fetchUpcomingProcedures()).resolves.toStrictEqual([ + const mockResult = [ { title: 'test', id: 1, @@ -71,25 +67,38 @@ describe('ProceduresService', () => { period: '', importantDocuments: [], }, - ]); - }); + ]; + + // Mock the chain of .find().sort().skip().limit() + const mockLimit = jest.fn().mockResolvedValue(mockResult); + const mockSkip = jest.fn().mockReturnValue({ limit: mockLimit }); + const mockSort = jest.fn().mockReturnValue({ skip: mockSkip }); + const mockFind = jest.fn().mockReturnValue({ sort: mockSort }); + + // Assign the mock to procedureModel.find + (procedureModel.find as jest.Mock).mockImplementation(mockFind); - it('should return an array of past procedures', () => { - (procedureModel.find as jest.Mock).mockReturnValueOnce({ - sort: jest.fn().mockReturnValueOnce({ - limit: jest.fn().mockResolvedValue([ - { - title: 'test', - id: 1, - type: '', - period: '', - importantDocuments: [], - }, - ]), + expect( + service.fetchUpcomingProcedures({ + page: 1, + limit: 1, }), + ).resolves.toStrictEqual({ + procedures: [ + { + title: 'test', + id: 1, + type: '', + period: '', + importantDocuments: [], + }, + ], + count: 1, }); + }); - expect(service.fetchPastProcedures()).resolves.toStrictEqual([ + it('should return an array of past procedures', async () => { + const mockResult = [ { title: 'test', id: 1, @@ -97,6 +106,33 @@ describe('ProceduresService', () => { period: '', importantDocuments: [], }, - ]); + ]; + + // Mock the chain of .find().sort().skip().limit() + const mockLimit = jest.fn().mockResolvedValue(mockResult); + const mockSkip = jest.fn().mockReturnValue({ limit: mockLimit }); + const mockSort = jest.fn().mockReturnValue({ skip: mockSkip }); + const mockFind = jest.fn().mockReturnValue({ sort: mockSort }); + + // Assign the mock to procedureModel.find + (procedureModel.find as jest.Mock).mockImplementation(mockFind); + + await expect( + service.fetchPastProcedures({ + page: 1, + limit: 1, + }), + ).resolves.toStrictEqual({ + procedures: [ + { + title: 'test', + id: 1, + type: '', + period: '', + importantDocuments: [], + }, + ], + count: 1, + }); }); }); diff --git a/services/procedures/src/procedures/procedures.service.ts b/services/procedures/src/procedures/procedures.service.ts index 954c7b815..d50b1e85a 100644 --- a/services/procedures/src/procedures/procedures.service.ts +++ b/services/procedures/src/procedures/procedures.service.ts @@ -8,39 +8,63 @@ export class ProceduresService { @InjectModel('Procedure') private procedureModel: typeof ProcedureModel, ) {} - async findAll() { - const procedures = await this.procedureModel.find(); - return procedures[0]; + async findAll({ page, limit }: { page: number; limit: number }) { + const procedures = await this.procedureModel + .find() + .skip((page - 1) * limit) + .limit(limit); + + const count = await this.procedureModel.countDocuments(); + + return { procedures, count }; } - async fetchUpcomingProcedures() { - return await this.procedureModel - .find({ - $or: [ - { - $and: [ - { voteDate: { $gte: new Date() } }, - { - $or: [{ voteEnd: { $exists: false } }, { voteEnd: undefined }], - }, - ], - }, - { voteEnd: { $gte: new Date() } }, - ], - }) + async fetchUpcomingProcedures({ + page, + limit, + }: { + page: number; + limit: number; + }) { + const filter = { + $or: [ + { + $and: [ + { voteDate: { $gte: new Date() } }, + { + $or: [{ voteEnd: { $exists: false } }, { voteEnd: undefined }], + }, + ], + }, + { voteEnd: { $gte: new Date() } }, + ], + }; + const procedures = await this.procedureModel + .find(filter) .sort({ voteDate: 1, voteEnd: 1, votes: -1 }) - .limit(100); + .skip((page - 1) * limit) + .limit(limit); + + const count = await this.procedureModel.countDocuments(filter); + + return { procedures, count }; } - async fetchPastProcedures() { - return await this.procedureModel - .find({ - $or: [ - { voteDate: { $lt: new Date() } }, - { voteEnd: { $lt: new Date() } }, - ], - }) + async fetchPastProcedures({ page, limit }: { page: number; limit: number }) { + const filter = { + $or: [ + { voteDate: { $lt: new Date() } }, + { voteEnd: { $lt: new Date() } }, + ], + }; + const procedures = await this.procedureModel + .find(filter) .sort({ voteDate: -1, voteEnd: -1, votes: -1 }) - .limit(100); + .skip((page - 1) * limit) + .limit(limit); + + const count = await this.procedureModel.countDocuments(filter); + + return { procedures, count }; } }