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:
79
.dependency-cruiser.cjs
Normal file
79
.dependency-cruiser.cjs
Normal 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
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
npm test
|
||||
5
.prettierignore
Normal file
5
.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
.next
|
||||
coverage
|
||||
pnpm-lock.yaml
|
||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
**Goal:** Any engineer can clone, install, and start developing.
|
||||
|
||||
**Execution Order:**
|
||||
|
||||
1. **[TEC-1415] Monorepo Scaffolding** + **[TEC-1416] Docker Compose** (parallel — no deps)
|
||||
2. **[TEC-1420] ESLint/Prettier** (after F1)
|
||||
3. **[TEC-1417] Prisma Schema** (after F1 + F2)
|
||||
@@ -30,6 +31,7 @@ F2 (Docker) ─────┘
|
||||
**Goal:** Users can register, post listings, and search properties.
|
||||
|
||||
**Execution Order:**
|
||||
|
||||
1. **[TEC-1421] Auth Backend** (after F3, F4)
|
||||
2. **[TEC-1425] Security Hardening** + **[TEC-1426] Error Handling** (parallel, after F1/F4)
|
||||
3. **[TEC-1422] Auth Frontend** (after C1)
|
||||
@@ -70,27 +72,27 @@ C5 + A2 ──→ A3 (MCP Servers)
|
||||
|
||||
## Dependency Map
|
||||
|
||||
| Task | Depends On |
|
||||
|------|-----------|
|
||||
| TEC-1415 (F1) | None |
|
||||
| TEC-1416 (F2) | None |
|
||||
| TEC-1417 (F3) | F1, F2 |
|
||||
| TEC-1418 (F4) | F1 |
|
||||
| TEC-1419 (F5) | F1 |
|
||||
| TEC-1420 (F6) | F1 |
|
||||
| TEC-1421 (C1) | F3, F4 |
|
||||
| TEC-1422 (C2) | C1 |
|
||||
| TEC-1423 (C3) | C1, F3 |
|
||||
| TEC-1424 (C5) | C3, F2 |
|
||||
| TEC-1425 (X1) | F1 |
|
||||
| TEC-1426 (X3) | F4 |
|
||||
| TEC-1427 (C4) | C3 |
|
||||
| TEC-1428 (C6) | C5 |
|
||||
| TEC-1429 (M1) | C1 |
|
||||
| TEC-1430 (M2) | M1 |
|
||||
| TEC-1431 (M3) | C1 |
|
||||
| TEC-1432 (M4) | C1, C3 |
|
||||
| TEC-1433 (X4) | Phase 1 |
|
||||
| Task | Depends On |
|
||||
| ------------- | ---------- |
|
||||
| TEC-1415 (F1) | None |
|
||||
| TEC-1416 (F2) | None |
|
||||
| TEC-1417 (F3) | F1, F2 |
|
||||
| TEC-1418 (F4) | F1 |
|
||||
| TEC-1419 (F5) | F1 |
|
||||
| TEC-1420 (F6) | F1 |
|
||||
| TEC-1421 (C1) | F3, F4 |
|
||||
| TEC-1422 (C2) | C1 |
|
||||
| TEC-1423 (C3) | C1, F3 |
|
||||
| TEC-1424 (C5) | C3, F2 |
|
||||
| TEC-1425 (X1) | F1 |
|
||||
| TEC-1426 (X3) | F4 |
|
||||
| TEC-1427 (C4) | C3 |
|
||||
| TEC-1428 (C6) | C5 |
|
||||
| TEC-1429 (M1) | C1 |
|
||||
| TEC-1430 (M2) | M1 |
|
||||
| TEC-1431 (M3) | C1 |
|
||||
| TEC-1432 (M4) | C1, C3 |
|
||||
| TEC-1433 (X4) | Phase 1 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -8,37 +8,37 @@
|
||||
|
||||
## Phase 0: Foundation (P0 — Critical)
|
||||
|
||||
| Issue | Title | Owner | Priority | Status | Blockers |
|
||||
|-------|-------|-------|----------|--------|----------|
|
||||
| [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-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-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 |
|
||||
| Issue | Title | Owner | Priority | Status | Blockers |
|
||||
| -------------------------------- | --------------------------------------------------- | ------------------ | -------- | ------ | -------- |
|
||||
| [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-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-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 |
|
||||
|
||||
## Phase 1: Core Auth & Listings (P1)
|
||||
|
||||
| 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-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-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-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-1428](/TEC/issues/TEC-1428) | Search + Landing Page Frontend | Senior Frontend Engineer | High | backlog | C5 |
|
||||
| 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-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-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-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-1428](/TEC/issues/TEC-1428) | Search + Landing Page Frontend | Senior Frontend Engineer | High | backlog | C5 |
|
||||
|
||||
## Phase 2: Monetization & Operations (P2)
|
||||
|
||||
| Issue | Title | Owner | Priority | Status | Blockers |
|
||||
|-------|-------|-------|----------|--------|----------|
|
||||
| [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-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-1433](/TEC/issues/TEC-1433) | E2E Testing Setup (Playwright) | QA Engineer | Medium | backlog | Phase 1 |
|
||||
| Issue | Title | Owner | Priority | Status | Blockers |
|
||||
| -------------------------------- | ----------------------------------------------- | ----------------------- | -------- | ------- | -------- |
|
||||
| [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-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-1433](/TEC/issues/TEC-1433) | E2E Testing Setup (Playwright) | QA Engineer | Medium | backlog | Phase 1 |
|
||||
|
||||
## Phase 3: AI & Advanced (P3) — Not yet created
|
||||
|
||||
@@ -51,10 +51,10 @@
|
||||
|
||||
## Summary
|
||||
|
||||
| Phase | Total | Done | In Progress | Blocked | Backlog/Todo |
|
||||
|-------|-------|------|-------------|---------|--------------|
|
||||
| Phase 0 | 6 | 0 | 0 | 0 | 6 |
|
||||
| Phase 1 | 8 | 0 | 0 | 0 | 8 |
|
||||
| Phase 2 | 5 | 0 | 0 | 0 | 5 |
|
||||
| Phase 3 | 4 | — | — | — | Not created |
|
||||
| **Total** | **19** | **0** | **0** | **0** | **19** |
|
||||
| Phase | Total | Done | In Progress | Blocked | Backlog/Todo |
|
||||
| --------- | ------ | ----- | ----------- | ------- | ------------ |
|
||||
| Phase 0 | 6 | 0 | 0 | 0 | 6 |
|
||||
| Phase 1 | 8 | 0 | 0 | 0 | 8 |
|
||||
| Phase 2 | 5 | 0 | 0 | 0 | 5 |
|
||||
| Phase 3 | 4 | — | — | — | Not created |
|
||||
| **Total** | **19** | **0** | **0** | **0** | **19** |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"build": "nest build",
|
||||
"start": "node dist/main",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,test}/**/*.ts\"",
|
||||
"lint": "eslint src/",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
@@ -15,7 +15,12 @@
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/core": "^11.0.0",
|
||||
"@nestjs/cqrs": "^11.0.0",
|
||||
"@nestjs/event-emitter": "^3.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",
|
||||
"rxjs": "^7.8.0"
|
||||
},
|
||||
@@ -26,6 +31,7 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"prisma": "^6.0.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { SharedModule } from '@modules/shared';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { AppController } from './app.controller';
|
||||
|
||||
@Module({
|
||||
imports: [CqrsModule.forRoot()],
|
||||
imports: [CqrsModule.forRoot(), SharedModule],
|
||||
controllers: [AppController],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"declaration": false,
|
||||
"declarationMap": false
|
||||
"declarationMap": false,
|
||||
"paths": {
|
||||
"@modules/*": ["../../src/modules/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"include": ["src/**/*", "../../src/modules/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -6,11 +6,7 @@ export const metadata: Metadata = {
|
||||
description: 'Vietnam Real Estate Platform',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="vi">
|
||||
<body>{children}</body>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
],
|
||||
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ES2022"
|
||||
],
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "preserve",
|
||||
@@ -17,9 +13,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
@@ -28,14 +22,6 @@
|
||||
"allowJs": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".next"
|
||||
]
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", ".next"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
container_name: goodgo-postgres
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${DB_PORT:-5432}:5432"
|
||||
- '${DB_PORT:-5432}:5432'
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-goodgo}
|
||||
POSTGRES_USER: ${DB_USER:-goodgo}
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
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
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -25,12 +25,12 @@ services:
|
||||
container_name: goodgo-redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
- '${REDIS_PORT:-6379}:6379'
|
||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -43,15 +43,15 @@ services:
|
||||
container_name: goodgo-typesense
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${TYPESENSE_PORT:-8108}:8108"
|
||||
- '${TYPESENSE_PORT:-8108}:8108'
|
||||
environment:
|
||||
TYPESENSE_API_KEY: ${TYPESENSE_API_KEY:-ts_dev_key_change_me}
|
||||
TYPESENSE_DATA_DIR: /data
|
||||
TYPESENSE_ENABLE_CORS: "true"
|
||||
TYPESENSE_ENABLE_CORS: 'true'
|
||||
volumes:
|
||||
- typesense_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:8108/health"]
|
||||
test: ['CMD', 'curl', '-sf', 'http://localhost:8108/health']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -64,8 +64,8 @@ services:
|
||||
container_name: goodgo-minio
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${MINIO_API_PORT:-9000}:9000"
|
||||
- "${MINIO_CONSOLE_PORT:-9001}:9001"
|
||||
- '${MINIO_API_PORT:-9000}:9000'
|
||||
- '${MINIO_CONSOLE_PORT:-9001}:9001'
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_USER:-minioadmin}
|
||||
@@ -73,7 +73,7 @@ services:
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
test: ['CMD', 'mc', 'ready', 'local']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
@@ -20,12 +20,12 @@ docker compose ps
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Port(s) | Description | Dashboard/UI |
|
||||
| ---------- | ------------ | ------------------------------ | ------------------------------- |
|
||||
| PostgreSQL | 5432 | Database with PostGIS | — |
|
||||
| Redis | 6379 | Cache, sessions, queue | — |
|
||||
| Typesense | 8108 | Full-text search engine | http://localhost:8108/health |
|
||||
| MinIO | 9000 / 9001 | S3-compatible object storage | http://localhost:9001 (console) |
|
||||
| Service | Port(s) | Description | Dashboard/UI |
|
||||
| ---------- | ----------- | ---------------------------- | ------------------------------- |
|
||||
| PostgreSQL | 5432 | Database with PostGIS | — |
|
||||
| Redis | 6379 | Cache, sessions, queue | — |
|
||||
| Typesense | 8108 | Full-text search engine | http://localhost:8108/health |
|
||||
| MinIO | 9000 / 9001 | S3-compatible object storage | http://localhost:9001 (console) |
|
||||
|
||||
## Common Commands
|
||||
|
||||
|
||||
121
eslint.config.mjs
Normal file
121
eslint.config.mjs
Normal 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',
|
||||
},
|
||||
},
|
||||
);
|
||||
49
package.json
49
package.json
@@ -8,17 +8,58 @@
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@nestjs/core",
|
||||
"esbuild"
|
||||
"@prisma/client",
|
||||
"@prisma/engines",
|
||||
"esbuild",
|
||||
"prisma"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "turbo run dev",
|
||||
"build": "turbo run build",
|
||||
"lint": "turbo run lint",
|
||||
"lint": "eslint .",
|
||||
"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": {
|
||||
"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
1943
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||
import { Injectable, type OnModuleDestroy } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user