# 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/ # 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 `. 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/`).