From 318668068718b67fa0a2644bb00b071f73e03e8f Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Thu, 5 Oct 2023 14:54:16 +0200 Subject: [PATCH] EW-561 base implementation part 1 --- .../persistence/person-sorting.mapper.ts | 21 +++++++ src/modules/person/persistence/person.repo.ts | 62 +++++++++++++++---- .../person/persistence/person.scope.ts | 34 ++++++++++ src/shared/interface/find-options.ts | 16 +++++ src/shared/interface/page.ts | 10 +++ src/shared/query/empty-result.query.ts | 5 ++ src/shared/repo/scope.ts | 43 +++++++++++++ 7 files changed, 178 insertions(+), 13 deletions(-) create mode 100644 src/modules/person/persistence/person-sorting.mapper.ts create mode 100644 src/modules/person/persistence/person.scope.ts create mode 100644 src/shared/interface/find-options.ts create mode 100644 src/shared/interface/page.ts create mode 100644 src/shared/query/empty-result.query.ts create mode 100644 src/shared/repo/scope.ts diff --git a/src/modules/person/persistence/person-sorting.mapper.ts b/src/modules/person/persistence/person-sorting.mapper.ts new file mode 100644 index 000000000..1d9b6b8cd --- /dev/null +++ b/src/modules/person/persistence/person-sorting.mapper.ts @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ +/* eslint-disable import/extensions */ +import { QueryOrderMap } from '@mikro-orm/core'; +import { SortOrderMap } from '../../../shared/interface/find-options'; +import { PersonDo } from '../domain/person.do'; +import { PersonEntity } from './person.entity'; + +export class PersonSortingMapper { + static mapDOSortOrderToQueryOrder(sort: SortOrderMap>): QueryOrderMap { + const queryOrderMap: SortOrderMap = { + id: sort.id, + firstName: sort.firstName, + lastName: sort.lastName, + birthDate: sort.birthDate, + }; + Object.keys(queryOrderMap) + .filter((key) => queryOrderMap[key] === undefined) + .forEach((key) => delete queryOrderMap[key]); + return queryOrderMap; + } +} diff --git a/src/modules/person/persistence/person.repo.ts b/src/modules/person/persistence/person.repo.ts index e83daf43e..06d5dde7c 100644 --- a/src/modules/person/persistence/person.repo.ts +++ b/src/modules/person/persistence/person.repo.ts @@ -1,10 +1,17 @@ +/* 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 { Loaded } from '@mikro-orm/core'; +import { 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 { PersonScope } from './person.scope.js'; +import { PersonSortingMapper } from './person-sorting.mapper.js'; @Injectable() export class PersonRepo { public constructor(private readonly em: EntityManager, @Inject(getMapperToken()) private readonly mapper: Mapper) {} @@ -58,21 +65,50 @@ export class PersonRepo { return this.mapper.map(person, PersonEntity, PersonDo); } - public async findAll(personDo: PersonDo): Promise[]> { - const query: Record = {}; + public async findAll( + personDo: PersonDo, + options?: IFindOptions[]>, + ): Promise[]> { + const pagination: IPagination = options?.pagination || {}; + const order: QueryOrderMap = PersonSortingMapper.mapDOSortOrderToQueryOrder(options?.order || {}); + const scope: Scope = new PersonScope() + .byFirstName(personDo.firstName) + .byLastName(personDo.lastName) + .byBirthDate(personDo.birthDate) + .allowEmptyQuery(true); - if (personDo.firstName) { - query['firstName'] = { $ilike: personDo.firstName }; + if (order.id == null) { + order.firstName = SortOrder.asc; } - if (personDo.lastName) { - query['lastName'] = { $ilike: personDo.lastName }; - } + // if (personDo.firstName) { + // query['firstName'] = { $ilike: personDo.firstName }; + // } - if (personDo.referrer) { - query['referrer'] = personDo.referrer; - } - const result: PersonEntity[] = await this.em.find(PersonEntity, query); - return result.map((person: PersonEntity) => this.mapper.map(person, PersonEntity, PersonDo)); + // if (personDo.lastName) { + // query['lastName'] = { $ilike: personDo.lastName }; + // } + + // if (personDo.referrer) { + // query['referrer'] = personDo.referrer; + // } + + const [entities, total]: [PersonEntity[], number] = await this.em.findAndCount(PersonEntity, scope.personDo, { + offset: pagination?.skip, + limit: pagination?.limit, + }); + + const entityDos: PersonDo[] = entities.map((person: PersonEntity) => + this.mapper.map(person, PersonEntity, PersonDo), + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const page: Page[]> = new Page>(entityDos, total); + // return result.map((person: PersonEntity) => this.mapper.map(person, PersonEntity, PersonDo)); + return page; } + // mapEntityToDO(entity: PersonEntity): PersonDo { + // const domainObject = PersonRepo.mapEntityToDO(entity); + + // return domainObject; + // } } diff --git a/src/modules/person/persistence/person.scope.ts b/src/modules/person/persistence/person.scope.ts new file mode 100644 index 000000000..59b0071f7 --- /dev/null +++ b/src/modules/person/persistence/person.scope.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ + +// TODO should Refactor with german +import { Scope } from '../../../shared/repo/scope.js'; +import { PersonEntity } from './person.entity.js'; +export class PersonScope extends Scope { + 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; + } + + byBirthDate(birthDate: Date | undefined): this { + if (birthDate) { + this.addQuery({ birthDate }); + } + return this; + } + + /* byBirthPlace(birthPlace: string | undefined): this { + if (birthPlace) { + this.addQuery({ birthPlace }); + } + return this; + } */ +} diff --git a/src/shared/interface/find-options.ts b/src/shared/interface/find-options.ts new file mode 100644 index 000000000..915624503 --- /dev/null +++ b/src/shared/interface/find-options.ts @@ -0,0 +1,16 @@ +export interface IPagination { + skip?: number; + limit?: number; +} + +export enum SortOrder { + asc = 'asc', + desc = 'desc', +} + +export type SortOrderMap = Partial>; + +export interface IFindOptions { + pagination?: IPagination; + order?: SortOrderMap; +} diff --git a/src/shared/interface/page.ts b/src/shared/interface/page.ts new file mode 100644 index 000000000..0fba6d047 --- /dev/null +++ b/src/shared/interface/page.ts @@ -0,0 +1,10 @@ +export class Page { + public data: T[]; + + public total: number; + + public constructor(data: T[], total: number) { + this.data = data; + this.total = total; + } +} diff --git a/src/shared/query/empty-result.query.ts b/src/shared/query/empty-result.query.ts new file mode 100644 index 000000000..777a57e9d --- /dev/null +++ b/src/shared/query/empty-result.query.ts @@ -0,0 +1,5 @@ +/** + * When this query is added ($and) to an existing query, + * it should ensure an empty result + */ +export const EmptyResultQuery = { $and: [{ id: false }] }; diff --git a/src/shared/repo/scope.ts b/src/shared/repo/scope.ts new file mode 100644 index 000000000..3add96247 --- /dev/null +++ b/src/shared/repo/scope.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ +/* eslint-disable no-underscore-dangle */ + +import { FilterQuery } from '@mikro-orm/core'; +import { EmptyResultQuery } from '../query/empty-result.query.js'; + +type EmptyResultQueryType = typeof EmptyResultQuery; + +type ScopeOperator = '$and' | '$or'; + +export class Scope { + private _queries: FilterQuery[] = []; + + private _operator: ScopeOperator; + + private _allowEmptyQuery: boolean; + + constructor(operator: ScopeOperator = '$and') { + this._operator = operator; + this._allowEmptyQuery = false; + } + + get query(): FilterQuery { + if (this._queries.length === 0) { + if (this._allowEmptyQuery) { + return {} as FilterQuery; + } + return EmptyResultQuery as FilterQuery; + } + // eslint-disable-next-line @typescript-eslint/typedef + const query = this._queries.length > 1 ? { [this._operator]: this._queries } : this._queries[0]; + return query as FilterQuery; + } + + addQuery(query: FilterQuery): void { + this._queries.push(query); + } + + allowEmptyQuery(isEmptyQueryAllowed: boolean): Scope { + this._allowEmptyQuery = isEmptyQueryAllowed; + return this; + } +}