Skip to content

Commit

Permalink
EW-561 adding paging interceptor
Browse files Browse the repository at this point in the history
  • Loading branch information
psachmann committed Oct 13, 2023
1 parent e7c275d commit 5a98abf
Show file tree
Hide file tree
Showing 18 changed files with 164 additions and 78 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"axios": "^1.5.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"express": "^4.18.2",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"nest-commander": "^3.9.0",
Expand Down
2 changes: 1 addition & 1 deletion src/console/console.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { DbInitConsole } from './db-init.console.js';
entitiesTs: ['./src/**/*.entity.ts'],
driverOptions: {
connection: {
ssl: true,
ssl: false,
},
},
});
Expand Down
16 changes: 12 additions & 4 deletions src/modules/person/api/person.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Mapper } from '@automapper/core';
import { getMapperToken } from '@automapper/nestjs';
import { Body, Controller, Get, Inject, Post, Param, HttpException, HttpStatus, Query } from '@nestjs/common';
import { Body, Controller, Get, Inject, Post, Param, HttpException, HttpStatus, Query, Res } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiCreatedResponse,
Expand All @@ -18,6 +18,8 @@ import { PersonByIdParams } from './person-by-id.param.js';
import { PersonenQueryParam } from './personen-query.param.js';
import { FindPersonDatensatzDTO } from './finde-persondatensatz-dto.js';
import { PersonenDatensatz } from './personendatensatz.js';
import { Paged, setPaginationHeaders } from '../../../shared/paging/index.js';
import { Response } from 'express';

