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

79
.dependency-cruiser.cjs Normal file
View File

@@ -0,0 +1,79 @@
/** @type {import('dependency-cruiser').IConfiguration} */
module.exports = {
forbidden: [
// No circular dependencies
{
name: 'no-circular',
severity: 'error',
comment: 'Circular dependencies are not allowed.',
from: {},
to: { circular: true },
},
// Modules must not import internals of other modules directly.
// Only import via the module's public barrel (index.ts).
{
name: 'no-cross-module-internals',
severity: 'error',
comment:
'Modules must not import internal files of other modules. Import from the module index (barrel) instead.',
from: {
path: 'src/modules/([^/]+)/',
},
to: {
path: 'src/modules/([^/]+)/.+',
pathNot: [
// Allow importing from the module's own files
'src/modules/$1/',
// Allow importing from another module's barrel index
'src/modules/[^/]+/index\\.ts$',
],
},
},
// Apps should not import module internals either
{
name: 'no-app-to-module-internals',
severity: 'error',
comment: 'Apps must import modules via their barrel index, not internal files.',
from: {
path: 'apps/',
},
to: {
path: 'src/modules/([^/]+)/.+',
pathNot: ['src/modules/[^/]+/index\\.ts$'],
},
},
// No orphan modules (files not reachable from any entry point)
{
name: 'no-orphans',
severity: 'warn',
comment: 'Orphan modules may indicate dead code.',
from: {
orphan: true,
pathNot: ['\\.(spec|test)\\.ts$', '__tests__/', '\\.d\\.ts$', 'index\\.ts$'],
},
to: {},
},
],
options: {
doNotFollow: {
path: ['node_modules', '\\.next', 'dist'],
},
tsPreCompilationDeps: true,
tsConfig: {
fileName: 'tsconfig.base.json',
},
enhancedResolveOptions: {
exportsFields: ['exports'],
conditionNames: ['import', 'require', 'node', 'default'],
},
reporterOptions: {
text: {
highlightFocused: true,
},
},
},
};

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npm test

5
.prettierignore Normal file
View File

@@ -0,0 +1,5 @@
node_modules
dist
.next
coverage
pnpm-lock.yaml

10
.prettierrc Normal file
View File

@@ -0,0 +1,10 @@
{
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}

View File

@@ -11,6 +11,7 @@
**Goal:** Any engineer can clone, install, and start developing. **Goal:** Any engineer can clone, install, and start developing.
**Execution Order:** **Execution Order:**
1. **[TEC-1415] Monorepo Scaffolding** + **[TEC-1416] Docker Compose** (parallel — no deps) 1. **[TEC-1415] Monorepo Scaffolding** + **[TEC-1416] Docker Compose** (parallel — no deps)
2. **[TEC-1420] ESLint/Prettier** (after F1) 2. **[TEC-1420] ESLint/Prettier** (after F1)
3. **[TEC-1417] Prisma Schema** (after F1 + F2) 3. **[TEC-1417] Prisma Schema** (after F1 + F2)
@@ -30,6 +31,7 @@ F2 (Docker) ─────┘
**Goal:** Users can register, post listings, and search properties. **Goal:** Users can register, post listings, and search properties.
**Execution Order:** **Execution Order:**
1. **[TEC-1421] Auth Backend** (after F3, F4) 1. **[TEC-1421] Auth Backend** (after F3, F4)
2. **[TEC-1425] Security Hardening** + **[TEC-1426] Error Handling** (parallel, after F1/F4) 2. **[TEC-1425] Security Hardening** + **[TEC-1426] Error Handling** (parallel, after F1/F4)
3. **[TEC-1422] Auth Frontend** (after C1) 3. **[TEC-1422] Auth Frontend** (after C1)
@@ -70,27 +72,27 @@ C5 + A2 ──→ A3 (MCP Servers)
## Dependency Map ## Dependency Map
| Task | Depends On | | Task | Depends On |
|------|-----------| | ------------- | ---------- |
| TEC-1415 (F1) | None | | TEC-1415 (F1) | None |
| TEC-1416 (F2) | None | | TEC-1416 (F2) | None |
| TEC-1417 (F3) | F1, F2 | | TEC-1417 (F3) | F1, F2 |
| TEC-1418 (F4) | F1 | | TEC-1418 (F4) | F1 |
| TEC-1419 (F5) | F1 | | TEC-1419 (F5) | F1 |
| TEC-1420 (F6) | F1 | | TEC-1420 (F6) | F1 |
| TEC-1421 (C1) | F3, F4 | | TEC-1421 (C1) | F3, F4 |
| TEC-1422 (C2) | C1 | | TEC-1422 (C2) | C1 |
| TEC-1423 (C3) | C1, F3 | | TEC-1423 (C3) | C1, F3 |
| TEC-1424 (C5) | C3, F2 | | TEC-1424 (C5) | C3, F2 |
| TEC-1425 (X1) | F1 | | TEC-1425 (X1) | F1 |
| TEC-1426 (X3) | F4 | | TEC-1426 (X3) | F4 |
| TEC-1427 (C4) | C3 | | TEC-1427 (C4) | C3 |
| TEC-1428 (C6) | C5 | | TEC-1428 (C6) | C5 |
| TEC-1429 (M1) | C1 | | TEC-1429 (M1) | C1 |
| TEC-1430 (M2) | M1 | | TEC-1430 (M2) | M1 |
| TEC-1431 (M3) | C1 | | TEC-1431 (M3) | C1 |
| TEC-1432 (M4) | C1, C3 | | TEC-1432 (M4) | C1, C3 |
| TEC-1433 (X4) | Phase 1 | | TEC-1433 (X4) | Phase 1 |
--- ---

