feat(SPG-122) Abstraction Based Data

master
ashar 2024-05-30 09:30:56 +07:00
parent fcfb30d4d1
commit 38e420856e
24 changed files with 375 additions and 10 deletions

View File

@ -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: [
/**

View File

@ -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;

View File

@ -0,0 +1,8 @@
import { Entity, PrimaryGeneratedColumn } from "typeorm";
import { BaseCoreEntity } from "../../domain/entities/base-core.entity";
@Entity()
export abstract class BaseCoreModel<Entity> implements BaseCoreEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
}

View File

@ -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<Entity> extends BaseModel<Entity> implements BaseStatusEntity {
@Column('enum', { name: 'status', enum: STATUS, default: STATUS.DRAFT })
status: STATUS;
}

View File

@ -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<Entity> extends BaseCoreModel<Entity> 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;
}

View File

@ -0,0 +1,54 @@
import { EntityTarget, FindManyOptions, QueryRunner, Repository } from "typeorm";
export abstract class BaseDataService<Entity> {
constructor(private repository: Repository<Entity>) {}
getRepository(): Repository<Entity> {
return this.repository;
}
async create(
queryRunner: QueryRunner,
entityTarget: EntityTarget<Entity>,
entity: Entity,
): Promise<Entity> {
const newEntity = queryRunner.manager.create(entityTarget, entity);
return await queryRunner.manager.save(newEntity);
}
async update(
queryRunner: QueryRunner,
entityTarget: EntityTarget<Entity>,
filterUpdate: any,
entity: Entity,
): Promise<Entity> {
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<Entity>,
id: string,
): Promise<void> {
await queryRunner.manager.delete(entityTarget, { id });
}
async deleteByOptions(
queryRunner: QueryRunner,
entityTarget: EntityTarget<Entity>,
findManyOptions: FindManyOptions<Entity>,
): Promise<void> {
await queryRunner.manager.delete(entityTarget, findManyOptions);
}
async getOneByOptions(findOneOptions): Promise<Entity> {
return await this.repository.findOne(findOneOptions);
}
}

View File

@ -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<Entity> {
constructor(private repository: Repository<Entity>) {}
getRepository(): Repository<Entity> {
return this.repository;
}
async getIndex(
queryBuilder: SelectQueryBuilder<Entity>,
params: BaseFilterEntity,
): Promise<PaginationResponse<Entity>> {
const [data, total] = await queryBuilder
.take(+params.limit)
.skip(+params.limit * +params.page - +params.limit)
.getManyAndCount();
return {
data,
total,
};
}
async getOneByOptions(findOneOptions): Promise<Entity> {
return await this.repository.findOne(findOneOptions);
}
async getOneOrFailByOptions(
findOneOptions: FindOneOptions<Entity>,
): Promise<Entity> {
return await this.repository.findOneOrFail(findOneOptions);
}
async getManyByOptions(findManyOptions): Promise<Entity[]> {
return await this.repository.find(findManyOptions);
}
}

View File

@ -0,0 +1,52 @@
import { Repository, TreeRepository } from "typeorm";
import { BaseReadService } from "./base-read.service";
export abstract class BaseTreeReadService<Entity> extends BaseReadService<Entity> {
constructor(
private dataRepository: Repository<Entity>,
private treeRepository: TreeRepository<Entity>
) {
super(dataRepository);
}
async findRoots() {
return this.treeRepository.findRoots()
}
async findDescendants(
parent,
relations = [],
): Promise<Entity[]> {
return this.treeRepository.findDescendants(parent, {
relations: relations,
});
}
async findDescendantsTree(
parent,
relations = [],
): Promise<Entity> {
return this.treeRepository.findDescendantsTree(parent, {
relations: relations,
});
}
async findAncestors(
parent,
relations = [],
): Promise<Entity[]> {
return await this.treeRepository.findAncestors(parent, {
relations: relations,
});
}
async findAncestorsTree(
parent,
relations = [],
): Promise<Entity> {
return await this.treeRepository.findAncestorsTree(parent, {
relations: relations,
});
}
}

View File

@ -0,0 +1,3 @@
export interface BaseCoreEntity {
id: string;
}

View File

@ -0,0 +1,5 @@
import { BaseCoreEntity } from "../../domain/entities/base-core.entity";
export class BaseCoreDto implements BaseCoreEntity {
id: string;
}

View File

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

View File

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

View File

@ -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<Response>();
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);
}
}

View File

@ -1,6 +1,4 @@
export interface UsersSession {
id: number;
username: string;
name: string;
roles: string[];
}

View File

@ -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,
}

View File

@ -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<Entity = any> {
id: string;
old: null | Entity;
data: null | Entity;
user: UsersSession;
description: null | string;
module: string;
op: OPERATION;
}

View File

@ -0,0 +1,3 @@
export enum MODULE_NAME {
USER_PRIVILEGE = 'user-privileges'
}

View File

@ -0,0 +1,3 @@
export enum TABLE_NAME {
USER_PRIVILEGE = 'user_privileges'
}

View File

View File

@ -0,0 +1 @@
## Error message for response

View File

@ -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();