feat(SPG-123) Abstraction Transaction Data

pull/2/head
ashar 2024-05-31 11:06:50 +07:00
parent bbff8352af
commit 9ce4c58dc1
34 changed files with 651 additions and 483 deletions

View File

@ -44,7 +44,7 @@
"@nestjs/testing": "^10.0.0", "@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"@types/node": "18.11.18", "@types/node": "^20.12.13",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0", "@typescript-eslint/parser": "^5.0.0",

View File

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

View File

@ -1,10 +1,13 @@
import { Column, Entity } from "typeorm"; import { Column, Entity } from 'typeorm';
import { BaseModel } from "./base.model"; import { BaseModel } from './base.model';
import { STATUS } from "src/core/strings/constants/base.constants"; import { STATUS } from 'src/core/strings/constants/base.constants';
import { BaseStatusEntity } from "../../domain/entities/base-status.entity"; import { BaseStatusEntity } from '../../domain/entities/base-status.entity';
@Entity() @Entity()
export abstract class BaseStatusModel<Entity> extends BaseModel<Entity> implements BaseStatusEntity { export abstract class BaseStatusModel<Entity>
extends BaseModel<Entity>
implements BaseStatusEntity
{
@Column('enum', { name: 'status', enum: STATUS, default: STATUS.DRAFT }) @Column('enum', { name: 'status', enum: STATUS, default: STATUS.DRAFT })
status: STATUS; status: STATUS;
} }

View File