View File

@@ -8,37 +8,37 @@
## Phase 0: Foundation (P0 — Critical) ## Phase 0: Foundation (P0 — Critical)
| Issue | Title | Owner | Priority | Status | Blockers | | Issue | Title | Owner | Priority | Status | Blockers |
|-------|-------|-------|----------|--------|----------| | -------------------------------- | --------------------------------------------------- | ------------------ | -------- | ------ | -------- |
| [TEC-1415](/TEC/issues/TEC-1415) | Monorepo Scaffolding (Turborepo + NestJS + Next.js) | Founding Engineer | Critical | todo | None | | [TEC-1415](/TEC/issues/TEC-1415) | Monorepo Scaffolding (Turborepo + NestJS + Next.js) | Founding Engineer | Critical | todo | None |
| [TEC-1416](/TEC/issues/TEC-1416) | Docker Compose Dev Environment | DevOps Engineer | Critical | todo | None | | [TEC-1416](/TEC/issues/TEC-1416) | Docker Compose Dev Environment | DevOps Engineer | Critical | todo | None |
| [TEC-1417](/TEC/issues/TEC-1417) | Prisma Schema + Initial Migration + Seed Scripts | Database Architect | Critical | todo | F1, F2 | | [TEC-1417](/TEC/issues/TEC-1417) | Prisma Schema + Initial Migration + Seed Scripts | Database Architect | Critical | todo | F1, F2 |
| [TEC-1418](/TEC/issues/TEC-1418) | Shared Module (Domain Primitives + Infrastructure) | Architect | Critical | todo | F1 | | [TEC-1418](/TEC/issues/TEC-1418) | Shared Module (Domain Primitives + Infrastructure) | Architect | Critical | todo | F1 |
| [TEC-1419](/TEC/issues/TEC-1419) | CI/CD Pipeline (GitHub Actions) | DevOps Engineer | High | todo | F1 | | [TEC-1419](/TEC/issues/TEC-1419) | CI/CD Pipeline (GitHub Actions) | DevOps Engineer | High | todo | F1 |
| [TEC-1420](/TEC/issues/TEC-1420) | ESLint + Prettier + Module Boundary Rules | Founding Engineer | High | todo | F1 | | [TEC-1420](/TEC/issues/TEC-1420) | ESLint + Prettier + Module Boundary Rules | Founding Engineer | High | todo | F1 |
## Phase 1: Core Auth & Listings (P1) ## Phase 1: Core Auth & Listings (P1)
| Issue | Title | Owner | Priority | Status | Blockers | | Issue | Title | Owner | Priority | Status | Blockers |
|-------|-------|-------|----------|--------|----------| | -------------------------------- | ------------------------------------------------- | ------------------------ | -------- | ------- | -------- |
| [TEC-1421](/TEC/issues/TEC-1421) | Auth Module Backend (Register, Login, JWT, OAuth) | Senior Backend Engineer | Critical | backlog | F3, F4 | | [TEC-1421](/TEC/issues/TEC-1421) | Auth Module Backend (Register, Login, JWT, OAuth) | Senior Backend Engineer | Critical | backlog | F3, F4 |
| [TEC-1422](/TEC/issues/TEC-1422) | Auth Frontend (Login/Register + OAuth) | Senior Frontend Engineer | High | backlog | C1 | | [TEC-1422](/TEC/issues/TEC-1422) | Auth Frontend (Login/Register + OAuth) | Senior Frontend Engineer | High | backlog | C1 |
| [TEC-1423](/TEC/issues/TEC-1423) | Listings Module Backend (CRUD, Media, Moderation) | Senior Backend Engineer | High | backlog | C1, F3 | | [TEC-1423](/TEC/issues/TEC-1423) | Listings Module Backend (CRUD, Media, Moderation) | Senior Backend Engineer | High | backlog | C1, F3 |
| [TEC-1424](/TEC/issues/TEC-1424) | Search Module Backend (Typesense + Geo) | API Architect | High | backlog | C3, F2 | | [TEC-1424](/TEC/issues/TEC-1424) | Search Module Backend (Typesense + Geo) | API Architect | High | backlog | C3, F2 |
| [TEC-1425](/TEC/issues/TEC-1425) | Security Hardening (Rate Limiting, CORS, Helmet) | Security Engineer | High | backlog | F1 | | [TEC-1425](/TEC/issues/TEC-1425) | Security Hardening (Rate Limiting, CORS, Helmet) | Security Engineer | High | backlog | F1 |
| [TEC-1426](/TEC/issues/TEC-1426) | Error Handling & Logging Strategy | Architect | High | backlog | F4 | | [TEC-1426](/TEC/issues/TEC-1426) | Error Handling & Logging Strategy | Architect | High | backlog | F4 |
| [TEC-1427](/TEC/issues/TEC-1427) | Listings Frontend (Create/Edit + Detail) | Senior Frontend Engineer | High | backlog | C3 | | [TEC-1427](/TEC/issues/TEC-1427) | Listings Frontend (Create/Edit + Detail) | Senior Frontend Engineer | High | backlog | C3 |
| [TEC-1428](/TEC/issues/TEC-1428) | Search + Landing Page Frontend | Senior Frontend Engineer | High | backlog | C5 | | [TEC-1428](/TEC/issues/TEC-1428) | Search + Landing Page Frontend | Senior Frontend Engineer | High | backlog | C5 |
## Phase 2: Monetization & Operations (P2) ## Phase 2: Monetization & Operations (P2)
| Issue | Title | Owner | Priority | Status | Blockers | | Issue | Title | Owner | Priority | Status | Blockers |
|-------|-------|-------|----------|--------|----------| | -------------------------------- | ----------------------------------------------- | ----------------------- | -------- | ------- | -------- |
| [TEC-1429](/TEC/issues/TEC-1429) | Payments Module (VNPay + MoMo + ZaloPay) | Senior Backend Engineer | Medium | backlog | C1 | | [TEC-1429](/TEC/issues/TEC-1429) | Payments Module (VNPay + MoMo + ZaloPay) | Senior Backend Engineer | Medium | backlog | C1 |
| [TEC-1430](/TEC/issues/TEC-1430) | Subscriptions Module (Plans, Quotas, Billing) | Senior Backend Engineer | Medium | backlog | M1 | | [TEC-1430](/TEC/issues/TEC-1430) | Subscriptions Module (Plans, Quotas, Billing) | Senior Backend Engineer | Medium | backlog | M1 |
| [TEC-1431](/TEC/issues/TEC-1431) | Notifications Module (Email, SMS, Zalo OA, FCM) | Founding Engineer | Medium | backlog | C1 | | [TEC-1431](/TEC/issues/TEC-1431) | Notifications Module (Email, SMS, Zalo OA, FCM) | Founding Engineer | Medium | backlog | C1 |
| [TEC-1432](/TEC/issues/TEC-1432) | Admin Module (Backend + Frontend) | Senior Backend Engineer | Medium | backlog | C1, C3 | | [TEC-1432](/TEC/issues/TEC-1432) | Admin Module (Backend + Frontend) | Senior Backend Engineer | Medium | backlog | C1, C3 |
| [TEC-1433](/TEC/issues/TEC-1433) | E2E Testing Setup (Playwright) | QA Engineer | Medium | backlog | Phase 1 | | [TEC-1433](/TEC/issues/TEC-1433) | E2E Testing Setup (Playwright) | QA Engineer | Medium | backlog | Phase 1 |
## Phase 3: AI & Advanced (P3) — Not yet created ## Phase 3: AI & Advanced (P3) — Not yet created
@@ -51,10 +51,10 @@
## Summary ## Summary
| Phase | Total | Done | In Progress | Blocked | Backlog/Todo | | Phase | Total | Done | In Progress | Blocked | Backlog/Todo |
|-------|-------|------|-------------|---------|--------------| | --------- | ------ | ----- | ----------- | ------- | ------------ |
| Phase 0 | 6 | 0 | 0 | 0 | 6 | | Phase 0 | 6 | 0 | 0 | 0 | 6 |
| Phase 1 | 8 | 0 | 0 | 0 | 8 | | Phase 1 | 8 | 0 | 0 | 0 | 8 |
| Phase 2 | 5 | 0 | 0 | 0 | 5 | | Phase 2 | 5 | 0 | 0 | 0 | 5 |
| Phase 3 | 4 | — | — | — | Not created | | Phase 3 | 4 | — | — | — | Not created |
| **Total** | **19** | **0** | **0** | **0** | **19** | | **Total** | **19** | **0** | **0** | **0** | **19** |

