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/_components/pagination-navigation.tsx b/bundestag.io/admin/src/app/list/_components/pagination-navigation.tsx new file mode 100644 index 000000000..e15f736bc --- /dev/null +++ b/bundestag.io/admin/src/app/list/_components/pagination-navigation.tsx @@ -0,0 +1,13 @@ +import Link from 'next/link'; + +export const PaginationNavigation = ({ currentPage, totalPages }) => ( +
+ + + + {`Page ${currentPage} of ${totalPages}`} + + + +
+); diff --git a/bundestag.io/admin/src/app/list/past/page.tsx b/bundestag.io/admin/src/app/list/past/page.tsx index 61d7240cd..1f215468c 100644 --- a/bundestag.io/admin/src/app/list/past/page.tsx +++ b/bundestag.io/admin/src/app/list/past/page.tsx @@ -1,16 +1,47 @@ import Entry from '../_components/entry'; import { IProcedure } from '@democracy-deutschland/bundestagio-common'; import { unstable_noStore as noStore } from 'next/cache'; +import Link from 'next/link'; +import { PaginationNavigation } from '../_components/pagination-navigation'; + +const ITEMS_PER_PAGE = 10; export const dynamic = 'force-dynamic'; -export default async function Page() { +async function getData(page: number = 1): Promise<{ procedures: IProcedure[]; count: number }> { + const limit = ITEMS_PER_PAGE; + + console.log( + 'getData', + page, + `${process.env.PROCEDURES_SERVER_URL}/procedures/list/past?limit=${limit}&offset=${page}`, + ); + const res = await fetch(`${process.env.PROCEDURES_SERVER_URL}/procedures/list/past?limit=${limit}&page=${page}`, { + headers: { + 'Cache-Control': 'no-cache', + cache: 'no-store', + }, + }); + + if (!res.ok) { + throw new Error('Fehler beim Abrufen der Daten'); + } + + return res.json(); +} + +export default async function Page({ searchParams }: { searchParams: { page?: string } }) { noStore(); - const data = await getData(); + const currentPage = searchParams.page ? parseInt(searchParams.page, 10) : 1; // Standardwert ist Seite 1 + const { procedures, count } = await getData(currentPage); + const totalPages = Math.ceil(count / ITEMS_PER_PAGE); // Berechne die Gesamtseitenzahl basierend auf der Anzahl der Elemente + return ( <>

Past procedures

- {data.map((procedure) => ( + + + {procedures.map((procedure) => ( ))} + + ); } - -async function getData(): Promise { - const res = await fetch(`${process.env.PROCEDURES_SERVER_URL}/procedures/list/past`, { - headers: { - 'Cache-Control': 'no-cache', - cache: 'no-store', - }, - }); - - if (!res.ok) { - throw new Error('Fehler beim Abrufen der Daten'); - } - - return res.json(); -} diff --git a/bundestag.io/admin/src/app/list/upcoming/page.tsx b/bundestag.io/admin/src/app/list/upcoming/page.tsx index f2869bc99..8ca1b3c74 100644 --- a/bundestag.io/admin/src/app/list/upcoming/page.tsx +++ b/bundestag.io/admin/src/app/list/upcoming/page.tsx @@ -1,16 +1,48 @@ import Entry from '../_components/entry'; import { IProcedure } from '@democracy-deutschland/bundestagio-common'; import { unstable_noStore as noStore } from 'next/cache'; +import Link from 'next/link'; +import { PaginationNavigation } from '../_components/pagination-navigation'; + +const ITEMS_PER_PAGE = 10; export const dynamic = 'force-dynamic'; -export default async function Page() { +async function getData(page: number = 1): Promise<{ procedures: IProcedure[]; count: number }> { + const limit = ITEMS_PER_PAGE; + + console.log( + 'getData', + page, + `${process.env.PROCEDURES_SERVER_URL}/procedures/list/upcoming?limit=${limit}&offset=${page}`, + ); + const res = await fetch(`${process.env.PROCEDURES_SERVER_URL}/procedures/list/upcoming?limit=${limit}&page=${page}`, { + headers: { + 'Cache-Control': 'no-cache', + cache: 'no-store', + }, + }); + + if (!res.ok) { + throw new Error('Fehler beim Abrufen der Daten'); + } + + return res.json(); +} + +export default async function Page({ searchParams }: { searchParams: { page?: string } }) { noStore(); - const data = await getData(); + const currentPage = searchParams.page ? parseInt(searchParams.page, 10) : 1; // Standardwert ist Seite 1 + const { procedures, count } = await getData(currentPage); + const totalPages = Math.ceil(count / ITEMS_PER_PAGE); // Berechne die Gesamtseitenzahl basierend auf der Anzahl der Elemente + return ( <>

Upcoming procedures

- {data.map((procedure) => ( + + + + {procedures.map((procedure) => ( ))} + + ); } - -async function getData(): Promise { - const res = await fetch(`${process.env.PROCEDURES_SERVER_URL}/procedures/list/upcoming`, { - headers: { - 'Cache-Control': 'no-cache', - cache: 'no-store', - }, - }); - - if (!res.ok) { - throw new Error('Fehler beim Abrufen der Daten'); - } - - return res.json(); -} 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..87ad66bce 100644 --- a/services/procedures/src/procedures/procedures.controller.spec.ts +++ b/services/procedures/src/procedures/procedures.controller.spec.ts @@ -1,7 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ProceduresController } from './procedures.controller'; import { ProceduresService } from './procedures.service'; -import { IProcedure } from '@democracy-deutschland/bundestagio-common/dist/models/Procedure/schema'; describe('ProceduresController', () => { let controller: ProceduresController; @@ -10,36 +9,51 @@ describe('ProceduresController', () => { jest.Mocked> > = { findAll: jest.fn( - () => + ({}: { page: number; limit: number }) => ({ - title: 'test', - id: 1, - procedureId: '123456', - type: '', - period: '', - importantDocuments: [], - }) as unknown as Promise, + procedures: [ + { + title: 'test', + id: 1, + procedureId: '123456', + type: '', + period: '', + importantDocuments: [], + }, + ], + count: 1, + }) as any, ), fetchUpcomingProcedures: jest.fn( - () => + ({}: { page: number; limit: number }) => ({ - title: 'test', - id: 1, - procedureId: '123456', - type: '', - period: '', - importantDocuments: [], + procedures: [ + { + title: 'test', + id: 1, + procedureId: '123456', + type: '', + period: '', + importantDocuments: [], + }, + ], + count: 1, }) as any, ), fetchPastProcedures: jest.fn( - () => + ({}: { page: number; limit: number }) => ({ - title: 'test', - id: 1, - procedureId: '123456', - type: '', - period: '', - importantDocuments: [], + procedures: [ + { + title: 'test', + id: 1, + procedureId: '123456', + type: '', + period: '', + importantDocuments: [], + }, + ], + count: 1, }) as any, ), }; @@ -60,35 +74,65 @@ describe('ProceduresController', () => { }); it('should return an array of procedures', async () => { - expect(controller.findAll()).toStrictEqual({ - title: 'test', - id: 1, - procedureId: '123456', - type: '', - period: '', - importantDocuments: [], + expect( + controller.findAll({ + page: 1, + limit: 1, + }), + ).toStrictEqual({ + procedures: [ + { + title: 'test', + id: 1, + procedureId: '123456', + type: '', + period: '', + importantDocuments: [], + }, + ], + count: 1, }); }); it('should return an array of upcoming procedures', async () => { - expect(controller.upcomingProcedures()).toStrictEqual({ - title: 'test', - id: 1, - procedureId: '123456', - type: '', - period: '', - importantDocuments: [], + expect( + controller.upcomingProcedures({ + page: 1, + limit: 1, + }), + ).toStrictEqual({ + procedures: [ + { + title: 'test', + id: 1, + procedureId: '123456', + type: '', + period: '', + importantDocuments: [], + }, + ], + count: 1, }); }); it('should return an array of past procedures', async () => { - expect(controller.pastProcedures()).toStrictEqual({ - title: 'test', - id: 1, - procedureId: '123456', - type: '', - period: '', - importantDocuments: [], + expect( + controller.pastProcedures({ + page: 1, + limit: 1, + }), + ).toStrictEqual({ + procedures: [ + { + title: 'test', + id: 1, + procedureId: '123456', + type: '', + period: '', + importantDocuments: [], + }, + ], + count: 1, }); }); }); 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..5e2f54326 100644 --- a/services/procedures/src/procedures/procedures.service.spec.ts +++ b/services/procedures/src/procedures/procedures.service.spec.ts @@ -7,18 +7,24 @@ describe('ProceduresService', () => { let procedureModel: Partial; beforeEach(async () => { + const mockResult = [ + { + title: 'test', + id: 1, + type: '', + period: '', + importantDocuments: [], + }, + ]; + + // Mock the chain of .find().sort().skip().limit() + const mockLimit = jest.fn().mockResolvedValue(mockResult); + const mockSkip = jest.fn().mockReturnValue({ limit: mockLimit }); + const mockFind = jest.fn().mockReturnValue({ skip: mockSkip }); + procedureModel = { - find: jest.fn().mockReturnValue([ - [ - { - title: 'test', - id: 1, - type: '', - period: '', - importantDocuments: [], - }, - ], - ]), + find: jest.fn().mockImplementation(mockFind), + countDocuments: jest.fn().mockResolvedValue(1), }; const module: TestingModule = await Test.createTestingModule({ @@ -29,7 +35,7 @@ describe('ProceduresService', () => { }).compile(); service = module.get(ProceduresService); - // jest.clearAllMocks(); + jest.clearAllMocks(); }); it('should be defined', () => { @@ -37,33 +43,27 @@ describe('ProceduresService', () => { }); it('should return an array of procedures', () => { - expect(service.findAll()).resolves.toStrictEqual([ - { - title: 'test', - id: 1, - type: '', - period: '', - importantDocuments: [], - }, - ]); - }); - - 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.findAll({ + page: 1, + limit: 1, }), + ).resolves.toStrictEqual({ + procedures: [ + { + title: 'test', + id: 1, + type: '', + period: '', + importantDocuments: [], + }, + ], + count: 1, }); + }); - expect(service.fetchUpcomingProcedures()).resolves.toStrictEqual([ + it('should return an array of upcoming procedures', () => { + const mockResult = [ { title: 'test', id: 1, @@ -71,25 +71,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 +110,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 }; } }