chore: harden generation context baseline

This commit is contained in:
MaKarin
2026-03-22 18:34:22 +03:00
parent 7e6b76cef2
commit d1ea297dfc
40 changed files with 2038 additions and 3517 deletions

View File

@@ -1,3 +1,64 @@
This directory defines the AI generation context.
This repository keeps the current LLM-first CRUD generation architecture as the primary working baseline.
All code generation must follow the rules described in these documents.
It is not a new generator engine and it is not a compiler platform. The repository remains:
- an AI generation context
- an active generated and maintained fullstack CRUD project
- `server/` as the active backend target output path
- `client/` as the active frontend target output path
- an LLM-first orchestration baseline with CLI-first framework bootstrap
- a compact rule set that strengthens the existing pipeline with:
- `domain-summary.json`
- a physical root-level realm artifact
- a lightweight automated validation gate
## Active knowledge blocks
The active prompt corpus is intentionally normalized to six stable blocks:
1. [prompts/general-prompt.md](prompts/general-prompt.md)
2. [prompts/auth-rules.md](prompts/auth-rules.md)
3. [prompts/backend-rules.md](prompts/backend-rules.md)
4. [prompts/frontend-rules.md](prompts/frontend-rules.md)
5. [prompts/runtime-rules.md](prompts/runtime-rules.md)
6. [prompts/validation-rules.md](prompts/validation-rules.md)
## Baseline contracts
- `domain/*.dsl` is the source of truth for the domain model.
- [domain-summary.json](domain-summary.json) is a derived artifact for LLM stabilization and validation.
- [toir-realm.json](toir-realm.json) is the physical Keycloak bootstrap artifact baseline.
- `server/` and `client/` are the active target output paths for this repository.
- `server/` must remain a valid NestJS workspace baseline.
- `client/` must remain a valid Vite React TypeScript workspace baseline.
## Scaffold baseline
- Generation remains LLM-first for orchestration and domain-derived feature code.
- Framework bootstrap is CLI-first:
- backend baseline starts from official Nest CLI conventions
- frontend baseline starts from official Vite React TypeScript conventions
- If `server/` or `client/` drift away from a valid workspace, repair the workspace baseline before generating more feature code.
- Do not replace the framework workspace with a hand-written minimal skeleton.
## Anti-regression contract
- The active prompts define forbidden generation patterns, required invariants, and recovery rules for future agents.
- Buildability is part of the baseline contract, not an optional follow-up.
- Validation targets `domain/*.dsl` as reusable source inputs, while TOiR names remain project defaults/examples.
## Repository layout
- [docs/repository-structure.md](docs/repository-structure.md) explains the normalized folder structure.
- Active prompts live in `prompts/`.
- Helper scripts live in `tools/`.
## Commands
```bash
npm run generate:domain-summary
npm run validate:generation
npm run validate:generation:runtime
```
`npm run validate:generation` now checks both contract shape and workspace validity. When dependencies are installed, it also verifies `npm run build` in `server/` and `client/`. If dependencies are missing, it reports build verification as skipped instead of pretending the baseline is fully green.

View File

@@ -1,120 +0,0 @@
# Backend Auth Rules
This document defines mandatory backend authentication and authorization behavior for generated applications.
---
# Generated Backend Auth Surface
The generator must create explicit auth infrastructure in the generated NestJS backend.
At minimum generate:
- `server/src/auth/auth.module.ts`
- JWT auth guard
- roles guard
- `@Public()`
- `@Roles()`
- typed authenticated principal interface
- typed environment validation in `server/src/config/`
The generator must not describe backend auth as an external manual integration step.
---
# Standards-Based JWT Verification
The generated backend must validate JWTs against Keycloak using standards-based libraries.
Rules:
1. Verify **issuer**
2. Verify **audience**
3. Verify signature via **JWKS**
4. Do **not** use deprecated Keycloak-specific Node adapters such as `keycloak-connect`
The default library rule for this repository is:
- **`jose`** for JWT and JWKS verification
---
# JWT Verification Contract
The generated backend must verify tokens with:
- `KEYCLOAK_ISSUER_URL`
- `KEYCLOAK_AUDIENCE`
JWKS resolution priority must be exactly:
1. explicit `KEYCLOAK_JWKS_URL`
2. OIDC discovery
3. fallback `${issuer}/protocol/openid-connect/certs`
The generator must encode this priority explicitly.
---
# Role Extraction
Authorization roles must be extracted **only** from:
- `realm_access.roles`
The generator must not mix in:
- resource roles
- custom frontend-only permissions
- undocumented claim fallbacks
`realm_access.roles` is the single RBAC source for this repository.
---
# Default RBAC Policy
Apply these RBAC defaults to generated CRUD controllers:
- `GET`: `viewer`, `editor`, `admin`
- `POST`: `editor`, `admin`
- `PATCH`: `editor`, `admin`
- `PUT`: `editor`, `admin`
- `DELETE`: `admin`
`GET /health` must remain public and must use the generated `@Public()` mechanism.
All other generated CRUD routes must be protected by default.
---
# Typed Principal
The generated backend must attach a typed authenticated principal to the request context.
At minimum, the generated principal type must be able to represent:
- `sub`
- user identity fields when present
- `roles`
- raw claims payload
This principal type is required so guards, controllers, and tests share one consistent contract.
---
# Backend Environment Contract
The generated backend env contract must include:
- `PORT`
- `DATABASE_URL`
- `CORS_ALLOWED_ORIGINS`
- `KEYCLOAK_ISSUER_URL`
- `KEYCLOAK_AUDIENCE`
- `KEYCLOAK_JWKS_URL` (optional)
The generated backend config must **fail fast** if required auth variables are missing.
The generated backend must not silently fall back to production auth settings in code.

View File

@@ -1,156 +0,0 @@
# Frontend Auth Rules
This document defines mandatory frontend authentication behavior for generated applications.
---
# Generated Files
The generator must create at minimum:
- `client/src/config/env.ts`
- `client/src/auth/keycloak.ts`
- `client/src/auth/authProvider.ts`
- `client/src/App.tsx`
- `client/src/main.tsx`
- `client/src/dataProvider.ts`
- `client/.env.example`
`App.tsx`, `main.tsx`, and `dataProvider.ts` must be generated in an auth-aware form. Auth must not be bolted on later.
---
# Login Model
The generated frontend must use:
- **Keycloak JS**
- **Authorization Code Flow + PKCE**
- **PKCE method `S256`**
- **redirect-based login only**
The generator must **not** create a custom in-app username/password login form.
The generated SPA must initialize Keycloak **before rendering** the application. The app must not operate anonymously once auth is enabled.
---
# React Admin Integration
The generated frontend must use a React Admin **`authProvider`** and connect it at the `Admin` root.
Rules:
1. `authProvider` is mandatory.
2. The generated `Admin` root must enforce authenticated operation.
3. Auth bootstrap must happen before rendering `Admin`.
The generated frontend must not rely on anonymous access with later lazy auth attachment.
---
# Identity Resolution
The generated `authProvider.getIdentity()` must derive identity from token claims already present in the parsed access token / parsed token.
Preferred claims:
- `sub`
- `preferred_username`
- `email`
- `name`
Rules:
1. `getIdentity()` must be token-claim based by default.
2. The generated frontend must **not** call `keycloak.loadUserProfile()` during normal app startup or baseline identity resolution.
3. The generated frontend must **not** depend on the Keycloak `/account` endpoint for baseline CRUD/admin generation.
4. The default generator strategy is to avoid the `/account` request entirely, not to broaden Keycloak CORS behavior.
5. Any network-based account-profile integration requires an explicit future prompt.
The generator must not introduce startup/profile-fetch requests that are unnecessary for authorization.
---
# Shared Request Seam
The generated frontend must use the shared request seam in `client/src/dataProvider.ts` as the single place where access tokens are attached.
Rules:
1. All backend requests must carry `Authorization: Bearer <access_token>`.
2. This must cover all React Admin calls, including:
- list
- get one
- get many
- get many reference
- create
- update
- delete
3. Reference/resource lookups must flow through the same authenticated request seam.
The generator must not scatter token attachment across resource components.
---
# Error Semantics
The generated `authProvider.checkError` must distinguish authentication failures from authorization failures:
- `401` -> force logout / re-authentication
- `403` -> do **not** re-authenticate; surface access denied / permission error to React Admin
The generator must not treat `401` and `403` as equivalent.
---
# Token Refresh
The generated frontend must refresh tokens before protected API calls when needed.
Refresh behavior must be **concurrency-safe**:
- use one shared in-flight refresh operation
- parallel requests must wait for the same refresh promise
- do not trigger multiple parallel refresh requests for the same expiry window
The generator must explicitly describe or implement the shared in-flight refresh pattern.
---
# Browser Storage Rules
The generated frontend must **not** store access tokens or refresh tokens in:
- `localStorage`
- `sessionStorage`
In-memory handling via Keycloak JS behavior is the default rule for this repository.
---
# Frontend Environment Contract
The generated frontend env contract must include:
- `VITE_API_URL`
- `VITE_KEYCLOAK_URL`
- `VITE_KEYCLOAK_REALM`
- `VITE_KEYCLOAK_CLIENT_ID`
The generated frontend config module must **fail fast** if required auth variables are missing.
The generated frontend must not silently fall back to production auth settings in code.
---
# Default Values for Examples
`client/.env.example` must use these repository defaults as examples:
```env
VITE_API_URL=http://localhost:3000
VITE_KEYCLOAK_URL=https://sso.greact.ru
VITE_KEYCLOAK_REALM=toir
VITE_KEYCLOAK_CLIENT_ID=toir-frontend
```

View File

@@ -1,89 +0,0 @@
# Keycloak Architecture
This repository generates a fullstack CRUD application with **default Keycloak authentication and authorization**. Authentication is not an optional add-on and must not be described as a manual post-generation task.
---
# Default Auth Model
The generated application must use this runtime topology:
- **Frontend:** SPA served by Vite + React Admin
- **Backend:** NestJS API
- **Auth transport:** `Authorization: Bearer <access_token>`
- **Identity provider:** external Keycloak server
The generated application must **not** use:
- a BFF layer
- cookie/session authentication
- a custom in-app username/password login form
The generated application must use **redirect-based Keycloak login** only.
---
# Auth Is Part of Default Generation
The generator must produce:
- authenticated frontend bootstrap and request flow
- authenticated backend bootstrap and request guards
- auth-aware env examples
- auth-aware validation rules
- a root-level Keycloak realm import artifact that describes the Keycloak realm/client bootstrap
Repository defaults may use names such as `toir`, `toir-frontend`, `toir-backend`, and `toir-realm.json`, but future generations must parameterize realm name, client IDs, production URLs, and artifact filename from the generated project or explicit auth configuration.
The generated application must not require a separate prompt step such as "now add auth manually".
---
# Public and Protected Routes
The generated backend must keep:
- `GET /health` — public
All generated CRUD routes must be protected by default.
Authorization must be role-based and must use **only** Keycloak realm roles from `realm_access.roles`.
---
# Default Role Policy
Apply these RBAC defaults to generated CRUD controllers:
- `GET` list/detail: `viewer`, `editor`, `admin`
- `POST`, `PATCH`, `PUT`: `editor`, `admin`
- `DELETE`: `admin`
The frontend may use roles for display and permission awareness, but the backend remains the enforcement point.
---
# Token Contract
Generated auth context must guarantee that access tokens used by the SPA and API reliably contain:
- `sub`
- `aud`
- `realm_access.roles`
The frontend client and backend audience/client must be documented explicitly. The generator must not rely on undocumented Keycloak defaults for these claims.
---
# Runtime Boundaries
The repository's local runtime topology remains unchanged:
- `docker-compose.yml` provisions PostgreSQL only
- Keycloak remains external to this repo's Docker runtime
The generator must therefore produce:
- runtime documentation for importing/configuring the generated realm import artifact
- env contracts for frontend and backend
- validation rules that assume an external but reachable Keycloak server

View File

@@ -1,152 +0,0 @@
# Keycloak Realm Template Rules
This document defines the required Keycloak realm/bootstrap artifact that the generator must produce.
---
# Required Artifact
The generator must create a root-level Keycloak import artifact:
- project-specific realm import artifact
- repository default example filename: `toir-realm.json`
This artifact is part of the normal generated result and must be documented in the runtime bootstrap flow.
The generator must parameterize at minimum:
- realm name
- frontend SPA client ID
- backend audience/resource client ID
- production frontend URLs
- realm-artifact filename
Repository defaults may use `toir`, `toir-frontend`, `toir-backend`, `toir-realm.json`, and `https://toir-frontend.greact.ru` as examples, but those names must not be treated as universal requirements for all future generated applications.
---
# Strategy Choice
This repository uses the **robust bootstrap strategy** by default.
That means the generated realm/client setup must be:
- self-contained
- reproducible after import
- explicit about audience delivery
- explicit about role delivery
- explicit about required claims
The generator must **not** rely on built-in client scopes magically existing and attaching correctly after realm import.
---
# Required Realm Structure
The generated realm import artifact must define:
- realm name derived from generated project auth config
- realm roles:
- `admin`
- `editor`
- `viewer`
- frontend SPA client ID derived from generated project auth config
- backend audience/resource client ID derived from generated project auth config
- dedicated audience client scope for audience delivery
- repository default example: `api-audience`
The audience client scope must explicitly add the generated backend audience/client ID to SPA access tokens.
---
# Required Frontend Client Rules
The generated SPA client must be configured as a public SPA client with:
- `publicClient: true`
- `standardFlowEnabled: true`
- `implicitFlowEnabled: false`
- `directAccessGrantsEnabled: false`
- `serviceAccountsEnabled: false`
- `pkce.code.challenge.method: S256`
Default rule:
- keep `fullScopeAllowed: true`
Reason:
- this repository uses realm-role RBAC
- earlier failures came from fragile or implicit scope behavior
- the robust default is explicit audience delivery plus explicit claim mappers, not a partially documented scoped-role setup
If a future prompt chooses `fullScopeAllowed: false`, it must also fully document role scope mappings and validation rules. The generator must not switch to that strategy silently.
---
# Required Claim Delivery
The generated realm/client configuration must reliably produce access tokens containing:
- `sub`
- `aud`
- `realm_access.roles`
The generator must explicitly address claim delivery and must not leave it implicit.
The generated realm import artifact must therefore define explicit protocol mappers for:
- `sub`
- user identity claims needed by the frontend
- `realm_access.roles`
The generator must not assume these claims will appear through undeclared built-in scopes.
Post-generation validation must fail if the generated realm contract leaves `sub`, `aud`, or `realm_access.roles` implicit instead of explicitly delivered.
---
# Required Role Delivery
The generated realm/client configuration must explicitly deliver realm roles into the access token.
Rules:
1. Role delivery must be configured intentionally.
2. `realm_access.roles` must be present in the access token after import.
3. The generator must validate that realm role delivery is part of the generated realm artifact.
---
# Redirect URIs and Web Origins
The generated realm template guidance must document local and production frontend URLs derived from the generated project configuration.
Repository default production examples may include:
- `http://localhost:5173`
- `https://toir-frontend.greact.ru`
The generated SPA client must include matching redirect URIs and web origins for the generated local and production frontend URLs. These values must be explicit in the generated realm artifact or in a generated artifact template with explicit placeholders and validation instructions.
---
# Anti-Regression Rule
The earlier broken realm-template behavior must not be reproduced.
The generator context must explicitly prevent:
- missing audience in SPA access tokens
- missing `sub`
- missing `realm_access.roles`
- realm imports that depend on undeclared built-in scopes being present
- ambiguous role-delivery behavior
The generated realm/bootstrap guidance must be self-contained enough that another engineer can import the artifact and predict the access-token shape without relying on hidden Keycloak defaults.
The generator must guarantee through the generated realm artifact plus post-generation validation that imported access tokens reliably contain:
- `sub`
- `aud`
- `realm_access.roles`

View File

