docs: Added agents file to support ai assisted coding
This commit is contained in:
282
AGENTS.md
Normal file
282
AGENTS.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user