Skip to content

Commit

Permalink
EW-561 introducing search scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
psachmann committed Oct 12, 2023
1 parent e6ca965 commit e7c275d
Show file tree
Hide file tree
Showing 18 changed files with 214 additions and 155 deletions.
29 changes: 29 additions & 0 deletions playground.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@host = 127.0.0.1
@port = 9090

###

GET http://{{host}}:{{port}}/docs HTTP/1.1

###

POST http://{{host}}:{{port}}/api/organisation HTTP/1.1
Content-Type: application/json

{
"kennung": "NI-HK-KGS",
"name": "Kooperative Gesamtschule Schwarmstedt",
"namensergaenzung": "string",
"kuerzel": "KGS",
"typ": "SCHULE"
}

###

@orgId = 78d9ae41-a101-4947-82cc-b0545d2b2e85

GET http://{{host}}:{{port}}/api/organisation/{{orgId}} HTTP/1.1

###

GET http://localhost:8080/metrics HTTP/1.1
2 changes: 2 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ declare type Option<T> = T | null | undefined;
declare type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

declare type Persisted<T, WasPersisted extends boolean> = WasPersisted extends true ? T : Option<T>;

declare type Counted<T> = [T[], number];
5 changes: 3 additions & 2 deletions src/modules/person/api/person.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ApiForbiddenResponse,
ApiInternalServerErrorResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
Expand Down Expand Up @@ -35,7 +36,7 @@ export class PersonController {
}

@Get(':personId')
@ApiCreatedResponse({ description: 'The person was successfully pulled.' })
@ApiOkResponse({ description: 'The person was successfully returned.' })
@ApiBadRequestResponse({ description: 'Person ID is required' })
@ApiUnauthorizedResponse({ description: 'Not authorized to get the person.' })
@ApiNotFoundResponse({ description: 'The person does not exist.' })
Expand All @@ -51,7 +52,7 @@ export class PersonController {
}

@Get()
@ApiCreatedResponse({ description: 'The persons were successfully pulled.' })
@ApiOkResponse({ description: 'The persons were successfully returned.' })
@ApiUnauthorizedResponse({ description: 'Not authorized to get persons.' })
@ApiForbiddenResponse({ description: 'Insufficient permissions to get persons.' })
@ApiInternalServerErrorResponse({ description: 'Internal server error while getting all persons.' })
Expand Down
22 changes: 18 additions & 4 deletions src/modules/person/api/personen-query.param.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import { IsOptional, IsString } from 'class-validator';
import { AutoMap } from '@automapper/classes';