@ApiTags('person')
@Controller({ path: 'person' })
Expand Down Expand Up @@ -56,13 +58,19 @@ export class PersonController {
@ApiUnauthorizedResponse({ description: 'Not authorized to get persons.' })
@ApiForbiddenResponse({ description: 'Insufficient permissions to get persons.' })
@ApiInternalServerErrorResponse({ description: 'Internal server error while getting all persons.' })
public async findPersons(@Query() queryParams: PersonenQueryParam): Promise<PersonenDatensatz[]> {
public async findPersons(
@Query() queryParams: PersonenQueryParam,
@Res() res: Response,
): Promise<PersonenDatensatz[]> {
const persondatensatzDTO: FindPersonDatensatzDTO = this.mapper.map(
queryParams,
PersonenQueryParam,
FindPersonDatensatzDTO,
);
const persons: PersonenDatensatz[] = await this.uc.findAll(persondatensatzDTO);
return persons;
const persons: Paged<PersonenDatensatz> = await this.uc.findAll(persondatensatzDTO);

setPaginationHeaders(res, persons);

return persons.items;
}
}
34 changes: 24 additions & 10 deletions src/modules/person/api/person.uc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { KeycloakUserService, UserDo } from '../../keycloak-administration/index
import { CreatePersonDto } from '../domain/create-person.dto.js';
import { PersonService } from '../domain/person.service.js';
import { PersonDo } from '../domain/person.do.js';
import { FindPersonDatensatzDTO } from './finde-persondatensatz-dto.js';
import { FindPersonDatensatzDTO as FindPersonDatensatzDto } from './finde-persondatensatz-dto.js';
import { PersonenDatensatz } from './personendatensatz.js';
import { Paged } from '../../../shared/paging/index.js';

@Injectable()
export class PersonUc {
Expand Down Expand Up @@ -51,15 +52,28 @@ export class PersonUc {
throw result.error;
}

public async findAll(personDto: FindPersonDatensatzDTO): Promise<PersonenDatensatz[]> {
const personDo: PersonDo<false> = this.mapper.map(personDto, FindPersonDatensatzDTO, PersonDo);
const result: PersonDo<true>[] = await this.personService.findAllPersons(personDo);
if (result.length !== 0) {
const persons: PersonenDatensatz[] = result.map((person: PersonDo<true>) =>
this.mapper.map(person, PersonDo, PersonenDatensatz),
);
return persons;
public async findAll(personDto: FindPersonDatensatzDto): Promise<Paged<PersonenDatensatz>> {
const personDo: PersonDo<false> = this.mapper.map(personDto, FindPersonDatensatzDto, PersonDo);
const result: Paged<PersonDo<true>> = await this.personService.findAllPersons(undefined, undefined, personDo);

if (result.total === 0) {
return {
total: result.total,
offset: result.offset,
limit: result.limit,
items: [],
};
}
return [];

const persons: PersonenDatensatz[] = result.items.map((person: PersonDo<true>) =>
this.mapper.map(person, PersonDo, PersonenDatensatz),
);

return {
total: result.total,
offset: result.offset,
limit: result.limit,
items: persons,
};
}
}
20 changes: 12 additions & 8 deletions src/modules/person/domain/person.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,25 @@ export class PersonService {
return { ok: false, error: new EntityNotFoundError(`Person with the following ID ${id} does not exist`) };
}

public async findAllPersons(personDto: PersonDo<false>): Promise<Paged<PersonDo<true>>> {
public async findAllPersons(
offset: Option<number>,
limit: Option<number>,
personDo: Partial<PersonDo<false>>,
): Promise<Paged<PersonDo<true>>> {
const scope: PersonScope = new PersonScope()
.searchBy({
firstName: personDto.firstName,
lastName: personDto.lastName,
birthDate: personDto.birthDate,
.findBy({
firstName: personDo.firstName,
lastName: personDo.lastName,
birthDate: personDo.birthDate,
})
.sortBy('firstName', ScopeOrder.ASC)
.paged(0, 100);
.paged(offset, limit);
const [persons, total]: Counted<PersonDo<true>> = await this.personRepo.findBy(scope);

return {
total,
offset: 0,
limit: 100,
offset: offset ?? 0,
limit: limit ?? total,
items: persons,
};
}
Expand Down
22 changes: 0 additions & 22 deletions src/modules/person/persistence/person-sorting.mapper.ts

This file was deleted.

14 changes: 7 additions & 7 deletions src/modules/person/persistence/person.scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EntityName } from '@mikro-orm/core';
import { ScopeBase, ScopeOperator } from '../../../shared/persistence/index.js';
import { PersonEntity } from './person.entity.js';

type SearchProps = {
type FindProps = {
firstName?: string;
lastName?: string;
birthDate?: Date;
Expand All @@ -13,14 +13,14 @@ export class PersonScope extends ScopeBase<PersonEntity> {
return PersonEntity;
}

public searchBy(searchProps: SearchProps): this {
this.findBy(
public findBy(findProps: FindProps, operator: ScopeOperator = ScopeOperator.AND): this {
this.findByInternal(
{
firstName: searchProps.firstName,
lastName: searchProps.lastName,
birthDate: searchProps.birthDate,
firstName: findProps.firstName,
lastName: findProps.lastName,
birthDate: findProps.birthDate,
},
ScopeOperator.AND,
operator,
);

return this;
Expand Down
11 changes: 8 additions & 3 deletions src/server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,28 @@ import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger';
import { HostConfig, ServerConfig } from '../shared/config/index.js';
import { GlobalValidationPipe } from '../shared/validation/index.js';
import { ServerModule } from './server.module.js';
import { GlobalPaginationHeadersInterceptor } from '../shared/paging/index.js';

async function bootstrap(): Promise<void> {
const app: INestApplication = await NestFactory.create(ServerModule);
app.useGlobalPipes(new GlobalValidationPipe());
const configService: ConfigService<ServerConfig, true> = app.get(ConfigService<ServerConfig, true>);
const port: number = configService.getOrThrow<HostConfig>('HOST').PORT;
const swagger: Omit<OpenAPIObject, 'paths'> = new DocumentBuilder()
.setTitle('dBildungs IAM')
.setDescription('The dBildungs IAM server API description')
.setVersion('1.0')
.build();
const configService: ConfigService<ServerConfig, true> = app.get(ConfigService<ServerConfig, true>);
const port: number = configService.getOrThrow<HostConfig>('HOST').PORT;

app.useGlobalInterceptors(new GlobalPaginationHeadersInterceptor());
app.useGlobalPipes(new GlobalValidationPipe());
app.setGlobalPrefix('api', {
exclude: ['health'],
});

SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, swagger));

await app.listen(port);

console.info(`\nListening on: http://127.0.0.1:${port}`);
console.info(`API documentation can be found on: http://127.0.0.1:${port}/docs`);
}
Expand Down
2 changes: 1 addition & 1 deletion src/server/server.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { OrganisationApiModule } from '../modules/organisation/organisation-api.
type: 'postgresql',
driverOptions: {
connection: {
ssl: true,
ssl: false,
},
},
});
Expand Down
23 changes: 23 additions & 0 deletions src/shared/paging/global-pagination-headers.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Observable, map } from 'rxjs';
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { setPaginationHeaders } from './helpers.js';
import { PagedResponse } from './paged.response.js';
import { Response } from 'express';

export class GlobalPaginationHeadersInterceptor implements NestInterceptor {
public intercept(context: ExecutionContext, next: CallHandler<unknown>): Observable<unknown | unknown[]> {
return next.handle().pipe(
map((value: unknown) => {
if (value instanceof PagedResponse) {
const response: Response = context.switchToHttp().getResponse();

setPaginationHeaders(response, value);

return value.items as unknown[];
}

return value;
}),
);
}
}
8 changes: 8 additions & 0 deletions src/shared/paging/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Response } from 'express';
import { PagedResponse } from './paged.response.js';

export function setPaginationHeaders<T>(response: Response, payload: PagedResponse<T>): void {
response.setHeader('Pagination-Total', payload.total);
response.setHeader('Pagination-Offset', payload.offset);
response.setHeader('Pagination-Limit', payload.limit);
}
5 changes: 5 additions & 0 deletions src/shared/paging/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './global-pagination-headers.interceptor.js';
export * from './helpers.js';
export * from './paged.js';
export * from './paged.query.params.js';
export * from './paged.response.js';
15 changes: 15 additions & 0 deletions src/shared/paging/paged.query.params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiPropertyOptional } from '@nestjs/swagger';

export abstract class PagedQueryParams {
@ApiPropertyOptional({
description: 'The offset of the paginated list.',
})
public readonly offset?: number;

@ApiPropertyOptional({
description: 'The requested limit for the page size.',
})
public readonly limit?: number;
}

Check warning on line 14 in src/shared/paging/paged.query.params.ts

View workflow job for this annotation

GitHub Actions / nest_lint / Nest Lint

Delete `⏎⏎`

24 changes: 24 additions & 0 deletions src/shared/paging/paged.response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';
import { Paged } from './paged.js';

export class PagedResponse<T> {
@Exclude()
public readonly total: number;

@Exclude()
public readonly offset: number;

@Exclude()
public readonly limit: number;

@ApiProperty()
public readonly items: T[];

public constructor(page: Paged<T>) {
this.total = page.total;
this.offset = page.offset;
this.limit = page.limit;
this.items = page.items;
}
}
2 changes: 1 addition & 1 deletion src/shared/persistence/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './repo-base.js';
// export * from './repo-base.js';
export * from './scope-base.js';
export * from './scope.enums.js';
26 changes: 13 additions & 13 deletions src/shared/persistence/repo-base.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { AnyEntity, EntityName } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/postgresql';
// import { AnyEntity, EntityName } from '@mikro-orm/core';
// import { EntityManager } from '@mikro-orm/postgresql';

export abstract class RepoBase<T extends AnyEntity> {
protected constructor(protected readonly em: EntityManager) {}
// export abstract class RepoBase<T extends AnyEntity> {
// protected constructor(protected readonly em: EntityManager) {}

public abstract get entityName(): EntityName<T>;
// public abstract get entityName(): EntityName<T>;

public async findById(id: string): Promise<Option<PersonDo<true>>> {
const person: Option<PersonEntity> = await this.em.findOne(this.entityName, { id });
if (person) {
return this.mapper.map(person, PersonEntity, PersonDo);
}
return null;
}
}
// public async findById(id: string): Promise<Option<PersonDo<true>>> {
// const person: Option<PersonEntity> = await this.em.findOne(this.entityName, { id });
// if (person) {
// return this.mapper.map(person, PersonEntity, PersonDo);
// }
// return null;
// }
// }
Loading

0 comments on commit 5a98abf

Please sign in to comment.