@@ -1,318 +0,0 @@
# Backend Architecture
Backend stack:
- Node.js
- TypeScript
- NestJS
- Prisma ORM
- PostgreSQL
- jose
The backend is generated from `domain/*.dsl`.
Each DSL entity becomes:
- Prisma model
- NestJS module
- CRUD controller
- Service
- DTO definitions
---
# Single Source of Truth
- `domain/*.dsl` is the only required input for backend generation.
- DTOs and REST API contracts must be derived from the domain model, primary keys, foreign keys, and enums defined in the domain DSL.
- Backend documentation, generation rules, and optional overrides must not duplicate entity, attribute, or relation structures outside the domain DSL.
- Deprecated multi-DSL inputs are compatibility-only artifacts and must never be treated as authoritative backend inputs or used to redefine entities, attributes, primary keys, foreign keys, relations, or enums.
---
# Project Structure
server/
package.json
prisma/
schema.prisma
src/
main.ts
app.module.ts
auth/
auth.module.ts
guards/
decorators/
interfaces/
config/
env.validation.ts
modules/
{entity}/
{entity}.module.ts
{entity}.controller.ts
{entity}.service.ts
dto/
create-{entity}.dto.ts
update-{entity}.dto.ts
{entity}.response.dto.ts
---
# Module Rules
Each entity generates exactly one NestJS module.
Example:
Entity:
Equipment
Module:
modules/
equipment/
equipment.module.ts
equipment.controller.ts
equipment.service.ts
---
# Controller Rules
Each entity controller must expose these endpoints:
- GET /{resource} — list
- GET /{resource}/:pk — get one
- POST /{resource} — create
- PATCH /{resource}/:pk — update
- DELETE /{resource}/:pk — delete
The path parameter **:pk** must use the **primary key attribute name** from the DSL, not always `:id`.
## Path parameter by primary key
| Entity | Primary key (DSL) | Path parameter | Example routes |
| ------------- | ----------------- | -------------- | -------------------------------------------------------- |
| Equipment | id | :id | GET /equipment/:id, PATCH /equipment/:id |
| EquipmentType | code | :code | GET /equipment-types/:code, PATCH /equipment-types/:code |
| RepairOrder | id | :id | GET /repair-orders/:id, PATCH /repair-orders/:id |
**Rule:** Use the actual primary key name. For example, **EquipmentType** has PK `code`, so routes must be:
- GET /equipment-types/:code
- PATCH /equipment-types/:code
- DELETE /equipment-types/:code
**Do not** use `/equipment-types/:id` when the entity primary key is `code`.
---
# Health Endpoint
Every generated backend must expose a **health endpoint** so that runtime and orchestration can verify the API is up.
- **Path:** `GET /health`
- **Response:** JSON with a status indicator (e.g. `{ "status": "ok" }`).
## Controller example
```typescript
@Public()
@Controller("health")
export class HealthController {
@Get()
getHealth() {
return { status: "ok" };
}
}
```
Register the health controller in the root app module (or a dedicated health module). No authentication required for this endpoint.
---
# Authentication and Authorization
The generated backend must include explicit auth infrastructure by default.
Generate:
- `AuthModule`
- JWT guard
- roles guard
- `@Public()`
- `@Roles()`
- typed authenticated principal
- typed env validation for auth/runtime variables
Rules:
1. `/health` must remain public.
2. Generated CRUD routes must be protected by default.
3. JWT verification must use issuer + audience + JWKS.
4. Authorization roles must be extracted only from `realm_access.roles`.
5. The generated backend must not use deprecated Keycloak-specific Node adapters.
## CRUD RBAC defaults
Apply these defaults to generated CRUD controllers:
- `GET` -> `viewer`, `editor`, `admin`
- `POST` -> `editor`, `admin`
- `PATCH` -> `editor`, `admin`
- `PUT` -> `editor`, `admin`
- `DELETE` -> `admin`
These defaults must be encoded in generated guards/decorators, not left as informal guidance.
---
# List Endpoint
List endpoint must support pagination and filters via query parameters.
Example:
GET /equipment?page=0&size=10
Response format must follow React Admin requirements:
{
"data": [],
"total": number
}
Sorting rules:
1. Generated list/query logic must use actual model field names in ORM `orderBy` clauses.
2. If an entity primary key is not literally `id` but the API exposes synthetic `id` for React Admin compatibility, incoming `_sort=id` must be mapped to the real primary key field before building the query.
3. This mapping rule applies generally to all natural-key entities, not as a one-off entity hack.
---
# Service Layer
Services implement CRUD operations using Prisma.
Example:
findAll()
findOne(id)
create(data)
update(id, data)
remove(id)
---
# DTO Rules
DTOs are generated automatically from the domain DSL and are never a separate required DSL input.
Create DTO:
- contains required fields
- does NOT contain generated primary keys
Update DTO:
- all fields optional
Response DTO:
- mirrors domain entity attributes
---
# Naming Conventions
## Entity naming
DSL entities use PascalCase. Generated backend artifacts use the same base name in lowercase for folders and file prefixes.
- **Equipment** → equipment module
- **EquipmentType** → equipment-type module (or equipment-type as path segment)
- **RepairOrder** → repair-order module
## Module naming
One entity = one module folder. Folder name = entity name in kebab-case (lowercase, hyphen-separated).
- Equipment → `modules/equipment/`
- EquipmentType → `modules/equipment-type/`
- RepairOrder → `modules/repair-order/`
## Controller naming
- File: `{entity-kebab}.controller.ts`
- Class: `EquipmentController`, `EquipmentTypeController`, `RepairOrderController`
Examples:
- `equipment.controller.ts`
- `equipment-type.controller.ts`
- `repair-order.controller.ts`
## Service naming
- File: `{entity-kebab}.service.ts`
- Class: `EquipmentService`, `EquipmentTypeService`, `RepairOrderService`
Examples:
- `equipment.service.ts`
- `equipment-type.service.ts`
- `repair-order.service.ts`
## DTO naming
- Create: `create-{entity-kebab}.dto.ts` (e.g. `create-equipment.dto.ts`, `create-repair-order.dto.ts`)
- Update: `update-{entity-kebab}.dto.ts` (e.g. `update-equipment.dto.ts`)
- Response: `{entity-kebab}.response.dto.ts` or use entity name for list/detail response types
---
# Resource Naming Rules
API resource paths are derived from the entity name:
1. **PascalCase → kebab-case:** Replace camelCase with lowercase hyphenated segments.
2. **Pluralize:** Use plural form for the resource path (list endpoint represents a collection).
| Entity (DSL) | API path (resource) |
| ------------- | ------------------- |
| Equipment | /equipment |
| EquipmentType | /equipment-types |
| RepairOrder | /repair-orders |
Rules:
- **Equipment** → `equipment` (already singular-looking; path is still `/equipment` for consistency with REST resource naming).
- **EquipmentType** → `equipment-types` (camelCase "EquipmentType" → "equipment-type", then plural → "equipment-types").
- **RepairOrder** → `repair-orders` ("RepairOrder" → "repair-order" → "repair-orders").
Standard endpoints per resource:
- GET /{resource} — list
- GET /{resource}/:pk — get one (pk = primary key name, e.g. :id or :code)
- POST /{resource} — create
- PATCH /{resource}/:pk — update
- DELETE /{resource}/:pk — delete
See **Controller Rules** above for the rule that :pk must match the entity's primary key attribute name.
---
# Environment and runtime
- **Environment variables:** Backend requires runtime and auth variables. See **backend/runtime-rules.md**.
- **.env:** Generated project must include a `.env` (and `.env.example`) with database, auth, and CORS variables so the app starts without runtime errors.
- **PrismaService:** Must follow **backend/prisma-service.md** (OnModuleInit, $connect; no beforeExit).
- **Prisma client:** Add `"postinstall": "prisma generate"` (or equivalent) to package.json so the client is generated after install.
- **Migrations:** Document or run `npx prisma migrate dev` after schema generation. See **backend/runtime-rules.md** and **generation/backend-generation.md**.

View File

@@ -1,70 +0,0 @@
# Database Runtime
The generated project must include a **PostgreSQL development database** so the application can run immediately after generation without manual database setup.
Use **Docker** to provision the database.
---
# docker-compose.yml
The generator must create a `docker-compose.yml` file at the **project root** (same level as `server/` and `client/` directories).
## Example
```yaml
version: "3.9"
services:
postgres:
image: postgres:16
container_name: toir-postgres
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: toir
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
```
---
# Rules
- **Location:** Project root (e.g. `TOiR-generation/docker-compose.yml` or monorepo root).
- **Service name:** Can be `postgres` or project-specific (e.g. `toir-postgres` as container_name).
- **Credentials and DB name** must match the `DATABASE_URL` in `server/.env`:
- User: `postgres`
- Password: `postgres`
- Database: `toir`
- Host: `localhost`
- Port: `5432`
- **Volume:** Use a named volume so data persists across container restarts.
---
# Usage
Start the database before running the backend:
```bash
docker compose up -d
```
Stop:
```bash
docker compose down
```
Without this file and a running database, the backend will fail at runtime with errors such as:
```
PrismaClientInitializationError P1001: Can't reach database server at localhost:5432
```

View File

@@ -1,166 +0,0 @@
# DSL → Prisma Mapping Rules
The domain DSL defines the database schema.
Each DSL entity becomes a Prisma model.
---
# Type Mapping (Prisma schema)
| DSL Type | Prisma Type |
|--------|-------------|
| string | String |
| uuid | String @id @default(uuid()) |
| integer | Int |
| decimal | Decimal |
| date | DateTime |
| text | String |
---
# DTO Type Mapping (API / serialization)
For DTOs and API responses, use types that serialize to JSON without errors. Prisma's `Decimal` and `DateTime` do not always serialize predictably; map them as follows in generated DTOs:
| DSL Type | Prisma Type | DTO / API type | Notes |
|----------|-------------|----------------|-------|
| string | String | string | — |
| uuid | String | string | — |
| integer | Int | number | — |
| decimal | Decimal | **string** | Avoid Prisma Decimal in DTO; use string to prevent serialization issues. |
| date | DateTime | **string** | ISO 8601 date string (e.g. `"2025-03-08T00:00:00.000Z"`). |
| text | String | string | — |
| enum | enum | string | Enum value as string. |
**Rules:**
- **decimal** → In create/update/response DTOs, use `string` (or a type that serializes to string). Convert to/from Prisma `Decimal` only in the service layer.
- **date** → In DTOs, use `string` (ISO date). Convert to/from Prisma `DateTime` in the service layer.
---
# Enum Mapping
DSL enum becomes Prisma enum.
Example:
DSL
enum EquipmentStatus {
Active
Repair
}
Prisma
enum EquipmentStatus {
Active
Repair
}
---
# Foreign Keys
DSL foreign keys become Prisma relations.
Example:
DSL
attribute equipmentTypeCode {
type string;
key foreign {
relates EquipmentType.code;
}
}
Prisma
equipmentTypeCode String
equipmentType EquipmentType @relation(fields: [equipmentTypeCode], references: [code])
---
# Primary Keys
Primary keys defined in DSL must become Prisma model identifiers.
Example:
attribute id {
type uuid;
key primary;
}
Prisma
id String @id @default(uuid())
---
# Prisma Service Rules
Use Prisma v5 compatible service. See **backend/prisma-service.md** for full specification.
- PrismaService MUST implement `OnModuleInit` and call `await this.$connect()` in `onModuleInit()`.
- PrismaService MUST NOT use the deprecated `this.$on('beforeExit', ...)` hook (not supported in Prisma 5).
---
# Prisma Lifecycle Rules
These lifecycle rules must be applied in generated projects so Prisma runtime is reliable after generation and after dependency installation.
## 1) Generate Prisma client after schema changes
Whenever `prisma/schema.prisma` is generated or updated, run:
```bash
npx prisma generate
```
## 2) postinstall lifecycle
Generated `server/package.json` must include:
```json
{
"scripts": {
"postinstall": "prisma generate"
}
}
```
This ensures Prisma client generation after `npm install`.
## 3) Migration lifecycle
Use development migration workflow:
```bash
npx prisma migrate dev
```
Run this after database startup and before starting the backend server.
## 4) Seed lifecycle
If seed is configured (see `backend/seed-rules.md`), run:
```bash
npx prisma db seed
```
Generated `server/package.json` should include:
```json
{
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
```
Use `tsx prisma/seed.ts` if the project standard uses `tsx` instead of `ts-node`.

View File

@@ -1,80 +0,0 @@
# PrismaService Implementation
The generated backend must provide a NestJS injectable service that wraps PrismaClient. This document defines the **only** supported implementation for Prisma 5+.
---
# Correct Implementation
```typescript
import { Injectable, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
}
```
## Why this pattern
- **OnModuleInit:** NestJS lifecycle hook; `onModuleInit()` runs when the module is initialized, ensuring the database connection is established before handling requests.
- **$connect():** Explicitly connects the Prisma client. Required for reliable connection handling in serverless or long-running apps.
---
# Deprecated: Do NOT Use
The following pattern is **deprecated** in Prisma 5 and must **not** be generated:
```typescript
// WRONG — do not generate
this.$on("beforeExit", async () => {
await this.$disconnect();
});
```
- `beforeExit` is not supported in Prisma 5.
- Using it will cause runtime errors or warnings.
## Rule for generators
Generated `PrismaService` (or equivalent) must:
1. Extend `PrismaClient`.
2. Implement `OnModuleInit`.
3. Call `await this.$connect()` in `onModuleInit()`.
4. **Not** use `this.$on('beforeExit', ...)` or any `beforeExit` hook.
---
# Module Registration
Register the service in a dedicated Prisma module (or in `AppModule`) so it can be injected into other services:
```typescript
// prisma.module.ts (or app.module.ts)
import { Global, Module } from "@nestjs/common";
import { PrismaService } from "./prisma.service";
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
```
Using `@Global()` allows injecting `PrismaService` in any module without re-importing.
---
# File location
Generated file:
- `src/prisma.prisma.service.ts` or `src/prisma.service.ts`
Class name: `PrismaService`.

View File

@@ -1,152 +0,0 @@
# Backend Runtime Rules
This document defines runtime configuration requirements for the generated backend. Generators must produce a project that runs without errors when these rules are followed.
---
# Environment Variables
The backend **must** have a `.env` file at the project root (e.g. `server/.env` or backend package root). This file must **not** be committed with real credentials; provide an example instead (e.g. `.env.example`).
## Required variables
| Variable | Required | Description |
| -------------------- | -------- | ----------- |
| PORT | Yes | Backend listen port |
| DATABASE_URL | Yes | PostgreSQL connection string for Prisma |
| CORS_ALLOWED_ORIGINS | Yes | Comma-separated SPA origins allowed to call the API |
| KEYCLOAK_ISSUER_URL | Yes | Keycloak issuer URL used for JWT verification |
| KEYCLOAK_AUDIENCE | Yes | Backend audience/client expected in access tokens |
| KEYCLOAK_JWKS_URL | No | Optional explicit JWKS URL |
## Example
```env
PORT=3000
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/toir"
CORS_ALLOWED_ORIGINS="http://localhost:5173,https://toir-frontend.greact.ru"
KEYCLOAK_ISSUER_URL="https://sso.greact.ru/realms/toir"
KEYCLOAK_AUDIENCE="toir-backend"
# KEYCLOAK_JWKS_URL="https://sso.greact.ru/realms/toir/protocol/openid-connect/certs"
```
## Generation requirement
When generating the backend, **always** create:
1. **`.env.example`** — with placeholder DATABASE_URL and instructions.
2. **`.env`** — with local development example values so the app can start.
If the generator does not create `.env`, the first run will fail with:
```
Environment variable not found: DATABASE_URL
```
## Fail-fast requirement
The generated backend must validate required runtime and auth variables at startup.
Rules:
1. Missing required auth variables must stop startup immediately.
2. The generated backend must not silently fall back to production auth settings in code.
3. Auth env validation must be implemented explicitly in generated config/bootstrap code.
---
# Authentication Runtime Rules
The generated backend must verify JWTs against Keycloak using:
- issuer
- audience
- JWKS
JWKS resolution priority must be exactly:
1. explicit `KEYCLOAK_JWKS_URL`
2. OIDC discovery
3. `${issuer}/protocol/openid-connect/certs`
The generated backend must extract authorization roles only from `realm_access.roles`.
---
# Prisma Client Generation
After the Prisma schema is generated or modified, the Prisma client must be generated.
## Command
```bash
npx prisma generate
```
## Lifecycle rule
Add to generated `package.json` so that `prisma generate` runs after every `npm install`:
```json
{
"scripts": {
"postinstall": "prisma generate"
}
}
```
This ensures that after cloning or installing dependencies, the Prisma client is available without a manual step.
**Note:** Path may need to be adjusted if Prisma schema lives in a subfolder (e.g. `prisma generate` is typically run from the package root where `prisma/schema.prisma` exists).
---
# Database Migration
The generation process must document and support database migration.
## Command
```bash
npx prisma migrate dev
```
Run after:
1. Prisma schema has been generated or updated.
2. `npx prisma generate` has been run.
## Generation requirement
1. Include migration in the backend generation pipeline (see `generation/backend-generation.md`).
2. Document in README or post-generation validation that the user must run `npx prisma migrate dev` before first run (or provide a setup script that runs it).
---
# CORS Rules
The generated backend must support the SPA -> API bearer-token model explicitly.
Rules:
1. Use `CORS_ALLOWED_ORIGINS` as the allowed origin list.
2. Allow at minimum these example origins in `.env.example`:
- `http://localhost:5173`
- `https://toir-frontend.greact.ru`
3. Allow `Authorization` and JSON content headers.
4. Expose `Content-Range` because React Admin depends on it.
5. Do not enable credentials by default.
---
# Summary
| Requirement | Action |
| ----------------------- | ------ |
| Runtime env | Create `.env` and `.env.example` with DB, auth, and CORS variables |
| Fail-fast config | Validate required auth/runtime variables at startup |
| Prisma client | Run `npx prisma generate`; add `postinstall` script |
| Database schema | Document/run `npx prisma migrate dev` after schema generation |
| JWT verification | Use issuer + audience + JWKS with explicit resolution priority |
| Role extraction | Use only `realm_access.roles` |
| CORS | Support SPA -> API bearer flow and expose `Content-Range` |

View File

@@ -1,82 +0,0 @@
# Seed Data Rules
The generator must create a **Prisma seed script** so the development database contains minimal sample data. This allows the frontend and API to be used immediately after migration.
---
# Seed Script Location
**File:** `server/prisma/seed.ts`
(Or `server/prisma/seed.js` if the project uses JavaScript; TypeScript is preferred and may require `ts-node` or `tsx` to run.)
---
# Seed Data Requirements
The seed script must create **minimal sample data** for at least one record per main entity, so that:
- List views show data.
- Reference fields (e.g. equipment type, equipment) have valid options.
- The app is demo-ready without manual data entry.
## Example scope (TOiR-style domain)
- **One EquipmentType** (e.g. code `"pump"`, name `"Pump"`).
- **One Equipment** (e.g. linked to that type, with required fields filled).
- **One RepairOrder** (e.g. linked to that equipment, with required fields filled).
Order matters: create EquipmentType first, then Equipment (references type), then RepairOrder (references equipment). Use Prisma `create` with the generated client; respect unique constraints and foreign keys.
---
# Prisma Seed Configuration
The generator must add the following to **`server/package.json`**:
```json
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
```
If the project uses a different runner (e.g. `tsx`), use that instead:
```json
"prisma": {
"seed": "tsx prisma/seed.ts"
}
```
Ensure the seed runner is installed (e.g. `ts-node` as dev dependency) so that:
```bash
npx prisma db seed
```
runs successfully.
---
# Running the Seed
After migrations:
```bash
cd server
npx prisma migrate dev
npx prisma db seed
```
Or document that the user can run `npx prisma db seed` once after the first migration to populate sample data.
---
# Summary
| Requirement | Action |
|--------------------|--------|
| Seed script | Create `server/prisma/seed.ts` (or equivalent). |
| Sample data | At least one EquipmentType, one Equipment, one RepairOrder (or equivalent for the DSL). |
| package.json | Add `"prisma": { "seed": "ts-node prisma/seed.ts" }` (or tsx). |
| Seed runner | Ensure ts-node (or tsx) is available so `prisma db seed` works. |

View File

@@ -1,97 +0,0 @@
# Service Layer Rules
Generated NestJS services must follow these rules so that update operations work correctly with React Admin and Prisma.
---
# Update Payload Sanitization
React Admin (and many REST clients) **always send `id`** in PATCH/PUT request bodies.
Example payload from React Admin:
```json
{
"id": "003",
"name": "Pump"
}
```
Some entities use a **different primary key** (e.g. `code`). The API response includes `id` (mapped from the PK) for React Admin compatibility, but the Prisma model may have no `id` field—only `code`. If the service passes the incoming DTO directly to Prisma:
```typescript
// WRONG — causes runtime error
prisma.equipmentType.update({
where: { code },
data: dto // dto contains "id", which is not a Prisma field
});
```
Prisma throws because `id` (and possibly other non-updatable fields) are not on the model or must not be written in `data`.
---
# Rules
1. **Update payload must be sanitized** before passing to the ORM. Do not pass the raw request body as `data` to `prisma.*.update()`.
2. **Remove `id`** from the update payload. React Admin sends `id` for identity; it must not be written to the database as a column (unless the entity actually has an `id` column and it is intended to be immutable on update).
3. **Remove the primary key** field from the update payload. The primary key is used in `where`; it must not appear in `data`. For example, remove `code` from `data` when updating by `code`.
4. **Remove readonly attributes** (e.g. created timestamps, server-generated fields) if they are present in the DTO, so they are not passed to Prisma `data`.
---
# Example Implementation
Destructure identity and primary key (and any other non-updatable fields) out of the DTO, then pass only the rest as `data`:
**Entity with primary key `code` (e.g. EquipmentType):**
```typescript
update(code: string, dto: UpdateEquipmentTypeDto) {
const { id, code: _pk, ...data } = dto as any;
return this.prisma.equipmentType.update({
where: { code },
data,
});
}
```
Or, if the DTO type does not include `id` or `code`, explicitly omit only the fields that must not be written:
```typescript
update(code: string, dto: UpdateEquipmentTypeDto) {
const { id, code: _pk, ...data } = dto as Record<string, unknown>;
return this.prisma.equipmentType.update({
where: { code },
data,
});
}
```
**Entity with primary key `id` (e.g. Equipment):**
```typescript
update(id: string, dto: UpdateEquipmentDto) {
const { id: _pk, ...data } = dto as any;
return this.prisma.equipment.update({
where: { id },
data,
});
}
```
This way, `id` (and the PK) are never passed into `data`, and Prisma does not receive unknown or read-only fields.
---
# Summary
| Rule | Action |
|------|--------|
| Sanitize update payload | Before `prisma.*.update()`, strip non-data fields from the DTO. |
| Remove `id` | Do not pass `id` in `data` unless the entity has an updatable `id` (rare). |
| Remove primary key | Use PK only in `where`; omit from `data`. |
| Remove readonly fields | Omit created_at, server-only fields, etc. from `data`. |

