From 38e420856e8225ec2c0324a8cb810396974f8d41 Mon Sep 17 00:00:00 2001 From: ashar Date: Thu, 30 May 2024 09:30:56 +0700 Subject: [PATCH] feat(SPG-122) Abstraction Based Data --- src/app.module.ts | 33 +++++++++++- src/auth/domain/services/auth.service.ts | 4 +- .../modules/data/model/base-core.model.ts | 8 +++ .../modules/data/model/base-status.model.ts | 10 ++++ src/core/modules/data/model/base.model.ts | 24 +++++++++ .../modules/data/service/base-data.service.ts | 54 +++++++++++++++++++ .../modules/data/service/base-read.service.ts | 42 +++++++++++++++ .../data/service/base-tree-read.service.ts | 52 ++++++++++++++++++ .../domain/entities/base-core.entity.ts | 3 ++ .../infrastructure/dto/base-core.dto.ts | 5 ++ .../infrastructure/dto/base-status.dto.ts | 7 +++ .../modules/infrastructure/dto/base.dto.ts | 11 ++++ .../domain/exceptions/handled-exception.ts | 36 +++++++++++-- .../entities/user-sessions.interface.ts | 2 - src/core/strings/constants/base.constants.ts | 43 +++++++++++++++ .../strings/constants/interface.constants.ts | 17 ++++++ .../strings/constants/module.constants.ts | 3 ++ src/core/strings/constants/table.constants.ts | 3 ++ src/core/strings/errors/general/contansts.ts | 0 src/core/strings/errors/general/messages.ts | 0 src/core/strings/errors/index.ts | 0 src/core/strings/errors/readme.md | 1 + src/main.ts | 27 +++++++++- src/modules/configuration/couch/constants.ts | 0 24 files changed, 375 insertions(+), 10 deletions(-) create mode 100644 src/core/modules/data/model/base-core.model.ts create mode 100644 src/core/modules/data/model/base-status.model.ts create mode 100644 src/core/modules/data/model/base.model.ts create mode 100644 src/core/modules/data/service/base-data.service.ts create mode 100644 src/core/modules/data/service/base-read.service.ts create mode 100644 src/core/modules/data/service/base-tree-read.service.ts create mode 100644 src/core/modules/domain/entities/base-core.entity.ts create mode 100644 src/core/modules/infrastructure/dto/base-core.dto.ts create mode 100644 src/core/modules/infrastructure/dto/base-status.dto.ts create mode 100644 src/core/modules/infrastructure/dto/base.dto.ts create mode 100644 src/core/strings/constants/base.constants.ts create mode 100644 src/core/strings/constants/interface.constants.ts create mode 100644 src/core/strings/constants/module.constants.ts create mode 100644 src/core/strings/constants/table.constants.ts create mode 100644 src/core/strings/errors/general/contansts.ts create mode 100644 src/core/strings/errors/general/messages.ts create mode 100644 src/core/strings/errors/index.ts create mode 100644 src/core/strings/errors/readme.md create mode 100644 src/modules/configuration/couch/constants.ts diff --git a/src/app.module.ts b/src/app.module.ts index 64b35f5..2c8fa5f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,9 +4,40 @@ import { AuthModule } from './auth/auth.module'; import { JWTGuard } from './core/guards'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { HttpExceptionFilter, TransformInterceptor } from './core/response'; +import { ApmModule } from './core/apm'; +import { CONNECTION_NAME } from './core/strings/constants/base.constants'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ConfigModule } from '@nestjs/config'; +import { UserPrivilegeModule } from './modules/user-related/user-privilege/user-privilege.module'; +import { UserPrivilegeModel } from './modules/user-related/user-privilege/data/model/user-privilege.model'; +import { CqrsModule } from '@nestjs/cqrs'; +import { CouchModule } from './modules/configuration/couch/couch.module'; @Module({ - imports: [SessionModule, AuthModule], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + TypeOrmModule.forRoot({ + name: CONNECTION_NAME.DEFAULT, + type: 'postgres', + host: process.env.DEFAULT_DB_HOST, + port: parseInt(process.env.DEFAULT_DB_PORT), + username: process.env.DEFAULT_DB_USER, + password: process.env.DEFAULT_DB_PASS, + database: process.env.DEFAULT_DB_NAME, + entities: [ + UserPrivilegeModel, + ], + synchronize: true, + }), + CqrsModule, + SessionModule, + AuthModule, + CouchModule, + + UserPrivilegeModule, + ], controllers: [], providers: [ /** diff --git a/src/auth/domain/services/auth.service.ts b/src/auth/domain/services/auth.service.ts index 35eeec8..4c43368 100644 --- a/src/auth/domain/services/auth.service.ts +++ b/src/auth/domain/services/auth.service.ts @@ -19,9 +19,9 @@ export class AuthService { const token = this.session.createAccessToken({ id: user.id, - username: user.username, + // username: user.username, name: user.name, - roles: user.roles, + // roles: user.roles, }); return token; diff --git a/src/core/modules/data/model/base-core.model.ts b/src/core/modules/data/model/base-core.model.ts new file mode 100644 index 0000000..50a08f4 --- /dev/null +++ b/src/core/modules/data/model/base-core.model.ts @@ -0,0 +1,8 @@ +import { Entity, PrimaryGeneratedColumn } from "typeorm"; +import { BaseCoreEntity } from "../../domain/entities/base-core.entity"; + +@Entity() +export abstract class BaseCoreModel implements BaseCoreEntity { + @PrimaryGeneratedColumn('uuid') + id: string; +} diff --git a/src/core/modules/data/model/base-status.model.ts b/src/core/modules/data/model/base-status.model.ts new file mode 100644 index 0000000..a8cece6 --- /dev/null +++ b/src/core/modules/data/model/base-status.model.ts @@ -0,0 +1,10 @@ +import { Column, Entity } from "typeorm"; +import { BaseModel } from "./base.model"; +import { STATUS } from "src/core/strings/constants/base.constants"; +import { BaseStatusEntity } from "../../domain/entities/base-status.entity"; + +@Entity() +export abstract class BaseStatusModel extends BaseModel implements BaseStatusEntity { + @Column('enum', { name: 'status', enum: STATUS, default: STATUS.DRAFT }) + status: STATUS; +} \ No newline at end of file diff --git a/src/core/modules/data/model/base.model.ts b/src/core/modules/data/model/base.model.ts new file mode 100644 index 0000000..6be4c60 --- /dev/null +++ b/src/core/modules/data/model/base.model.ts @@ -0,0 +1,24 @@ +import { Column, Entity } from 'typeorm'; +import { BaseCoreModel } from './base-core.model'; +import { BaseEntity } from '../../domain/entities/base.entity'; + +@Entity() +export abstract class BaseModel extends BaseCoreModel implements BaseEntity { + @Column('varchar', { name: 'creator_id', length: 36, nullable: true }) + creator_id: string; + + @Column('varchar', { name: 'creator_name', length: 125, nullable: true }) + creator_name: string; + + @Column('varchar', { name: 'editor_id', length: 36, nullable: true }) + editor_id: string; + + @Column('varchar', { name: 'editor_name', length: 125, nullable: true }) + editor_name: string; + + @Column({ type: 'bigint', nullable: false }) + created_at: number; + + @Column({ type: 'bigint', nullable: false }) + updated_at: number; +} diff --git a/src/core/modules/data/service/base-data.service.ts b/src/core/modules/data/service/base-data.service.ts new file mode 100644 index 0000000..6d0f585 --- /dev/null +++ b/src/core/modules/data/service/base-data.service.ts @@ -0,0 +1,54 @@ +import { EntityTarget, FindManyOptions, QueryRunner, Repository } from "typeorm"; + +export abstract class BaseDataService { + + constructor(private repository: Repository) {} + + getRepository(): Repository { + return this.repository; + } + + async create( + queryRunner: QueryRunner, + entityTarget: EntityTarget, + entity: Entity, + ): Promise { + const newEntity = queryRunner.manager.create(entityTarget, entity); + return await queryRunner.manager.save(newEntity); + } + + async update( + queryRunner: QueryRunner, + entityTarget: EntityTarget, + filterUpdate: any, + entity: Entity, + ): Promise { + const newEntity = await queryRunner.manager.findOne(entityTarget, { + where: filterUpdate, + }); + + if (!newEntity) throw new Error('Data not found!'); + Object.assign(newEntity, entity); + return await queryRunner.manager.save(newEntity); + } + + async deleteById( + queryRunner: QueryRunner, + entityTarget: EntityTarget, + id: string, + ): Promise { + await queryRunner.manager.delete(entityTarget, { id }); + } + + async deleteByOptions( + queryRunner: QueryRunner, + entityTarget: EntityTarget, + findManyOptions: FindManyOptions, + ): Promise { + await queryRunner.manager.delete(entityTarget, findManyOptions); + } + + async getOneByOptions(findOneOptions): Promise { + return await this.repository.findOne(findOneOptions); + } +} \ No newline at end of file diff --git a/src/core/modules/data/service/base-read.service.ts b/src/core/modules/data/service/base-read.service.ts new file mode 100644 index 0000000..a840884 --- /dev/null +++ b/src/core/modules/data/service/base-read.service.ts @@ -0,0 +1,42 @@ +import { FindOneOptions, Repository, SelectQueryBuilder } from "typeorm"; +import { BaseFilterEntity } from "../../domain/entities/base-filter.entity"; +import { PaginationResponse } from "src/core/response/domain/ok-response.interface"; + +export abstract class BaseReadService { + + constructor(private repository: Repository) {} + + getRepository(): Repository { + return this.repository; + } + + async getIndex( + queryBuilder: SelectQueryBuilder, + params: BaseFilterEntity, + ): Promise> { + const [data, total] = await queryBuilder + .take(+params.limit) + .skip(+params.limit * +params.page - +params.limit) + .getManyAndCount(); + + return { + data, + total, + }; + } + + async getOneByOptions(findOneOptions): Promise { + return await this.repository.findOne(findOneOptions); + } + + async getOneOrFailByOptions( + findOneOptions: FindOneOptions, + ): Promise { + return await this.repository.findOneOrFail(findOneOptions); + } + + async getManyByOptions(findManyOptions): Promise { + return await this.repository.find(findManyOptions); + } + +} \ No newline at end of file diff --git a/src/core/modules/data/service/base-tree-read.service.ts b/src/core/modules/data/service/base-tree-read.service.ts new file mode 100644 index 0000000..2e18469 --- /dev/null +++ b/src/core/modules/data/service/base-tree-read.service.ts @@ -0,0 +1,52 @@ +import { Repository, TreeRepository } from "typeorm"; +import { BaseReadService } from "./base-read.service"; + +export abstract class BaseTreeReadService extends BaseReadService { + + constructor( + private dataRepository: Repository, + private treeRepository: TreeRepository + ) { + super(dataRepository); + } + + async findRoots() { + return this.treeRepository.findRoots() + } + + async findDescendants( + parent, + relations = [], + ): Promise { + return this.treeRepository.findDescendants(parent, { + relations: relations, + }); + } + + async findDescendantsTree( + parent, + relations = [], + ): Promise { + return this.treeRepository.findDescendantsTree(parent, { + relations: relations, + }); + } + + async findAncestors( + parent, + relations = [], + ): Promise { + return await this.treeRepository.findAncestors(parent, { + relations: relations, + }); + } + + async findAncestorsTree( + parent, + relations = [], + ): Promise { + return await this.treeRepository.findAncestorsTree(parent, { + relations: relations, + }); + } +} \ No newline at end of file diff --git a/src/core/modules/domain/entities/base-core.entity.ts b/src/core/modules/domain/entities/base-core.entity.ts new file mode 100644 index 0000000..4035652 --- /dev/null +++ b/src/core/modules/domain/entities/base-core.entity.ts @@ -0,0 +1,3 @@ +export interface BaseCoreEntity { + id: string; +} \ No newline at end of file diff --git a/src/core/modules/infrastructure/dto/base-core.dto.ts b/src/core/modules/infrastructure/dto/base-core.dto.ts new file mode 100644 index 0000000..9792a19 --- /dev/null +++ b/src/core/modules/infrastructure/dto/base-core.dto.ts @@ -0,0 +1,5 @@ +import { BaseCoreEntity } from "../../domain/entities/base-core.entity"; + +export class BaseCoreDto implements BaseCoreEntity { + id: string; +} \ No newline at end of file diff --git a/src/core/modules/infrastructure/dto/base-status.dto.ts b/src/core/modules/infrastructure/dto/base-status.dto.ts new file mode 100644 index 0000000..a641891 --- /dev/null +++ b/src/core/modules/infrastructure/dto/base-status.dto.ts @@ -0,0 +1,7 @@ +import { STATUS } from "src/core/strings/constants/base.constants"; +import { BaseStatusEntity } from "../../domain/entities/base-status.entity"; +import { BaseDto } from "./base.dto"; + +export class BaseStatusDto extends BaseDto implements BaseStatusEntity { + status: STATUS; +} \ No newline at end of file diff --git a/src/core/modules/infrastructure/dto/base.dto.ts b/src/core/modules/infrastructure/dto/base.dto.ts new file mode 100644 index 0000000..1f600cd --- /dev/null +++ b/src/core/modules/infrastructure/dto/base.dto.ts @@ -0,0 +1,11 @@ +import { BaseEntity } from "../../domain/entities/base.entity"; +import { BaseCoreDto } from "./base-core.dto"; + +export class BaseDto extends BaseCoreDto implements BaseEntity { + creator_id: string; + creator_name: string; + editor_id: string; + editor_name: string; + created_at: number; + updated_at: number; +} \ No newline at end of file diff --git a/src/core/response/domain/exceptions/handled-exception.ts b/src/core/response/domain/exceptions/handled-exception.ts index 8170e22..57bdff1 100644 --- a/src/core/response/domain/exceptions/handled-exception.ts +++ b/src/core/response/domain/exceptions/handled-exception.ts @@ -3,16 +3,44 @@ import { Catch, ArgumentsHost, HttpException, + HttpStatus, } from '@nestjs/common'; import { Response } from 'express'; -@Catch(HttpException) +@Catch() export class HttpExceptionFilter implements ExceptionFilter { - catch(exception: HttpException, host: ArgumentsHost) { + catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); - const status = exception.getStatus(); + let status: HttpStatus; + let body: any; + let exceptionResponse; - response.status(status).json(exception.getResponse()); + try { + exceptionResponse = JSON.parse(exception.message); + } catch (error) {} + + if (exception instanceof HttpException) { + if (Array.isArray(exception.getResponse()['message'])) { + exception.getResponse()['message'] = exception + .getResponse() + ['message'].join(','); + } + + status = exception.getStatus(); + body = exception.getResponse(); + } else if (typeof exceptionResponse == 'object') { + status = exceptionResponse.statusCode; + body = exceptionResponse; + } else { + status = HttpStatus.FORBIDDEN; + body = { + statusCode: HttpStatus.FORBIDDEN, + message: exception.message, + error: exception.name, + }; + } + + response.status(status).json(body); } } diff --git a/src/core/sessions/domain/entities/user-sessions.interface.ts b/src/core/sessions/domain/entities/user-sessions.interface.ts index 4f818e9..a4a4478 100644 --- a/src/core/sessions/domain/entities/user-sessions.interface.ts +++ b/src/core/sessions/domain/entities/user-sessions.interface.ts @@ -1,6 +1,4 @@ export interface UsersSession { id: number; - username: string; name: string; - roles: string[]; } diff --git a/src/core/strings/constants/base.constants.ts b/src/core/strings/constants/base.constants.ts new file mode 100644 index 0000000..8d968b7 --- /dev/null +++ b/src/core/strings/constants/base.constants.ts @@ -0,0 +1,43 @@ +export enum STATUS { + ACTIVE = 'active', + CANCEL = 'cancel', + CONFIRMED = 'confirmed', + DRAFT = 'draft', + EXPIRED = 'expired', + INACTIVE = 'inactive', + PENDING = 'pending', + REFUNDED = 'refunded', + SETTLED = 'settled', +} + +export enum ORDER_TYPE { + ASC = 'ASC', + DESC = 'DESC', +} + +export enum CONNECTION_NAME { + DEFAULT = 'default', +} + +export enum METHOD { + POST = 'POST', + GET = 'GET', + PUT = 'PUT', + PATCH = 'PATCH', + DELETE = 'DELETE', +} + +export enum OPERATION { + CREATE = 'Create', + READ = 'Read', + UPDATE = 'Update', + UPDATE_BATCH = 'Batch Update', + DELETE = 'Delete', + DELETE_BATCH = 'Batch Delete', + SYNC = 'Sync', +} + +export const BLANK_USER = { + id: null, + name: null, +} \ No newline at end of file diff --git a/src/core/strings/constants/interface.constants.ts b/src/core/strings/constants/interface.constants.ts new file mode 100644 index 0000000..e5f2600 --- /dev/null +++ b/src/core/strings/constants/interface.constants.ts @@ -0,0 +1,17 @@ +import { UsersSession } from "src/core/sessions"; +import { OPERATION } from "./base.constants"; + +export interface EventTopics { + topic: any, + data: IEvent, +} + +export interface IEvent { + id: string; + old: null | Entity; + data: null | Entity; + user: UsersSession; + description: null | string; + module: string; + op: OPERATION; +} \ No newline at end of file diff --git a/src/core/strings/constants/module.constants.ts b/src/core/strings/constants/module.constants.ts new file mode 100644 index 0000000..1bb3e26 --- /dev/null +++ b/src/core/strings/constants/module.constants.ts @@ -0,0 +1,3 @@ +export enum MODULE_NAME { + USER_PRIVILEGE = 'user-privileges' +} \ No newline at end of file diff --git a/src/core/strings/constants/table.constants.ts b/src/core/strings/constants/table.constants.ts new file mode 100644 index 0000000..7d91910 --- /dev/null +++ b/src/core/strings/constants/table.constants.ts @@ -0,0 +1,3 @@ +export enum TABLE_NAME { + USER_PRIVILEGE = 'user_privileges' +} \ No newline at end of file diff --git a/src/core/strings/errors/general/contansts.ts b/src/core/strings/errors/general/contansts.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/core/strings/errors/general/messages.ts b/src/core/strings/errors/general/messages.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/core/strings/errors/index.ts b/src/core/strings/errors/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/core/strings/errors/readme.md b/src/core/strings/errors/readme.md new file mode 100644 index 0000000..0ef25af --- /dev/null +++ b/src/core/strings/errors/readme.md @@ -0,0 +1 @@ +## Error message for response diff --git a/src/main.ts b/src/main.ts index 13cad38..193843f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,33 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { ValidationPipe } from '@nestjs/common'; +import { DocumentBuilder, SwaggerCustomOptions, SwaggerModule } from '@nestjs/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule); - await app.listen(3000); + + app.setGlobalPrefix('api/v1'); + app.enableCors(); + app.useGlobalPipes( + new ValidationPipe({ transform: true, forbidUnknownValues: false }), + ); + + const config = new DocumentBuilder() + .setTitle('Skyworld POS') + .setDescription('POS API Documentation') + .setVersion('1.0') + .addBearerAuth( + { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + 'JWT', + ) + .build(); + + const options: SwaggerCustomOptions = { + swaggerOptions: { docExpansion: 'list' }, + }; + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api/v1/pos/docs', app, document, options); + + await app.listen(process.env.PORT || 3000); } bootstrap(); diff --git a/src/modules/configuration/couch/constants.ts b/src/modules/configuration/couch/constants.ts new file mode 100644 index 0000000..e69de29