283 lines
12 KiB
Markdown
283 lines
12 KiB
Markdown
# AGENTS.md — LfK Backend
|
|
|
|
Guidance for agentic coding agents working in this repository.
|
|
|
|
---
|
|
|
|
## Project Overview
|
|
|
|
Express + [`routing-controllers`](https://github.com/typestack/routing-controllers) REST API written in TypeScript. Uses TypeORM for database access (SQLite in dev/test, PostgreSQL or MySQL in production). OpenAPI docs are auto-generated from decorators at startup.
|
|
|
|
**Runtime & Package Manager**: Bun (replaces Node.js + npm/pnpm).
|
|
|
|
---
|
|
|
|
## Build / Run / Test Commands
|
|
|
|
### Development
|
|
|
|
```sh
|
|
bun run dev # Start dev server with auto-reload (uses Bun's --watch)
|
|
```
|
|
|
|
**Auto-reload**: The `dev` script uses Bun's built-in `--watch` flag, which automatically restarts the server when TypeScript files in `src/` change. Bun runs TypeScript directly - no build step needed.
|
|
|
|
**Performance**: Bun delivers 8-15% better latency under concurrent load compared to Node.js. See `BUN_BENCHMARK_RESULTS.md` for details.
|
|
|
|
### Build
|
|
|
|
```sh
|
|
bun run build # rimraf dist && tsc && copy static assets → dist/
|
|
```
|
|
|
|
**Note**: The build script exists for legacy compatibility and type-checking, but is **not required** for development or production. Bun runs TypeScript source files directly.
|
|
|
|
### Production
|
|
|
|
```sh
|
|
bun start # bun src/app.ts (runs TypeScript directly)
|
|
```
|
|
|
|
### Tests
|
|
|
|
Tests are **integration tests** that hit a live running server via HTTP. The server must be started before Jest is invoked.
|
|
|
|
```sh
|
|
# Full CI test flow (generates .env, starts server, runs jest):
|
|
bun run test:ci
|
|
|
|
# Run Jest directly (server must already be running):
|
|
bun test
|
|
|
|
# Watch mode:
|
|
bun run test:watch
|
|
|
|
# Run a single test file:
|
|
bunx jest src/tests/runners/runner_add.spec.ts
|
|
|
|
# Run tests matching a name pattern:
|
|
bunx jest --testNamePattern="POST /api/runners"
|
|
|
|
# Run all tests in a subdirectory:
|
|
bunx jest src/tests/runners/
|
|
```
|
|
|
|
# Run all tests in a subdirectory:
|
|
bunx jest src/tests/runners/
|
|
```
|
|
|
|
> **Important:** `bun test` alone will fail unless the dev server is already running on `http://localhost:<config.internal_port>`. In CI, `start-server-and-test` handles this automatically via `bun run test:ci`.
|
|
|
|
### Other Utilities
|
|
|
|
```sh
|
|
bun run seed # Sync DB schema and run seeders
|
|
bun run openapi:export # Export OpenAPI spec to file
|
|
bun run docs # Generate TypeDoc documentation
|
|
bun run licenses:export # Export third-party license report
|
|
```
|
|
|
|
---
|
|
|
|
## TypeScript Configuration
|
|
|
|
- **Target:** ES2020, **Module:** CommonJS
|
|
- **`strict: false`** — TypeScript strictness is disabled; types are used but not exhaustively enforced
|
|
- **`experimentalDecorators: true`** and **`emitDecoratorMetadata: true`** — required by `routing-controllers`, `TypeORM`, and `class-validator`
|
|
- Spec files (`**/*.spec.ts`) are excluded from compilation
|
|
- Source root: `src/`, output: `dist/`
|
|
|
|
---
|
|
|
|
## Code Style Guidelines
|
|
|
|
### No Linter / Formatter Configured
|
|
|
|
There is no ESLint or Prettier configuration. Follow the patterns already established in the codebase rather than introducing new tooling.
|
|
|
|
### Imports
|
|
|
|
- Use named imports for decorator packages: `import { Get, JsonController, Param } from 'routing-controllers'`
|
|
- Use named imports for TypeORM: `import { Column, Entity, getConnectionManager } from 'typeorm'`
|
|
- Use named imports for class-validator: `import { IsInt, IsOptional, IsString } from 'class-validator'`
|
|
- Use `import * as X from 'module'` for modules without clean default exports (e.g., `import * as jwt from 'jsonwebtoken'`)
|
|
- Use default imports for simple modules (e.g., `import cookie from 'cookie'`)
|
|
- `reflect-metadata` is imported once at the top of `src/app.ts` — do not re-import it
|
|
- No barrel/index re-export files; import source files directly by path
|
|
|
|
### Naming Conventions
|
|
|
|
| Construct | Convention | Example |
|
|
|---|---|---|
|
|
| Classes | `PascalCase` | `RunnerController`, `CreateRunner` |
|
|
| Files | `PascalCase.ts` matching class name | `RunnerController.ts` |
|
|
| Local variables | `camelCase` (some `snake_case` in tests) | `accessToken`, `access_token` |
|
|
| DB entity fields | `snake_case` preferred | `created_at`, `updated_at` |
|
|
| Controller methods | REST-conventional | `getAll`, `getOne`, `post`, `put`, `remove` |
|
|
| Custom errors | `{Entity}{Issue}Error` | `RunnerNotFoundError`, `RunnerIdsNotMatchingError` |
|
|
| Response DTOs | `Response{Entity}` | `ResponseRunner`, `ResponseAuth` |
|
|
| Create DTOs | `Create{Entity}` | `CreateRunner` |
|
|
| Update DTOs | `Update{Entity}` | `UpdateRunner` |
|
|
| Enums | `PascalCase` | `ResponseObjectType`, `PermissionAction` |
|
|
|
|
### Formatting
|
|
|
|
- 4-space indentation (observed throughout the codebase)
|
|
- Single quotes for string literals in most files
|
|
- No trailing semicolons style inconsistency — follow what's already in the file you're editing
|
|
|
|
### Types
|
|
|
|
- Add TypeScript types to all function parameters and return values
|
|
- Use `class-validator` decorators (`@IsString`, `@IsInt`, `@IsOptional`, `@IsUUID`, etc.) on every DTO and response class field — these drive both runtime validation and OpenAPI schema generation
|
|
- Use abstract classes for shared entity base types (e.g., `abstract class Participant`)
|
|
- Use interfaces for response contracts (e.g., `interface IResponse`)
|
|
- Use enums for typed string/number constants
|
|
- Avoid `any` where possible; when unavoidable, keep it localised
|
|
- `strict` is off — but still annotate types explicitly rather than relying on inference
|
|
|
|
### Controller Pattern
|
|
|
|
```typescript
|
|
import { Authorized, Body, Delete, Get, JsonController, Param, Post, Put } from 'routing-controllers';
|
|
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
|
|
|
@JsonController('/runners')
|
|
@Authorized()
|
|
export class RunnerController {
|
|
@Get('/')
|
|
@OpenAPI({ description: 'Returns all runners' })
|
|
@ResponseSchema(ResponseRunner, { isArray: true })
|
|
async getAll() { ... }
|
|
|
|
@Get('/:id')
|
|
@ResponseSchema(ResponseRunner)
|
|
async getOne(@Param('id') id: number) { ... }
|
|
|
|
@Post('/')
|
|
@ResponseSchema(ResponseRunner)
|
|
async post(@Body({ validate: true }) createRunner: CreateRunner) { ... }
|
|
|
|
@Put('/:id')
|
|
@ResponseSchema(ResponseRunner)
|
|
async put(@Param('id') id: number, @Body({ validate: true }) updateRunner: UpdateRunner) { ... }
|
|
|
|
@Delete('/:id')
|
|
@ResponseSchema(ResponseRunner)
|
|
async remove(@Param('id') id: number) { ... }
|
|
}
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
- Define custom error classes in `src/errors/` extending `routing-controllers` error types (`NotFoundError`, `NotAcceptableError`, etc.)
|
|
- Every custom error class must include `@IsString()` decorated `name` and `message` fields for OpenAPI schema generation
|
|
- Throw custom errors directly in controllers: `throw new RunnerNotFoundError()`
|
|
- Use try/catch in controllers and re-throw meaningful errors; do not swallow errors silently
|
|
- The global `ErrorHandler` middleware (registered in `src/middlewares/`) catches all unhandled errors and serialises them as JSON — do not duplicate this logic in controllers
|
|
- Auth errors are thrown from `src/middlewares/authchecker.ts`, not from individual controllers
|
|
|
|
### Entity Pattern (TypeORM)
|
|
|
|
- Entities live in `src/models/entities/`
|
|
- Decorate every entity with `@Entity()` and every column with the appropriate `@Column`, `@PrimaryGeneratedColumn`, etc.
|
|
- Use `@CreateDateColumn()` / `@UpdateDateColumn()` for timestamp fields
|
|
- Use table inheritance (`@TableInheritance` + `@ChildEntity`) for polymorphic entities
|
|
- Access repositories via `getConnectionManager().get().getRepository(EntityClass)` — do not inject repositories as constructor dependencies
|
|
- Database schema is synchronised automatically on startup (`connection.synchronize()`) — no manual migration files
|
|
|
|
### DTO Pattern (Create / Update)
|
|
|
|
- Create DTOs in `src/models/actions/create/` and `src/models/actions/update/`
|
|
- Use `class-validator` decorators for every field
|
|
- `@IsOptional()` for fields that are not required on update; all fields on create DTOs should be mandatory unless explicitly optional in the API contract
|
|
- Response DTOs live in `src/models/responses/` and follow the `Response{Entity}` naming pattern
|
|
|
|
---
|
|
|
|
## Test Style Guidelines
|
|
|
|
> **IMPORTANT: Do not run existing tests and do not create new tests.** The existing test suite in `src/tests/` is outdated and no longer reflects the current state of the codebase. Ignore all test files when working in this repository. Do not write new tests for any changes or additions.
|
|
|
|
All tests are integration tests in `src/tests/` organised by domain entity:
|
|
|
|
```
|
|
src/tests/
|
|
auth/
|
|
auth_login.spec.ts
|
|
auth_refresh.spec.ts
|
|
runners/
|
|
runner_add.spec.ts
|
|
runner_get.spec.ts
|
|
runner_update.spec.ts
|
|
runner_delete.spec.ts
|
|
...
|
|
```
|
|
|
|
### Test File Template
|
|
|
|
```typescript
|
|
import axios from 'axios';
|
|
import { config } from '../../config';
|
|
const base = "http://localhost:" + config.internal_port;
|
|
|
|
let access_token: string;
|
|
let axios_config: object;
|
|
|
|
beforeAll(async () => {
|
|
jest.setTimeout(20000);
|
|
const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" });
|
|
access_token = res.data["access_token"];
|
|
axios_config = {
|
|
headers: { "authorization": "Bearer " + access_token },
|
|
validateStatus: undefined // prevents axios from throwing on non-2xx responses
|
|
};
|
|
});
|
|
|
|
describe('POST /api/runners working', () => {
|
|
it('creating a runner with required params should return 200', async () => {
|
|
const res = await axios.post(base + '/api/runners', { ... }, axios_config);
|
|
expect(res.status).toEqual(200);
|
|
expect(res.headers['content-type']).toContain("application/json");
|
|
});
|
|
});
|
|
|
|
describe('POST /api/runners failing', () => {
|
|
it('creating a runner without required params should return 400', async () => {
|
|
const res = await axios.post(base + '/api/runners', {}, axios_config);
|
|
expect(res.status).toEqual(400);
|
|
});
|
|
});
|
|
```
|
|
|
|
- Always set `validateStatus: undefined` in `axios_config` to prevent axios throwing on error responses
|
|
- Group tests by HTTP verb + route in `describe()` blocks; separate "working" and "failing" cases
|
|
- Use `jest.setTimeout(20000)` in `beforeAll` for slow integration tests
|
|
- Assert both `res.status` and `res.headers['content-type']` on success paths
|
|
|
|
---
|
|
|
|
## Environment Configuration
|
|
|
|
- Copy `.env.example` to `.env` and fill in values before running locally
|
|
- Database type is set via `DB_TYPE` env var (`sqlite`, `postgres`, or `mysql`)
|
|
- Server port is set via `INTERNAL_PORT` (accessed as `config.internal_port` in code)
|
|
- All config values are validated at startup in `src/config.ts`
|
|
- CI env is generated by `bun run test:ci:generate_env` (`scripts/create_testenv.ts`)
|
|
|
|
### NATS Configuration
|
|
|
|
The backend uses **NATS JetStream** as a KV cache for scan intake performance optimization.
|
|
|
|
- `NATS_URL` — connection URL for NATS server (default: `nats://localhost:4222`)
|
|
- `NATS_PREWARM` — if `true`, preloads all runner state into the KV cache at startup to eliminate DB reads from the first scan onward (default: `false`)
|
|
|
|
**KV buckets** (auto-created by `NatsClient` at startup):
|
|
- `station_state` — station token cache (1-hour TTL)
|
|
- `card_state` — card→runner mapping cache (1-hour TTL)
|
|
- `runner_state` — runner display name, total distance, latest scan timestamp (no TTL, CAS-based updates)
|
|
|
|
**Development**: NATS runs in Docker via `docker-compose.yml` (port 4222). The JetStream volume is persisted to `./nats-data/` to survive container restarts.
|
|
|
|
**Station intake hot path**: `POST /api/scans/trackscans` from scan stations uses a KV-first flow that eliminates DB reads on cache hits and prevents race conditions via compare-and-swap (CAS) updates. See `SCAN_NATS_PLAN.md` for full architecture details.
|