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>
8.3 KiB
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
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.jsonglobssrc/controllers/**/*Controller.tsandnpm run buildregeneratessrc/routes.ts(Express route registration, called viaRegisterRoutes(app)insrc/app.ts) andsrc/swagger.jsonfrom tsoa decorators. Any time you add/change a controller method or its decorators, you must runnpm run build(or at leasttsoa 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.tsare 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/) extendEntityBase(src/entities/base/Base.ts), which suppliesid(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, prefixedHR_*);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 asHttpError(src/interfaces/ http-error.ts, carries anHttpStatus+ Thai message) — the global error middleware (src/middlewares/error.ts) catches thrownHttpError/exceptions and formats the response.src/interfaces/http-status.tsis 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 inAuthorization: Bearer <token>. Verified either offline (AUTH_PUBLIC_KEY, local RS256 verification viafast-jwt) or online (AUTH_REALM_URLuserinfo endpoint), selected byAUTH_PREFERRED_MODE. Populatesreq.app.locals.logDatawith user/org-tree ids from the token for logging. Type:RequestWithUser(src/middlewares/user.ts).webServiceAuth—X-API-Keyheader, resolved against theApiKeyentity insrc/middlewares/authWebService.ts(looks up allowedapiNames/org scope). Type:RequestWithUserWebService.internalAuth—api-key/api_key/apikeyheader checked against theAPI_KEYenv var (src/middlewares/authInternal.ts), for trusted internal services (e.g. the .NET HRMS system).- If
NODE_ENV !== "production"andAUTH_BYPASSis set, auth is skipped entirely ({preferred_username: "bypassed"}) — dev/test convenience only. - Role gating within
bearerAuthroutes usesauthRole()(src/middlewares/role.ts) and thepermissionhelper (src/interfaces/permission.ts) which calls out to an external/org/permissioncheck viaCallAPI.
Background work
- Cron jobs are all registered inline in
src/app.tsvianode-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) plussrc/middlewares/logs.tsbuild a per-request log sequence (including DB queries logged via the custom TypeORMLoggerinsrc/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,maxQueryExecutionTimeall env-tunable).synchronizeis alwaysfalse— 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 runnode scripts/clean-migration-fk-idx.jsto strip auto-generatedFK_*/idx_*lines from the migration'sup/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 (noHttpError/HttpSuccessimports insrc/services/). - Don't use
momentfor new code (nativeDate/Intlonly —momentremains for legacy call sites). Don't bypass tsoa validation with manualreq.bodycasting. tsconfig.jsonexcludessrc/__tests__/**and*.spec.ts/*.test.tsfrom the production build; tests only run underts-jestviajest.config.js(path alias@/→src/).