feat(shared): add shared module with domain primitives, infrastructure services, and utils

Domain primitives: BaseEntity, AggregateRoot, ValueObject, DomainEvent, Result<T,E>
Infrastructure: PrismaService, RedisService, LoggerService (pino), EventBusService
Utils: Vietnam phone validator/normalizer, VND currency formatter, Vietnamese slug generator
Includes 45 unit tests covering all domain primitives, validators, and formatters.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 00:07:27 +07:00
parent 83d55de65b
commit 1fb7bb39d2
30 changed files with 282 additions and 92 deletions

View File

@@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { type EventEmitter2 } from '@nestjs/event-emitter';
import { type DomainEvent } from '../domain/domain-event';
@Injectable()
export class EventBusService {
constructor(private readonly eventEmitter: EventEmitter2) {}
publish(event: DomainEvent): void {
this.eventEmitter.emit(event.eventName, event);
}
publishAll(events: DomainEvent[]): void {
for (const event of events) {
this.publish(event);
}
}
async publishAsync(event: DomainEvent): Promise<void> {
await this.eventEmitter.emitAsync(event.eventName, event);
}
}

View File

@@ -0,0 +1,4 @@
export { PrismaService } from './prisma.service';
export { RedisService } from './redis.service';
export { LoggerService } from './logger.service';
export { EventBusService } from './event-bus.service';

View File

@@ -0,0 +1,41 @@
import { Injectable, type LoggerService as NestLoggerService } from '@nestjs/common';
import pino, { type Logger } from 'pino';
@Injectable()
export class LoggerService implements NestLoggerService {
private readonly logger: Logger;
constructor() {
this.logger = pino({
level: process.env['LOG_LEVEL'] ?? 'info',
transport:
process.env['NODE_ENV'] !== 'production'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
});
}
log(message: string, context?: string): void {
this.logger.info({ context }, message);
}
error(message: string, trace?: string, context?: string): void {
this.logger.error({ context, trace }, message);
}
warn(message: string, context?: string): void {
this.logger.warn({ context }, message);
}
debug(message: string, context?: string): void {
this.logger.debug({ context }, message);
}
verbose(message: string, context?: string): void {
this.logger.trace({ context }, message);
}
child(bindings: Record<string, unknown>): Logger {
return this.logger.child(bindings);
}
}

View File

@@ -0,0 +1,13 @@
import { Injectable, type OnModuleInit, type OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit(): Promise<void> {
await this.$connect();
}
async onModuleDestroy(): Promise<void> {
await this.$disconnect();
}
}

View File

@@ -0,0 +1,40 @@
import { Injectable, type OnModuleDestroy } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class RedisService implements OnModuleDestroy {
private readonly client: Redis;
constructor() {
this.client = new Redis({
host: process.env['REDIS_HOST'] ?? 'localhost',
port: Number(process.env['REDIS_PORT'] ?? 6379),
password: process.env['REDIS_PASSWORD'] ?? undefined,
lazyConnect: true,
});
}
async onModuleDestroy(): Promise<void> {
await this.client.quit();
}
getClient(): Redis {
return this.client;
}
async get(key: string): Promise<string | null> {
return this.client.get(key);
}
async set(key: string, value: string, ttlSeconds?: number): Promise<void> {
if (ttlSeconds) {
await this.client.set(key, value, 'EX', ttlSeconds);
} else {
await this.client.set(key, value);
}
}
async del(key: string): Promise<void> {
await this.client.del(key);
}
}