# 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.