View File

@@ -2,7 +2,7 @@ services:
postgres:
image: postgres:16
container_name: toir-postgres
restart: always
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -10,7 +10,7 @@ services:
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- postgres-data:/var/lib/postgresql/data
volumes:
postgres_data:
postgres-data:

View File

@@ -0,0 +1,35 @@
# Repository Structure
`KIS-TOiR` keeps the existing LLM-first generation philosophy and organizes the repository by meaning:
- `domain/`
- canonical DSL inputs
- DSL specification
- `prompts/`
- active prompt corpus used to drive generation
- `docs/`
- overview and repository-level architecture notes
- `tools/`
- helper scripts for summary generation and validation
- `server/`
- active backend target output
- `client/`
- active frontend target output
The repository keeps LLM-first generation orchestration, but framework bootstrap is CLI-first:
- `server/` must remain a valid NestJS workspace baseline
- `client/` must remain a valid Vite React TypeScript workspace baseline
- repair a broken workspace before applying more domain-derived generation changes
- future agents must treat forbidden generation patterns in `prompts/` as contract violations, not suggestions
Root-level files stay limited to repository-level artifacts such as:
- `README.md`
- `package.json`
- `docker-compose.yml`
- `domain-summary.json`
- `toir-realm.json`
- `.gitignore`
The repository does not introduce a new generator engine or compiler platform. It keeps the current LLM-first pipeline and makes it cleaner, more explicit, and easier to navigate.

324
domain-summary.json Normal file
View File

@@ -0,0 +1,324 @@
{
"sourceFiles": [
"domain/TOiR.domain.dsl"
],
"entities": [
{
"name": "EquipmentType",
"primaryKey": "code",
"fields": [
{
"name": "code",
"type": "string",
"required": true,
"unique": true,
"default": null
},
{
"name": "name",
"type": "string",
"required": true,
"unique": false,
"default": null
},
{
"name": "manufacturer",
"type": "string",
"required": false,
"unique": false,
"default": null
},
{
"name": "maintenanceIntervalHours",
"type": "integer",
"required": false,
"unique": false,
"default": null
},
{
"name": "overhaulIntervalHours",
"type": "integer",
"required": false,
"unique": false,
"default": null
}
],
"foreignKeys": []
},
{
"name": "Equipment",
"primaryKey": "id",
"fields": [
{
"name": "id",
"type": "uuid",
"required": false,
"unique": false,
"default": null
},
{
"name": "inventoryNumber",
"type": "string",
"required": true,
"unique": true,
"default": null
},
{
"name": "serialNumber",
"type": "string",
"required": false,
"unique": false,
"default": null
},
{
"name": "name",
"type": "string",
"required": true,
"unique": false,
"default": null
},
{
"name": "equipmentTypeCode",
"type": "string",
"required": true,
"unique": false,
"default": null
},
{
"name": "status",
"type": "EquipmentStatus",
"required": true,
"unique": false,
"default": "Active"
},
{
"name": "location",
"type": "string",
"required": false,
"unique": false,
"default": null
},
{
"name": "commissionedAt",
"type": "date",
"required": false,
"unique": false,
"default": null
},
{
"name": "totalEngineHours",
"type": "decimal",
"required": false,
"unique": false,
"default": null
},
{
"name": "engineHoursSinceLastRepair",
"type": "decimal",
"required": false,
"unique": false,
"default": null
},
{
"name": "lastRepairAt",
"type": "date",
"required": false,
"unique": false,
"default": null
},
{
"name": "notes",
"type": "text",
"required": false,
"unique": false,
"default": null
}
],
"foreignKeys": [
{
"field": "equipmentTypeCode",
"references": {
"entity": "EquipmentType",
"field": "code"
}
}
]
},
{
"name": "RepairOrder",
"primaryKey": "id",
"fields": [
{
"name": "id",
"type": "uuid",
"required": false,
"unique": false,
"default": null
},
{
"name": "number",
"type": "string",
"required": true,
"unique": true,
"default": null
},
{
"name": "equipmentId",
"type": "uuid",
"required": true,
"unique": false,
"default": null
},
{
"name": "repairKind",
"type": "RepairKind",
"required": true,
"unique": false,
"default": null
},
{
"name": "status",
"type": "RepairOrderStatus",
"required": true,
"unique": false,
"default": "Draft"
},
{
"name": "plannedAt",
"type": "date",
"required": true,
"unique": false,
"default": null
},
{
"name": "startedAt",
"type": "date",
"required": false,
"unique": false,
"default": null
},
{
"name": "completedAt",
"type": "date",
"required": false,
"unique": false,
"default": null
},
{
"name": "contractor",
"type": "string",
"required": false,
"unique": false,
"default": null
},
{
"name": "engineHoursAtRepair",
"type": "decimal",
"required": false,
"unique": false,
"default": null
},
{
"name": "description",
"type": "text",
"required": false,
"unique": false,
"default": null
},
{
"name": "notes",
"type": "text",
"required": false,
"unique": false,
"default": null
}
],
"foreignKeys": [
{
"field": "equipmentId",
"references": {
"entity": "Equipment",
"field": "id"
}
}
]
}
],
"enums": [
{
"name": "EquipmentStatus",
"values": [
{
"name": "Active",
"label": "В эксплуатации"
},
{
"name": "Repair",
"label": "В ремонте"
},
{
"name": "Reserve",
"label": "В резерве"
},
{
"name": "WriteOff",
"label": "Списано"
}
]
},
{
"name": "RepairKind",
"values": [
{
"name": "TO",
"label": "Техническое обслуживание"
},
{
"name": "TR",
"label": "Текущий ремонт"
},
{
"name": "TRE",
"label": "Текущий расширенный ремонт"
},
{
"name": "KR",
"label": "Капитальный ремонт"
},
{
"name": "AR",
"label": "Аварийный ремонт"
},
{
"name": "MP",
"label": "Метрологическая поверка"
}
]
},
{
"name": "RepairOrderStatus",
"values": [
{
"name": "Draft",
"label": "Черновик"
},
{
"name": "Approved",
"label": "Утверждена"
},
{
"name": "InWork",
"label": "В работе"
},
{
"name": "Done",
"label": "Выполнена"
},
{
"name": "Cancelled",
"label": "Отменена"
}
]
}
]
}

View File

