Add CLAUDE.md with project architecture and command reference
Documents the tsoa-driven routing, auth schemes, background jobs, and DB conventions so future Claude Code sessions can ramp up faster. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
4ef29ad4db
commit
26732420c2
1 changed files with 134 additions and 0 deletions
134
CLAUDE.md
Normal file
134
CLAUDE.md
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
TypeScript REST API for **Bangkok Metropolitan Administration (BMA)** — a Human Resource
|
||||
Management System (HRMS) module managing organization structure (โครงสร้างอัตรากำลัง),
|
||||
positions, personnel profiles, salary/tenure calculations, and related HR workflows.
|
||||
|
||||
**Stack:** Express.js · TypeORM (MySQL) · tsoa (controller-first OpenAPI generation) · Keycloak ·
|
||||
RabbitMQ · Redis · Elasticsearch · WebSocket (socket.io) · node-cron
|
||||
|
||||
## Commands
|
||||
|
||||
```sh
|
||||
npm install # install deps (npm is canonical — CI/Docker use npm install, not pnpm)
|
||||
npm run dev # dev server with hot reload (nodemon)
|
||||
npm run build # tsoa spec-and-routes (regenerates src/routes.ts, src/swagger.json) + tsc
|
||||
npm run check # tsc --noEmit
|
||||
npm start # run compiled dist/app.js
|
||||
npm run format # prettier --write .
|
||||
|
||||
npm run migration:generate src/migration/<name> # generate a TypeORM migration
|
||||
npm run migration:run # run pending migrations
|
||||
node scripts/clean-migration-fk-idx.js # strip FK_/idx_ lines from generated migrations (run after every generate)
|
||||
|
||||
npm test # jest
|
||||
npm run test:watch # jest --watch
|
||||
npm run test:coverage # jest --coverage
|
||||
npx jest src/__tests__/unit/OrganizationController.spec.ts # run a single test file
|
||||
```
|
||||
|
||||
A repo note (README) documents building/testing the release pipeline locally with `act`
|
||||
(`act workflow_dispatch -W .github/workflows/release.yaml ...`), but the actual CI is Forgejo
|
||||
Actions (`.forgejo/workflows/`) and an OneDev buildspec (`.onedev-buildspec.yml`); both run
|
||||
`npm install && npm run build` and build `docker/Dockerfile`.
|
||||
|
||||
**Note:** despite `pnpm-lock.yaml` being present in the repo, all CI/Docker paths use `npm`. Use
|
||||
`npm` for scripts and dependency changes unless told otherwise.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Request flow: tsoa controllers → (optional) services → TypeORM repositories
|
||||
|
||||
- Routes are **not** hand-written. `tsoa.json` globs `src/controllers/**/*Controller.ts` and
|
||||
`npm run build` regenerates `src/routes.ts` (Express route registration, called via
|
||||
`RegisterRoutes(app)` in `src/app.ts`) and `src/swagger.json` from tsoa decorators. **Any time
|
||||
you add/change a controller method or its decorators, you must run `npm run build`** (or at
|
||||
least `tsoa spec-and-routes`) or the route/swagger changes won't take effect.
|
||||
- In practice most controllers get their repositories directly via
|
||||
`AppDataSource.getRepository(Entity)` in the constructor/method body rather than going through a
|
||||
service layer — the codebase is controller-heavy (`OrganizationController.ts`,
|
||||
`ImportDataController.ts`, `PositionController.ts`, `ReportController.ts` are each hundreds of
|
||||
KB). `src/services/` exists for genuinely cross-cutting or heavier business logic (org command
|
||||
execution, salary/tenure batch jobs, RabbitMQ, WebSocket) but is not a strict mandatory layer for
|
||||
every endpoint.
|
||||
- All entities (`src/entities/`) extend `EntityBase` (`src/entities/base/Base.ts`), which supplies
|
||||
`id` (UUID), `createdAt`/`lastUpdatedAt`, and creator/updater id+name audit columns — every table
|
||||
is audited by default. `src/entities/mis/` holds legacy/external MIS-system table mappings
|
||||
(read-mostly, prefixed `HR_*`); `src/entities/view/` holds TypeORM entities mapped onto SQL
|
||||
views (`viewCurrentTenure*`, `viewDirector*`, etc.) used for computed/reporting reads.
|
||||
- Responses are wrapped in `HttpSuccess` (`src/interfaces/http-success.ts`, `{status, message,
|
||||
result}`, Thai success message by default) or thrown as `HttpError` (`src/interfaces/
|
||||
http-error.ts`, carries an `HttpStatus` + Thai message) — the global error middleware
|
||||
(`src/middlewares/error.ts`) catches thrown `HttpError`/exceptions and formats the response.
|
||||
`src/interfaces/http-status.ts` is the status-code enum used everywhere instead of magic numbers.
|
||||
|
||||
### Auth (three schemes, selected per-route via `@Security(...)`)
|
||||
|
||||
All resolved in `expressAuthentication` (`src/middlewares/auth.ts`), which tsoa calls per-request
|
||||
based on the controller's `@Security` decorator:
|
||||
|
||||
- `bearerAuth` — Keycloak JWT in `Authorization: Bearer <token>`. Verified either **offline**
|
||||
(`AUTH_PUBLIC_KEY`, local RS256 verification via `fast-jwt`) or **online** (`AUTH_REALM_URL`
|
||||
userinfo endpoint), selected by `AUTH_PREFERRED_MODE`. Populates `req.app.locals.logData` with
|
||||
user/org-tree ids from the token for logging. Type: `RequestWithUser` (`src/middlewares/user.ts`).
|
||||
- `webServiceAuth` — `X-API-Key` header, resolved against the `ApiKey` entity in
|
||||
`src/middlewares/authWebService.ts` (looks up allowed `apiNames`/org scope). Type:
|
||||
`RequestWithUserWebService`.
|
||||
- `internalAuth` — `api-key`/`api_key`/`apikey` header checked against the `API_KEY` env var
|
||||
(`src/middlewares/authInternal.ts`), for trusted internal services (e.g. the .NET HRMS system).
|
||||
- If `NODE_ENV !== "production"` and `AUTH_BYPASS` is set, auth is skipped entirely
|
||||
(`{preferred_username: "bypassed"}`) — dev/test convenience only.
|
||||
- Role gating within `bearerAuth` routes uses `authRole()` (`src/middlewares/role.ts`) and the
|
||||
`permission` helper (`src/interfaces/permission.ts`) which calls out to an external `/org/permission`
|
||||
check via `CallAPI`.
|
||||
|
||||
### Background work
|
||||
|
||||
- **Cron jobs** are all registered inline in `src/app.ts` via `node-cron` (6-field: seconds
|
||||
included), each wrapping a controller/service call in try/catch that only logs on failure —
|
||||
daily org revision cache refresh, retirement status updates (Oct 1st), org DNA sync, tenure
|
||||
recalculation, and posting retirement data to an external "Exprofile" system.
|
||||
- **RabbitMQ** (`src/services/rabbitmq.ts`) publishes org-structure change events
|
||||
(`sendToQueueOrg` / `sendToQueueOrgDraft`); connects with infinite retry
|
||||
(`setTimeout(runMessageQueue, 1000)` on failure) — don't add redundant retry logic around it.
|
||||
Fire-and-forget from callers; don't block HTTP responses on publish.
|
||||
- **WebSocket** (`src/services/webSocket.ts`, `initWebSocket()`) for real-time push, initialized
|
||||
before the HTTP server starts listening.
|
||||
- **`OrgStructureCache`** (`src/utils/OrgStructureCache.ts`) is an in-memory TTL cache (30 min) for
|
||||
org-tree reads, keyed by revision+root id, initialized/destroyed alongside the app lifecycle. Use
|
||||
it instead of re-querying the same org structure repeatedly within a request.
|
||||
- **`LogMemoryStore`** (`src/utils/LogMemoryStore.ts`) plus `src/middlewares/logs.ts` build a
|
||||
per-request log sequence (including DB queries logged via the custom TypeORM `Logger` in
|
||||
`src/database/data-source.ts`) — this is what's shipped to Elasticsearch for auditing.
|
||||
|
||||
### Database
|
||||
|
||||
- MySQL via TypeORM, connection pool configured in `src/database/data-source.ts`
|
||||
(`connectionLimit`, `poolSize`, `maxQueryExecutionTime` all env-tunable). `synchronize` is
|
||||
always `false` — schema changes go through migrations only.
|
||||
- Timezone is pinned to Bangkok (`+07:00`) at the connection level; store/compare datetimes
|
||||
accordingly rather than assuming UTC.
|
||||
- After `migration:generate`, always run `node scripts/clean-migration-fk-idx.js` to strip
|
||||
auto-generated `FK_*`/`idx_*` lines from the migration's `up`/`down` — this is a hard project
|
||||
convention, not optional cleanup.
|
||||
|
||||
### Data dictionary
|
||||
|
||||
`docs/data-dictionary/` holds a generated schema data dictionary (`.docx`); the `data-dictionary`
|
||||
skill / `scripts/generate-docx.py` regenerate it from `docs/data-dictionary/generate-prompt.md`.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Files/classes: PascalCase (`OrganizationController.ts`). Variables/functions: camelCase.
|
||||
- DB column comments and HTTP error/success messages are written in **Thai** — keep this
|
||||
consistent when adding columns or throwing `HttpError`.
|
||||
- Services must not import from `controllers/`; keep services HTTP-agnostic (no `HttpError`/
|
||||
`HttpSuccess` imports in `src/services/`).
|
||||
- Don't use `moment` for new code (native `Date`/`Intl` only — `moment` remains for legacy call
|
||||
sites). Don't bypass tsoa validation with manual `req.body` casting.
|
||||
- `tsconfig.json` excludes `src/__tests__/**` and `*.spec.ts`/`*.test.ts` from the production
|
||||
build; tests only run under `ts-jest` via `jest.config.js` (path alias `@/` → `src/`).
|
||||
Loading…
Add table
Add a link
Reference in a new issue