Skip to content

Commit 3ab0f6d

Browse files
Pagination for cases when fetching all templates or instances (#404)
* first pass * VIBE CODE * MORE VIBE CODE * add display names * format * fix some enable dstuff * remove unnecessary context * templates service should not return instances * gen-client * fix tests * use pagination buttons * move create button * don't use pagination in template directory * support listing total number of instances * fix lint * better pagination for templates on create-form-instance --------- Co-authored-by: Kaiyang Zheng <kaiyang.zheng@gmail.com>
1 parent 277a9d2 commit 3ab0f6d

19 files changed

+1098
-481
lines changed

apps/server/src/form-instances/form-instances.controller.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
4242
import { SignFormInstanceDto } from './dto/sign-form-instance.dto';
4343
import { ParseFormDataJsonPipe } from '../form-templates/form-templates.controller';
4444
import { EmployeesService } from '../employees/employees.service';
45+
import { FormInstanceFindAllResponse } from './response/form-instance-find-all.response';
4546

4647
@ApiTags('form-instances')
4748
@Controller('form-instances')
@@ -71,19 +72,21 @@ export class FormInstancesController {
7172

7273
@Get()
7374
@UseGuards(AdminAuthGuard)
74-
@ApiOkResponse({ type: [FormInstanceEntity] })
75+
@ApiOkResponse({ type: FormInstanceFindAllResponse })
7576
@ApiForbiddenResponse({ description: AppErrorMessage.FORBIDDEN })
7677
@ApiBadRequestResponse({ description: AppErrorMessage.UNPROCESSABLE_ENTITY })
7778
@ApiQuery({
78-
name: 'limit',
79+
name: 'cursor',
7980
type: Number,
80-
description: 'Limit on number of form instances to return',
81+
description: 'Pagination cursor for form instances to return (pages of 8)',
8182
required: false,
8283
})
83-
async findAll(@Query('limit') limit?: number) {
84-
const formInstances = await this.formInstancesService.findAll(limit);
85-
return formInstances.map(
86-
(formInstance) => new FormInstanceEntity(formInstance),
84+
async findAll(@Query('cursor') cursor?: number) {
85+
const formInstances = await this.formInstancesService.findAll(cursor);
86+
const totalCount = await this.formInstancesService.findAllCount();
87+
return new FormInstanceFindAllResponse(
88+
totalCount,
89+
formInstances.map((formInstance) => new FormInstanceEntity(formInstance)),
8790
);
8891
}
8992

apps/server/src/form-instances/form-instances.integration.spec.ts

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,7 @@ describe('FormInstancesIntegrationTest', () => {
601601

602602
describe('findAll', () => {
603603
beforeEach(async () => {
604+
// Create the first two form instances separately for direct reference
604605
formInstance1 = await service.create({
605606
name: 'Form Instance',
606607
assignedGroups: [
@@ -617,6 +618,7 @@ describe('FormInstancesIntegrationTest', () => {
617618
formDocLink: 'formDocLink',
618619
description: 'description',
619620
});
621+
620622
formInstance2 = await service.create({
621623
name: 'Form Instance 2',
622624
assignedGroups: [
@@ -633,19 +635,87 @@ describe('FormInstancesIntegrationTest', () => {
633635
formDocLink: 'formDocLink',
634636
description: 'description',
635637
});
638+
639+
// Create the remaining 8 form instances in parallel
640+
await Promise.all(
641+
Array(8)
642+
.fill(0)
643+
.map((_, i) =>
644+
service.create({
645+
name: `Form Instance ${i + 3}`,
646+
assignedGroups: [
647+
{
648+
order: 0,
649+
signerType: $Enums.SignerType.USER,
650+
fieldGroupId: formTemplate1.fieldGroups[0].id,
651+
signerEmployeeList: [],
652+
signerEmployeeId: employeeId1,
653+
},
654+
],
655+
originatorId: employeeId1,
656+
formTemplateId: formTemplate1.id,
657+
formDocLink: 'formDocLink',
658+
description: 'description',
659+
}),
660+
),
661+
);
636662
});
637663

638664
it('should find all form instances', async () => {
639665
const formInstances = await service.findAll();
640666

641-
expect(formInstances).toHaveLength(2);
642-
expect(formInstances[0].id).toBe(formInstance1.id);
643-
expect(formInstances[1].id).toBe(formInstance2.id);
667+
expect(formInstances).toHaveLength(10);
644668
});
645-
it('should limit the number of form instances returned', async () => {
646-
const formInstances = await service.findAll(1);
669+
it('should paginate the form instances returned', async () => {
670+
const formInstancesPage0 = await service.findAll(0);
671+
const formInstancesPage1 = await service.findAll(1);
647672

648-
expect(formInstances).toHaveLength(1);
673+
expect(formInstancesPage0).toHaveLength(8);
674+
expect(formInstancesPage1).toHaveLength(2);
675+
});
676+
});
677+
describe('findAllCount', () => {
678+
beforeEach(async () => {
679+
// Create the first two form instances separately for direct reference
680+
formInstance1 = await service.create({
681+
name: 'Form Instance',
682+
assignedGroups: [
683+
{
684+
order: 0,
685+
fieldGroupId: formTemplate1.fieldGroups[0].id,
686+
signerType: $Enums.SignerType.USER,
687+
signerEmployeeId: employeeId1,
688+
signerEmployeeList: [],
689+
},
690+
],
691+
originatorId: employeeId1,
692+
formTemplateId: formTemplate1.id,
693+
formDocLink: 'formDocLink',
694+
description: 'description',
695+
});
696+
697+
formInstance2 = await service.create({
698+
name: 'Form Instance 2',
699+
assignedGroups: [
700+
{
701+
order: 0,
702+
signerType: $Enums.SignerType.USER,
703+
fieldGroupId: formTemplate1.fieldGroups[0].id,
704+
signerEmployeeList: [],
705+
signerEmployeeId: employeeId1,
706+
},
707+
],
708+
originatorId: employeeId1,
709+
formTemplateId: formTemplate1.id,
710+
formDocLink: 'formDocLink',
711+
description: 'description',
712+
});
713+
});
714+
715+
it('should return the count of all form instances', async () => {
716+
const count = await service.findAllCount();
717+
718+
expect(count).toBe(2);
649719
});
650720
});
651721
describe('findOne', () => {

apps/server/src/form-instances/form-instances.service.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ const db = {
248248
save: jest.fn(),
249249
update: jest.fn().mockResolvedValue(oneFormInstance),
250250
delete: jest.fn().mockResolvedValue(oneFormInstance),
251+
count: jest.fn().mockResolvedValue(formInstancesArray.length),
251252
},
252253
formTemplate: {
253254
findFirstOrThrow: jest.fn().mockResolvedValue(formTemplate),
@@ -567,6 +568,14 @@ describe('FormInstancesService', () => {
567568
});
568569
});
569570

571+
describe('findAllCount', () => {
572+
it('should return the count of all form instances', () => {
573+
expect(service.findAllCount()).resolves.toEqual(
574+
formInstancesArray.length,
575+
);
576+
});
577+
});
578+
570579
describe('findOne', () => {
571580
it('should find a form instance by id', () => {
572581
expect(service.findOne(formInstance1Id)).resolves.toEqual(

apps/server/src/form-instances/form-instances.service.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,12 +342,12 @@ export class FormInstancesService {
342342

343343
/**
344344
* Find all form instances.
345-
* @param limit the number of form instances to retrieve
345+
* @param cursor the form instances to retrieve, paginated
346346
* @returns all form instances, hydrated
347347
*/
348-
async findAll(limit?: number) {
348+
async findAll(cursor?: number) {
349349
const formInstances = await this.prisma.formInstance.findMany({
350-
take: limit,
350+
...(cursor !== undefined ? { take: 8, skip: cursor * 8 } : {}),
351351
include: {
352352
originator: {
353353
include: {
@@ -383,6 +383,13 @@ export class FormInstancesService {
383383
return formInstances;
384384
}
385385

386+
/**
387+
* Find the count of all form instances.
388+
*/
389+
async findAllCount() {
390+
return await this.prisma.formInstance.count();
391+
}
392+
386393
/**
387394
* Find a form instance by id.
388395
* @param id the form instance id
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { FormInstanceEntity } from '../entities/form-instance.entity';
3+
4+
export class FormInstanceFindAllResponse {
5+
@ApiProperty()
6+
count: number;
7+
8+
@ApiProperty({ type: [FormInstanceEntity] })
9+
formInstances: FormInstanceEntity[];
10+
11+
constructor(count: number, formInstances: FormInstanceEntity[]) {
12+
this.count = count;
13+
this.formInstances = formInstances;
14+
}
15+
}

apps/server/src/form-templates/entities/form-template.entity.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ApiProperty } from '@nestjs/swagger';
22
import { FormTemplate } from '@prisma/client';
33
import { Exclude } from 'class-transformer';
4-
import { FormInstanceEntity } from './../../form-instances/entities/form-instance.entity';
54
import { FieldGroupBaseEntity } from '../../field-group/entities/field-group.entity';
65
import { IsOptional } from 'class-validator';
76

@@ -46,21 +45,13 @@ export class FormTemplateEntity extends FormTemplateBaseEntity {
4645
})
4746
fieldGroups: FieldGroupBaseEntity[];
4847

49-
@ApiProperty()
50-
formInstances: FormInstanceEntity[];
51-
5248
constructor(partial: Partial<FormTemplateEntity>) {
5349
super(partial);
5450
if (partial.fieldGroups) {
5551
partial.fieldGroups = partial.fieldGroups.map(
5652
(fieldGroup) => new FieldGroupBaseEntity(fieldGroup),
5753
);
5854
}
59-
if (partial.formInstances) {
60-
partial.formInstances = partial.formInstances.map(
61-
(formInstance) => new FormInstanceEntity(formInstance),
62-
);
63-
}
6455
Object.assign(this, partial);
6556
}
6657
}

apps/server/src/form-templates/form-templates.controller.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { Express } from 'express';
3939
import { FileInterceptor } from '@nestjs/platform-express';
4040
import { ContributorAuthGuard } from '../auth/guards/contributor-auth.guard';
4141
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
42+
import { FormTemplateFindAllResponse } from './responses/form-template-find-all.response';
4243

4344
export class ParseFormDataJsonPipe implements PipeTransform {
4445
constructor() {}
@@ -115,19 +116,23 @@ export class FormTemplatesController {
115116

116117
@Get()
117118
@UseGuards(JwtAuthGuard)
118-
@ApiOkResponse({ type: [FormTemplateEntity] })
119+
@ApiOkResponse({ type: FormTemplateFindAllResponse })
119120
@ApiForbiddenResponse({ description: AppErrorMessage.FORBIDDEN })
120121
@ApiBadRequestResponse({ description: AppErrorMessage.UNPROCESSABLE_ENTITY })
121122
@ApiQuery({
122-
name: 'limit',
123+
name: 'cursor',
123124
type: Number,
124-
description: 'Limit on number of form templates to return',
125+
description: 'Pagination cursor for form templates to return (pages of 8)',
125126
required: false,
126127
})
127-
async findAll(@Query('limit') limit?: number) {
128-
const formTemplates = await this.formTemplatesService.findAll(limit);
129-
return formTemplates.map(
130-
(formTemplate) => new FormTemplateEntity(formTemplate),
128+
async findAll(@Query('cursor') cursor?: number) {
129+
const formTemplates = await this.formTemplatesService.findAll(cursor);
130+
const formTemplatesCount = await this.formTemplatesService.findAllCount();
131+
return new FormTemplateFindAllResponse(
132+
formTemplatesCount,
133+
formTemplates?.map(
134+
(formTemplate) => new FormTemplateEntity(formTemplate),
135+
),
131136
);
132137
}
133138

0 commit comments

Comments
 (0)