View File

@@ -7,7 +7,7 @@
"build": "nest build", "build": "nest build",
"start": "node dist/main", "start": "node dist/main",
"start:prod": "node dist/main", "start:prod": "node dist/main",
"lint": "eslint \"{src,test}/**/*.ts\"", "lint": "eslint src/",
"test": "vitest run", "test": "vitest run",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
@@ -15,7 +15,12 @@
"@nestjs/common": "^11.0.0", "@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0", "@nestjs/core": "^11.0.0",
"@nestjs/cqrs": "^11.0.0", "@nestjs/cqrs": "^11.0.0",
"@nestjs/event-emitter": "^3.0.0",
"@nestjs/platform-express": "^11.0.0", "@nestjs/platform-express": "^11.0.0",
"@prisma/client": "^6.0.0",
"ioredis": "^5.4.0",
"pino": "^9.0.0",
"pino-pretty": "^13.0.0",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.0" "rxjs": "^7.8.0"
}, },
@@ -26,6 +31,7 @@
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"typescript": "^5.7.0", "typescript": "^5.7.0",
"prisma": "^6.0.0",
"vitest": "^3.0.0" "vitest": "^3.0.0"
} }
} }

View File

@@ -1,9 +1,10 @@
import { SharedModule } from '@modules/shared';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs'; import { CqrsModule } from '@nestjs/cqrs';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
@Module({ @Module({
imports: [CqrsModule.forRoot()], imports: [CqrsModule.forRoot(), SharedModule],
controllers: [AppController], controllers: [AppController],
}) })
export class AppModule {} export class AppModule {}

