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.
|
**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)
|
||||||
@@ -71,7 +73,7 @@ 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 |
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
## 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 |
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
## 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 |
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
## 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 |
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
## 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 |
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ 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 |
|
||||||
|
|||||||
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": {
|
"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
1943
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
|||||||
packages:
|
packages:
|
||||||
- "apps/*"
|
- 'apps/*'
|
||||||
- "packages/*"
|
- 'packages/*'
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user