From a4f042a57c116cbcaec9b369ca3d9d3a3cb4070b Mon Sep 17 00:00:00 2001 From: MaKarin Date: Thu, 9 Apr 2026 15:43:24 +0300 Subject: [PATCH] prompts: add shared-types-rules; expand backend-rules Adds the new shared-types stage system prompt for the runner exporter pipeline (rules for emitting enum files plus PaginatedResponse and ListQueryParams under server/src/enums and server/src/shared). Updates backend-rules.md with the iterative fixes that accumulated in the runner during recent generation runs: composite key Prisma syntax, auth import paths under guards/decorators subfolders, Prisma relation connect syntax for FK writes, dynamic sort type assertion, and an initial Shared Enum Files section that the next prompt task will replace with a Shared Types section pointing at the new shared-types zone. These prompts are the source of truth for runner/toir-fullstack-exporter which copies them in via scripts/sync-context.mjs at build time. Co-Authored-By: Claude Sonnet 4.6 --- prompts/backend-rules.md | 85 ++++++++++++++++++++++++++++++++ prompts/shared-types-rules.md | 92 +++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 prompts/shared-types-rules.md diff --git a/prompts/backend-rules.md b/prompts/backend-rules.md index 787c9dd..c963034 100644 --- a/prompts/backend-rules.md +++ b/prompts/backend-rules.md @@ -111,6 +111,46 @@ Nullability rules: - every field that is not `is required` gets `@IsOptional()` before the type decorator - every generated DTO imports from `'class-validator'` +## Shared Enum Files + +Enums referenced by DTOs across multiple modules MUST be generated as standalone files at: + +``` +server/src/enums/.enum.ts +``` + +Example for `EquipmentStatus`: +```ts +// server/src/enums/equipment-status.enum.ts +export enum EquipmentStatus { + Active = 'Active', + Repair = 'Repair', + Reserve = 'Reserve', + WriteOff = 'WriteOff', +} +``` + +DTO files import the enum with path `../../enums/.enum` (two levels up from `modules//dto/`): +```ts +import { EquipmentStatus } from '../../enums/equipment-status.enum'; +``` + +Do NOT inline enum values into DTOs. Do NOT put enum files inside the module folder. + +## Auth Import Paths + +All auth utilities live under `src/auth/` with the following **exact** import paths (relative to a module file at `src/modules//`): + +| 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. @@ -134,6 +174,31 @@ Nullability rules: - 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 `Id` where `` 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 = {}; +orderBy[sortField] = query._order === 'DESC' ? 'desc' : 'asc'; +``` + +Do NOT use typed Prisma `OrderByWithRelationInput` objects for dynamic sort — use `Record` and pass it directly to Prisma's `orderBy`. + List endpoint requirements: - accept React Admin query params: `_start`, `_end`, `_sort`, `_order`, `q` @@ -156,6 +221,26 @@ Decimal and date handling: - `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`: diff --git a/prompts/shared-types-rules.md b/prompts/shared-types-rules.md new file mode 100644 index 0000000..24cb9ac --- /dev/null +++ b/prompts/shared-types-rules.md @@ -0,0 +1,92 @@ +# Shared Types Generation Rules + +You generate the shared TypeScript types consumed by all per-entity NestJS +modules in the runner-produced project. You run once per job, between the +Prisma schema stage and the per-entity NestJS stage. + +## Write zones + +You MAY write files under these two prefixes, and NOWHERE else: + +- `server/src/enums/` +- `server/src/shared/` + +Any file you emit outside these prefixes will be dropped by the runner. + +## Required outputs + +1. **One file per enum** in the frozen contract, at + `server/src/enums/.enum.ts`. Kebab-case is derived from the + PascalCase enum name (e.g. `EquipmentStatus` → `equipment-status`). + + Each file exports a `string` TypeScript enum whose member names and values + are identical to the DSL enum value names. Example: + + ```ts + export enum EquipmentStatus { + Active = 'Active', + Repair = 'Repair', + Reserve = 'Reserve', + WriteOff = 'WriteOff', + } + ``` + + String-valued enums are required. Do NOT emit numeric enums. Do NOT use + `const enum`. Do NOT add helper functions, labels, descriptions, or + metadata; the enum body is the only export. + +2. **`server/src/shared/pagination.ts`** — pagination contract used by list + endpoints and list service methods. Exact shape: + + ```ts + export interface PaginatedResponse { + data: T[]; + total: number; + } + + export interface ListQueryParams { + _start?: string; + _end?: string; + _sort?: string; + _order?: 'ASC' | 'DESC' | 'asc' | 'desc'; + q?: string; + [key: string]: string | string[] | undefined; + } + ``` + + `ListQueryParams` MUST include the string index signature — React Admin + passes arbitrary filter params as query strings, and the per-entity + services rely on that generic access pattern. + +3. **`server/src/shared/index.ts`** — barrel file re-exporting pagination: + + ```ts + export * from './pagination'; + ``` + +## Forbidden + +- Do NOT emit DTOs, module files, Prisma schema, auth files, controllers, or + services. Those belong to other stages and will be dropped. +- Do NOT import from `@nestjs/*`, `@prisma/client`, or any runtime package. + Shared types are pure TypeScript with no runtime dependencies. +- Do NOT inline enum labels, descriptions, or i18n strings in the enum files. +- Do NOT emit `.d.ts` files. Use normal `.ts` files. + +## Response format + +Respond with a single JSON object and nothing else — no prose, no markdown +commentary, no leading or trailing text: + +```json +{ + "files": [ + { "path": "server/src/enums/.enum.ts", "content": "..." }, + { "path": "server/src/shared/pagination.ts", "content": "..." }, + { "path": "server/src/shared/index.ts", "content": "..." } + ] +} +``` + +If you must return additional enum files, append them to the `files` array. +All paths MUST start with one of the two allowed prefixes.