View File

@@ -8,8 +8,11 @@
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"declaration": false, "declaration": false,
"declarationMap": false "declarationMap": false,
"paths": {
"@modules/*": ["../../src/modules/*"]
}
}, },
"include": ["src/**/*"], "include": ["src/**/*", "../../src/modules/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
} }

View File

@@ -6,11 +6,7 @@ export const metadata: Metadata = {
description: 'Vietnam Real Estate Platform', description: 'Vietnam Real Estate Platform',
}; };
export default function RootLayout({ export default function RootLayout({ children }: { children: React.ReactNode }) {
children,
}: {
children: React.ReactNode;
}) {
return ( return (
<html lang="vi"> <html lang="vi">
<body>{children}</body> <body>{children}</body>

View File

@@ -1,10 +1,7 @@
import type { Config } from 'tailwindcss'; import type { Config } from 'tailwindcss';
const config: Config = { const config: Config = {
content: [ content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
'./app/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
],
theme: { theme: {
extend: {}, extend: {},
}, },

View File

@@ -2,11 +2,7 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2017",
"lib": [ "lib": ["DOM", "DOM.Iterable", "ES2022"],
"DOM",
"DOM.Iterable",
"ES2022"
],
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"jsx": "preserve", "jsx": "preserve",
@@ -17,9 +13,7 @@
} }
], ],
"paths": { "paths": {
"@/*": [ "@/*": ["./*"]
"./*"
]
}, },
"declaration": false, "declaration": false,
"declarationMap": false, "declarationMap": false,
@@ -28,14 +22,6 @@
"allowJs": true, "allowJs": true,
"isolatedModules": true "isolatedModules": true
}, },
"include": [ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"next-env.d.ts", "exclude": ["node_modules", ".next"]
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules",
".next"
]
} }