Expand All @@ -21,7 +20,6 @@ export class PersonenQueryParam {
@AutoMap()
@IsOptional()
@IsString()
@Expose({ name: 'familienname' })
@ApiProperty({
name: 'familienname',
required: false,
Expand All @@ -32,7 +30,6 @@ export class PersonenQueryParam {
@AutoMap()
@IsOptional()
@IsString()
@Expose({ name: 'vorname' })
@ApiProperty({
name: 'vorname',
required: false,
Expand All @@ -44,7 +41,6 @@ export class PersonenQueryParam {
/* @AutoMap()
@IsOptional()
@IsEnum(SichtfreigabeType)
@Expose({ name: 'sichtfreigabe' })
@ApiProperty({
name: 'sichtfreigabe',
enum: SichtfreigabeType,
Expand All @@ -53,4 +49,22 @@ export class PersonenQueryParam {
nullable: true,
})
public readonly sichtfreigabe: SichtfreigabeType = SichtfreigabeType.NEIN;*/

@AutoMap()
@IsOptional({})
@ApiProperty({
default: 0,
required: false,
nullable: false,
})
public readonly offset: number = 0;

@AutoMap()
@IsOptional({})
@ApiProperty({
default: 0,
required: false,
nullable: false,
})
public readonly limit: number = 100;
}
23 changes: 20 additions & 3 deletions src/modules/person/domain/person.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Injectable } from '@nestjs/common';
import { DomainError, EntityNotFoundError, PersonAlreadyExistsError } from '../../../shared/error/index.js';
import { PersonDo } from '../domain/person.do.js';
import { PersonRepo } from '../persistence/person.repo.js';
import { PersonScope } from '../persistence/person.scope.js';
import { Paged } from '../../../shared/paging/paged.js';
import { ScopeOrder } from '../../../shared/persistence/scope.enums.js';

@Injectable()
export class PersonService {
Expand All @@ -26,8 +29,22 @@ export class PersonService {
return { ok: false, error: new EntityNotFoundError(`Person with the following ID ${id} does not exist`) };
}

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

return {
total,
offset: 0,
limit: 100,
items: persons,
};
}
}
47 changes: 11 additions & 36 deletions src/modules/person/persistence/person.repo.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { Mapper } from '@automapper/core';
import { getMapperToken } from '@automapper/nestjs';
import { EntityManager } from '@mikro-orm/postgresql';
import { Inject, Injectable } from '@nestjs/common';
import { PersonDo } from '../domain/person.do.js';
import { PersonEntity } from './person.entity.js';
import { EntityName, Loaded, QueryOrderMap } from '@mikro-orm/core';
import { IFindOptions, IPagination, SortOrder } from '../../../shared/interface/find-options.js';
import { Page } from '../../../shared/interface/page.js';
import { Scope } from '../../../shared/repo/scope.js';
import { EntityName, Loaded } from '@mikro-orm/core';
import { PersonScope } from './person.scope.js';
import { PersonSortingMapper } from './person-sorting.mapper.js';
import { PersonSearchQuery } from '../../../shared/interface/person-search-query.js';

@Injectable()
export class PersonRepo {
public constructor(private readonly em: EntityManager, @Inject(getMapperToken()) private readonly mapper: Mapper) {}
Expand All @@ -21,6 +15,15 @@ export class PersonRepo {
return PersonEntity;
}

public async findBy(scope: PersonScope): Promise<Counted<PersonDo<true>>> {
const [entities, total]: Counted<PersonEntity> = await scope.executeQuery(this.em);
const dos: PersonDo<true>[] = entities.map((entity: PersonEntity) =>
this.mapper.map(entity, PersonEntity, PersonDo),
);

return [dos, total];
}

public async findById(id: string): Promise<Option<PersonDo<true>>> {
const person: Option<PersonEntity> = await this.em.findOne(PersonEntity, { id });
if (person) {
Expand Down Expand Up @@ -69,32 +72,4 @@ export class PersonRepo {
await this.em.persistAndFlush(person);
return this.mapper.map(person, PersonEntity, PersonDo);
}

public async findAll(
query: PersonSearchQuery,
options?: IFindOptions<PersonDo<boolean>>,
): Promise<Page<PersonDo<boolean>>> {
const pagination: IPagination = options?.pagination || {};
const order: QueryOrderMap<PersonEntity> = PersonSortingMapper.mapDOSortOrderToQueryOrder(options?.order || {});
const scope: Scope<PersonEntity> = new PersonScope()
.byFirstName(query.firstName)
.byLastName(query.lastName)
.byBirthDate(query.birthDate)
.allowEmptyQuery(true);

if (order.firstName == null) {
order.firstName = SortOrder.asc;
}

const [entities, total]: [PersonEntity[], number] = await this.em.findAndCount(PersonEntity, scope.query, {
...pagination,
orderBy: order,
});

const entityDos: PersonDo<boolean>[] = entities.map((person: PersonEntity) =>
this.mapper.map(person, PersonEntity, PersonDo),
);
const page: Page<PersonDo<boolean>> = new Page<PersonDo<boolean>>(entityDos, total);
return page;
}
}
48 changes: 21 additions & 27 deletions src/modules/person/persistence/person.scope.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,28 @@
/* eslint-disable @typescript-eslint/explicit-member-accessibility */

// TODO should Refactor with german
import { Scope } from '../../../shared/repo/scope.js';
import { EntityName } from '@mikro-orm/core';
import { ScopeBase, ScopeOperator } from '../../../shared/persistence/index.js';
import { PersonEntity } from './person.entity.js';
export class PersonScope extends Scope<PersonEntity> {
byFirstName(firstName: string | undefined): this {
if (firstName) {
this.addQuery({ firstName: { $re: firstName } });
}
return this;
}

byLastName(lastName: string | undefined): this {
if (lastName) {
this.addQuery({ lastName: { $re: lastName } });
}
return this;
}
type SearchProps = {
firstName?: string;
lastName?: string;
birthDate?: Date;
};

byBirthDate(birthDate: Date | undefined): this {
if (birthDate) {
this.addQuery({ birthDate });
}
return this;
export class PersonScope extends ScopeBase<PersonEntity> {
public override get entityName(): EntityName<PersonEntity> {
return PersonEntity;
}

/* byBirthPlace(birthPlace: string | undefined): this {
if (birthPlace) {
this.addQuery({ birthPlace });
}
public searchBy(searchProps: SearchProps): this {
this.findBy(
{
firstName: searchProps.firstName,
lastName: searchProps.lastName,
birthDate: searchProps.birthDate,
},
ScopeOperator.AND,
);

return this;
} */
}
}
16 changes: 0 additions & 16 deletions src/shared/interface/find-options.ts

This file was deleted.

10 changes: 0 additions & 10 deletions src/shared/interface/page.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/shared/interface/person-search-query.ts

This file was deleted.

21 changes: 21 additions & 0 deletions src/shared/paging/paged.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface Paged<T> {
/**
* The number off items skipped from the start.
*/
offset: number;

/**
* The maximal amount of items that can be fetched with one request.
*/
limit: number;

/**
* The total amount of items tha can be fetched.
*/
total: number;

/**
* The requested items.
*/
items: T[];
}
3 changes: 3 additions & 0 deletions src/shared/persistence/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './repo-base.js';
export * from './scope-base.js';
export * from './scope.enums.js';
16 changes: 16 additions & 0 deletions src/shared/persistence/repo-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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) {}

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;
}
}
Loading

0 comments on commit e7c275d

Please sign in to comment.