@ -3,7 +3,10 @@ import { BaseCoreModel } from './base-core.model';
import { BaseEntity } from '../../domain/entities/base.entity'; import { BaseEntity } from '../../domain/entities/base.entity';
@Entity() @Entity()
export abstract class BaseModel<Entity> extends BaseCoreModel<Entity> implements BaseEntity { export abstract class BaseModel<Entity>
extends BaseCoreModel<Entity>
implements BaseEntity
{
@Column('varchar', { name: 'creator_id', length: 36, nullable: true }) @Column('varchar', { name: 'creator_id', length: 36, nullable: true })
creator_id: string; creator_id: string;

View File

@ -1,7 +1,11 @@
import { EntityTarget, FindManyOptions, QueryRunner, Repository } from "typeorm"; import {
EntityTarget,
FindManyOptions,
QueryRunner,
Repository,
} from 'typeorm';
export abstract class BaseDataService<Entity> { export abstract class BaseDataService<Entity> {
constructor(private repository: Repository<Entity>) {} constructor(private repository: Repository<Entity>) {}
getRepository(): Repository<Entity> { getRepository(): Repository<Entity> {

View File

@ -1,9 +1,8 @@
import { FindOneOptions, Repository, SelectQueryBuilder } from "typeorm"; import { FindOneOptions, Repository, SelectQueryBuilder } from 'typeorm';
import { BaseFilterEntity } from "../../domain/entities/base-filter.entity"; import { BaseFilterEntity } from '../../domain/entities/base-filter.entity';
import { PaginationResponse } from "src/core/response/domain/ok-response.interface"; import { PaginationResponse } from 'src/core/response/domain/ok-response.interface';
export abstract class BaseReadService<Entity> { export abstract class BaseReadService<Entity> {
constructor(private repository: Repository<Entity>) {} constructor(private repository: Repository<Entity>) {}
getRepository(): Repository<Entity> { getRepository(): Repository<Entity> {
@ -38,5 +37,4 @@ export abstract class BaseReadService<Entity> {
async getManyByOptions(findManyOptions): Promise<Entity[]> { async getManyByOptions(findManyOptions): Promise<Entity[]> {
return await this.repository.find(findManyOptions); return await this.repository.find(findManyOptions);
} }
} }

View File

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

View File

@ -1,4 +1,4 @@
import { ORDER_TYPE, STATUS } from "src/core/strings/constants/base.constants"; import { ORDER_TYPE, STATUS } from 'src/core/strings/constants/base.constants';
export interface BaseFilterEntity { export interface BaseFilterEntity {
page: number; page: number;

View File

@ -1,5 +1,5 @@
import { STATUS } from "src/core/strings/constants/base.constants"; import { STATUS } from 'src/core/strings/constants/base.constants';
import { BaseEntity } from "./base.entity"; import { BaseEntity } from './base.entity';
export interface BaseStatusEntity extends BaseEntity { export interface BaseStatusEntity extends BaseEntity {
status: STATUS; status: STATUS;

View File

@ -1,4 +1,4 @@
import { BaseCoreEntity } from "./base-core.entity"; import { BaseCoreEntity } from './base-core.entity';
export interface BaseEntity extends BaseCoreEntity { export interface BaseEntity extends BaseCoreEntity {
creator_id: string; creator_id: string;

View File

@ -1,11 +1,10 @@
import { Inject, Injectable, Logger } from "@nestjs/common"; import { Inject, Injectable, Logger } from '@nestjs/common';
import { UserProvider, UsersSession } from "src/core/sessions"; import { UserProvider, UsersSession } from 'src/core/sessions';
import { BLANK_USER } from "src/core/strings/constants/base.constants"; import { BLANK_USER } from 'src/core/strings/constants/base.constants';
import { TABLE_NAME } from "src/core/strings/constants/table.constants"; import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
@Injectable() @Injectable()
export abstract class BaseReadManager { export abstract class BaseReadManager {
public user: UsersSession; public user: UsersSession;
public dataService: any; public dataService: any;
public queryBuilder: any; public queryBuilder: any;
@ -25,7 +24,9 @@ export abstract class BaseReadManager {
setService(dataService) { setService(dataService) {
this.dataService = dataService; this.dataService = dataService;
this.queryBuilder = this.dataService.getRepository().createQueryBuilder(this.tableName); this.queryBuilder = this.dataService
.getRepository()
.createQueryBuilder(this.tableName);
} }
async execute(): Promise<void> { async execute(): Promise<void> {

View File

@ -1,9 +1,13 @@
import { BadRequestException, Inject, Injectable, Logger } from "@nestjs/common"; import {
import { EventBus } from "@nestjs/cqrs"; Inject,
import { UserProvider, UsersSession } from "src/core/sessions"; Injectable,
import { BLANK_USER } from "src/core/strings/constants/base.constants"; Logger,
import { EventTopics } from "src/core/strings/constants/interface.constants"; } from '@nestjs/common';
import { QueryRunner } from "typeorm"; import { EventBus } from '@nestjs/cqrs';
import { UserProvider, UsersSession } from 'src/core/sessions';
import { BLANK_USER } from 'src/core/strings/constants/base.constants';
import { EventTopics } from 'src/core/strings/constants/interface.constants';
import { QueryRunner } from 'typeorm';
@Injectable() @Injectable()
export abstract class BaseManager { export abstract class BaseManager {
@ -28,7 +32,9 @@ export abstract class BaseManager {
setService(dataService) { setService(dataService) {
this.dataService = dataService; this.dataService = dataService;
this.queryRunner = this.dataService.getRepository().manager.connection.createQueryRunner(); this.queryRunner = this.dataService
.getRepository()
.manager.connection.createQueryRunner();
} }
abstract get eventTopics(): EventTopics[]; abstract get eventTopics(): EventTopics[];
@ -41,8 +47,8 @@ export abstract class BaseManager {
this.baseLog.verbose('prepareData'); this.baseLog.verbose('prepareData');
await this.prepareData(); await this.prepareData();
if (!this.data || !this.dataService) { if (!this.dataService) {
throw new Error("data or service not implemented."); throw new Error('data or service not implemented.');
} }
this.baseLog.verbose('validateProcess'); this.baseLog.verbose('validateProcess');
@ -80,6 +86,6 @@ export abstract class BaseManager {
abstract afterProcess(): Promise<void>; abstract afterProcess(): Promise<void>;
async publishEvents() { async publishEvents() {
if (!this.eventTopics.length) return if (!this.eventTopics.length) return;
}; }
} }

View File

@ -0,0 +1,67 @@
import { BatchResult } from 'src/core/response/domain/ok-response.interface';
import { BaseManager } from '../base.manager';
import { HttpStatus, NotFoundException } from '@nestjs/common';
export abstract class BaseBatchDeleteManager<Entity> extends BaseManager {
protected dataIds: string[];
protected result: BatchResult;
abstract get entityTarget(): any;
setData(ids: string[]): void {
this.dataIds = ids;
}
validateProcess(): Promise<void> {
return;
}
prepareData(): Promise<void> {
return;
}
async process(): Promise<void> {
let totalFailed = 0;
let totalSuccess = 0;
let messages = [];
for (const id of this.dataIds) {
try {
const entity = await this.dataService.getOneByOptions({
where: {
id: id,
},
});
if (!entity) {
throw new NotFoundException({
statusCode: HttpStatus.NOT_FOUND,
message: `Failed! Entity with id ${id} not found`,
error: 'Entity Not Found',
});
}
await this.validateData(entity);
await this.dataService.deleteById(
this.queryRunner,
this.entityTarget,
id,
);
totalSuccess = totalSuccess + 1;
} catch (error) {
totalFailed = totalFailed + 1;
messages.push(error.response?.message ?? error.message);
}
}
this.result = {
total_items: this.dataIds.length,
total_failed: totalFailed,
total_success: totalSuccess,
messages: messages,
};
}
abstract validateData(data: Entity): Promise<void>;
abstract getResult(): BatchResult;
}

View File

@ -0,0 +1,75 @@
import { BatchResult } from 'src/core/response/domain/ok-response.interface';
import { BaseManager } from '../base.manager';
import { HttpStatus, NotFoundException } from '@nestjs/common';
import { STATUS } from 'src/core/strings/constants/base.constants';
export abstract class BaseBatchUpdateStatusManager<Entity> extends BaseManager {
protected dataIds: string[];
protected result: BatchResult;
protected dataStatus: STATUS;
abstract get entityTarget(): any;
setData(ids: string[], status: STATUS): void {
this.dataIds = ids;
this.dataStatus = status;
}
validateProcess(): Promise<void> {
return;
}
prepareData(): Promise<void> {
return;
}
async process(): Promise<void> {
let totalFailed = 0;
let totalSuccess = 0;
let messages = [];
for (const id of this.dataIds) {
try {
const entity = await this.dataService.getOneByOptions({
where: {
id: id,
},
});
if (!entity) {
throw new NotFoundException({
statusCode: HttpStatus.NOT_FOUND,
message: `Failed! Entity with id ${id} not found`,
error: 'Entity Not Found',
});
}
await this.validateData(entity);
await this.dataService.update(
this.queryRunner,
this.entityTarget,
{ id: id },
{
status: this.dataStatus,
editor_id: this.user.id,
editor_name: this.user.name,
updated_at: new Date().getTime(),
},
);
totalSuccess = totalSuccess + 1;
} catch (error) {
totalFailed = totalFailed + 1;
messages.push(error.response?.message ?? error.message);
}
}
this.result = {
total_items: this.dataIds.length,
total_failed: totalFailed,
total_success: totalSuccess,
messages: messages,
};
}
abstract validateData(data: Entity): Promise<void>;
abstract getResult(): BatchResult;
}

View File

@ -1,9 +1,7 @@
import { BaseManager } from "../base.manager"; import { BaseManager } from '../base.manager';
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
@Injectable()
export abstract class BaseCreateManager<Entity> extends BaseManager { export abstract class BaseCreateManager<Entity> extends BaseManager {
protected result: Entity; protected result: Entity;
protected duplicateColumn: string[]; protected duplicateColumn: string[];
abstract get entityTarget(): any; abstract get entityTarget(): any;
@ -32,9 +30,8 @@ export abstract class BaseCreateManager<Entity> extends BaseManager {
async getResult(): Promise<Entity> { async getResult(): Promise<Entity> {
return await this.dataService.getOneByOptions({ return await this.dataService.getOneByOptions({
where: { where: {
id: this.result['id'] id: this.result['id'],
},
});
} }
})
}
} }

View File

@ -1,9 +1,11 @@
import { HttpStatus, Injectable, UnauthorizedException, UnprocessableEntityException } from "@nestjs/common"; import {
import { BaseManager } from "../base.manager"; HttpStatus,
Injectable,
UnprocessableEntityException,
} from '@nestjs/common';
import { BaseManager } from '../base.manager';
@Injectable()
export abstract class BaseDeleteManager<Entity> extends BaseManager { export abstract class BaseDeleteManager<Entity> extends BaseManager {
protected dataId: string; protected dataId: string;
protected result: Entity; protected result: Entity;
abstract get entityTarget(): any; abstract get entityTarget(): any;
@ -15,9 +17,9 @@ export abstract class BaseDeleteManager<Entity> extends BaseManager {
async prepareData(): Promise<void> { async prepareData(): Promise<void> {
this.data = await this.dataService.getOneByOptions({ this.data = await this.dataService.getOneByOptions({
where: { where: {
id: this.dataId id: this.dataId,
} },
}) });
if (!this.data) if (!this.data)
throw new UnprocessableEntityException({ throw new UnprocessableEntityException({

View File

@ -1,11 +1,8 @@
import { Injectable } from "@nestjs/common"; import { BaseManager } from '../base.manager';
import { BaseManager } from "../base.manager"; import { STATUS } from 'src/core/strings/constants/base.constants';
import { STATUS } from "src/core/strings/constants/base.constants"; import { UserPrivilegeModel } from 'src/modules/user-related/user-privilege/data/model/user-privilege.model';
import { UserPrivilegeModel } from "src/modules/user-related/user-privilege/data/model/user-privilege.model";
@Injectable()
export abstract class BaseUpdateStatusManager<Entity> extends BaseManager { export abstract class BaseUpdateStatusManager<Entity> extends BaseManager {
protected dataId: string; protected dataId: string;
protected result: Entity; protected result: Entity;
protected dataStatus: STATUS; protected dataStatus: STATUS;

View File

@ -1,9 +1,7 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { BaseManager } from "../base.manager"; import { BaseManager } from '../base.manager';
@Injectable()
export abstract class BaseUpdateManager<Entity> extends BaseManager { export abstract class BaseUpdateManager<Entity> extends BaseManager {
protected dataId: string; protected dataId: string;
protected result: Entity; protected result: Entity;
protected duplicateColumn: string[]; protected duplicateColumn: string[];
@ -34,8 +32,8 @@ export abstract class BaseUpdateManager<Entity> extends BaseManager {
async getResult(): Promise<Entity> { async getResult(): Promise<Entity> {
return await this.dataService.getOneByOptions({ return await this.dataService.getOneByOptions({
where: { where: {
id: this.dataId id: this.dataId,
} },
}) });
} }
} }

View File

@ -1,7 +1,13 @@
import { BaseDataOrchestrator } from "./base-data.orchestrator"; import { BatchResult } from 'src/core/response/domain/ok-response.interface';
import { BaseDataOrchestrator } from './base-data.orchestrator';
export abstract class BaseDataTransactionOrchestrator<Entity> extends BaseDataOrchestrator<Entity> { export abstract class BaseDataTransactionOrchestrator<
Entity,
> extends BaseDataOrchestrator<Entity> {
abstract active(dataId: string): Promise<String>; abstract active(dataId: string): Promise<String>;
abstract confirm(dataId: string): Promise<String>; abstract confirm(dataId: string): Promise<String>;
abstract inactive(dataId: string): Promise<String>; abstract inactive(dataId: string): Promise<String>;
abstract batchConfirm(dataIds: string[]): Promise<BatchResult>;
abstract batchActive(dataIds: string[]): Promise<BatchResult>;
abstract batchInactive(dataIds: string[]): Promise<BatchResult>;
} }

View File

@ -1,7 +1,8 @@
export abstract class BaseDataOrchestrator<Entity> { import { BatchResult } from 'src/core/response/domain/ok-response.interface';
export abstract class BaseDataOrchestrator<Entity> {
abstract create(data: Entity): Promise<Entity>; abstract create(data: Entity): Promise<Entity>;
abstract update(dataId: string, data: Entity): Promise<Entity>; abstract update(dataId: string, data: Entity): Promise<Entity>;
abstract delete(dataId: string): Promise<String>; abstract delete(dataId: string): Promise<String>;
abstract batchDelete(dataIds: string[]): Promise<BatchResult>;
} }

View File

@ -1,4 +1,4 @@
import { PaginationResponse } from "src/core/response/domain/ok-response.interface"; import { PaginationResponse } from 'src/core/response/domain/ok-response.interface';
export abstract class BaseReadOrchestrator<Entity> { export abstract class BaseReadOrchestrator<Entity> {
abstract index(params): Promise<PaginationResponse<Entity>>; abstract index(params): Promise<PaginationResponse<Entity>>;

View File

@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class BatchIdsDto {
@ApiProperty({ type: [String] })
ids: string[];
}

View File

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

View File

@ -1,8 +1,14 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
import { BaseFilterEntity } from "../../domain/entities/base-filter.entity"; import { BaseFilterEntity } from '../../domain/entities/base-filter.entity';
import { Transform } from "class-transformer"; import { Transform } from 'class-transformer';
import { IsArray, IsEnum, IsNumber, IsString, ValidateIf } from "class-validator"; import {
import { ORDER_TYPE, STATUS } from "src/core/strings/constants/base.constants"; IsArray,
IsEnum,
IsNumber,
IsString,
ValidateIf,
} from 'class-validator';
import { ORDER_TYPE, STATUS } from 'src/core/strings/constants/base.constants';
export class BaseFilterDto implements BaseFilterEntity { export class BaseFilterDto implements BaseFilterEntity {
@ApiProperty({ type: Number, required: false, default: 1 }) @ApiProperty({ type: Number, required: false, default: 1 })

View File

@ -1,6 +1,6 @@
import { STATUS } from "src/core/strings/constants/base.constants"; import { STATUS } from 'src/core/strings/constants/base.constants';
import { BaseStatusEntity } from "../../domain/entities/base-status.entity"; import { BaseStatusEntity } from '../../domain/entities/base-status.entity';
import { BaseDto } from "./base.dto"; import { BaseDto } from './base.dto';
export class BaseStatusDto extends BaseDto implements BaseStatusEntity { export class BaseStatusDto extends BaseDto implements BaseStatusEntity {
status: STATUS; status: STATUS;

View File

@ -1,5 +1,5 @@
import { BaseEntity } from "../../domain/entities/base.entity"; import { BaseEntity } from '../../domain/entities/base.entity';
import { BaseCoreDto } from "./base-core.dto"; import { BaseCoreDto } from './base-core.dto';
export class BaseDto extends BaseCoreDto implements BaseEntity { export class BaseDto extends BaseCoreDto implements BaseEntity {
creator_id: string; creator_id: string;

View File

@ -15,3 +15,10 @@ export interface PaginationResponse<T> {
data: T[]; data: T[];
total: number; total: number;
} }
export interface BatchResult {
messages: string[];
total_items: number;
total_success: number;
total_failed: number;
}

View File

@ -40,4 +40,4 @@ export enum OPERATION {
export const BLANK_USER = { export const BLANK_USER = {
id: null, id: null,
name: null, name: null,
} };

View File

@ -1,9 +1,9 @@
import { UsersSession } from "src/core/sessions"; import { UsersSession } from 'src/core/sessions';
import { OPERATION } from "./base.constants"; import { OPERATION } from './base.constants';
export interface EventTopics { export interface EventTopics {
topic: any, topic: any;
data: IEvent, data?: IEvent;
} }
export interface IEvent<Entity = any> { export interface IEvent<Entity = any> {

View File

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

View File

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

View File

@ -1120,10 +1120,12 @@
dependencies: dependencies:
undici-types "~5.26.4" undici-types "~5.26.4"
"@types/node@18.11.18": "@types/node@^20.12.13":
version "18.11.18" version "20.12.13"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.13.tgz#90ed3b8a4e52dd3c5dc5a42dde5b85b74ad8ed88"
integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== integrity sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==
dependencies:
undici-types "~5.26.4"
"@types/qs@*": "@types/qs@*":
version "6.9.15" version "6.9.15"