View File

@@ -4,7 +4,7 @@ services:
container_name: goodgo-postgres container_name: goodgo-postgres
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${DB_PORT:-5432}:5432" - '${DB_PORT:-5432}:5432'
environment: environment:
POSTGRES_DB: ${DB_NAME:-goodgo} POSTGRES_DB: ${DB_NAME:-goodgo}
POSTGRES_USER: ${DB_USER:-goodgo} POSTGRES_USER: ${DB_USER:-goodgo}
@@ -12,7 +12,7 @@ services:
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-goodgo} -d ${DB_NAME:-goodgo}"] test: ['CMD-SHELL', 'pg_isready -U ${DB_USER:-goodgo} -d ${DB_NAME:-goodgo}']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -25,12 +25,12 @@ services:
container_name: goodgo-redis container_name: goodgo-redis
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${REDIS_PORT:-6379}:6379" - '${REDIS_PORT:-6379}:6379'
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes: volumes:
- redis_data:/data - redis_data:/data
healthcheck: healthcheck:
test: ["CMD", "redis-cli", "ping"] test: ['CMD', 'redis-cli', 'ping']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -43,15 +43,15 @@ services:
container_name: goodgo-typesense container_name: goodgo-typesense
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${TYPESENSE_PORT:-8108}:8108" - '${TYPESENSE_PORT:-8108}:8108'
environment: environment:
TYPESENSE_API_KEY: ${TYPESENSE_API_KEY:-ts_dev_key_change_me} TYPESENSE_API_KEY: ${TYPESENSE_API_KEY:-ts_dev_key_change_me}
TYPESENSE_DATA_DIR: /data TYPESENSE_DATA_DIR: /data
TYPESENSE_ENABLE_CORS: "true" TYPESENSE_ENABLE_CORS: 'true'
volumes: volumes:
- typesense_data:/data - typesense_data:/data
healthcheck: healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8108/health"] test: ['CMD', 'curl', '-sf', 'http://localhost:8108/health']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -64,8 +64,8 @@ services:
container_name: goodgo-minio container_name: goodgo-minio
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${MINIO_API_PORT:-9000}:9000" - '${MINIO_API_PORT:-9000}:9000'
- "${MINIO_CONSOLE_PORT:-9001}:9001" - '${MINIO_CONSOLE_PORT:-9001}:9001'
command: server /data --console-address ":9001" command: server /data --console-address ":9001"
environment: environment:
MINIO_ROOT_USER: ${MINIO_USER:-minioadmin} MINIO_ROOT_USER: ${MINIO_USER:-minioadmin}
@@ -73,7 +73,7 @@ services:
volumes: volumes:
- minio_data:/data - minio_data:/data
healthcheck: healthcheck:
test: ["CMD", "mc", "ready", "local"] test: ['CMD', 'mc', 'ready', 'local']
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5

View File

@@ -20,12 +20,12 @@ docker compose ps
## Services ## Services
| Service | Port(s) | Description | Dashboard/UI | | Service | Port(s) | Description | Dashboard/UI |
| ---------- | ------------ | ------------------------------ | ------------------------------- | | ---------- | ----------- | ---------------------------- | ------------------------------- |
| PostgreSQL | 5432 | Database with PostGIS | — | | PostgreSQL | 5432 | Database with PostGIS | — |
| Redis | 6379 | Cache, sessions, queue | — | | Redis | 6379 | Cache, sessions, queue | — |
| Typesense | 8108 | Full-text search engine | http://localhost:8108/health | | Typesense | 8108 | Full-text search engine | http://localhost:8108/health |
| MinIO | 9000 / 9001 | S3-compatible object storage | http://localhost:9001 (console) | | MinIO | 9000 / 9001 | S3-compatible object storage | http://localhost:9001 (console) |
## Common Commands ## Common Commands

121
eslint.config.mjs Normal file
View File