@@ -3,10 +3,6 @@
Сущности: Equipment (Оборудование), EquipmentType (Вид оборудования), RepairOrder (Заявка на ремонт)
*/
// ─────────────────────────────────────────────
// Перечисления
// ─────────────────────────────────────────────
enum EquipmentStatus {
value Active {
label "В эксплуатации";
@@ -61,11 +57,6 @@ enum RepairOrderStatus {
}
}
// ─────────────────────────────────────────────
// Справочник: Вид оборудования
// ─────────────────────────────────────────────
entity EquipmentType {
description "Вид (марка) оборудования — нормативный справочник НСИ";
@@ -88,7 +79,6 @@ entity EquipmentType {
type string;
}
// Нормативный межремонтный ресурс (моточасы)
attribute maintenanceIntervalHours {
description "Периодичность ТО, моточасов";
type integer;
@@ -100,11 +90,6 @@ entity EquipmentType {
}
}
// ─────────────────────────────────────────────
// Основная сущность: Оборудование
// ─────────────────────────────────────────────
entity Equipment {
description "Единица оборудования — объект ремонта и технического обслуживания";
@@ -131,14 +116,13 @@ entity Equipment {
is required;
}
// Связь с видом оборудования (справочник НСИ)
attribute equipmentTypeCode {
type string;
key foreign {
relates EquipmentType.code;
}
is required;
attribute equipmentTypeCode {
type string;
key foreign {
relates EquipmentType.code;
}
is required;
}
attribute status {
description "Текущий статус";
@@ -157,7 +141,6 @@ entity Equipment {
type date;
}
// Наработка фиксируется вручную или из производственной программы
attribute totalEngineHours {
description "Общая наработка, моточасов";
type decimal;
@@ -179,11 +162,6 @@ entity Equipment {
}
}
// ─────────────────────────────────────────────
// Заявка на ремонт
// ─────────────────────────────────────────────
entity RepairOrder {
description "Заявка на ремонт — формируется по ППР или по факту обнаруженного дефекта";

View File

@@ -2,6 +2,8 @@
This document describes the single DSL (Domain Specific Language) used to specify fullstack CRUD applications. The only required DSL input is `domain/*.dsl`.
`domain-summary.json` is a derived artifact generated from this DSL to stabilize LLM-first generation and feed the lightweight validation gate. It must never replace the DSL as the source of truth. The active prompt corpus that consumes this contract lives in `prompts/`.
---
# DSL Responsibility

View File

@@ -1,192 +0,0 @@
# Frontend Architecture
Frontend stack:
- React
- TypeScript
- Vite
- React Admin
- shadcn/ui
- Keycloak JS
The frontend is generated from `domain/*.dsl`.
Each entity becomes a React Admin resource.
The generated frontend must also include Keycloak authentication by default.
---
# Single Source of Truth
- `domain/*.dsl` is the only required input for frontend generation.
- React Admin resources, fields, references, and routes must be derived from the domain model, primary keys, foreign keys, and enums defined in the domain DSL.
- Frontend documentation, generation rules, and optional overrides must not duplicate entity, attribute, or relation structures outside the domain DSL.
- Deprecated multi-DSL inputs are compatibility-only artifacts and must never be treated as authoritative frontend inputs or used to redefine entities, attributes, primary keys, foreign keys, relations, or enums.
---
# Project Structure
client/
src/
App.tsx
main.tsx
dataProvider.ts
auth/
keycloak.ts
authProvider.ts
config/
env.ts
resources/
{entity}/
{entity}List.tsx
{entity}Create.tsx
{entity}Edit.tsx
{entity}Show.tsx
---
# Resource Registration
Each resource must be registered in App.tsx.
The generated `App.tsx` must register:
- `dataProvider`
- `authProvider`
The generated `Admin` root must enforce authenticated operation. The generated frontend must not operate anonymously once auth is enabled.
The generated `authProvider.getIdentity()` must resolve identity from token claims already present in the parsed token and must not trigger a baseline Keycloak `/account` request.
Example:
<Resource
name="equipment"
list={EquipmentList}
create={EquipmentCreate}
edit={EquipmentEdit}
show={EquipmentShow}
/>
---
# Data Provider
React Admin uses a generated shared REST-compatible data provider.
API format must follow:
GET /resource
GET /resource/:id
POST /resource
PATCH /resource/:id
DELETE /resource/:id
List response format:
{
data: [],
total: number
}
The generated `dataProvider.ts` must remain the **single shared request seam** for backend API calls.
Rules:
1. Use an env-driven API base URL.
2. Attach `Authorization: Bearer <access_token>` in this shared seam.
3. Cover all React Admin operations, including references and bulk fetches.
4. Do not scatter auth headers across resource components.
---
# Application Bootstrap
The generated `main.tsx` must initialize Keycloak before rendering the SPA.
Rules:
1. Use redirect-based Keycloak login only.
2. Use Authorization Code + PKCE (`S256`).
3. Do not generate a custom in-app username/password login form.
4. Do not render the authenticated admin app before Keycloak initialization completes.
5. Do not introduce `keycloak.loadUserProfile()` or `/account` profile-fetch requests as part of baseline app startup or identity resolution.
---
# Config
The generated frontend must include a dedicated config module in `src/config/`.
Required env variables:
- `VITE_API_URL`
- `VITE_KEYCLOAK_URL`
- `VITE_KEYCLOAK_REALM`
- `VITE_KEYCLOAK_CLIENT_ID`
The generated frontend config must fail fast if required auth variables are missing. The generated frontend must not silently fall back to production auth settings in code.
---
# Foreign Keys
Foreign keys must use ReferenceInput and ReferenceField.
Example:
<ReferenceInput source="equipmentTypeCode" reference="equipment-types" />
---
# Naming Conventions
## React component naming
Components are named after the entity in PascalCase. One entity = one resource with four main views.
- **List:** `{Entity}List.tsx` (e.g. `EquipmentList.tsx`, `RepairOrderList.tsx`)
- **Create:** `{Entity}Create.tsx` (e.g. `EquipmentCreate.tsx`)
- **Edit:** `{Entity}Edit.tsx` (e.g. `EquipmentEdit.tsx`)
- **Show:** `{Entity}Show.tsx` (e.g. `EquipmentShow.tsx`)
Examples:
- Equipment → `EquipmentList.tsx`, `EquipmentCreate.tsx`, `EquipmentEdit.tsx`, `EquipmentShow.tsx`
- EquipmentType → `EquipmentTypeList.tsx`, `EquipmentTypeCreate.tsx`, `EquipmentTypeEdit.tsx`, `EquipmentTypeShow.tsx`
- RepairOrder → `RepairOrderList.tsx`, `RepairOrderCreate.tsx`, `RepairOrderEdit.tsx`, `RepairOrderShow.tsx`
## Resource folder naming
Folder under `resources/` uses kebab-case, matching the React Admin resource name:
- `resources/equipment/`
- `resources/equipment-type/`
- `resources/repair-order/`
---
# Resource Naming Rules
React Admin resource name (used in `<Resource name="..." />` and in `reference` for ReferenceInput) must match the API resource path (no leading slash, same segment string).
1. **Entity → resource name:** PascalCase entity name is converted to kebab-case and pluralized.
2. **Consistency with API:** The resource name must be the same as the backend path segment so that the data provider calls the correct URL.
| Entity (DSL) | Resource name (React Admin) | API path |
|----------------|-----------------------------|------------|
| Equipment | equipment | /equipment |
| EquipmentType | equipment-types | /equipment-types |
| RepairOrder | repair-orders | /repair-orders |
Examples in App.tsx:
- `<Resource name="equipment" list={EquipmentList} create={EquipmentCreate} edit={EquipmentEdit} show={EquipmentShow} />`
- `<Resource name="equipment-types" list={EquipmentTypeList} ... />`
- `<Resource name="repair-orders" list={RepairOrderList} ... />`

View File

@@ -1,164 +0,0 @@
# DSL → React Admin Mapping
Entity attributes determine UI fields.
---
# Authentication
Generated React Admin applications in this repository must include an `authProvider`.
Rules:
1. `authProvider` is mandatory.
2. The generated app must use redirect-based Keycloak login only.
3. The generator must not create a custom in-app username/password form.
4. The generated app must initialize authentication before rendering the admin UI.
---
# Shared Authenticated Request Layer
The generated frontend must attach bearer tokens through the shared request seam in `client/src/dataProvider.ts`.
Rules:
1. All resource calls must use the same authenticated request layer.
2. Reference lookups must use the same authenticated request layer.
3. The generated frontend must not attach auth headers directly inside resource components.
---
# Error Handling
The generated `authProvider.checkError` must distinguish authentication failures from authorization failures:
- `401` -> force logout / re-authentication
- `403` -> do not re-authenticate; surface access denied / permission error
The generator must not treat `401` and `403` as the same outcome.
---
# Identity Resolution
The generated `authProvider.getIdentity()` must use token claims already present in the parsed token.
Rules:
1. Prefer `sub`, `preferred_username`, `email`, and `name`.
2. Do not call `keycloak.loadUserProfile()` by default.
3. Do not rely on the Keycloak `/account` endpoint for baseline CRUD/admin generation.
The default generator strategy is to avoid the `/account` request entirely rather than solving it through broader Keycloak CORS settings.
---
# Token Handling
The generated frontend must use Keycloak JS token handling with these rules:
1. Use Authorization Code + PKCE (`S256`).
2. Refresh tokens before protected API calls when needed.
3. Token refresh must be concurrency-safe:
- one shared in-flight refresh operation
- no parallel refresh stampede
4. Do not store access tokens or refresh tokens in `localStorage` or `sessionStorage`.
---
# Type Mapping
| DSL Type | React Admin Component |
|---------|-----------------------|
| string | TextInput / TextField |
| integer | NumberInput |
| decimal | NumberInput |
| date | DateInput |
| enum | SelectInput |
| foreign key | ReferenceInput |
---
# Example
DSL
attribute name {
type string;
}
React Admin
<TextInput source="name" />
---
# Enum Example
DSL
attribute status {
type EquipmentStatus;
}
React Admin
<SelectInput source="status" choices={statusChoices} />
---
# Foreign Key Example
DSL
attribute equipmentTypeCode {
type string;
}
React Admin
<ReferenceInput source="equipmentTypeCode" reference="equipment-types" />
---
# React Admin ID Field Requirement
React Admin requires every record in list and detail responses to contain a field named **`id`**. It uses this field for resource identity, cache keys, and references.
**Rules:**
1. Every record returned by the API must contain an **`id`** field.
2. If the DSL primary key is not named `id`, the generator must **map** the primary key value to an `id` field in the API response (backend) or in a frontend adapter.
3. The `id` field must contain the **value of the primary key** (e.g. uuid string, or `code` value for EquipmentType).
4. If the primary key is not literally `id`, backend list/query logic must map React Admin `_sort=id` to the real primary key field before constructing ORM sorting.
**Example:**
DSL entity with primary key `code`:
```
entity EquipmentType {
attribute code {
key primary;
type string;
}
attribute name { type string; }
}
```
API response must include `id` so React Admin can identify the record:
```json
{
"id": "pump",
"code": "pump",
"name": "Pump"
}
```
If the response only had `{ "code": "pump", "name": "Pump" }`, React Admin would not work correctly because it expects `id`. The backend or frontend adapter must therefore set `id: record.code` (or equivalent) when the primary key is not `id`.
If React Admin later sends `_sort=id`, the generated backend must map that synthetic `id` sort back to the real primary key field (for example `code`) before building the Prisma `orderBy` clause.
This rule ensures compatibility with React Admin resource identity handling.

View File

@@ -1,508 +0,0 @@
ROLE
You are a Staff-level Fullstack Platform Engineer.
Your task is to generate a fully runnable fullstack CRUD application from the DSL context of this repository.
Use context7.
Follow official best practices from:
NestJS documentation
Prisma documentation
React Admin documentation
Vite documentation
Keycloak documentation
Docker documentation
The generated application must run without manual fixes.
PROJECT CONTEXT
You must read the project documentation in the following strict order:
domain/dsl-spec.md
domain/*.dsl
If present, read optional overrides after the domain DSL:
overrides/api-overrides.dsl
overrides/ui-overrides.dsl
backend/architecture.md
backend/prisma-rules.md
backend/prisma-service.md
backend/service-rules.md
backend/runtime-rules.md
backend/database-runtime.md
backend/seed-rules.md
frontend/architecture.md
frontend/react-admin-rules.md
auth/keycloak-architecture.md
auth/frontend-auth-rules.md
auth/backend-auth-rules.md
auth/keycloak-realm-template-rules.md
generation/scaffolding-rules.md
generation/backend-generation.md
generation/frontend-generation.md
generation/runtime-bootstrap.md
generation/post-generation-validation.md
Do not ignore any rules defined in these documents.
INPUT CONTRACT
Required DSL input:
domain/*.dsl
Optional override inputs:
overrides/api-overrides.dsl
overrides/ui-overrides.dsl
Rules:
- Domain DSL is the single source of truth for entities, attributes, primary keys, foreign keys, and enums.
- DTO, API, and UI must be derived from the domain DSL.
- Optional overrides must not duplicate or redefine the domain model.
- Generation must work without override files.
- Ignore deprecated multi-DSL inputs if they are present in the repository; they are not authoritative generation inputs.
- Do not require standalone DTO, API, or UI DSL inputs.
GOAL
Generate a domain-DSL-driven fullstack CRUD system with default Keycloak authentication and authorization.
Repository-specific defaults and examples may use names such as `toir`, `toir-frontend`, `toir-backend`, `toir-realm.json`, and `*.greact.ru`, but the generator must parameterize realm name, client IDs, production URLs, and realm-artifact filename for other generated projects.
Stack:
Backend
Node.js
NestJS
Prisma ORM
PostgreSQL
jose
Frontend
React
Vite
React Admin
MUI
shadcn/ui
Keycloak JS
PROJECT STRUCTURE
Root
.gitignore
docker-compose.yml
root-level Keycloak realm import artifact (default example filename: `toir-realm.json`)
server/
client/
Backend
server/
src/
auth/
config/
modules/{entity}/
prisma/schema.prisma
prisma/seed.ts
.gitignore
.env
.env.example
Frontend
client/
.gitignore
src/
auth/
config/
resources/{entity}/
App.tsx
main.tsx
dataProvider.ts
.env.example
STEP 1 — Parse Domain DSL
Parse domain/*.dsl and extract:
Entities
Attributes
Primary keys
Foreign keys
Enums
If present, read optional override files only after the domain model has been parsed. Overrides may refine derived API or UI behavior but must never redefine entities, attributes, primary keys, foreign keys, or enums.
Do not consult any supplemental DTO/API/UI DSL source when deriving backend or frontend artifacts.
Respect the DSL specification.
STEP 2 — CLI scaffolding
Use official CLIs.
Backend
npx @nestjs/cli@10.3.2 new server --package-manager npm --skip-git
Frontend
npm create vite@5.2.0 client -- --template react-ts
STEP 3 — Install dependencies
Backend
@prisma/client
prisma
@nestjs/config
jose
Frontend
react-admin
ra-data-simple-rest
@mui/material
@emotion/react
@emotion/styled
keycloak-js
STEP 4 — Generate Prisma schema
From DSL domain generate:
models
enums
relations
primary keys
Type mapping
decimal → Decimal
date → DateTime
DTO mapping
decimal → string
date → ISO string
STEP 5 — Generate NestJS CRUD modules and derived DTOs
Per entity generate:
module
controller
service
dto
Controller routes
GET /resource
GET /resource/:pk
POST /resource
PATCH /resource/:pk
DELETE /resource/:pk
Path parameter must match the DSL primary key name.
Examples
/equipment/:id
/equipment-types/:code
/repair-orders/:id
STEP 6 — Generate backend auth infrastructure
Generate:
AuthModule
JWT guard
roles guard
@Public()
@Roles()
typed authenticated principal
typed env validation
Rules:
- `/health` must remain public
- CRUD routes must be protected by default
- RBAC source must be `realm_access.roles`
- JWT verification must use issuer + audience + JWKS
- JWKS resolution priority must be:
1. explicit `KEYCLOAK_JWKS_URL`
2. OIDC discovery
3. `${issuer}/protocol/openid-connect/certs`
- Do not use deprecated Keycloak-specific Node adapters
STEP 7 — Generate Service Layer
Service layer must follow backend/service-rules.md.
Important rule:
React Admin sends the id field in update payloads even when the primary key is not named id.
Therefore update payload must be sanitized before passing data to Prisma.
Services MUST NOT pass raw request DTO directly into Prisma.
Incorrect:
prisma.entity.update({
where,
data: dto
})
Correct pattern:
const { id, <primaryKey>, ...data } = dto
return prisma.entity.update({
where,
data
})
Example (PK = code)
const { id, code, ...data } = dto
return prisma.equipmentType.update({
where: { code },
data
})
Example (PK = id)
const { id: _pk, ...data } = dto
return prisma.entity.update({
where: { id },
data
})
Rules
Update payload passed to Prisma must not contain:
id
primary key attribute
readonly attributes
STEP 8 — Generate frontend auth integration
Generate:
client/src/config/env.ts
client/src/auth/keycloak.ts
client/src/auth/authProvider.ts
Rules:
- Keycloak login must be redirect-based only
- Use Authorization Code + PKCE (`S256`)
- Initialize Keycloak before rendering the SPA
- Attach `Authorization: Bearer <access_token>` through the shared request seam in `client/src/dataProvider.ts`
- `authProvider.getIdentity()` must derive identity from parsed token claims such as `sub`, `preferred_username`, `email`, and `name`
- Do not call `keycloak.loadUserProfile()` by default
- Do not rely on the Keycloak `/account` endpoint for baseline CRUD/admin generation
- Avoid the `/account` request entirely by default rather than broadening Keycloak CORS behavior
- `401` must force re-authentication
- `403` must surface access denied without forcing re-authentication
- Token refresh must be concurrency-safe
- Do not store tokens in `localStorage` or `sessionStorage`
- Frontend auth config must fail fast if required auth vars are missing
STEP 9 — Generate runtime infrastructure
Create:
server/.env
server/.env.example
client/.env.example
root/.gitignore
server/.gitignore
client/.gitignore
root-level Keycloak realm import artifact (default example filename: `toir-realm.json`)
Backend env examples must include:
PORT
DATABASE_URL
CORS_ALLOWED_ORIGINS
KEYCLOAK_ISSUER_URL
KEYCLOAK_AUDIENCE
KEYCLOAK_JWKS_URL (optional)
Frontend env examples must include:
VITE_API_URL
VITE_KEYCLOAK_URL
VITE_KEYCLOAK_REALM
VITE_KEYCLOAK_CLIENT_ID
Add to package.json:
postinstall: prisma generate
Generated `.gitignore` files must prevent local-only artifacts from entering git, including:
node_modules
dist
dist-ssr
coverage
*.tsbuildinfo
.env
.env.local
.env.*.local
STEP 10 — Database runtime
Generate root:
docker-compose.yml
PostgreSQL container
postgres:16
port 5432
STEP 11 — Generate seed
Create:
server/prisma/seed.ts
Seed minimal data for:
EquipmentType
Equipment
RepairOrder
Add to package.json:
prisma.seed
STEP 12 — Generate React Admin resources
Generate React Admin resources automatically from the domain DSL.
For each entity generate:
Field mapping
string → TextInput
number → NumberInput
date → DateInput
enum → SelectInput
FK → ReferenceInput
API responses MUST contain:
If PK ≠ id, map primary key to id.
If PK ≠ id, backend list/query logic must map React Admin `_sort=id` to the real primary key field before constructing ORM sorting.
Example
{
id: record.code,
code: record.code
}
STEP 13 — Validation
Verify:
docker-compose.yml exists
database container starts
prisma migrate dev works
prisma db seed works
API responds /health
React Admin receives id
update services sanitize payload before Prisma
frontend auth files exist
backend auth files exist
auth env examples exist
root/server/client .gitignore files exist
gitignore rules exclude local dependency, build, env, coverage, and tsbuildinfo artifacts
frontend auth code does not call `keycloak.loadUserProfile()`
frontend `getIdentity()` is token-claim based and does not rely on `/account`
public /health is preserved
unauthenticated protected route returns 401
insufficient role returns 403
natural-key entities map React Admin `_sort=id` to the real primary key field
generated realm import artifact is self-contained and guarantees `sub`, `aud`, and `realm_access.roles`
OUTPUT
Provide:
FULLSTACK GENERATION REPORT
Include:
1 Parsed DSL
2 Prisma models
3 Backend modules
4 API endpoints
5 React Admin resources
6 Authentication and authorization
7 Runtime configuration
8 Validation results
RUN INSTRUCTIONS
The generated application must run successfully with:
Import the generated root-level Keycloak realm artifact (for example `toir-realm.json`) into the external Keycloak server
docker compose up -d
cd server
npm install
npx prisma migrate dev
npm run start
cd client
npm install
npm run dev

View File

@@ -1,264 +0,0 @@
# Backend Generation Process
Backend generation follows a pipeline aligned with runtime, auth, and validation docs:
DSL
CLI scaffolding
backend code generation
auth generation
runtime infrastructure
database runtime
migration
seed
validation
Follow:
- `backend/architecture.md`
- `backend/runtime-rules.md`
- `backend/prisma-rules.md`
- `backend/prisma-service.md`
- `backend/database-runtime.md`
- `backend/seed-rules.md`
- `backend/service-rules.md`
- `auth/keycloak-architecture.md`
- `auth/backend-auth-rules.md`
- `auth/keycloak-realm-template-rules.md`
---
# Input Contract
Required input:
- `domain/*.dsl`
Optional extension input:
- `overrides/api-overrides.dsl`
Optional extension layout:
```text
overrides/
api-overrides.dsl
```
Rules:
- Parse `domain/*.dsl` as the only authoritative DSL input.
- Generate DTOs and REST API contracts automatically from the parsed domain model.
- The generator must work when `overrides/api-overrides.dsl` is absent.
- Optional overrides may refine derived API behavior but must not redefine entities, attributes, primary keys, foreign keys, relations, or enums.
- Supplemental DTO/API DSL inputs must not participate in backend parsing, dependency resolution, or backend generation decisions.
- Do not read standalone DTO or API DSL files.
---
# Step 1 — Parse Domain DSL
Read `domain/*.dsl` and extract:
- entities
- attributes, including the actual primary key attribute per entity
- enums
- foreign keys
All entities, attributes, primary keys, foreign keys, relations, and enums used by the backend pipeline must come from the parsed domain DSL.
If `overrides/api-overrides.dsl` exists, process it only after the domain model has been parsed and only as optional non-authoritative metadata.
The generator must treat auth as default runtime infrastructure, not as a DSL feature toggle.
---
# Step 2 — CLI scaffolding
Use official CLIs before generating backend code:
- NestJS project scaffold in `server/` (see `generation/scaffolding-rules.md`)
- install backend dependencies:
- `@prisma/client`
- `prisma`
- `@nestjs/config`
- `jose`
- seed runner when needed by the chosen package tooling
The generator must **not** use deprecated Keycloak-specific Node adapters such as `keycloak-connect`.
---
# Step 3 — Core backend code generation
Generate backend source artifacts:
1. **Prisma schema** (`server/prisma/schema.prisma`) from the domain DSL:
- attributes
- primary keys
- relations
- enums
2. **NestJS modules** per entity:
- module
- controller
- service
3. **DTO files**:
- `create-entity.dto.ts`
- `update-entity.dto.ts`
- `entity.response.dto.ts` (or equivalent)
4. **PrismaService**:
- implement `OnModuleInit`
- call `await this.$connect()` in `onModuleInit()`
- do not use a `beforeExit` hook
5. **Service update methods**:
- sanitize update payload before Prisma
- remove `id`, the entity primary key, and readonly attributes from `data`
- do not pass the raw request body directly to `prisma.*.update()`
6. **List/query sorting methods**:
- use only actual model field names in ORM `orderBy`
- if the API exposes synthetic `id` for React Admin but the real primary key is different, map incoming `_sort=id` to the real primary key field before building `orderBy`
- apply this rule to every entity with a non-`id` primary key
All backend DTOs and REST endpoints are derived artifacts. The generator must not require or parse separate DTO/API DSL documents.
Use mapping rules from `backend/prisma-rules.md`:
- DSL `decimal` -> DTO `string`
- DSL `date` -> DTO `string` (ISO)
---
# Step 4 — Backend auth generation
Generate auth infrastructure as part of the normal backend output. Auth must not be deferred to a manual post-step.
Required generated auth artifacts:
- `server/src/auth/auth.module.ts`
- JWT auth guard
- roles guard
- `@Public()` decorator
- `@Roles()` decorator
- typed authenticated principal interface
- typed auth-aware config validation in `server/src/config/`
JWT verification rules:
- verify JWTs with issuer, audience, and JWKS
- use a standards-based library such as `jose`
- resolve JWKS in this exact order:
1. explicit `KEYCLOAK_JWKS_URL`
2. OIDC discovery
3. `${issuer}/protocol/openid-connect/certs`
RBAC rules:
- extract authorization roles only from `realm_access.roles`
- do not infer roles from other claims
- apply CRUD defaults by HTTP method:
- `GET` -> `viewer`, `editor`, `admin`
- `POST`, `PATCH`, `PUT` -> `editor`, `admin`
- `DELETE` -> `admin`
- mark `/health` as public with `@Public()`
Controller generation rules:
- generated CRUD controllers must receive auth decorators by default
- path params must still use the actual entity primary key name (`:id`, `:code`, etc.)
- public routes must be explicit rather than implicit
---
# Step 5 — Runtime infrastructure
Generate backend runtime config files:
- `server/.env`
- `server/.env.example`
- `server/src/config/*` typed validation helpers
- `server/package.json` lifecycle:
- `"postinstall": "prisma generate"`
- `server/package.json` Prisma seed:
- `"prisma": { "seed": "ts-node prisma/seed.ts" }` (or `tsx` equivalent if that is the chosen project standard)
The generated backend env contract must include:
- `PORT`
- `DATABASE_URL`
- `CORS_ALLOWED_ORIGINS`
- `KEYCLOAK_ISSUER_URL`
- `KEYCLOAK_AUDIENCE`
- `KEYCLOAK_JWKS_URL` (optional)
Fail-fast config rule:
- backend startup must fail fast when required auth or database env vars are missing
- the generator must not silently fall back to production auth values in code
Commands that must be supported and documented:
- `npx prisma generate`
- `npx prisma migrate dev`
- `npx prisma db seed`
---
# Step 6 — Database runtime
Create `docker-compose.yml` at the **project root** with PostgreSQL only.
Minimum required compose characteristics:
- `services.postgres`
- `image: postgres:16`
- `ports: ["5432:5432"]`
Credentials and database name in compose must match `DATABASE_URL`.
Keycloak must remain an external runtime dependency and must not be added to `docker-compose.yml` in this repository.
---
# Step 7 — Migration
Apply schema to the development database:
```bash
cd server
npx prisma migrate dev
```
---
# Step 8 — Seed
Run development seed:
```bash
cd server
npx prisma db seed
```
Seed file location: `server/prisma/seed.ts`.
---
# Step 9 — Validation
Run runtime, auth, and contract checks from `generation/post-generation-validation.md`, including:
- docker-compose exists and DB container starts
- Prisma lifecycle commands succeed
- seed runs
- `/health` responds without auth
- protected routes reject unauthenticated requests with `401`
- authenticated users with insufficient role receive `403`
- React Admin-compatible API responses include `id` for every record
- natural-key entities translate React Admin `_sort=id` to the real primary key field

View File

@@ -1,137 +0,0 @@
# Developer Workflow
This document describes the **developer workflow** for running a generated fullstack application locally. The generator must produce a project that supports this workflow so the app is **fully runnable** after generation, including authentication.
This workflow assumes the project was generated from `domain/*.dsl` plus optional non-duplicating overrides only. Developers must not need to prepare separate DTO/API/UI DSL inputs before running the app.
---
# Prerequisites
- **Node.js** (LTS, e.g. 18+)
- **npm**
- **Docker** and **Docker Compose** (for the development database)
- **External Keycloak server** reachable by the generated frontend and backend
The generated project must not require the developer to invent auth wiring manually after generation.
---
# Workflow Steps
## 1. Prepare Keycloak and env files
From the project root, the generated project must include:
- root-level generated Keycloak realm import artifact
- repository default example filename: `toir-realm.json`
- `server/.env.example`
- `client/.env.example`
Required workflow:
1. Copy the generated env examples to real env files as needed.
2. Fill all required backend and frontend auth variables.
3. Import or verify the generated Keycloak realm import artifact in the external Keycloak server before starting the app.
The generator must document that auth config is fail-fast. Missing required auth env vars must stop startup instead of silently falling back to production values in code.
---
## 2. Start the database
From the **project root**:
```bash
docker compose up -d
```
This starts the PostgreSQL container defined in `docker-compose.yml`. Wait a few seconds for the database to accept connections.
Verify if needed:
```bash
docker compose ps
```
---
## 3. Backend setup and start
From the **server** directory:
```bash
cd server
npm install
npx prisma generate
npx prisma migrate dev
npx prisma db seed
npm run start
```
- `npm install` installs dependencies and runs `postinstall` when configured.
- `npx prisma generate` explicitly generates Prisma client.
- `npx prisma migrate dev` creates/applies migrations.
- `npx prisma db seed` inserts minimal development data.
- `npm run start` starts the NestJS backend.
The API should be available at the configured port (for example `http://localhost:3000`).
Verify:
```bash
curl http://localhost:3000/health
```
Expected: `{ "status": "ok" }` (or equivalent).
Generated backend behavior must also ensure:
- protected CRUD routes require authentication by default
- insufficient roles result in `403`
- `/health` remains public
---
## 4. Frontend setup and start
In a **separate terminal**, from the **project root**:
```bash
cd client
npm install
npm run dev
```
- `npm install` installs frontend dependencies including `keycloak-js`.
- `npm run dev` starts the Vite dev server (for example `http://localhost:5173`).
Open the frontend URL in a browser. The generated React Admin app must:
- initialize Keycloak before render
- use redirect-based login only
- authenticate against the configured Keycloak realm/client
- call the backend with bearer tokens through the shared request seam
---
# Summary
| Step | Command / location |
|------|---------------------|
| Prepare Keycloak + env | Fill `server/.env` and `client/.env`; import or verify the generated realm import artifact |
| Start database | From root: `docker compose up -d` |
| Backend setup/start | `cd server && npm install && npx prisma generate && npx prisma migrate dev && npx prisma db seed && npm run start` |
| Frontend setup/start | `cd client && npm install && npm run dev` |
The generator must produce all required artifacts so that this workflow succeeds:
- docker-compose
- env examples
- schema
- migrations
- seed
- health endpoint
- frontend auth integration
- backend auth infrastructure
- root-level Keycloak realm import artifact

View File

@@ -1,183 +0,0 @@
# Frontend Generation Process
Frontend generation must produce the React Admin CRUD application **and** the default Keycloak integration required by the runtime architecture.
Follow:
- `frontend/architecture.md`
- `frontend/react-admin-rules.md`
- `auth/keycloak-architecture.md`
- `auth/frontend-auth-rules.md`
- `auth/keycloak-realm-template-rules.md`
---
# Input Contract
Required input:
- `domain/*.dsl`
Optional extension input:
- `overrides/ui-overrides.dsl`
Optional extension layout:
```text
overrides/
ui-overrides.dsl
```
Rules:
- Parse `domain/*.dsl` as the only authoritative DSL input.
- Generate React Admin resources, views, and field mappings automatically from the parsed domain model.
- The generator must work when `overrides/ui-overrides.dsl` is absent.
- Optional overrides may refine derived UI behavior but must not redefine entities, attributes, primary keys, foreign keys, relations, or enums.
- Supplemental UI DSL inputs must not participate in frontend parsing, dependency resolution, or frontend generation decisions.
- Do not read a standalone UI DSL file.
---
# Step 1 — Parse Domain DSL
Extract:
- entities
- attributes
- relation fields required for reference inputs and reference displays
All frontend resources, fields, references, routes, and type-driven widget choices must be derived from the parsed domain DSL before optional overrides are considered.
If `overrides/ui-overrides.dsl` exists, process it only after the domain model has been parsed and only as optional non-authoritative metadata.
The generator must treat auth as default frontend infrastructure rather than as an optional feature.
---
# Step 2 — Generate frontend runtime structure
Generate the base frontend structure required by the proven runtime:
- `client/src/main.tsx`
- `client/src/App.tsx`
- `client/src/dataProvider.ts`
- `client/src/config/env.ts`
- `client/src/auth/keycloak.ts`
- `client/src/auth/authProvider.ts`
- `client/.env.example`
The generated frontend must:
- initialize Keycloak before rendering the SPA
- register React Admin with a mandatory `authProvider`
- enforce authenticated operation rather than anonymous operation
- use environment-driven runtime config and fail fast when required auth vars are missing
---
# Step 3 — Generate React Admin resources
For each entity create:
- `EntityList.tsx`
- `EntityCreate.tsx`
- `EntityEdit.tsx`
- `EntityShow.tsx`
Resource generation must remain compatible with the auth-aware shared request seam and must be derived from domain metadata rather than a separate UI DSL.
---
# Step 4 — Map fields
Map DSL attributes to React Admin components according to existing field rules and relation semantics.
Minimum type mapping:
- `string`, `text` -> `TextInput`, `TextField`
- `integer`, `decimal` -> `NumberInput`, `NumberField`
- `date` -> `DateInput`, `DateField`
- `enum` -> `SelectInput`
- foreign key -> `ReferenceInput`, `ReferenceField`
Reference/resource lookups must continue to flow through the same shared authenticated request layer used by the main CRUD resources.
---
# Step 5 — Generate auth-aware API layer
Generate a shared `dataProvider.ts` that:
- reads the API base URL from `VITE_API_URL`
- attaches `Authorization: Bearer <access_token>` to every backend request
- uses the same request seam for all React Admin operations, including:
- list
- get one
- get many
- get many reference
- create
- update
- delete
- handles token refresh before protected requests
- keeps token refresh concurrency-safe by sharing one in-flight refresh operation
- does not store access tokens or refresh tokens in `localStorage` or `sessionStorage`
The generator must not create multiple competing HTTP clients for authenticated and unauthenticated traffic. The shared request seam is the single bearer injection point.
---
# Step 6 — Generate frontend auth flow
Generate Keycloak frontend integration with these required rules:
- use `keycloak-js`
- redirect-based login only
- no custom in-app username/password login form
- Authorization Code + PKCE with `S256`
- initialize Keycloak before React render
- provide a React Admin `authProvider`
- derive `authProvider.getIdentity()` from token claims already present in the parsed token
- prefer `sub`, `preferred_username`, `email`, and `name` for identity resolution
- do not call `keycloak.loadUserProfile()` by default
- do not rely on the Keycloak `/account` endpoint for baseline CRUD/admin generation
- avoid the `/account` request entirely by default rather than broadening Keycloak CORS behavior
- distinguish auth failures from authorization failures:
- `401` -> force logout / re-authentication
- `403` -> do not re-authenticate; surface access denied / permission error
The generator must not silently fall back to production auth settings in code. Example values belong in `.env.example`, but runtime config must fail fast if required values are absent.
---
# Step 7 — Register resources and auth
Register resources in `App.tsx` and wire them into an authenticated `Admin` root.
Generated `App.tsx` must:
- register the shared `dataProvider`
- register the mandatory `authProvider`
- configure the app so it does not operate anonymously once auth is enabled
Example shape:
```tsx
<Admin dataProvider={dataProvider} authProvider={authProvider}>
<Resource name="equipment" ... />
</Admin>
```
---
# Step 8 — Frontend runtime config
Generate frontend env examples and config access for:
- `VITE_API_URL`
- `VITE_KEYCLOAK_URL`
- `VITE_KEYCLOAK_REALM`
- `VITE_KEYCLOAK_CLIENT_ID`
`client/.env.example` must use filled example values that match the documented Keycloak and local dev topology, while runtime code must fail fast if any required variable is missing.

View File

@@ -1,271 +0,0 @@
# Post-Generation Validation
After generating the backend or fullstack application, run these checks to ensure the project will start cleanly and that the default auth model has been generated correctly.
---
# Validation Checklist
## Source-of-truth input contract
- [ ] Fullstack generation succeeds when `domain/*.dsl` is the only required DSL input.
- [ ] The generator can produce backend and frontend outputs from `domain/*.dsl` alone before optional overrides are considered.
- [ ] DTO, API, and UI artifacts are derived automatically from the domain model, keys, relations, and enums.
- [ ] Optional override files are not required for a successful generation run.
- [ ] Optional overrides, if present, refine only derived API/UI output and do not duplicate the domain model.
- [ ] No generator step depends on duplicated domain structures outside `domain/*.dsl`.
**Failure symptoms:** generation requires extra DSL inputs, generated layers drift from the domain model, or the pipeline fails when override files are absent.
## 1. Frontend and backend env files
- [ ] `server/.env.example` exists and documents:
- `PORT`
- `DATABASE_URL`
- `CORS_ALLOWED_ORIGINS`
- `KEYCLOAK_ISSUER_URL`
- `KEYCLOAK_AUDIENCE`
- optional `KEYCLOAK_JWKS_URL`
- [ ] `client/.env.example` exists and documents:
- `VITE_API_URL`
- `VITE_KEYCLOAK_URL`
- `VITE_KEYCLOAK_REALM`
- `VITE_KEYCLOAK_CLIENT_ID`
- [ ] Runtime code fails fast when required auth or database env vars are missing.
- [ ] Runtime code does not silently fall back to production auth settings.
**Failure symptoms:** startup succeeds with undefined auth config, or fails later with opaque auth/runtime errors.
---
## 2. Git ignore hygiene
- [ ] Root `.gitignore` exists.
- [ ] `server/.gitignore` exists.
- [ ] `client/.gitignore` exists.
- [ ] Generated gitignore rules exclude local dependency directories such as `node_modules/`.
- [ ] Generated gitignore rules exclude build artifacts such as `dist/` and `dist-ssr/`.
- [ ] Generated gitignore rules exclude local env files such as `.env`, `.env.local`, and `.env.*.local`.
- [ ] Generated gitignore rules exclude `coverage/` and `*.tsbuildinfo`.
- [ ] Generated gitignore rules do **not** exclude committed project artifacts such as source files, docs, and `.env.example`.
**Failure symptoms:** `npm install`, local builds, or local env setup explode git status with thousands of files that should remain untracked.
---
## 3. Keycloak realm artifact
- [ ] A root-level generated Keycloak realm import artifact exists.
- [ ] If the repository default filename `toir-realm.json` is not used, the project-specific equivalent is documented consistently across bootstrap and workflow docs.
- [ ] The generated realm artifact is self-contained and reproducible.
- [ ] The generated realm artifact parameterizes realm name, frontend client ID, backend audience/client ID, production URLs, and artifact filename consistently with the generated project auth config.
- [ ] It defines realm roles:
- `admin`
- `editor`
- `viewer`
- [ ] It defines a frontend SPA client consistent with the generated frontend Keycloak client ID.
- [ ] It defines a backend audience/resource client consistent with the generated backend audience/client ID.
- [ ] It defines explicit audience delivery, such as an `api-audience` client scope.
- [ ] It does not rely on undeclared built-in client scopes being present after import.
- [ ] It explicitly addresses delivery of:
- `sub`
- `aud`
- `realm_access.roles`
**Failure symptoms:** access tokens are missing required claims, or realm import succeeds but generated apps still cannot authenticate/authorize reliably.
---
## 4. Frontend auth files and behavior
- [ ] Generated frontend includes:
- `client/src/config/env.ts`
- `client/src/auth/keycloak.ts`
- `client/src/auth/authProvider.ts`
- `client/src/main.tsx`
- `client/src/App.tsx`
- `client/src/dataProvider.ts`
- [ ] `keycloak-js` is installed.
- [ ] Keycloak is initialized before the SPA renders.
- [ ] Login is redirect-based only.
- [ ] No custom in-app username/password login form is generated.
- [ ] `Authorization Code + PKCE (S256)` is encoded in the frontend auth flow.
- [ ] `client/src/dataProvider.ts` or the documented shared request seam injects `Authorization: Bearer <access_token>` into all API requests.
- [ ] `authProvider.getIdentity()` derives identity from parsed token claims such as `sub`, `preferred_username`, `email`, and `name`.
- [ ] Generated frontend auth code does not call `keycloak.loadUserProfile()`.
- [ ] Generated frontend auth code does not rely on the Keycloak `/account` endpoint for baseline CRUD/admin generation.
- [ ] Token refresh is concurrency-safe:
- one shared in-flight refresh operation
- no parallel refresh stampede
- [ ] Generated auth code does not persist access tokens or refresh tokens in `localStorage` or `sessionStorage`.
- [ ] React Admin auth semantics distinguish:
- `401` -> force logout / re-authentication
- `403` -> do not re-authenticate; surface access denied / permission error
**Failure symptoms:** app renders before auth is ready, reference calls miss auth headers, refresh storms occur, or `403` incorrectly forces logout.
---
## 5. Backend auth files and behavior
- [ ] Generated backend includes:
- `server/src/auth/auth.module.ts`
- JWT guard
- roles guard
- `@Public()` decorator
- `@Roles()` decorator
- typed authenticated principal interface
- typed config validation in `server/src/config/`
- [ ] `jose` is installed.
- [ ] JWT verification uses issuer + audience + JWKS.
- [ ] JWKS resolution follows this exact priority:
1. `KEYCLOAK_JWKS_URL`
2. OIDC discovery
3. `${issuer}/protocol/openid-connect/certs`
- [ ] Authorization roles are extracted only from `realm_access.roles`.
- [ ] Deprecated Keycloak-specific Node adapters are not used.
**Failure symptoms:** invalid tokens are accepted, valid tokens are rejected due to bad JWKS resolution, or RBAC depends on unstable/non-standard claims.
---
## 6. CRUD protection and RBAC defaults
- [ ] `/health` is public.
- [ ] Each generated CRUD controller method other than explicit public routes is protected by the generated auth/RBAC infrastructure.
- [ ] CRUD RBAC defaults are present:
- `GET` -> `viewer | editor | admin`
- `POST`, `PATCH`, `PUT` -> `editor | admin`
- `DELETE` -> `admin`
- [ ] Unauthenticated request to a protected route returns `401`.
- [ ] Authenticated user with insufficient role receives `403`.
**Failure symptoms:** anonymous CRUD access remains open, or insufficient-role users are not denied properly.
---
## 7. PrismaService implementation
- [ ] A `PrismaService` (or equivalent) class exists and extends `PrismaClient`.
- [ ] It implements `OnModuleInit` and calls `await this.$connect()` in `onModuleInit()`.
- [ ] It does not use `this.$on('beforeExit', ...)` or any `beforeExit` hook.
**Failure symptom:** deprecation/runtime errors in Prisma 5 when using `beforeExit`.
**Reference:** `backend/prisma-service.md`
---
## 8. Prisma client lifecycle
- [ ] `package.json` includes a script that runs Prisma client generation:
- either `"postinstall": "prisma generate"` (or `npx prisma generate`)
- or clear documentation to run `npx prisma generate` after install
- [ ] After schema generation or change, `npx prisma generate` has been run or will run via `postinstall`.
**Failure symptom:** `Cannot find module '@prisma/client'` or missing generated types.
---
## 9. Database migration
- [ ] Migration workflow is documented.
- [ ] Instruction to run `npx prisma migrate dev` exists after first generation or schema change.
- [ ] Generation pipeline includes or documents the migration step.
**Failure symptom:** tables do not exist; Prisma errors on first query.
---
## 10. REST route parameters
- [ ] For each entity, path parameters use the correct primary key name from the DSL.
- [ ] Entity with PK `id` uses `/:id`.
- [ ] Entity with non-`id` primary key (for example `code`) uses the actual PK name such as `/:code`, not `/:id`.
**Failure symptom:** controller expects one param name while the route defines another, leading to broken CRUD behavior.
**Reference:** `backend/architecture.md`
---
## 11. DTO type mapping and React Admin ID compatibility
- [ ] DSL `decimal` maps to DTO/API `string`.
- [ ] DSL `date` maps to DTO/API `string` (ISO) or equivalent string serialization.
- [ ] Every API response object contains a field named `id`.
- [ ] If the entity primary key is not named `id`, the response maps the primary key to `id`.
- [ ] For entities with non-`id` primary keys, backend list/query logic translates React Admin `_sort=id` to the real primary key field.
- [ ] Generated ORM `orderBy` clauses never reference synthetic `id` when the underlying model field does not exist.
**Failure symptoms:** serialization issues for decimals/dates, or React Admin cannot identify records.
---
## 12. Update payload sanitization
- [ ] Update endpoints do not pass `id` or the primary key in Prisma `data`.
- [ ] Generated update methods remove `id`, the entity primary key, and readonly attributes before calling `prisma.*.update()`.
**Failure symptom:** Prisma throws because immutable or invalid fields are passed in update `data`.
---
## 13. Database runtime
- [ ] `docker-compose.yml` exists at the project root.
- [ ] It defines a PostgreSQL service with image `postgres:16`, port `5432`, and credentials matching `DATABASE_URL`.
- [ ] `docker compose up -d` starts the database successfully.
- [ ] `docker-compose.yml` does not add Keycloak in this repository.
**Failure symptom:** Prisma cannot reach the development database or repo topology drifts from the documented external-Keycloak model.
---
## 14. Migrations, seed, and health endpoint
- [ ] `npx prisma migrate dev` runs successfully from `server/`.
- [ ] Seed script exists at `server/prisma/seed.ts` (or equivalent).
- [ ] `npx prisma db seed` runs without error.
- [ ] Backend exposes `GET /health`.
- [ ] `GET /health` returns HTTP 200 with a status payload.
**Failure symptom:** development bootstrap cannot complete end-to-end.
---
# Summary Table
| Check | Required artifact / rule |
| --- | --- |
| Frontend env | `client/.env.example` with required Vite auth vars |
| Backend env | `server/.env.example` with DB, CORS, and Keycloak vars |
| Git ignore | Root/server/client `.gitignore` exclude local-only artifacts |
| Fail-fast config | Startup fails when required auth env is missing |
| Realm artifact | Root generated realm import artifact with self-contained auth setup |
| Frontend auth | `keycloak.ts`, `authProvider.ts`, authenticated `dataProvider.ts` |
| Frontend identity | Token-claim based `getIdentity()`; no `loadUserProfile()` / `/account` dependency |
| Backend auth | `AuthModule`, guards, decorators, typed principal |
| JWKS strategy | explicit URL -> discovery -> certs fallback |
| Role source | `realm_access.roles` only |
| CRUD RBAC | GET viewer/editor/admin; write editor/admin; delete admin |
| `/health` | Public and returns 200 |
| Protected route unauthenticated | Returns `401` |
| Protected route insufficient role | Returns `403` |
| Token storage | No `localStorage` / `sessionStorage` persistence |
| Token refresh | Concurrency-safe single in-flight refresh |
| Prisma lifecycle | `OnModuleInit` + `$connect()`, no `beforeExit` |
| Update sanitization | Strip `id` / PK / readonly before Prisma update |
| React Admin `id` | Every record includes `id` |
| Natural-key sorting | Map React Admin `_sort=id` to the real primary key field |
| Database runtime | PostgreSQL compose exists and starts |
---
# Integration with generation pipeline
1. Backend and frontend generation must produce artifacts that satisfy the above by default.
2. The generator must successfully build the fullstack app from `domain/*.dsl` alone; optional overrides may refine output but cannot be required.
3. Runtime bootstrap must include Keycloak realm import/verification before app startup.
4. After generation, run this checklist manually or via an automated script.
5. If any check fails, update the generator context so future runs pass without manual repair.

View File

@@ -1,122 +0,0 @@
# Runtime Bootstrap
After project generation, the following commands and prerequisites must work in order so that the application runs without ad-hoc auth or database setup.
The generator must produce a **runnable development environment** consisting of:
- external Keycloak identity provider
- backend API
- frontend SPA
- PostgreSQL database
Runtime bootstrap assumes the application was generated from `domain/*.dsl` plus optional non-duplicating overrides only. There is no separate DTO/API/UI DSL bootstrap step.
The generator must also produce the runtime artifacts required to bootstrap auth from zero, including a root-level Keycloak realm import artifact. The repository default example filename is `toir-realm.json`, but future generations must allow a project-specific equivalent.
---
# Bootstrap Sequence
## 1. Prepare Keycloak
Before starting the application, ensure an external Keycloak server is reachable and import or verify the generated realm artifact:
- root-level generated Keycloak realm import artifact
- repository default example filename: `toir-realm.json`
The runtime instructions must require importing or verifying this realm before frontend/backend startup.
Keycloak bootstrap expectations:
- realm import is self-contained
- frontend client exists
- backend audience client exists
- realm roles exist
- issued access tokens reliably contain `sub`, `aud`, and `realm_access.roles`
The generator must not rely on undeclared built-in client scopes magically existing after realm import.
---
## 2. Start the database
From the **project root**:
```bash
docker compose up -d
```
This starts the PostgreSQL container. The backend connects to it using `DATABASE_URL` from `server/.env`.
---
## 3. Backend setup and start
```bash
cd server
npm install
npx prisma generate
npx prisma migrate dev
npx prisma db seed
npm run start
```
- `npm install` installs dependencies and runs `postinstall` if configured.
- `npx prisma generate` ensures Prisma client is generated explicitly after install or schema changes.
- `npx prisma migrate dev` creates/applies migrations.
- `npx prisma db seed` inserts minimal development data.
- `npm run start` starts the NestJS server.
Backend startup requirements:
- required database and auth env vars must be present
- startup must fail fast if required env vars are missing
- CORS must support the SPA -> API bearer-token model
- `/health` must remain public
---
## 4. Frontend setup and start
In a separate terminal, from the **project root**:
```bash
cd client
npm install
npm run dev
```
- `npm install` installs frontend dependencies including `keycloak-js`.
- `npm run dev` starts the Vite dev server.
Frontend startup requirements:
- required Vite auth vars must be present
- startup must fail fast if required auth vars are missing
- Keycloak must initialize before the SPA is rendered
- login must use redirect-based Keycloak authentication only
---
# Success Criteria
After completing the bootstrap sequence:
- Keycloak is reachable and the generated realm has been imported or verified.
- Database container is running and Prisma can connect.
- Backend responds on `/health` without auth.
- Protected backend routes require a valid bearer token.
- Frontend loads, redirects through Keycloak login when needed, and uses bearer auth for all API calls.
The generator is responsible for producing all artifacts and instructions needed for this sequence:
- `docker-compose.yml`
- schema
- migrations
- seed
- backend/frontend env examples
- health endpoint
- auth infrastructure
- root-level Keycloak realm import artifact
Docker scope must remain limited to PostgreSQL. Keycloak remains an external dependency in this repository.

View File

@@ -1,114 +0,0 @@
# Project Scaffolding Rules
The generator must use **official CLI tools** to create base project structures.
The AI must **not** manually generate the entire project skeleton by hand. CLI scaffolding reduces drift and keeps the generated project aligned with current NestJS and Vite conventions.
Auth is part of the default generated runtime. Scaffolding must therefore install the required frontend and backend auth dependencies during the normal project bootstrap path.
---
# Backend Scaffolding
Use **NestJS CLI**.
## Command
```bash
npx @nestjs/cli@10.3.2 new server --package-manager npm --skip-git
```
## Rules
- **Project directory** must be `server`.
- **TypeScript** must be used.
- **npm** must be the package manager.
- **Git** initialization must be skipped.
## After scaffolding — install required dependencies
Run from the `server` directory:
```bash
npm install @prisma/client
npm install @nestjs/config
npm install jose
npm install prisma --save-dev
```
## Backend auth dependency rules
- `jose` must be installed by default because JWT verification is part of the default generated backend.
- The generator must **not** install deprecated Keycloak-specific Node adapters such as `keycloak-connect`.
---
# Frontend Scaffolding
Use **Vite CLI**.
## Command
```bash
npm create vite@5.2.0 client -- --template react-ts
```
## Rules
- **Project directory** must be `client`.
- **React + TypeScript** template must be used.
## After scaffolding — install required dependencies
Run from the `client` directory:
```bash
npm install react-admin
npm install ra-data-simple-rest
npm install @mui/material @emotion/react @emotion/styled
npm install keycloak-js
```
## Frontend auth dependency rules
- `keycloak-js` must be installed by default because redirect-based Keycloak login is part of the default generated frontend.
- The generated frontend must use `keycloak-js` for Authorization Code + PKCE and must not generate a custom in-app username/password login form.
---
# Scaffolding Strategy
Generation pipeline order:
1. **Parse DSL** — Read `domain/*.dsl` as the single required input. If present, optional override files under `overrides/` may be applied after domain parsing, but DTO/API/UI DSL files must not be required.
2. **Run CLI scaffolding** — Create `server` with NestJS CLI and `client` with Vite CLI; install runtime and auth dependencies listed above.
3. **Code generation** — Generate Prisma schema, NestJS modules/DTOs/PrismaService/auth infrastructure, and React Admin resources/auth integration.
4. **Runtime infrastructure** — Generate backend/frontend `.env.example`, root/package `.gitignore` files, runtime config files, lifecycle scripts, and a root-level Keycloak realm import artifact (repository default example filename: `toir-realm.json`).
5. **Database runtime** — Generate `docker-compose.yml` in project root with PostgreSQL service (`postgres`, image `postgres:16`, port `5432:5432`).
6. **Migration** — Apply schema with `npx prisma migrate dev`.
7. **Seed** — Populate minimal development data with `npx prisma db seed`.
8. **Validation** — Run checks from `generation/post-generation-validation.md`, including auth validation and realm-template validation.
Scaffolding (steps 12) must be done with the CLI. Steps 38 must be generated from `domain/*.dsl`, optional non-duplicating overrides in `overrides/`, and the project context documents, including the auth-specific context in `auth/*.md`.
There is no separate DTO/API/UI DSL preparation step in the scaffolding workflow.
## Git ignore rules
The generated project must include:
- root `.gitignore`
- `server/.gitignore`
- `client/.gitignore`
These files must keep local-only artifacts out of git, including at minimum:
- `node_modules/`
- `dist/`
- `dist-ssr/`
- `coverage/`
- `*.tsbuildinfo`
- `.env`
- `.env.local`
- `.env.*.local`
The generator must not ignore committed source, documentation, or `.env.example` files.

View File

@@ -1,47 +0,0 @@
# Update Strategy
When the DSL changes, regeneration must preserve the default auth-enabled runtime rather than falling back to CRUD-only output.
`domain/*.dsl` remains the single required source of truth for regeneration. DTOs, API contracts, and React Admin resources must be re-derived from it on every run. Optional overrides in `overrides/api-overrides.dsl` and `overrides/ui-overrides.dsl` may be applied after derivation, but they must never duplicate or redefine the domain model.
Regeneration must not resurrect or depend on supplemental DTO/API/UI DSL inputs. Every derived layer must be recalculated from `domain/*.dsl` plus optional non-duplicating overrides only.
## Required regeneration sequence
1. Regenerate `prisma/schema.prisma`.
2. Run `npx prisma migrate dev`.
3. Regenerate NestJS entity modules, DTOs, controllers, and services.
4. Regenerate backend auth infrastructure:
- `AuthModule`
- guards
- decorators
- typed authenticated principal
- typed config validation
- CRUD RBAC decorations
5. Regenerate React Admin resources.
6. Regenerate frontend auth infrastructure:
- `src/config/env.ts`
- `src/auth/keycloak.ts`
- `src/auth/authProvider.ts`
- authenticated `dataProvider.ts`
- `App.tsx` auth wiring
- `main.tsx` init-before-render flow
7. Regenerate backend and frontend `.env.example` files so the auth env contract stays in sync.
8. Regenerate root/package `.gitignore` files so local-only artifacts remain out of git after regeneration.
9. Regenerate the root-level Keycloak realm import artifact. The repository default example filename is `toir-realm.json`, but the generator must allow a project-specific equivalent.
10. Re-run post-generation validation, including:
- gitignore coverage for dependency, build, env, coverage, and tsbuildinfo artifacts
- auth dependency checks
- fail-fast env checks
- token-claim based identity with no `loadUserProfile()` / `/account` dependency
- `/health` public check
- unauthenticated protected route -> `401`
- insufficient role -> `403`
- natural-key `_sort=id` mapping checks
- realm-template validation
## Guardrails
- Regeneration must not strip auth back out of the project.
- Auth remains outside the DSL grammar, but it is part of the default generated runtime.
- If a DSL change affects entities or routes, the generator must re-apply the default CRUD RBAC rules automatically.
- If a DSL change affects runtime topology or naming, the generator must keep backend/frontend env examples, CORS rules, and the generated realm import artifact aligned with the generated app.

View File

@@ -0,0 +1,2 @@
// Optional overrides:
// resource EquipmentType path "equipment-types";

View File

@@ -0,0 +1,2 @@
// Optional overrides:
// field EquipmentType.code widget "text";

10
package.json Normal file
View File

@@ -0,0 +1,10 @@
{
"name": "toir-generation-context",
"private": true,
"scripts": {
"generate:domain-summary": "node tools/generate-domain-summary.mjs",
"validate:generation": "node tools/validate-generation.mjs",
"validate:generation:runtime": "node tools/validate-generation.mjs --run-runtime",
"validate:generation:artifacts": "node tools/validate-generation.mjs --artifacts-only"
}
}

79
prompts/auth-rules.md Normal file
View File

@@ -0,0 +1,79 @@
# Auth Rules
This repository keeps the current LLM-first CRUD generation architecture as the primary working baseline.
- Auth is part of the default generation path, not a post-generation addon.
- `server/` is the active backend target output path.
- `client/` is the active frontend target output path.
- The generated runtime stays SPA + API + external Keycloak + PostgreSQL only.
## Frontend auth invariants
- Use `keycloak-js` with redirect-based login only.
- Initialize Keycloak before rendering the SPA.
- Use Authorization Code Flow + PKCE (`S256`).
- Keep `authProvider`, `dataProvider`, `getIdentity()`, `getPermissions()`, and `checkError()` as stable provider seams.
- Derive identity from token claims already present in the parsed token.
- Do not call `loadUserProfile()` and do not depend on `/account` for the baseline app.
- `401` must force re-authentication; `403` must stay an authorization error.
- Keep token handling in memory and refresh through one shared in-flight operation.
## Working runtime defaults
Use the already working project defaults unless a prompt explicitly overrides them.
- Frontend Keycloak base URL example:
- `VITE_KEYCLOAK_URL=https://sso.greact.ru`
- Frontend realm and client example:
- `VITE_KEYCLOAK_REALM=toir`
- `VITE_KEYCLOAK_CLIENT_ID=toir-frontend`
- Backend issuer and audience example:
- `KEYCLOAK_ISSUER_URL=https://sso.greact.ru/realms/toir`
- `KEYCLOAK_AUDIENCE=toir-backend`
- CORS example:
- `CORS_ALLOWED_ORIGINS=http://localhost:5173,https://toir-frontend.greact.ru`
Anti-regression rule:
- Do not switch the baseline Keycloak example back to `http://localhost:8080` in generated `.env.example` files unless the prompt explicitly asks for a local Keycloak runtime.
- Localhost Keycloak values may exist in private `.env.local` or `.env` overrides, but they are not the default project examples.
## Backend auth invariants
- Verify JWTs with `jose`.
- Validate issuer + audience + signature via JWKS.
- Resolve JWKS in this order:
1. `KEYCLOAK_JWKS_URL`
2. OIDC discovery at `/.well-known/openid-configuration`
3. `${issuer}/protocol/openid-connect/certs`
- Extract roles only from `realm_access.roles`.
- Keep `/health` public and all generated CRUD routes protected by default.
## Auth anti-regression invariants
- The accepted JWKS resolution chain above is the only baseline truth path. Do not document one order and implement another.
- If auth implementation changes, `prompts/auth-rules.md` and `prompts/validation-rules.md` must be updated in the same change.
- Do not skip OIDC discovery when no explicit `KEYCLOAK_JWKS_URL` is provided.
- Do not switch role extraction to alternative claims unless the prompt explicitly changes the baseline contract.
- Do not reintroduce localhost Keycloak defaults into shared baseline examples.
## Realm artifact contract
- A physical root-level `*-realm.json` artifact is mandatory output.
- The artifact must be importable, versioned, and aligned with generated backend/frontend env contracts.
- It must parameterize:
- realm name
- frontend client id
- backend client id / audience
- local and production frontend URLs
- artifact filename
- It must explicitly deliver:
- `sub`
- `aud`
- `realm_access.roles`
- It must define:
- realm roles `admin`, `editor`, `viewer`
- a public SPA client with PKCE S256
- a bearer-only backend client
- an explicit audience client scope
- explicit protocol mappers for baseline identity and role claims

74
prompts/backend-rules.md Normal file
View File

@@ -0,0 +1,74 @@
# Backend Rules
The backend remains derived from `domain/*.dsl` inside the existing LLM-first pipeline. No compiler platform or generator engine is introduced.
## Backend scaffold baseline
- Start backend initialization from the official NestJS CLI workspace, not from manually created files.
- The backend must remain compatible with standard Nest workspace tooling such as `nest build` and `nest start`.
- Preserve the core Nest workspace files generated by the CLI, especially:
- `server/tsconfig.json`
- `server/tsconfig.build.json`
- `server/nest-cli.json`
- `server/src/main.ts`
- `server/src/app.module.ts`
- For domain resources, prefer official Nest CLI generation patterns for modules/controllers/services/resources and then adapt the generated code to Prisma and auth requirements.
- Do not delete required Nest workspace files just because the LLM can inline a smaller custom structure.
## Forbidden backend generation patterns
- Do not bootstrap `server/` by hand-writing a pseudo-Nest project from memory.
- Do not remove `tsconfig.json`, `tsconfig.build.json`, or `nest-cli.json` after generation.
- Do not replace standard Nest package scripts with ad hoc commands that break `nest build` or `nest start`.
- Do not continue CRUD generation on top of a degraded backend workspace without repairing the workspace first.
## Domain-derived output
- `domain/*.dsl` is the source of truth for entities, fields, primary keys, foreign keys, and enums.
- `domain-summary.json` is a derived artifact used to stabilize LLM generation and validation. It must never replace the DSL as the source of truth.
- Each entity becomes:
- a Prisma model
- a NestJS module
- a controller
- a service
- create/update DTOs
## DTO and Prisma mapping
- `decimal` -> Prisma `Decimal`, DTO/API `string`
- `date` -> Prisma `DateTime`, DTO/API `string`
- Enums remain string-valued in DTO/API contracts
## CRUD and natural-key invariants
- CRUD routes use the real primary key name in the path.
- Every API record returned to React Admin must include `id`.
- For entities whose primary key is not `id`, the backend must map the real key to `id`.
- Natural-key list/sort logic must never build ORM `orderBy` against a fake physical `id`.
## Service invariants
- Never pass raw update DTOs into Prisma update `data`.
- Remove `id`, the real primary key, and readonly fields from update payloads before calling Prisma.
- Keep PrismaService lightweight:
- extend `PrismaClient`
- implement `OnModuleInit`
- call `$connect()`
- do not use `beforeExit`
## Reproducibility invariants
- A freshly generated backend must be bootstrappable with ordinary Nest + Prisma commands from `prompts/runtime-rules.md`.
- Missing TypeScript or Nest workspace config is a generation failure, not an acceptable simplification.
- The baseline backend should fail only on missing runtime dependencies or env values, not because the Nest workspace itself is incomplete.
## Recovery rule if backend workspace degraded
- If required Nest scaffold files are missing or broken, restore the official workspace baseline before editing Prisma models, modules, controllers, services, or DTOs.
- Treat workspace repair as higher priority than feature generation, because generated domain code on top of a broken workspace is invalid baseline output.
## Backend auth defaults
- `GET` -> `viewer | editor | admin`
- `POST`, `PATCH`, `PUT` -> `editor | admin`
- `DELETE` -> `admin`

56
prompts/frontend-rules.md Normal file
View File

@@ -0,0 +1,56 @@
# Frontend Rules
The frontend stays a React Admin SPA generated from `domain/*.dsl` and anchored to the existing auth seams.
## Frontend scaffold baseline
- Start frontend initialization from the official Vite React TypeScript scaffold, not from manually assembled files.
- Preserve a valid Vite workspace baseline, including:
- `client/index.html`
- `client/tsconfig.json`
- `client/vite.config.*`
- `client/src/main.tsx`
- Add React Admin and auth seams on top of that baseline instead of replacing the workspace with a hand-written minimal shell.
- Do not delete required Vite entry/config files just because the LLM can write a shorter custom setup.
## Forbidden frontend generation patterns
- Do not bootstrap `client/` by hand-writing a pseudo-Vite project from memory.
- Do not remove `index.html`, `tsconfig*`, or `vite.config.*` after generation.
- Do not replace standard Vite package scripts with ad hoc commands that break `vite build`, `vite dev`, or `vite preview`.
- Do not continue React Admin resource generation on top of a degraded frontend workspace without repairing the workspace first.
## Resource generation
- Each entity becomes a React Admin resource with list/create/edit/show views.
- Resource names must stay aligned with backend path segments.
- Foreign keys must use `ReferenceInput` / `ReferenceField`.
## Provider seams
- `client/src/dataProvider.ts` is the single authenticated request seam.
- `client/src/auth/authProvider.ts` is the single React Admin auth seam.
- Auth logic must not leak into resource components.
## Identity and permissions
- `getIdentity()` must resolve from parsed token claims.
- `getPermissions()` may expose realm roles for UI awareness.
- Backend enforcement remains authoritative.
## React Admin compatibility
- Every resource record must include `id`.
- Natural-key resources must preserve route, update, and sort compatibility with React Admin contracts.
- Frontend requests must continue to work when the real primary key is not named `id`.
## Reproducibility invariants
- A freshly generated frontend must remain compatible with standard Vite commands such as `npm run dev` and `npm run build`.
- Missing Vite workspace files or missing local Vite executable wiring is a generation failure, not an acceptable simplification.
- The generated frontend should fail only on missing installation/env/runtime backend availability, not because the Vite app structure itself is incomplete.
## Recovery rule if frontend workspace degraded
- If required Vite scaffold files are missing or broken, restore the official workspace baseline before editing resources, auth seams, or UI code.
- Treat workspace repair as higher priority than feature generation, because generated React Admin code on top of a broken Vite workspace is invalid baseline output.

146
prompts/general-prompt.md Normal file
View File

@@ -0,0 +1,146 @@
ROLE
You are a Staff-level Fullstack Platform Engineer working inside the established LLM-first CRUD generation baseline.
Use context7 when official framework guidance is needed.
This repository is not a new generator engine. Do not redesign it into a planner/emitter/runtime platform.
GOAL
Strengthen and use the existing LLM-first CRUD generation pipeline.
- Keep `domain/*.dsl` as the source of truth for the domain model.
- Keep `server/` as the active backend target output path.
- Keep `client/` as the active frontend target output path.
- Keep Keycloak auth in the default generation path.
- Keep PostgreSQL as the only Dockerized runtime dependency.
- Generate and maintain:
- `domain-summary.json`
- a root-level `*-realm.json`
- backend/frontend auth seams
- runtime/env/bootstrap artifacts
- validation-gate-compatible output
Use the already working runtime defaults for this project unless the prompt explicitly overrides them:
- frontend Keycloak URL example: `https://sso.greact.ru`
- frontend realm/client examples: `toir`, `toir-frontend`
- backend issuer/audience examples: `https://sso.greact.ru/realms/toir`, `toir-backend`
- production frontend origin example: `https://toir-frontend.greact.ru`
Do not silently regress these examples to localhost Keycloak defaults.
ACTIVE KNOWLEDGE BLOCKS
Read in this order:
1. `domain/dsl-spec.md`
2. `domain/*.dsl`
3. `domain-summary.json` if present
4. `prompts/auth-rules.md`
5. `prompts/backend-rules.md`
6. `prompts/frontend-rules.md`
7. `prompts/runtime-rules.md`
8. `prompts/validation-rules.md`
Interpretation rules:
- `domain/*.dsl` is authoritative.
- `domain-summary.json` is derived. Regenerate or validate it against the DSL; never treat it as the source of truth.
INPUT CONTRACT
Required:
- `domain/*.dsl`
Optional:
- `overrides/api-overrides.dsl`
- `overrides/ui-overrides.dsl`
Rules:
- Do not require DTO/API/UI DSL files.
- Do not resurrect multi-DSL source-of-truth behavior.
- Optional overrides may refine derived API/UI behavior but must not redefine the domain model.
PIPELINE CONTRACT
1. Parse `domain/*.dsl`.
2. Generate or refresh `domain-summary.json`.
3. Scaffold with official framework CLI conventions where scaffolding is needed.
4. Generate or maintain backend/frontend code in `server/` and `client/`.
5. Generate or maintain the root-level realm artifact.
6. Generate or maintain env/bootstrap/runtime artifacts.
7. Run the lightweight validation gate.
REPAIR-BEFORE-GENERATE ORDER
1. Inspect whether `server/` and `client/` are still valid framework workspaces.
2. If either workspace is degraded, repair the official scaffold baseline first.
3. Only after workspace repair, generate or update domain-derived feature code.
4. Only after generation, run validation and buildability checks.
CLI-FIRST SCAFFOLDING CONTRACT
- Never hand-write a fresh framework workspace from scratch when an official CLI exists for that framework.
- For `server/`, initialize the workspace from the official NestJS CLI baseline first, then generate/adapt modules/resources inside that workspace.
- For `client/`, initialize the workspace from the official Vite React TypeScript baseline first, then add React Admin, auth seams, and generated resources.
- Treat CLI scaffolding as the required baseline for compiler config, package scripts, workspace metadata, and default entry files.
- Manual creation is allowed only for domain-derived feature code after the official workspace exists.
- If a workspace already exists but is missing required CLI baseline files, repair it back to a valid official-style workspace before adding more generated code.
ANTI-REGRESSION FAILURES
Treat the following as baseline violations, not acceptable improvisations:
- creating a NestJS workspace by manually writing `package.json`, `tsconfig*`, `nest-cli.json`, and `src/*` from memory instead of starting from official CLI conventions
- creating a Vite React TypeScript workspace by manually writing `package.json`, `index.html`, `tsconfig*`, `vite.config.*`, and `src/*` from memory instead of starting from official scaffold conventions
- deleting required framework scaffold files after generation because the app appears to work with a smaller custom structure
- declaring generation successful when workspace validity or buildability is broken or unverified
- letting prompts promise an auth/runtime contract that validation does not enforce
- treating one project-specific DSL filename as the only allowed source instead of supporting `domain/*.dsl`
OUTPUT CONTRACT
The baseline output must include:
- `server/prisma/schema.prisma`
- `server/.env.example`
- `client/.env.example`
- `client/src/auth/keycloak.ts`
- `client/src/auth/authProvider.ts`
- `client/src/dataProvider.ts`
- `server/src/auth/*`
- `docker-compose.yml`
- `domain-summary.json`
- root-level `*-realm.json`
The baseline output must also remain a real framework workspace, not a prompt-only file collection:
- `server/tsconfig.json`
- `server/tsconfig.build.json`
- `server/nest-cli.json`
- `client/index.html`
- `client/tsconfig.json`
- `client/vite.config.*`
NON-GOALS
- No new generator engine
- No compiler/IR platform
- No heavy codegen redesign
- No replacement of the old LLM-first architecture
COMPLETION INVARIANTS
- Generation is incomplete if `server/` is not a valid NestJS workspace.
- Generation is incomplete if `client/` is not a valid Vite React TypeScript workspace.
- Generation is incomplete if auth rules, runtime rules, and validation rules describe different truth paths.
- Generation is incomplete if buildability is broken.
- If buildability cannot be checked because dependencies are missing, report that state explicitly; do not report a green result for buildability.
VALIDATION
Before considering the output complete, satisfy `prompts/validation-rules.md`.

96
prompts/runtime-rules.md Normal file
View File

@@ -0,0 +1,96 @@
# Runtime Rules
This repository keeps the current LLM-first CRUD generation architecture as the primary working baseline and strengthens the existing pipeline instead of replacing it.
## Baseline runtime topology
- `server/` is the active backend target output path.
- `client/` is the active frontend target output path.
- Docker scope remains PostgreSQL only.
- Keycloak remains external to the repository runtime.
- The project remains LLM-first: markdown knowledge blocks in `prompts/` orchestrate generation, while active generated/maintained code lives in `server/` and `client/`.
## Required input and derived artifacts
- Source of truth:
- `domain/*.dsl`
- Required derived artifacts:
- `domain-summary.json`
- root-level `*-realm.json`
`domain-summary.json` exists to stabilize generation and validation; it must be regenerated from the DSL and treated as non-authoritative.
## Output contract
The strengthened baseline must produce and keep aligned:
- `server/prisma/schema.prisma`
- backend/frontend env examples
- backend/frontend auth seams
- root `.gitignore`, `server/.gitignore`, `client/.gitignore`
- `docker-compose.yml`
- `domain-summary.json`
- root-level realm import artifact
## Concrete runtime examples
Use these as the baseline examples for this project unless the prompt explicitly overrides them:
- Backend:
- `PORT=3000`
- `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/toir"`
- `CORS_ALLOWED_ORIGINS="http://localhost:5173,https://toir-frontend.greact.ru"`
- `KEYCLOAK_ISSUER_URL="https://sso.greact.ru/realms/toir"`
- `KEYCLOAK_AUDIENCE="toir-backend"`
- Frontend:
- `VITE_API_URL=http://localhost:3000`
- `VITE_KEYCLOAK_URL=https://sso.greact.ru`
- `VITE_KEYCLOAK_REALM=toir`
- `VITE_KEYCLOAK_CLIENT_ID=toir-frontend`
These example values come from the already working runtime shape and are preferred over local-only Keycloak placeholders.
## Runtime bootstrap
1. Import the root-level realm artifact into Keycloak.
2. Start PostgreSQL with `docker compose up -d`.
3. From `server/` run:
- initialize or repair the workspace with official Nest CLI scaffolding if required before generating domain code
- `npm install`
- `npx prisma generate`
- `npx prisma migrate dev`
- `npx prisma db seed`
- `npm run build`
- `npm run start`
4. From `client/` run:
- initialize or repair the workspace with official Vite React TypeScript scaffolding if required before generating app code
- `npm install`
- `npm run build`
- `npm run dev`
## Recovery and completion rules
- Repair degraded framework workspaces before applying any new domain-derived generation changes.
- Do not mark generation complete while `server/` or `client/` remains non-buildable.
- If dependency installation has not happened yet, buildability may be reported as skipped, but it must never be reported as green without verification.
- Runtime/bootstrap instructions are reusable project baseline rules; TOiR names remain examples, not the only supported domain project.
## Scaffold expectations
- NestJS workspace creation should follow the official Nest CLI path for new applications and resource scaffolding.
- Vite frontend creation should follow the official Vite `create-vite` path for React TypeScript applications.
- The LLM may customize generated code after scaffold creation, but must not replace official workspace initialization with ad hoc file creation.
## Common generation failures to avoid
- starting feature generation before scaffold repair
- leaving deleted framework config files unrepaired because the current diff looks smaller
- accepting a form-only validation pass while buildability is unknown
- binding runtime rules to one project-specific DSL filename instead of `domain/*.dsl`
## Baseline intent
- No new generator engine
- No compiler platform
- No planner/emitter/runtime redesign
- Only the current LLM-first pipeline, strengthened by summary, realm, and validation artifacts

View File

@@ -0,0 +1,85 @@
# Validation Rules
Validation is now a lightweight automated gate instead of a prose-only checklist.
## Commands
- `npm run generate:domain-summary`
- `npm run validate:generation`
- `npm run validate:generation:runtime`
## Prompt-Gate Alignment Rule
- Every invariant described as required in the active prompt corpus must either be enforced by this gate or be called out explicitly as a manual/runtime-only check.
- Validation must not stay silent about a violation that the prompts describe as forbidden.
- Validation must not report green buildability when build verification was skipped.
## Gate groups
### Build checks
- at least one `domain/*.dsl` file exists
- required artifacts exist
- Prisma schema exists
- frontend/backend env contracts exist
- frontend/backend framework workspace files exist
- `domain-summary.json` matches the current DSL
- project `.env.example` files keep the working domain-based Keycloak examples unless explicitly overridden
- `server/` remains a valid Nest workspace
- `client/` remains a valid Vite workspace
- generation must not pass validation if framework scaffolding files were deleted and replaced by a hand-written minimal skeleton
- if dependencies are installed, build verification runs for `server/` and `client/`
- if dependencies are missing, build verification is reported as skipped with reason instead of green
### Auth checks
- frontend auth seam files exist
- backend auth seam files exist
- `401` and `403` semantics stay split
- auth code keeps the required Keycloak/JWT contracts
- JWKS resolution chain matches the contract:
1. explicit `KEYCLOAK_JWKS_URL`
2. OIDC discovery
3. certs fallback
### Natural-key checks
- response records expose `id`
- route/update contracts use the real primary key
- natural-key sort/update paths do not regress to a fake physical `id`
### Realm checks
- a root `*-realm.json` artifact exists
- realm roles exist
- audience delivery exists
- required claims are explicit
- SPA/backend client structure is explicit
### Runtime checks
- compose topology stays PostgreSQL-only
- Prisma lifecycle scripts remain in place
- `/health` stays public
- backend can execute `npm run build` inside `server/`
- frontend can execute `npm run build` inside `client/` after dependencies are installed
- client/server `.env.example` stay aligned with the working runtime defaults:
- `https://sso.greact.ru`
- `toir`
- `toir-frontend`
- `toir-backend`
- `https://toir-frontend.greact.ru`
- optional runtime execution mode runs:
- `npx prisma generate`
- `npx prisma migrate dev`
- `npx prisma db seed`
### Scaffold checks
- backend initialization starts from official Nest CLI scaffolding
- frontend initialization starts from official Vite React TypeScript scaffolding
- feature generation happens after scaffold creation, not instead of scaffold creation
- repair happens before generation when workspace is degraded
- required framework configs and entry files must survive subsequent LLM edits
The automated gate is intentionally small. It enforces the critical reproducibility contract without turning the repository into a test platform or a generator engine.

172
toir-realm.json Normal file
View File

@@ -0,0 +1,172 @@
{
"realm": "toir",
"enabled": true,
"displayName": "TOIR",
"sslRequired": "external",
"registrationAllowed": false,
"registrationEmailAsUsername": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"rememberMe": true,
"verifyEmail": false,
"roles": {
"realm": [
{
"name": "admin",
"description": "Full administrative access"
},
{
"name": "editor",
"description": "Can create and modify data"
},
{
"name": "viewer",
"description": "Read-only access"
}
]
},
"clientScopes": [
{
"name": "api-audience",
"description": "Adds backend audience to SPA access token",
"protocol": "openid-connect",
"attributes": {
"display.on.consent.screen": "false",
"include.in.token.scope": "false"
},
"protocolMappers": [
{
"name": "aud-toir-backend",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "toir-backend",
"id.token.claim": "false",
"access.token.claim": "true",
"introspection.token.claim": "true"
}
}
]
}
],
"clients": [
{
"clientId": "toir-frontend",
"name": "toir-frontend",
"description": "Frontend SPA client",
"enabled": true,
"protocol": "openid-connect",
"publicClient": true,
"bearerOnly": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"fullScopeAllowed": true,
"rootUrl": "https://toir-frontend.greact.ru",
"baseUrl": "https://toir-frontend.greact.ru",
"redirectUris": [
"https://toir-frontend.greact.ru/*",
"http://localhost:5173/*"
],
"webOrigins": [
"https://toir-frontend.greact.ru",
"http://localhost:5173"
],
"attributes": {
"pkce.code.challenge.method": "S256"
},
"defaultClientScopes": [
"api-audience"
],
"optionalClientScopes": [
"offline_access"
],
"protocolMappers": [
{
"name": "sub",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "id",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "sub",
"jsonType.label": "String"
}
},
{
"name": "preferred_username",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "username",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "preferred_username",
"jsonType.label": "String"
}
},
{
"name": "email",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "email",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "email",
"jsonType.label": "String"
}
},
{
"name": "name",
"protocol": "openid-connect",
"protocolMapper": "oidc-full-name-mapper",
"consentRequired": false,
"config": {
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
},
{
"name": "realm roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "realm_access.roles",
"jsonType.label": "String"
}
}
]
},
{
"clientId": "toir-backend",
"name": "toir-backend",
"description": "Backend API resource server",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"bearerOnly": true,
"standardFlowEnabled": false,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"fullScopeAllowed": false
}
]
}

357
tools/dsl-summary.mjs Normal file
View File

@@ -0,0 +1,357 @@
import { readdirSync, readFileSync } from 'node:fs';
import path from 'node:path';
function stripInlineComment(line) {
let inString = false;
let result = '';
for (let index = 0; index < line.length; index += 1) {
const current = line[index];
const next = line[index + 1];
if (current === '"' && line[index - 1] !== '\\') {
inString = !inString;
result += current;
continue;
}
if (!inString && current === '/' && next === '/') {
break;
}
result += current;
}
return result.trim();
}
function parseDefaultValue(rawValue) {
const trimmed = rawValue.trim();
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
return trimmed.slice(1, -1);
}
if (/^-?\d+$/.test(trimmed)) {
return Number(trimmed);
}
return trimmed;
}
export function getDslFiles(rootDir) {
const domainDir = path.join(rootDir, 'domain');
return readdirSync(domainDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.dsl'))
.map((entry) => path.join(domainDir, entry.name))
.sort((left, right) => left.localeCompare(right));
}
function getOverrideFiles(rootDir) {
const overridesDir = path.join(rootDir, 'overrides');
const files = ['api-overrides.dsl', 'ui-overrides.dsl']
.map((name) => path.join(overridesDir, name))
.filter((filePath) => {
try {
return readdirSync(path.dirname(filePath)).includes(path.basename(filePath));
} catch {
return false;
}
});
return files;
}
export function parseDslFiles(rootDir) {
const dslFiles = getDslFiles(rootDir);
const enums = [];
const entities = [];
const stack = [];
for (const filePath of dslFiles) {
const contents = readFileSync(filePath, 'utf8');
const lines = contents.split(/\r?\n/);
for (const rawLine of lines) {
const line = stripInlineComment(rawLine);
if (!line) {
continue;
}
const top = stack.at(-1);
const enumMatch = line.match(/^enum\s+([A-Za-z][A-Za-z0-9_]*)\s*\{$/);
if (!top && enumMatch) {
const enumDefinition = { name: enumMatch[1], values: [] };
enums.push(enumDefinition);
stack.push({ type: 'enum', ref: enumDefinition });
continue;
}
const entityMatch = line.match(/^entity\s+([A-Za-z][A-Za-z0-9_]*)\s*\{$/);
if (!top && entityMatch) {
const entityDefinition = {
name: entityMatch[1],
primaryKey: null,
fields: [],
foreignKeys: [],
};
entities.push(entityDefinition);
stack.push({ type: 'entity', ref: entityDefinition });
continue;
}
const valueMatch = line.match(/^value\s+([A-Za-z][A-Za-z0-9_]*)\s*\{$/);
if (top?.type === 'enum' && valueMatch) {
const enumValue = { name: valueMatch[1] };
top.ref.values.push(enumValue);
stack.push({ type: 'enumValue', ref: enumValue });
continue;
}
const attributeMatch = line.match(/^attribute\s+([A-Za-z][A-Za-z0-9_]*)\s*\{$/);
if (top?.type === 'entity' && attributeMatch) {
const field = {
name: attributeMatch[1],
type: null,
required: false,
unique: false,
default: null,
};
top.ref.fields.push(field);
stack.push({ type: 'field', ref: field, entity: top.ref });
continue;
}
if (top?.type === 'field' && /^key\s+foreign\s*\{$/.test(line)) {
stack.push({ type: 'foreignKey', ref: top.ref, entity: top.entity });
continue;
}
if (top?.type === 'enumValue') {
const labelMatch = line.match(/^label\s+"(.*)"\s*;$/);
if (labelMatch) {
top.ref.label = labelMatch[1];
continue;
}
}
if (top?.type === 'field') {
const typeMatch = line.match(/^type\s+([A-Za-z][A-Za-z0-9_]*)\s*;$/);
if (typeMatch) {
top.ref.type = typeMatch[1];
continue;
}
if (/^key\s+primary\s*;$/.test(line)) {
top.ref.primary = true;
top.entity.primaryKey = top.ref.name;
continue;
}
const defaultMatch = line.match(/^default\s+(.+)\s*;$/);
if (defaultMatch) {
top.ref.default = parseDefaultValue(defaultMatch[1]);
continue;
}
if (/^is\s+required\s*;$/.test(line)) {
top.ref.required = true;
continue;
}
if (/^is\s+unique\s*;$/.test(line)) {
top.ref.unique = true;
continue;
}
}
if (top?.type === 'foreignKey') {
const relatesMatch = line.match(
/^relates\s+([A-Za-z][A-Za-z0-9_]*)\.([A-Za-z][A-Za-z0-9_]*)\s*;$/,
);
if (relatesMatch) {
const foreignKey = {
field: top.ref.name,
references: {
entity: relatesMatch[1],
field: relatesMatch[2],
},
};
top.ref.foreignKey = foreignKey.references;
top.entity.foreignKeys.push(foreignKey);
continue;
}
}
if (/^}\s*;?$/.test(line)) {
stack.pop();
}
}
}
return { dslFiles, enums, entities };
}
function assertNoDomainRedefinition(line, filePath) {
const blocked = [/^\s*entity\s+/i, /^\s*enum\s+/i, /^\s*attribute\s+/i, /^\s*key\s+primary/i, /^\s*key\s+foreign/i];
for (const pattern of blocked) {
if (pattern.test(line)) {
throw new Error(
`Override file ${path.basename(filePath)} attempts to redefine domain structure: ${line.trim()}`,
);
}
}
}
export function parseOverrides(rootDir, entities) {
const overrideFiles = getOverrideFiles(rootDir);
const entityNames = new Set(entities.map((entity) => entity.name));
const fieldNames = new Set(
entities.flatMap((entity) => entity.fields.map((field) => `${entity.name}.${field.name}`)),
);
const api = { resources: {} };
const ui = { fields: {} };
for (const filePath of overrideFiles) {
const isApi = filePath.endsWith('api-overrides.dsl');
const isUi = filePath.endsWith('ui-overrides.dsl');
const lines = readFileSync(filePath, 'utf8').split(/\r?\n/);
for (const rawLine of lines) {
const line = stripInlineComment(rawLine);
if (!line) {
continue;
}
assertNoDomainRedefinition(line, filePath);
if (isApi) {
const match = line.match(/^resource\s+([A-Za-z][A-Za-z0-9_]*)\s+path\s+"([^"]+)"\s*;$/);
if (!match) {
throw new Error(
`Unsupported API override syntax in ${path.basename(filePath)}: ${line}`,
);
}
const [, entityName, resourcePath] = match;
if (!entityNames.has(entityName)) {
throw new Error(`API override references unknown entity ${entityName}`);
}
api.resources[entityName] = { path: resourcePath };
}
if (isUi) {
const match = line.match(
/^field\s+([A-Za-z][A-Za-z0-9_]*)\.([A-Za-z][A-Za-z0-9_]*)\s+widget\s+"([^"]+)"\s*;$/,
);
if (!match) {
throw new Error(
`Unsupported UI override syntax in ${path.basename(filePath)}: ${line}`,
);
}
const [, entityName, fieldName, widget] = match;
const key = `${entityName}.${fieldName}`;
if (!fieldNames.has(key)) {
throw new Error(`UI override references unknown field ${key}`);
}
ui.fields[key] = { widget };
}
}
}
return { api, ui, sourceFiles: overrideFiles.map((filePath) => path.relative(rootDir, filePath).replaceAll('\\', '/')) };
}
export function buildDomainSummary(rootDir) {
const { dslFiles, enums, entities } = parseDslFiles(rootDir);
parseOverrides(rootDir, entities);
const entityByName = new Map(entities.map((entity) => [entity.name, entity]));
const enumByName = new Map(enums.map((entry) => [entry.name, entry]));
for (const entity of entities) {
if (!entity.primaryKey) {
throw new Error(`Entity ${entity.name} is missing a primary key`);
}
const primaryKeyField = entity.fields.find((field) => field.name === entity.primaryKey);
if (!primaryKeyField) {
throw new Error(
`Entity ${entity.name} declares primary key ${entity.primaryKey}, but the field is missing`,
);
}
if (entity.fields.some((field) => !field.type)) {
throw new Error(`Entity ${entity.name} has attributes with missing type`);
}
for (const foreignKey of entity.foreignKeys) {
const targetEntity = entityByName.get(foreignKey.references.entity);
if (!targetEntity) {
throw new Error(
`Foreign key ${entity.name}.${foreignKey.field} references missing entity ${foreignKey.references.entity}`,
);
}
const targetField = targetEntity.fields.find(
(field) => field.name === foreignKey.references.field,
);
if (!targetField) {
throw new Error(
`Foreign key ${entity.name}.${foreignKey.field} references missing field ${foreignKey.references.entity}.${foreignKey.references.field}`,
);
}
const sourceField = entity.fields.find((field) => field.name === foreignKey.field);
if (sourceField && sourceField.type !== targetField.type) {
throw new Error(
`Foreign key ${entity.name}.${foreignKey.field} type ${sourceField.type} does not match ${foreignKey.references.entity}.${foreignKey.references.field} type ${targetField.type}`,
);
}
}
const fieldNames = new Set();
for (const field of entity.fields) {
if (fieldNames.has(field.name)) {
throw new Error(`Entity ${entity.name} has duplicate field ${field.name}`);
}
fieldNames.add(field.name);
}
}
const entityNames = new Set();
for (const entity of entities) {
if (entityNames.has(entity.name)) {
throw new Error(`Duplicate entity definition: ${entity.name}`);
}
entityNames.add(entity.name);
}
for (const [enumName, enumDefinition] of enumByName.entries()) {
const valueNames = new Set();
for (const value of enumDefinition.values) {
if (valueNames.has(value.name)) {
throw new Error(`Enum ${enumName} has duplicate value ${value.name}`);
}
valueNames.add(value.name);
}
}
return {
sourceFiles: dslFiles.map((filePath) => path.relative(rootDir, filePath).replaceAll('\\', '/')),
entities: entities.map((entity) => ({
name: entity.name,
primaryKey: entity.primaryKey,
fields: entity.fields.map((field) => ({
name: field.name,
type: field.type,
required: field.required,
unique: field.unique,
default: field.default,
})),
foreignKeys: entity.foreignKeys,
})),
enums,
};
}

View File

@@ -0,0 +1,13 @@
import { mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { buildDomainSummary } from './dsl-summary.mjs';
const rootDir = process.cwd();
const outputPath = path.join(rootDir, 'domain-summary.json');
mkdirSync(path.dirname(outputPath), { recursive: true });
const summary = buildDomainSummary(rootDir);
writeFileSync(outputPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
console.log(`Generated ${path.relative(rootDir, outputPath)}`);

View File

@@ -0,0 +1,513 @@
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import {
buildDomainSummary,
getDslFiles,
parseDslFiles,
parseOverrides,
} from './dsl-summary.mjs';
const rootDir = process.cwd();
const args = new Set(process.argv.slice(2));
const artifactsOnly = args.has('--artifacts-only');
const runRuntime = args.has('--run-runtime');
const failures = [];
const warnings = [];
function assertCondition(condition, message) {
if (!condition) {
failures.push(message);
}
}
function warn(message) {
warnings.push(message);
}
function read(relativePath) {
return readFileSync(path.join(rootDir, relativePath), 'utf8');
}
function readIfExists(relativePath) {
const filePath = path.join(rootDir, relativePath);
if (!existsSync(filePath)) {
return null;
}
return readFileSync(filePath, 'utf8');
}
function requireFile(relativePath) {
assertCondition(existsSync(path.join(rootDir, relativePath)), `Missing file: ${relativePath}`);
}
function requireFiles(relativePaths) {
relativePaths.forEach(requireFile);
}
function requireContent(relativePath, pattern, message) {
const contents = readIfExists(relativePath);
assertCondition(Boolean(contents), `Missing file: ${relativePath}`);
if (!contents) {
return;
}
assertCondition(pattern.test(contents), `${message} (${relativePath})`);
}
function parseJson(relativePath) {
const raw = readIfExists(relativePath);
if (!raw) {
failures.push(`Missing file: ${relativePath}`);
return null;
}
try {
return JSON.parse(raw);
} catch (error) {
failures.push(`Invalid JSON in ${relativePath}: ${error.message}`);
return null;
}
}
function kebabCase(value) {
return value
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.replace(/\s+/g, '-')
.toLowerCase();
}
function getRealmArtifactPath() {
const rootFiles = readdirSync(rootDir, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name);
const realmArtifacts = rootFiles.filter((entry) => /-realm\.json$/i.test(entry));
assertCondition(realmArtifacts.length === 1, 'Expected exactly one root-level *-realm.json artifact');
return realmArtifacts[0] ?? null;
}
function getWorkspaceInfo() {
return {
server: {
dir: path.join(rootDir, 'server'),
packagePath: 'server/package.json',
scaffoldFiles: [
'server/package.json',
'server/tsconfig.json',
'server/tsconfig.build.json',
'server/nest-cli.json',
'server/src/main.ts',
'server/src/app.module.ts',
],
},
client: {
dir: path.join(rootDir, 'client'),
packagePath: 'client/package.json',
scaffoldFiles: [
'client/package.json',
'client/index.html',
'client/tsconfig.json',
'client/tsconfig.node.json',
'client/vite.config.ts',
'client/src/main.tsx',
'client/src/vite-env.d.ts',
],
},
};
}
function validateBuildChecks() {
requireFiles([
'README.md',
'package.json',
'domain/dsl-spec.md',
'domain-summary.json',
'server/prisma/schema.prisma',
'server/.env.example',
'client/.env.example',
'prompts/general-prompt.md',
'prompts/auth-rules.md',
'prompts/backend-rules.md',
'prompts/frontend-rules.md',
'prompts/runtime-rules.md',
'prompts/validation-rules.md',
]);
const dslFiles = getDslFiles(rootDir).map((filePath) => path.relative(rootDir, filePath).replaceAll('\\', '/'));
assertCondition(dslFiles.length > 0, 'Expected at least one domain/*.dsl file');
const actualSummaryRaw = readIfExists('domain-summary.json');
if (actualSummaryRaw) {
const expectedSummary = JSON.stringify(buildDomainSummary(rootDir), null, 2);
assertCondition(
actualSummaryRaw.trim() === expectedSummary,
'domain-summary.json is out of date. Run `npm run generate:domain-summary`.',
);
}
try {
const { entities } = parseDslFiles(rootDir);
parseOverrides(rootDir, entities);
} catch (error) {
failures.push(`Override validation failed: ${error.message}`);
}
const { server, client } = getWorkspaceInfo();
requireFiles(server.scaffoldFiles);
requireFiles(client.scaffoldFiles);
const serverPackage = parseJson(server.packagePath);
if (serverPackage) {
assertCondition(serverPackage.scripts?.build === 'nest build', 'server/package.json must keep `build = nest build`');
assertCondition(serverPackage.scripts?.start === 'nest start', 'server/package.json must keep `start = nest start`');
assertCondition(serverPackage.scripts?.['start:dev'] === 'nest start --watch', 'server/package.json must keep `start:dev = nest start --watch`');
assertCondition(Boolean(serverPackage.scripts?.['start:prod']), 'server/package.json must define a start:prod script');
assertCondition(Boolean(serverPackage.dependencies?.['@nestjs/core']), 'server/package.json must keep Nest runtime dependencies');
}
const clientPackage = parseJson(client.packagePath);
if (clientPackage) {
assertCondition(clientPackage.scripts?.dev === 'vite', 'client/package.json must keep `dev = vite`');
assertCondition(clientPackage.scripts?.build === 'vite build', 'client/package.json must keep `build = vite build`');
assertCondition(clientPackage.scripts?.preview === 'vite preview', 'client/package.json must keep `preview = vite preview`');
assertCondition(Boolean(clientPackage.devDependencies?.vite), 'client/package.json must keep Vite as a dev dependency');
assertCondition(Boolean(clientPackage.devDependencies?.['@vitejs/plugin-react']), 'client/package.json must keep @vitejs/plugin-react as a dev dependency');
}
}
function validateAuthChecks() {
requireFiles([
'client/src/auth/keycloak.ts',
'client/src/auth/authProvider.ts',
'client/src/dataProvider.ts',
'client/src/config/env.ts',
'client/src/main.tsx',
'client/src/App.tsx',
'server/src/auth/auth.module.ts',
'server/src/auth/auth.service.ts',
'server/src/auth/guards/jwt-auth.guard.ts',
'server/src/auth/guards/roles.guard.ts',
'server/src/auth/decorators/public.decorator.ts',
'server/src/auth/decorators/roles.decorator.ts',
]);
requireContent(
'client/src/auth/keycloak.ts',
/onLoad:\s*'login-required'/,
'Frontend auth must initialize Keycloak with login-required',
);
requireContent(
'client/src/auth/keycloak.ts',
/pkceMethod:\s*'S256'/,
'Frontend auth must use PKCE S256',
);
requireContent(
'client/src/auth/keycloak.ts',
/updateToken\(/,
'Frontend auth must refresh access tokens through the Keycloak adapter',
);
const keycloakSource = readIfExists('client/src/auth/keycloak.ts') ?? '';
assertCondition(
!/loadUserProfile\(/.test(keycloakSource),
'Frontend auth must not call keycloak.loadUserProfile()',
);
requireContent(
'client/src/dataProvider.ts',
/Authorization', `Bearer \$\{token\}`/,
'dataProvider must attach bearer tokens in the shared request seam',
);
const authProvider = readIfExists('client/src/auth/authProvider.ts') ?? '';
assertCondition(
/status === 401/.test(authProvider) && /status === 403/.test(authProvider),
'authProvider must distinguish 401 and 403 semantics',
);
const authService = readIfExists('server/src/auth/auth.service.ts') ?? '';
assertCondition(
/jwtVerify/.test(authService) && /KEYCLOAK_ISSUER_URL/.test(authService) && /KEYCLOAK_AUDIENCE/.test(authService),
'Backend auth must verify JWTs with issuer and audience',
);
assertCondition(/realm_access/.test(authService), 'Backend auth must extract roles from realm_access.roles');
assertCondition(/KEYCLOAK_JWKS_URL/.test(authService), 'Backend auth must support explicit KEYCLOAK_JWKS_URL');
assertCondition(/\.well-known\/openid-configuration/.test(authService), 'Backend auth must try OIDC discovery before fallback certs');
assertCondition(/protocol\/openid-connect\/certs/.test(authService), 'Backend auth must keep Keycloak certs fallback resolution');
}
function validateNaturalKeyChecks() {
const summary = parseJson('domain-summary.json');
if (!summary) {
return;
}
const naturalKeyEntities = summary.entities.filter((entity) => entity.primaryKey !== 'id');
for (const entity of naturalKeyEntities) {
const moduleName = kebabCase(entity.name);
const controllerPath = `server/src/modules/${moduleName}/${moduleName}.controller.ts`;
const servicePath = `server/src/modules/${moduleName}/${moduleName}.service.ts`;
const controller = readIfExists(controllerPath) ?? '';
const service = readIfExists(servicePath) ?? '';
assertCondition(Boolean(controller), `Missing file: ${controllerPath}`);
assertCondition(Boolean(service), `Missing file: ${servicePath}`);
if (!controller || !service) {
continue;
}
assertCondition(
controller.includes(`@Get(':${entity.primaryKey}')`) &&
controller.includes(`@Patch(':${entity.primaryKey}')`) &&
controller.includes(`@Delete(':${entity.primaryKey}')`),
`${entity.name} controller must use :${entity.primaryKey} route params`,
);
assertCondition(
service.includes(`id: item.${entity.primaryKey}`) || service.includes(`id: record.${entity.primaryKey}`),
`${entity.name} service must map the natural key to React Admin id`,
);
assertCondition(
service.includes(`const { id, ${entity.primaryKey}: _pk`) || service.includes(`const { id: _pk, ${entity.primaryKey}`),
`${entity.name} update path must sanitize id and primary key from Prisma update data`,
);
assertCondition(
/sortField\s*===\s*'id'/.test(service) || /sortField\s*===\s*"id"/.test(service),
`${entity.name} natural-key sort must map React Admin id sorting back to the real primary key`,
);
assertCondition(
!service.includes("query._sort || 'id'"),
`${entity.name} natural-key sort must not fall back to the physical id field`,
);
}
}
function validateRealmChecks() {
const realmArtifactName = getRealmArtifactPath();
if (!realmArtifactName) {
return;
}
const artifact = parseJson(realmArtifactName);
if (!artifact) {
return;
}
const realmRoles = artifact.roles?.realm?.map((role) => role.name) ?? [];
const frontendClient = artifact.clients?.find((client) => client.clientId?.endsWith('-frontend'));
const backendClient = artifact.clients?.find((client) => client.clientId?.endsWith('-backend'));
const audienceScope = artifact.clientScopes?.find((scope) => scope.name === 'api-audience');
['admin', 'editor', 'viewer'].forEach((role) => {
assertCondition(realmRoles.includes(role), `Realm artifact must define realm role ${role}`);
});
assertCondition(Boolean(frontendClient), 'Realm artifact must define the frontend SPA client');
assertCondition(Boolean(backendClient), 'Realm artifact must define the backend resource client');
assertCondition(Boolean(audienceScope), 'Realm artifact must define the api-audience client scope');
if (frontendClient) {
assertCondition(frontendClient.publicClient === true, 'Frontend realm client must be public');
assertCondition(
frontendClient.standardFlowEnabled === true &&
frontendClient.implicitFlowEnabled === false &&
frontendClient.directAccessGrantsEnabled === false,
'Frontend realm client must use standard flow only',
);
assertCondition(
frontendClient.attributes?.['pkce.code.challenge.method'] === 'S256',
'Frontend realm client must enforce PKCE S256',
);
const mapperNames = new Set((frontendClient.protocolMappers ?? []).map((mapper) => mapper.name));
['sub', 'preferred_username', 'email', 'name', 'realm roles'].forEach((mapperName) => {
assertCondition(mapperNames.has(mapperName), `Frontend realm client must include protocol mapper ${mapperName}`);
});
}
if (backendClient && audienceScope) {
const audienceMapper = (audienceScope.protocolMappers ?? []).find(
(mapper) => mapper.protocolMapper === 'oidc-audience-mapper',
);
assertCondition(Boolean(audienceMapper), 'api-audience scope must include an audience mapper');
assertCondition(
audienceMapper?.config?.['included.client.audience'] === backendClient.clientId,
'api-audience scope must deliver the backend audience/client id',
);
assertCondition(backendClient.bearerOnly === true, 'Backend realm client must be bearer-only');
}
}
function validateRuntimeContractChecks() {
requireFile('docker-compose.yml');
const compose = readIfExists('docker-compose.yml') ?? '';
assertCondition(/image:\s*postgres:16/.test(compose), 'docker-compose must provision postgres:16');
assertCondition(!/keycloak/i.test(compose), 'docker-compose must remain PostgreSQL-only');
const serverEnvExample = readIfExists('server/.env.example') ?? '';
assertCondition(
/KEYCLOAK_ISSUER_URL="https:\/\/sso\.greact\.ru\/realms\/toir"/.test(serverEnvExample),
'server/.env.example must keep the working Keycloak issuer example',
);
assertCondition(
/KEYCLOAK_AUDIENCE="?toir-backend"?/.test(serverEnvExample),
'server/.env.example must keep the working backend audience example',
);
assertCondition(
/CORS_ALLOWED_ORIGINS="http:\/\/localhost:5173,https:\/\/toir-frontend\.greact\.ru"/.test(serverEnvExample),
'server/.env.example must keep the working CORS example with the production frontend domain',
);
assertCondition(
!/KEYCLOAK_ISSUER_URL=http:\/\/localhost:8080\/realms\/toir/.test(serverEnvExample),
'server/.env.example must not regress to localhost Keycloak as the baseline issuer example',
);
const clientEnvExample = readIfExists('client/.env.example') ?? '';
assertCondition(
/VITE_KEYCLOAK_URL=https:\/\/sso\.greact\.ru/.test(clientEnvExample),
'client/.env.example must keep the working domain-based Keycloak URL example',
);
assertCondition(
/VITE_KEYCLOAK_REALM=toir/.test(clientEnvExample) && /VITE_KEYCLOAK_CLIENT_ID=toir-frontend/.test(clientEnvExample),
'client/.env.example must keep the working realm and frontend client examples',
);
assertCondition(
!/VITE_KEYCLOAK_URL=http:\/\/localhost:8080/.test(clientEnvExample),
'client/.env.example must not regress to localhost Keycloak as the baseline example',
);
const healthController = readIfExists('server/src/health/health.controller.ts') ?? '';
assertCondition(Boolean(healthController), 'Missing file: server/src/health/health.controller.ts');
if (healthController) {
assertCondition(
/@Public\(\)/.test(healthController) && /@Controller\('health'\)/.test(healthController),
'/health must stay public',
);
}
}
function runCommand(command, commandArgs, workdir, failureLabel) {
const runtimeEnv = { ...process.env };
const envExamplePath = path.join(workdir, '.env.example');
if (existsSync(envExamplePath)) {
const envExample = readFileSync(envExamplePath, 'utf8');
for (const line of envExample.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
const separator = trimmed.indexOf('=');
if (separator <= 0) {
continue;
}
const key = trimmed.slice(0, separator).trim();
const value = trimmed.slice(separator + 1).trim().replace(/^"|"$/g, '');
if (!(key in runtimeEnv)) {
runtimeEnv[key] = value;
}
}
}
const commandLine = [command, ...commandArgs].join(' ');
const result = spawnSync(commandLine, {
cwd: workdir,
encoding: 'utf8',
stdio: 'pipe',
shell: true,
env: runtimeEnv,
});
if (result.error) {
failures.push(`${failureLabel}: ${commandLine}\n${result.error.message}`);
return false;
}
if (result.status !== 0) {
const stderr = result.stderr?.trim();
const stdout = result.stdout?.trim();
failures.push(
`${failureLabel}: ${commandLine}${stderr ? `\n${stderr}` : stdout ? `\n${stdout}` : ''}`,
);
return false;
}
return true;
}
function maybeValidateWorkspaceBuild(relativeDir) {
const workspaceDir = path.join(rootDir, relativeDir);
if (!existsSync(path.join(workspaceDir, 'package.json'))) {
failures.push(`Missing file: ${relativeDir}/package.json`);
return;
}
if (!existsSync(path.join(workspaceDir, 'node_modules'))) {
warn(`Skipped build verification for ${relativeDir}: install dependencies in ${relativeDir}/ to validate workspace buildability.`);
return;
}
runCommand('npm', ['run', 'build'], workspaceDir, `Build verification failed in ${relativeDir}`);
}
function validateBuildExecutionChecks() {
maybeValidateWorkspaceBuild('server');
maybeValidateWorkspaceBuild('client');
}
function validateRuntimeExecutionChecks() {
const serverDir = path.join(rootDir, 'server');
if (!existsSync(path.join(serverDir, 'node_modules'))) {
failures.push(
'Runtime validation requires installed backend dependencies. Run `npm install` in server/ before `npm run validate:generation:runtime`.',
);
return;
}
runCommand('npx', ['prisma', 'generate'], serverDir, 'Prisma generate failed');
runCommand(
'npx',
['prisma', 'migrate', 'dev', '--name', 'baseline', '--skip-generate'],
serverDir,
'Prisma migrate failed',
);
runCommand('npx', ['prisma', 'db', 'seed'], serverDir, 'Prisma seed failed');
}
validateBuildChecks();
validateAuthChecks();
validateNaturalKeyChecks();
validateRealmChecks();
validateRuntimeContractChecks();
if (!artifactsOnly) {
validateBuildExecutionChecks();
}
if (!artifactsOnly && runRuntime) {
validateRuntimeExecutionChecks();
} else if (!artifactsOnly) {
warn('Runtime command execution skipped. Use --run-runtime after installing dependencies and starting the local database.');
}
for (const warning of warnings) {
console.warn(`WARN: ${warning}`);
}
if (failures.length > 0) {
console.error('Generation validation failed:');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log('Generation validation passed.');