feat: add ESLint flat config, Prettier, dependency-cruiser, and Husky

Setup code quality tooling for the monorepo:
- ESLint 9 flat config with TypeScript, import ordering, and NestJS rules
- Prettier with consistent formatting across all files
- dependency-cruiser enforcing module boundary rules (no cross-module internals, no circular deps)
- Husky + lint-staged for pre-commit hooks
- Auto-fixed existing files for type imports and import ordering

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-07 23:57:28 +07:00
parent e1e5fa6252
commit 83d55de65b
28 changed files with 2365 additions and 155 deletions

View File

@@ -1,5 +1,5 @@
import { BaseEntity } from './base-entity';
import { DomainEvent } from './domain-event';
import { type DomainEvent } from './domain-event';
export abstract class AggregateRoot<TId = string> extends BaseEntity<TId> {
private _domainEvents: DomainEvent[] = [];

View File

@@ -51,8 +51,6 @@ export class Result<T, E = Error> {
}
match<U>(handlers: { ok: (value: T) => U; err: (error: E) => U }): U {
return this._isOk
? handlers.ok(this._value as T)
: handlers.err(this._error as E);
return this._isOk ? handlers.ok(this._value as T) : handlers.err(this._error as E);
}
}

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { DomainEvent } from '../domain/domain-event';
import { type EventEmitter2 } from '@nestjs/event-emitter';
import { type DomainEvent } from '../domain/domain-event';
@Injectable()
export class EventBusService {

View File

@@ -1,5 +1,5 @@
import { Injectable, LoggerService as NestLoggerService } from '@nestjs/common';
import pino, { Logger } from 'pino';
import { Injectable, type LoggerService as NestLoggerService } from '@nestjs/common';
import pino, { type Logger } from 'pino';
@Injectable()
export class LoggerService implements NestLoggerService {

View File

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

View File

@@ -1,4 +1,4 @@
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import { Injectable, type OnModuleDestroy } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()

View File

@@ -1,9 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { EventBusService } from './infrastructure/event-bus.service';
import { LoggerService } from './infrastructure/logger.service';
import { PrismaService } from './infrastructure/prisma.service';
import { RedisService } from './infrastructure/redis.service';
import { LoggerService } from './infrastructure/logger.service';
import { EventBusService } from './infrastructure/event-bus.service';
@Global()
@Module({

View File

@@ -1,6 +1,4 @@
export type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
export type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
export function ok<T>(value: T): Result<T, never> {
return { ok: true, value };

View File

@@ -21,6 +21,6 @@ export function formatVNDCompact(amount: number): string {
export function parseVND(formatted: string): number | null {
const cleaned = formatted.replace(/[^\d]/g, '');
const value = Number(cleaned);
return Number.isNaN(value) ? null : value;
if (cleaned === '') return null;
return Number(cleaned);
}

View File

@@ -1,23 +1,79 @@
const VIETNAMESE_MAP: Record<string, string> = {
à: 'a', á: 'a', : 'a', ã: 'a', : 'a',
ă: 'a', : 'a', : 'a', : 'a', : 'a', : 'a',
â: 'a', : 'a', : 'a', : 'a', : 'a', : 'a',
à: 'a',
á: 'a',
: 'a',
ã: 'a',
: 'a',
ă: 'a',
: 'a',
: 'a',
: 'a',
: 'a',
: 'a',
â: 'a',
: 'a',
: 'a',
: 'a',
: 'a',
: 'a',
đ: 'd',
è: 'e', é: 'e', : 'e', : 'e', : 'e',
ê: 'e', ế: 'e', : 'e', : 'e', : 'e', : 'e',
ì: 'i', í: 'i', : 'i', ĩ: 'i', : 'i',
ò: 'o', ó: 'o', : 'o', õ: 'o', : 'o',
ô: 'o', : 'o', : 'o', : 'o', : 'o', : 'o',
ơ: 'o', : 'o', : 'o', : 'o', : 'o', : 'o',
ù: 'u', ú: 'u', : 'u', ũ: 'u', : 'u',
ư: 'u', : 'u', : 'u', : 'u', : 'u', : 'u',
: 'y', ý: 'y', : 'y', : 'y', : 'y',
è: 'e',
é: 'e',
: 'e',
: 'e',
: 'e',
ê: 'e',
ế: 'e',
: 'e',
: 'e',
: 'e',
: 'e',
ì: 'i',
í: 'i',
: 'i',
ĩ: 'i',
: 'i',
ò: 'o',
ó: 'o',
: 'o',
õ: 'o',
: 'o',
ô: 'o',
: 'o',
: 'o',
: 'o',
: 'o',
: 'o',
ơ: 'o',
: 'o',
: 'o',
: 'o',
: 'o',
: 'o',
ù: 'u',
ú: 'u',
: 'u',
ũ: 'u',
: 'u',
ư: 'u',
: 'u',
: 'u',
: 'u',
: 'u',
: 'u',
: 'y',
ý: 'y',
: 'y',
: 'y',
: 'y',
};
function removeVietnameseTones(str: string): string {
return str
.split('')
.map((char) => VIETNAMESE_MAP[char] ?? VIETNAMESE_MAP[char.toLowerCase()]?.toUpperCase() ?? char)
.map(
(char) => VIETNAMESE_MAP[char] ?? VIETNAMESE_MAP[char.toLowerCase()]?.toUpperCase() ?? char,
)
.join('');
}