@@ -0,0 +1,121 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import-x';
import eslintConfigPrettier from 'eslint-config-prettier';
import globals from 'globals';
export default tseslint.config(
// Global ignores
{
ignores: [
'**/node_modules/**',
'**/dist/**',
'**/.next/**',
'**/coverage/**',
'**/*.js',
'**/*.cjs',
'**/*.mjs',
'!eslint.config.mjs',
],
},
// Base JS recommended rules
js.configs.recommended,
// TypeScript recommended rules
...tseslint.configs.recommended,
// Import plugin
importPlugin.flatConfigs.recommended,
importPlugin.flatConfigs.typescript,
// Prettier (disables conflicting rules)
eslintConfigPrettier,
// Shared settings for all TS files
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
globals: {
...globals.node,
},
},
settings: {
'import-x/resolver': {
typescript: {
project: ['apps/*/tsconfig.json', 'tsconfig.base.json'],
},
},
},
rules: {
// TypeScript
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
],
// Import ordering
'import-x/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'never',
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
'import-x/no-duplicates': 'error',
'import-x/no-unresolved': 'off', // TypeScript handles this
// General
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
},
// NestJS-specific rules for the API app
{
files: ['apps/api/**/*.ts', 'src/modules/**/*.ts'],
rules: {
// NestJS uses empty classes for modules, allow them
'@typescript-eslint/no-extraneous-class': 'off',
// NestJS decorators require this pattern
'@typescript-eslint/no-unsafe-declaration-merging': 'off',
},
},
// React/Next.js overrides for web app
{
files: ['apps/web/**/*.ts', 'apps/web/**/*.tsx'],
languageOptions: {
globals: {
...globals.browser,
React: 'readonly',
},
},
rules: {
'no-console': 'error',
},
},
// Test files
{
files: ['**/*.spec.ts', '**/*.test.ts', '**/__tests__/**/*.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'no-console': 'off',
},
},
// Script files (seeds, migrations, etc.)
{
files: ['prisma/**/*.ts'],
rules: {
'no-console': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
},
},
);

View File

@@ -8,17 +8,58 @@
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@nestjs/core", "@nestjs/core",
"esbuild" "@prisma/client",
"@prisma/engines",
"esbuild",
"prisma"
] ]
}, },
"scripts": { "scripts": {
"dev": "turbo run dev", "dev": "turbo run dev",
"build": "turbo run build", "build": "turbo run build",
"lint": "turbo run lint", "lint": "eslint .",
"test": "turbo run test", "test": "turbo run test",
"typecheck": "turbo run typecheck" "typecheck": "turbo run typecheck",
"format": "prettier --write .",
"format:check": "prettier --check .",
"dep-cruise": "depcruise src/ apps/ --config .dependency-cruiser.cjs",
"db:generate": "prisma generate",
"db:migrate:dev": "prisma migrate dev",
"db:migrate:deploy": "prisma migrate deploy",
"db:seed": "prisma db seed",
"db:studio": "prisma studio",
"db:reset": "prisma migrate reset",
"prepare": "husky"
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yaml,yml}": [
"prettier --write"
]
},
"prisma": {
"seed": "tsx prisma/seed.ts"
}, },
"devDependencies": { "devDependencies": {
"turbo": "^2.9.4" "@eslint/js": "^9.39.4",
"dependency-cruiser": "^17.3.10",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.2",
"globals": "^17.4.0",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"prettier": "^3.8.1",
"prisma": "^6.19.3",
"tsx": "^4.21.0",
"turbo": "^2.9.4",
"typescript-eslint": "^8.58.0"
},
"dependencies": {
"@prisma/client": "^6.19.3"
} }
} }

1943
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
packages: packages:
- "apps/*" - 'apps/*'
- "packages/*" - 'packages/*'

View File

@@ -1,5 +1,5 @@
import { BaseEntity } from './base-entity'; 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> { export abstract class AggregateRoot<TId = string> extends BaseEntity<TId> {
private _domainEvents: DomainEvent[] = []; 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 { match<U>(handlers: { ok: (value: T) => U; err: (error: E) => U }): U {
return this._isOk return this._isOk ? handlers.ok(this._value as T) : handlers.err(this._error as E);
? handlers.ok(this._value as T)
: handlers.err(this._error as E);
} }
} }

View File

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

View File

@@ -1,5 +1,5 @@
import { Injectable, LoggerService as NestLoggerService } from '@nestjs/common'; import { Injectable, type LoggerService as NestLoggerService } from '@nestjs/common';
import pino, { Logger } from 'pino'; import pino, { type Logger } from 'pino';
@Injectable() @Injectable()
export class LoggerService implements NestLoggerService { 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'; import { PrismaClient } from '@prisma/client';
@Injectable() @Injectable()
export class PrismaService export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit(): Promise<void> { async onModuleInit(): Promise<void> {
await this.$connect(); 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'; import Redis from 'ioredis';
@Injectable() @Injectable()

View File

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

View File

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

View File

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

View File

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