268 lines
11 KiB
Markdown
268 lines
11 KiB
Markdown
# Backend Rules
|
|
|
|
Use this document during the **E. Parallel Specialized Generation** stage
|
|
defined in `prompts/general-prompt.md`.
|
|
|
|
## Purpose
|
|
|
|
Generate NestJS CRUD artifacts that match the DSL contract exactly and remain compatible with a standard NestJS workspace.
|
|
|
|
Ownership rule:
|
|
|
|
- this stage belongs to `generator_nest_resources` after contract freeze
|
|
- parent retains ownership of shared auth strategy, JWT/JWKS design, global backend infra, runtime, and deploy seams
|
|
- backend resource generation may attach already-defined auth platform seams where required by the frozen contract, but must not redesign them
|
|
|
|
## Mandatory Inputs
|
|
|
|
- `prompts/general-prompt.md`
|
|
- the active `api API.<Entity>` block from `domain/toir.api.dsl`
|
|
- referenced DTOs and enums from `domain/toir.api.dsl`
|
|
- an intact or repaired official NestJS scaffold under `server/`
|
|
|
|
`api-summary.json` may be consulted only as an auxiliary inventory or validator-related artifact. It must never replace the DSL as the backend source of truth.
|
|
|
|
## Expected Outputs
|
|
|
|
Per entity:
|
|
|
|
- `server/src/modules/<kebab>/<kebab>.module.ts`
|
|
- `server/src/modules/<kebab>/<kebab>.controller.ts`
|
|
- `server/src/modules/<kebab>/<kebab>.service.ts`
|
|
- `server/src/modules/<kebab>/dto/create-<kebab>.dto.ts`
|
|
- `server/src/modules/<kebab>/dto/update-<kebab>.dto.ts`
|
|
|
|
Repository-wide:
|
|
|
|
- `server/src/app.module.ts` registrations
|
|
|
|
## Scaffold Baseline
|
|
|
|
- Start backend initialization and repair from the official NestJS CLI workspace, not from hand-written files.
|
|
- Preserve Nest workspace essentials:
|
|
- `server/tsconfig.json`
|
|
- `server/tsconfig.build.json`
|
|
- `server/nest-cli.json`
|
|
- `server/src/main.ts`
|
|
- `server/src/app.module.ts`
|
|
- If the workspace is degraded, repair it before generating domain code.
|
|
|
|
Forbidden patterns:
|
|
|
|
- hand-written pseudo-Nest scaffolds
|
|
- deleting required Nest config files after generation
|
|
- replacing normal Nest build/start behavior with ad hoc scripts
|
|
|
|
## Approved Backend Dependency Baseline
|
|
|
|
When the backend workspace is created or repaired, pin the backend package manifest to these exact versions before continuing:
|
|
|
|
- `@nestjs/common`: `11.1.18`
|
|
- `@nestjs/core`: `11.1.18`
|
|
- `@nestjs/platform-express`: `11.1.18`
|
|
- `@nestjs/testing`: `11.1.18`
|
|
- `@nestjs/config`: `4.0.3`
|
|
- `@nestjs/cli`: `11.0.17`
|
|
- `@nestjs/schematics`: `11.0.10`
|
|
- `class-validator`: `0.15.1`
|
|
- `class-transformer`: `0.5.1`
|
|
- `jose`: `6.2.2`
|
|
- `reflect-metadata`: `0.2.2`
|
|
- `rxjs`: `7.8.1`
|
|
- backend `typescript`: `5.7.3`
|
|
|
|
Pinning rules:
|
|
|
|
- Use exact versions, not `latest` and not caret ranges.
|
|
- Keep the Nest runtime packages on the same approved major/minor line.
|
|
- Prisma-specific versions are governed by `prompts/prisma-rules.md`.
|
|
|
|
## Route And Resource Contract
|
|
|
|
- Use the shared entity-to-resource naming convention from `prompts/general-prompt.md`.
|
|
- Each entity becomes a NestJS module, controller, service, and create/update DTO pair.
|
|
- CRUD routes use the real primary key name in the path.
|
|
- Every API record returned to React Admin must include `id`.
|
|
- For natural-key entities, map the real primary key to `id` in responses and sort translation.
|
|
|
|
## DTO Contract
|
|
|
|
- `DTO.<Entity>Create` defines `Create<Entity>Dto`.
|
|
- `DTO.<Entity>Update` defines `Update<Entity>Dto`.
|
|
- Do not invent fields or pull field lists from memory.
|
|
- Never include `id` in Create/Update DTOs.
|
|
|
|
Type and decorator rules:
|
|
|
|
| DSL type | TS DTO type | class-validator decorator | Notes |
|
|
| --------- | ----------- | ------------------------- | ----------------------------- |
|
|
| `uuid` | `string` | `@IsUUID()` | |
|
|
| `string` | `string` | `@IsString()` | |
|
|
| `text` | `string` | `@IsString()` | |
|
|
| `integer` | `number` | `@IsInt()` | |
|
|
| `number` | `number` | `@IsNumber()` | |
|
|
| `decimal` | `string` | `@IsString()` | serialize with Prisma Decimal |
|
|
| `date` | `string` | `@IsString()` | serialize as ISO string |
|
|
| `boolean` | `boolean` | `@IsBoolean()` | |
|
|
| enum name | `EnumName` (imported from `../../enums/<kebab>.enum`) | `@IsEnum(EnumName)` | Do not use `string` |
|
|
|
|
Nullability rules:
|
|
|
|
- every field that is not `is required` gets `@IsOptional()` before the type decorator
|
|
- every generated DTO imports from `'class-validator'`
|
|
|
|
## Shared Types
|
|
|
|
Shared TypeScript types are generated by the `shared-types` stage BEFORE
|
|
`nest-entities` runs. Per-entity modules MUST import them; they must NEVER
|
|
redefine them.
|
|
|
|
Zones:
|
|
|
|
- `server/src/enums/<kebab-enum-name>.enum.ts` — one file per DSL enum
|
|
- `server/src/shared/pagination.ts` — `PaginatedResponse<T>` and `ListQueryParams`
|
|
- `server/src/shared/index.ts` — barrel for shared types
|
|
|
|
Import conventions depend on the importing file's depth:
|
|
|
|
- From a module/controller/service file at `server/src/modules/<kebab>/<file>.ts`:
|
|
```ts
|
|
import { EquipmentStatus } from '../../enums/equipment-status.enum';
|
|
import { PaginatedResponse, ListQueryParams } from '../../shared/pagination';
|
|
```
|
|
|
|
- From a DTO file at `server/src/modules/<kebab>/dto/<file>.ts` (one level deeper):
|
|
```ts
|
|
import { EquipmentStatus } from '../../../enums/equipment-status.enum';
|
|
import { PaginatedResponse, ListQueryParams } from '../../../shared/pagination';
|
|
```
|
|
|
|
Enum-typed DTO fields MUST be declared with the actual enum type, NOT
|
|
`string`. The enum is imported from the shared file above. `@IsEnum(EnumName)`
|
|
still applies at runtime for validation. Because the DTO type matches the
|
|
Prisma-generated enum type by name and by string values, service code can
|
|
pass DTOs into Prisma without `as EnumName` casts.
|
|
|
|
## Auth Import Paths
|
|
|
|
All auth utilities live under `src/auth/` with the following **exact** import paths (relative to a module file at `src/modules/<kebab>/`):
|
|
|
|
| Symbol | Import path |
|
|
|--------|-------------|
|
|
| `JwtAuthGuard` | `../../auth/guards/jwt-auth.guard` |
|
|
| `RolesGuard` | `../../auth/guards/roles.guard` |
|
|
| `Roles` | `../../auth/decorators/roles.decorator` |
|
|
| `Public` | `../../auth/decorators/public.decorator` |
|
|
| `PrismaService` | `../../prisma/prisma.service` |
|
|
|
|
Do NOT use `../../auth/jwt-auth.guard`, `../../auth/roles.guard`, or `../../auth/roles.decorator` — those paths do not exist.
|
|
|
|
## Controller Contract
|
|
|
|
- Apply `@UseGuards(JwtAuthGuard, RolesGuard)` at controller class level.
|
|
- Guard order: JwtAuthGuard must run before RolesGuard to ensure token validation precedes role extraction.
|
|
- Roles per verb:
|
|
- `GET` -> `viewer | editor | admin`
|
|
- `POST`, `PATCH`, `PUT` -> `editor | admin`
|
|
- `DELETE` -> `admin`
|
|
- Reconcile DSL HTTP shapes for repository compatibility:
|
|
- list endpoints declared as `POST .../page` generate as `@Get()` with React Admin query params
|
|
- update endpoints declared as `PUT` generate as `@Patch(':<pk>')`
|
|
- Path parameters are taken from the DSL endpoint contract, not invented from generic CRUD memory.
|
|
|
|
## Service Contract
|
|
|
|
- Never pass raw update DTOs directly into Prisma update `data`.
|
|
- Strip `id`, the real primary key, and readonly fields before writes.
|
|
- Keep `PrismaService` lightweight:
|
|
- extend `PrismaClient`
|
|
- implement `OnModuleInit`
|
|
- call `$connect()`
|
|
- do not add `beforeExit`
|
|
|
|
### Prisma Relation Writes
|
|
|
|
When a DTO field is a foreign key (e.g. `equipmentId: string`), **never** assign it directly on the Prisma data object. Instead, use the `connect` syntax targeting the relation field name (without the `Id` suffix):
|
|
|
|
```ts
|
|
// WRONG — Prisma rejects scalar FK assignments on relation inputs
|
|
data.equipmentId = dto.equipmentId;
|
|
|
|
// CORRECT — use connect on the relation field
|
|
data.equipment = { connect: { id: dto.equipmentId } };
|
|
```
|
|
|
|
Rule: for any DTO field named `<relation>Id` where `<relation>` is a Prisma relation field, map it as `{ connect: { id: value } }` on the relation name in both create and update payloads.
|
|
|
|
### Dynamic Sort Type Assertion
|
|
|
|
When building `orderBy` from a runtime sort field, TypeScript requires an explicit cast to avoid `TS7053`. Always use:
|
|
|
|
```ts
|
|
const orderBy: Record<string, 'asc' | 'desc'> = {};
|
|
orderBy[sortField] = query._order === 'DESC' ? 'desc' : 'asc';
|
|
```
|
|
|
|
Do NOT use typed Prisma `OrderByWithRelationInput` objects for dynamic sort — use `Record<string, 'asc' | 'desc'>` and pass it directly to Prisma's `orderBy`.
|
|
|
|
List endpoint requirements:
|
|
|
|
- accept React Admin query params: `_start`, `_end`, `_sort`, `_order`, `q`
|
|
- set `Content-Range: items <start>-<end>/<total>` header (RFC 7233 format for items, not bytes)
|
|
- set `Access-Control-Expose-Headers: Content-Range`
|
|
- return HTTP 200 for successful pagination
|
|
|
|
Filtering rules:
|
|
|
|
- string/text filters may use case-insensitive `contains`
|
|
- foreign-key scalar filters must use exact-match semantics
|
|
- enum filters must support both single and repeated params
|
|
- repeated enum params must map to Prisma `{ in: [...] }`
|
|
- `_sort=id` must map to the real primary key for natural-key entities
|
|
|
|
Decimal and date handling:
|
|
|
|
- `decimal` writes: `new Prisma.Decimal(value)`
|
|
- `decimal` reads: `.toString()`
|
|
- `date` writes: `new Date(value)`
|
|
- `date` reads: `.toISOString()`
|
|
|
|
## Composite Key Rules
|
|
|
|
When an entity's API endpoints use multiple path parameters (e.g. `GET /entity/{fieldA}/{fieldB}`), the entity has a **composite primary key** — there is NO auto-generated `id` field.
|
|
|
|
In this case:
|
|
- The Prisma model uses `@@id([fieldA, fieldB])` — never `id String @id @default(uuid())`
|
|
- Prisma's `WhereUniqueInput` accepts `{ fieldA_fieldB: { fieldA, fieldB } }` (underscore-joined)
|
|
- The NestJS service uses the composite key object, NOT `{ id }`:
|
|
|
|
```ts
|
|
// WRONG — id does not exist on composite-key entities
|
|
where: { id }
|
|
|
|
// CORRECT — use the Prisma compound unique input
|
|
where: { equipmentId_newStatus: { equipmentId, newStatus } }
|
|
```
|
|
|
|
- Controller route params extract all key fields, not a single `id`
|
|
- Responses include all key fields; React Admin requires a virtual `id` field — map it as a concatenation: `id: \`${record.equipmentId}_${record.newStatus}\``
|
|
|
|
## Natural-Key Rules
|
|
|
|
For entities whose physical primary key is not `id`:
|
|
|
|
- route params use the real primary key name
|
|
- responses expose `id` mapped from that primary key
|
|
- sort/update behavior never targets a fake physical `id`
|
|
- update payload sanitization removes both `id` and the real primary key
|
|
|
|
## Completion Expectations
|
|
|
|
Backend generation is incomplete if any of the following is true:
|
|
|
|
- required Nest scaffold files are missing
|
|
- DTO decorators are incomplete or type-incorrect
|
|
- controllers are missing guards or role decorators
|
|
- natural-key handling regresses to a fake physical `id`
|
|
- list/filter behavior is incompatible with React Admin expectations
|