hrms-api-org/CLAUDE.md
waruneeauy 26732420c2 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>
2026-07-17 21:12:59 +07:00

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.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).
  • webServiceAuthX-API-Key header, resolved against the ApiKey entity in src/middlewares/authWebService.ts (looks up allowed apiNames/org scope). Type: RequestWithUserWebService.
  • internalAuthapi-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/).