use only TOiR.domain.dsl like single source of truth for generation, update context for pinned .gitignore

This commit is contained in:
MaKarin
2026-03-21 17:14:37 +03:00
parent 8d6875f4b0
commit 7e6b76cef2
18 changed files with 394 additions and 1759 deletions

37
.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
# Dependencies
**/node_modules/
# Build outputs
**/dist/
**/dist-ssr/
**/coverage/
**/.cache/
**/*.tsbuildinfo
# Environment files
**/.env
**/.env.local
**/.env.*.local
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# OS files
.DS_Store
Thumbs.db
# Editor / IDE
.vscode/*
!.vscode/extensions.json
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,60 +0,0 @@
# Runtime Keycloak Auth
This repository now uses Keycloak-based authentication for the runtime application only (`client/` and `server/`).
## Required environment variables
### Frontend (`client/.env`)
- `VITE_API_URL` (example: `http://localhost:3000`)
- `VITE_KEYCLOAK_URL` (example: `https://sso.greact.ru`)
- `VITE_KEYCLOAK_REALM` (example: `toir`)
- `VITE_KEYCLOAK_CLIENT_ID` (example: `toir-frontend`)
The frontend fails fast at startup if any required auth/env variable is missing.
### Backend (`server/.env`)
- `PORT` (example: `3000`)
- `DATABASE_URL`
- `CORS_ALLOWED_ORIGINS` (comma-separated list)
- `KEYCLOAK_ISSUER_URL` (example: `https://sso.greact.ru/realms/toir`)
- `KEYCLOAK_AUDIENCE` (example: `toir-backend`)
- `KEYCLOAK_JWKS_URL` (optional)
Backend validates required env vars at startup and fails fast if missing.
## Frontend auth flow
- The SPA initializes Keycloak before rendering.
- Authentication is redirect-based Keycloak login only (no custom username/password form).
- Authorization Code + PKCE (`S256`) is used.
- Access token is attached to every API request via `Authorization: Bearer <token>`.
- `checkError` behavior:
- `401`: force re-authentication.
- `403`: keep session and surface access denied to React Admin.
- Token refresh is concurrency-safe: parallel requests share one in-flight refresh operation.
## Backend JWT validation and RBAC
- JWTs are verified against:
- issuer: `KEYCLOAK_ISSUER_URL`
- audience: `KEYCLOAK_AUDIENCE`
- JWKS resolution priority:
1. explicit `KEYCLOAK_JWKS_URL`
2. OIDC discovery (`/.well-known/openid-configuration`)
3. fallback `${issuer}/protocol/openid-connect/certs`
- RBAC source is only `realm_access.roles`.
Role policy applied to CRUD endpoints:
- `GET`: `viewer`, `editor`, `admin`
- `POST`, `PATCH` (and `PUT` if added): `editor`, `admin`
- `DELETE`: `admin`
Public route:
- `GET /health`
All other existing API routes require authentication.

View File

@@ -49,6 +49,29 @@ The generated frontend must not rely on anonymous access with later lazy auth at
---
# 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.
@@ -131,4 +154,3 @@ VITE_KEYCLOAK_URL=https://sso.greact.ru
VITE_KEYCLOAK_REALM=toir
VITE_KEYCLOAK_CLIENT_ID=toir-frontend
```

View File

@@ -9,7 +9,7 @@ Backend stack:
- PostgreSQL
- jose
The backend is generated from the DSL specification.
The backend is generated from `domain/*.dsl`.
Each DSL entity becomes:
@@ -21,6 +21,15 @@ Each DSL entity becomes:
---
# 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/
@@ -180,6 +189,12 @@ Response format must follow React Admin requirements:
"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
@@ -198,6 +213,8 @@ 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

View File

@@ -1,6 +1,44 @@
# DSL Language Specification
This document describes the DSL (Domain Specific Language) used to specify fullstack CRUD applications. The DSL has four layers: Domain, DTO, API, and UI.
This document describes the single DSL (Domain Specific Language) used to specify fullstack CRUD applications. The only required DSL input is `domain/*.dsl`.
---
# DSL Responsibility
The domain DSL defines only:
- domain model
- relations
- enums
The domain DSL is the single source of truth for:
- entities
- attributes
- primary keys
- foreign keys
- enums
The following layers are always derived from the domain DSL and must not be authored as standalone authoritative DSL inputs:
- DTO
- API
- UI
Optional extension mechanism:
```text
overrides/
api-overrides.dsl
ui-overrides.dsl
```
Override rules:
- Overrides are optional.
- The generator must work without them.
- Overrides may refine derived API or UI behavior, but they must not duplicate or redefine entities, attributes, primary keys, foreign keys, relations, or enums.
---
@@ -108,7 +146,7 @@ attribute equipmentTypeCode {
## required
- **is required** — attribute is non-nullable in domain and (unless overridden) in DTOs.
- **is required** — attribute is non-nullable in the domain model and drives requiredness in derived DTO/API/UI artifacts.
- Absence of `is required` means the attribute is optional (nullable).
---
@@ -120,33 +158,6 @@ attribute equipmentTypeCode {
---
## map
Used in **DTO** and **UI** layers to bind a DTO/UI field to a domain entity attribute.
**In DTO:**
```
attribute name {
type string;
map Equipment.name;
}
```
- Ensures DTO attribute corresponds to an existing `Entity.attribute` and that types align.
**In UI:**
```
attribute Наименование {
map Equipment.name;
}
```
- UI label (e.g. "Наименование") maps to domain field `Equipment.name` for correct data binding and generation.
---
# DSL → System Component Mapping
## DSL → Prisma
@@ -174,9 +185,9 @@ attribute Наименование {
| entity | One module (e.g. equipment.module.ts) |
| entity | Controller with CRUD endpoints |
| entity | Service with Prisma CRUD |
| DTO (Create) | create-{entity}.dto.ts |
| DTO (Update) | update-{entity}.dto.ts |
| DTO (Response) | Used for GET response shape |
| entity + attribute metadata | create-{entity}.dto.ts |
| entity + attribute metadata | update-{entity}.dto.ts |
| entity + attribute metadata | Response DTO / API shape |
API paths are derived from entity name: PascalCase → kebab-case, pluralized (e.g. `Equipment``/equipment`, `RepairOrder``/repair-orders`).
@@ -193,21 +204,12 @@ API paths are derived from entity name: PascalCase → kebab-case, pluralized (e
| type date | DateInput, DateField |
| enum | SelectInput with choices |
| foreign key | ReferenceInput, ReferenceField |
| UI attribute with map | Field with correct source |
---
# DTO Mapping
# Derived Layer Mapping
- **map Entity.attribute** — DTO attribute corresponds to domain attribute; types must match.
- **Create DTO** — must not include generated primary keys (e.g. no `id` for uuid PK).
- **Update DTO** — all fields optional (nullable) for partial updates.
- **List response DTO** — must expose `data` (array) and `total` (integer) for React Admin compatibility.
---
# UI Mapping
- Each UI attribute should have **map Entity.attribute** so it binds to a real domain field.
- UI attribute name is the label (e.g. "Наименование"); **source** in generated components is the domain attribute name (e.g. `name`).
- Enums → SelectInput; foreign keys → ReferenceInput/ReferenceField.
- **Create DTO** — derived from domain attributes and must not include generated primary keys (for example no `id` for uuid PKs).
- **Update DTO** — derived from the same domain attributes with all fields optional for partial updates.
- **API response shape** — derived from domain attributes and must expose React Admin-compatible identifiers when needed.
- **UI field mapping** — derived from attribute types, descriptions, enums, and foreign keys without a separate UI DSL.

View File

@@ -1,57 +0,0 @@
import ./TOiR;
ui UI.Equipment {
offset 600;
description "Единица оборудования — объект ремонта и технического обслуживания";
attribute Код {
map Equipment.id;
}
attribute ИнвентарныйНомер {
map Equipment.inventoryNumber;
}
attribute СерийныйНомер {
map Equipment.serialNumber;
}
attribute Наименование {
map Equipment.name;
}
// Связь с видом оборудования (справочник НСИ)
attribute Тип {
map Equipment.equipmentTypeCode;
}
attribute Статус {
map Equipment.status;
description "Текущий статус";
}
attribute МестоТекущее {
map Equipment.location;
}
attribute ДатаВвода {
map Equipment.commissionedAt;
}
attribute НаработкаВсего {
map Equipment.totalEngineHours;
}
attribute НаработкаТекущая {
map Equipment.engineHoursSinceLastRepair;
}
attribute Ремонт {
map Equipment.lastRepairAt;
}
attribute Примечания {
map Equipment.notes;
}
}

View File

@@ -1,811 +0,0 @@
// ─────────────────────────────────────────────
// DTO: Вид оборудования (EquipmentType)
// ─────────────────────────────────────────────
dto DTO.EquipmentType {
description "Вид оборудования — полный объект ответа";
attribute code {
type string;
description "Код вида оборудования";
map EquipmentType.code;
}
attribute name {
type string;
description "Наименование вида";
map EquipmentType.name;
}
attribute manufacturer {
type string;
description "Производитель";
is nullable;
map EquipmentType.manufacturer;
}
attribute maintenanceIntervalHours {
type integer;
description "Периодичность ТО, моточасов";
is nullable;
map EquipmentType.maintenanceIntervalHours;
}
attribute overhaulIntervalHours {
type integer;
description "Периодичность КР, моточасов";
is nullable;
map EquipmentType.overhaulIntervalHours;
}
}
dto DTO.EquipmentTypeCreate {
description "Вид оборудования — тело запроса на создание";
attribute code {
type string;
description "Код вида оборудования";
is required;
map EquipmentType.code;
}
attribute name {
type string;
description "Наименование вида";
is required;
map EquipmentType.name;
}
attribute manufacturer {
type string;
description "Производитель";
is nullable;
map EquipmentType.manufacturer;
}
attribute maintenanceIntervalHours {
type integer;
description "Периодичность ТО, моточасов";
is nullable;
map EquipmentType.maintenanceIntervalHours;
}
attribute overhaulIntervalHours {
type integer;
description "Периодичность КР, моточасов";
is nullable;
map EquipmentType.overhaulIntervalHours;
}
}
dto DTO.EquipmentTypeUpdate {
description "Вид оборудования — тело запроса на обновление (частичное)";
attribute code {
type string;
description "Код вида оборудования";
is nullable;
map EquipmentType.code;
}
attribute name {
type string;
description "Наименование вида";
is nullable;
map EquipmentType.name;
}
attribute manufacturer {
type string;
description "Производитель";
is nullable;
map EquipmentType.manufacturer;
}
attribute maintenanceIntervalHours {
type integer;
description "Периодичность ТО, моточасов";
is nullable;
map EquipmentType.maintenanceIntervalHours;
}
attribute overhaulIntervalHours {
type integer;
description "Периодичность КР, моточасов";
is nullable;
map EquipmentType.overhaulIntervalHours;
}
}
dto DTO.EquipmentTypeListResponse {
description "Список видов оборудования (формат React Admin)";
attribute data {
type DTO.EquipmentType[];
}
attribute total {
type integer;
}
}
// ─────────────────────────────────────────────
// DTO: Оборудование (Equipment)
// ─────────────────────────────────────────────
dto DTO.Equipment {
description "Единица оборудования — полный объект ответа";
attribute id {
type uuid;
map Equipment.id;
}
attribute inventoryNumber {
type string;
description "Инвентарный номер";
map Equipment.inventoryNumber;
}
attribute serialNumber {
type string;
description "Заводской (серийный) номер";
is nullable;
map Equipment.serialNumber;
}
attribute name {
type string;
description "Наименование единицы оборудования";
map Equipment.name;
}
attribute equipmentTypeCode {
type string;
description "Код вида оборудования";
map Equipment.equipmentTypeCode;
}
attribute status {
type EquipmentStatus;
description "Текущий статус";
map Equipment.status;
}
attribute location {
type string;
description "Место эксплуатации / скважина / куст";
is nullable;
map Equipment.location;
}
attribute commissionedAt {
type date;
description "Дата ввода в эксплуатацию";
is nullable;
map Equipment.commissionedAt;
}
attribute totalEngineHours {
type decimal;
description "Общая наработка, моточасов";
is nullable;
map Equipment.totalEngineHours;
}
attribute engineHoursSinceLastRepair {
type decimal;
description "Наработка с последнего ремонта, моточасов";
is nullable;
map Equipment.engineHoursSinceLastRepair;
}
attribute lastRepairAt {
type date;
description "Дата последнего ремонта";
is nullable;
map Equipment.lastRepairAt;
}
attribute notes {
type text;
description "Примечания";
is nullable;
map Equipment.notes;
}
}
dto DTO.EquipmentCreate {
description "Единица оборудования — тело запроса на создание";
attribute inventoryNumber {
type string;
description "Инвентарный номер";
is required;
map Equipment.inventoryNumber;
}
attribute serialNumber {
type string;
description "Заводской (серийный) номер";
is nullable;
map Equipment.serialNumber;
}
attribute name {
type string;
description "Наименование единицы оборудования";
is required;
map Equipment.name;
}
attribute equipmentTypeCode {
type string;
description "Код вида оборудования";
is required;
map Equipment.equipmentTypeCode;
}
attribute status {
type EquipmentStatus;
description "Текущий статус";
is nullable;
map Equipment.status;
}
attribute location {
type string;
description "Место эксплуатации / скважина / куст";
is nullable;
map Equipment.location;
}
attribute commissionedAt {
type date;
description "Дата ввода в эксплуатацию";
is nullable;
map Equipment.commissionedAt;
}
attribute totalEngineHours {
type decimal;
description "Общая наработка, моточасов";
is nullable;
map Equipment.totalEngineHours;
}
attribute engineHoursSinceLastRepair {
type decimal;
description "Наработка с последнего ремонта, моточасов";
is nullable;
map Equipment.engineHoursSinceLastRepair;
}
attribute lastRepairAt {
type date;
description "Дата последнего ремонта";
is nullable;
map Equipment.lastRepairAt;
}
attribute notes {
type text;
description "Примечания";
is nullable;
map Equipment.notes;
}
}
dto DTO.EquipmentUpdate {
description "Единица оборудования — тело запроса на обновление (частичное)";
attribute inventoryNumber {
type string;
description "Инвентарный номер";
is nullable;
map Equipment.inventoryNumber;
}
attribute serialNumber {
type string;
description "Заводской (серийный) номер";
is nullable;
map Equipment.serialNumber;
}
attribute name {
type string;
description "Наименование единицы оборудования";
is nullable;
map Equipment.name;
}
attribute equipmentTypeCode {
type string;
description "Код вида оборудования";
is nullable;
map Equipment.equipmentTypeCode;
}
attribute status {
type EquipmentStatus;
description "Текущий статус";
is nullable;
map Equipment.status;
}
attribute location {
type string;
description "Место эксплуатации / скважина / куст";
is nullable;
map Equipment.location;
}
attribute commissionedAt {
type date;
description "Дата ввода в эксплуатацию";
is nullable;
map Equipment.commissionedAt;
}
attribute totalEngineHours {
type decimal;
description "Общая наработка, моточасов";
is nullable;
map Equipment.totalEngineHours;
}
attribute engineHoursSinceLastRepair {
type decimal;
description "Наработка с последнего ремонта, моточасов";
is nullable;
map Equipment.engineHoursSinceLastRepair;
}
attribute lastRepairAt {
type date;
description "Дата последнего ремонта";
is nullable;
map Equipment.lastRepairAt;
}
attribute notes {
type text;
description "Примечания";
is nullable;
map Equipment.notes;
}
}
dto DTO.EquipmentListResponse {
description "Список оборудования (формат React Admin)";
attribute data {
type DTO.Equipment[];
}
attribute total {
type integer;
}
}
// ─────────────────────────────────────────────
// DTO: Заявка на ремонт (RepairOrder)
// ─────────────────────────────────────────────
dto DTO.RepairOrder {
description "Заявка на ремонт — полный объект ответа";
attribute id {
type uuid;
map RepairOrder.id;
}
attribute number {
type string;
description "Номер заявки";
map RepairOrder.number;
}
attribute equipmentId {
type uuid;
description "Идентификатор единицы оборудования";
map RepairOrder.equipmentId;
}
attribute repairKind {
type RepairKind;
description "Вид ремонта";
map RepairOrder.repairKind;
}
attribute status {
type RepairOrderStatus;
description "Статус заявки";
map RepairOrder.status;
}
attribute plannedAt {
type date;
description "Плановая дата начала";
map RepairOrder.plannedAt;
}
attribute startedAt {
type date;
description "Фактическая дата начала";
is nullable;
map RepairOrder.startedAt;
}
attribute completedAt {
type date;
description "Фактическая дата завершения";
is nullable;
map RepairOrder.completedAt;
}
attribute contractor {
type string;
description "Подрядная организация (если внешний ремонт)";
is nullable;
map RepairOrder.contractor;
}
attribute engineHoursAtRepair {
type decimal;
description "Наработка на момент ремонта, моточасов";
is nullable;
map RepairOrder.engineHoursAtRepair;
}
attribute description {
type text;
description "Описание работ / дефекта";
is nullable;
map RepairOrder.description;
}
attribute notes {
type text;
description "Примечания";
is nullable;
map RepairOrder.notes;
}
}
dto DTO.RepairOrderCreate {
description "Заявка на ремонт — тело запроса на создание";
attribute number {
type string;
description "Номер заявки";
is required;
map RepairOrder.number;
}
attribute equipmentId {
type uuid;
description "Идентификатор единицы оборудования";
is required;
map RepairOrder.equipmentId;
}
attribute repairKind {
type RepairKind;
description "Вид ремонта";
is required;
map RepairOrder.repairKind;
}
attribute status {
type RepairOrderStatus;
description "Статус заявки";
is nullable;
map RepairOrder.status;
}
attribute plannedAt {
type date;
description "Плановая дата начала";
is required;
map RepairOrder.plannedAt;
}
attribute startedAt {
type date;
description "Фактическая дата начала";
is nullable;
map RepairOrder.startedAt;
}
attribute completedAt {
type date;
description "Фактическая дата завершения";
is nullable;
map RepairOrder.completedAt;
}
attribute contractor {
type string;
description "Подрядная организация (если внешний ремонт)";
is nullable;
map RepairOrder.contractor;
}
attribute engineHoursAtRepair {
type decimal;
description "Наработка на момент ремонта, моточасов";
is nullable;
map RepairOrder.engineHoursAtRepair;
}
attribute description {
type text;
description "Описание работ / дефекта";
is nullable;
map RepairOrder.description;
}
attribute notes {
type text;
description "Примечания";
is nullable;
map RepairOrder.notes;
}
}
dto DTO.RepairOrderUpdate {
description "Заявка на ремонт — тело запроса на обновление (частичное)";
attribute number {
type string;
description "Номер заявки";
is nullable;
map RepairOrder.number;
}
attribute equipmentId {
type uuid;
description "Идентификатор единицы оборудования";
is nullable;
map RepairOrder.equipmentId;
}
attribute repairKind {
type RepairKind;
description "Вид ремонта";
is nullable;
map RepairOrder.repairKind;
}
attribute status {
type RepairOrderStatus;
description "Статус заявки";
is nullable;
map RepairOrder.status;
}
attribute plannedAt {
type date;
description "Плановая дата начала";
is nullable;
map RepairOrder.plannedAt;
}
attribute startedAt {
type date;
description "Фактическая дата начала";
is nullable;
map RepairOrder.startedAt;
}
attribute completedAt {
type date;
description "Фактическая дата завершения";
is nullable;
map RepairOrder.completedAt;
}
attribute contractor {
type string;
description "Подрядная организация (если внешний ремонт)";
is nullable;
map RepairOrder.contractor;
}
attribute engineHoursAtRepair {
type decimal;
description "Наработка на момент ремонта, моточасов";
is nullable;
map RepairOrder.engineHoursAtRepair;
}
attribute description {
type text;
description "Описание работ / дефекта";
is nullable;
map RepairOrder.description;
}
attribute notes {
type text;
description "Примечания";
is nullable;
map RepairOrder.notes;
}
}
dto DTO.RepairOrderListResponse {
description "Список заявок на ремонт (формат React Admin)";
attribute data {
type DTO.RepairOrder[];
}
attribute total {
type integer;
}
}
// ─────────────────────────────────────────────
// API: Виды оборудования
// ─────────────────────────────────────────────
api API.EquipmentTypes {
description "API управления справочником видов оборудования";
endpoint listEquipmentTypes {
label "GET /equipment-types";
description "Список видов оборудования (фильтры и пагинация — query-параметры)";
attribute response {
type DTO.EquipmentTypeListResponse;
}
}
endpoint getEquipmentType {
label "GET /equipment-types/{code}";
description "Получить вид оборудования по коду";
attribute code {
type string;
}
attribute response {
type DTO.EquipmentType;
}
}
endpoint createEquipmentType {
label "POST /equipment-types";
description "Создать вид оборудования";
attribute request {
type DTO.EquipmentTypeCreate;
}
}
endpoint updateEquipmentType {
label "PATCH /equipment-types/{code}";
description "Обновить вид оборудования";
attribute code {
type string;
}
attribute request {
type DTO.EquipmentTypeUpdate;
}
}
endpoint deleteEquipmentType {
label "DELETE /equipment-types/{code}";
description "Удалить вид оборудования";
attribute code {
type string;
}
}
}
// ─────────────────────────────────────────────
// API: Оборудование
// ─────────────────────────────────────────────
api API.Equipment {
description "API управления оборудованием";
endpoint listEquipment {
label "GET /equipment";
description "Список оборудования (фильтры и пагинация — query-параметры)";
attribute response {
type DTO.EquipmentListResponse;
}
}
endpoint getEquipment {
label "GET /equipment/{id}";
description "Получить единицу оборудования по идентификатору";
attribute id {
type uuid;
}
attribute response {
type DTO.Equipment;
}
}
endpoint createEquipment {
label "POST /equipment";
description "Создать единицу оборудования";
attribute request {
type DTO.EquipmentCreate;
}
}
endpoint updateEquipment {
label "PATCH /equipment/{id}";
description "Обновить единицу оборудования";
attribute id {
type uuid;
}
attribute request {
type DTO.EquipmentUpdate;
}
}
endpoint deleteEquipment {
label "DELETE /equipment/{id}";
description "Удалить единицу оборудования";
attribute id {
type uuid;
}
}
}
// ─────────────────────────────────────────────
// API: Заявки на ремонт
// ─────────────────────────────────────────────
api API.RepairOrders {
description "API управления заявками на ремонт";
endpoint listRepairOrders {
label "GET /repair-orders";
description "Список заявок на ремонт (фильтры и пагинация — query-параметры)";
attribute response {
type DTO.RepairOrderListResponse;
}
}
endpoint getRepairOrder {
label "GET /repair-orders/{id}";
description "Получить заявку на ремонт по идентификатору";
attribute id {
type uuid;
}
attribute response {
type DTO.RepairOrder;
}
}
endpoint createRepairOrder {
label "POST /repair-orders";
description "Создать заявку на ремонт";
attribute request {
type DTO.RepairOrderCreate;
}
}
endpoint updateRepairOrder {
label "PATCH /repair-orders/{id}";
description "Обновить заявку на ремонт";
attribute id {
type uuid;
}
attribute request {
type DTO.RepairOrderUpdate;
}
}
endpoint deleteRepairOrder {
label "DELETE /repair-orders/{id}";
description "Удалить заявку на ремонт";
attribute id {
type uuid;
}
}
}

View File

@@ -1,753 +0,0 @@
/*
КИС ТОиР — DTO
Структуры данных для обмена через API
*/
//import ./TOiR;
//only external;
// ─────────────────────────────────────────────
// Общие
// ─────────────────────────────────────────────
dto DTO.PageRequest {
description "Параметры постраничной выдачи";
attribute page {
description "Номер страницы (начиная с 0)";
type integer;
}
attribute size {
description "Размер страницы";
type integer;
}
}
dto DTO.Filter {
description "Элемент фильтра для запросов списка";
attribute field { type string; }
attribute operator { type string; }
attribute value { type string; }
}
dto DTO.PageInfo {
description "Метаданные постраничной выдачи";
attribute size { type integer; }
attribute number { type integer; }
attribute totalElements { type integer; }
attribute totalPages { type integer; }
}
// ─────────────────────────────────────────────
// Вид оборудования (EquipmentType)
// ─────────────────────────────────────────────
dto DTO.EquipmentType {
description "Вид оборудования — response";
attribute code {
type string;
map EquipmentType.code;
}
attribute name {
type string;
map EquipmentType.name;
}
attribute manufacturer {
type string;
is nullable;
map EquipmentType.manufacturer;
}
attribute maintenanceIntervalHours {
type integer;
is nullable;
map EquipmentType.maintenanceIntervalHours;
}
attribute overhaulIntervalHours {
type integer;
is nullable;
map EquipmentType.overhaulIntervalHours;
}
}
dto DTO.EquipmentTypeFilter {
description "Фильтры для списка видов оборудования";
attribute code {
description "Частичное совпадение по коду";
type string;
is nullable;
}
attribute name {
description "Частичное совпадение по наименованию";
type string;
is nullable;
}
attribute manufacturer {
description "Частичное совпадение по производителю";
type string;
is nullable;
}
}
dto DTO.EquipmentTypeCreate {
description "Тело запроса на создание вида оборудования";
attribute code {
description "Код вида оборудования";
type string;
is required;
}
attribute name {
description "Наименование вида";
type string;
is required;
}
attribute manufacturer {
description "Производитель";
type string;
is nullable;
}
attribute maintenanceIntervalHours {
description "Периодичность ТО, моточасов";
type integer;
is nullable;
}
attribute overhaulIntervalHours {
description "Периодичность КР, моточасов";
type integer;
is nullable;
}
}
dto DTO.EquipmentTypeUpdate {
description "Тело запроса на обновление вида оборудования";
attribute name {
description "Наименование вида";
type string;
is nullable;
}
attribute manufacturer {
description "Производитель";
type string;
is nullable;
}
attribute maintenanceIntervalHours {
description "Периодичность ТО, моточасов";
type integer;
is nullable;
}
attribute overhaulIntervalHours {
description "Периодичность КР, моточасов";
type integer;
is nullable;
}
}
// ─────────────────────────────────────────────
// Оборудование (Equipment)
// ─────────────────────────────────────────────
dto DTO.EquipmentListItem {
description "Строка списка оборудования";
attribute id {
type uuid;
map Equipment.id;
}
attribute inventoryNumber {
type string;
map Equipment.inventoryNumber;
}
attribute name {
type string;
map Equipment.name;
}
attribute equipmentTypeCode {
type string;
map Equipment.equipmentTypeCode;
}
attribute status {
type EquipmentStatus;
map Equipment.status;
}
attribute location {
type string;
is nullable;
map Equipment.location;
}
}
dto DTO.EquipmentDetail {
description "Полная информация об оборудовании";
attribute id {
type uuid;
map Equipment.id;
}
attribute inventoryNumber {
type string;
map Equipment.inventoryNumber;
}
attribute serialNumber {
type string;
is nullable;
map Equipment.serialNumber;
}
attribute name {
type string;
map Equipment.name;
}
attribute equipmentTypeCode {
type string;
map Equipment.equipmentTypeCode;
}
attribute status {
type EquipmentStatus;
map Equipment.status;
}
attribute location {
type string;
is nullable;
map Equipment.location;
}
attribute commissionedAt {
type date;
is nullable;
map Equipment.commissionedAt;
}
attribute totalEngineHours {
type decimal;
is nullable;
map Equipment.totalEngineHours;
}
attribute engineHoursSinceLastRepair {
type decimal;
is nullable;
map Equipment.engineHoursSinceLastRepair;
}
attribute lastRepairAt {
type date;
is nullable;
map Equipment.lastRepairAt;
}
attribute notes {
type text;
is nullable;
map Equipment.notes;
}
}
dto DTO.EquipmentFilter {
description "Фильтры для списка оборудования";
attribute inventoryNumber {
description "Частичное совпадение по инвентарному номеру";
type string;
is nullable;
}
attribute serialNumber {
description "Частичное совпадение по заводскому номеру";
type string;
is nullable;
}
attribute name {
description "Частичное совпадение по наименованию";
type string;
is nullable;
}
attribute equipmentTypeCode {
description "Точное совпадение по коду вида оборудования";
type string;
is nullable;
}
attribute status {
description "Фильтр по статусу";
type EquipmentStatus;
is nullable;
}
attribute location {
description "Частичное совпадение по месту эксплуатации";
type string;
is nullable;
}
}
dto DTO.EquipmentCreate {
description "Тело запроса на создание оборудования";
attribute inventoryNumber {
description "Инвентарный номер";
type string;
is required;
}
attribute serialNumber {
description "Заводской (серийный) номер";
type string;
is nullable;
}
attribute name {
description "Наименование единицы оборудования";
type string;
is required;
}
attribute equipmentTypeCode {
description "Код вида оборудования";
type string;
is required;
}
attribute status {
description "Текущий статус";
type EquipmentStatus;
is nullable;
}
attribute location {
description "Место эксплуатации / скважина / куст";
type string;
is nullable;
}
attribute commissionedAt {
description "Дата ввода в эксплуатацию";
type date;
is nullable;
}
attribute totalEngineHours {
description "Общая наработка, моточасов";
type decimal;
is nullable;
}
attribute engineHoursSinceLastRepair {
description "Наработка с последнего ремонта, моточасов";
type decimal;
is nullable;
}
attribute lastRepairAt {
description "Дата последнего ремонта";
type date;
is nullable;
}
attribute notes {
description "Примечания";
type text;
is nullable;
}
}
dto DTO.EquipmentUpdate {
description "Тело запроса на обновление оборудования";
attribute inventoryNumber {
description "Инвентарный номер";
type string;
is nullable;
}
attribute serialNumber {
description "Заводской (серийный) номер";
type string;
is nullable;
}
attribute name {
description "Наименование единицы оборудования";
type string;
is nullable;
}
attribute equipmentTypeCode {
description "Код вида оборудования";
type string;
is nullable;
}
attribute status {
description "Текущий статус";
type EquipmentStatus;
is nullable;
}
attribute location {
description "Место эксплуатации / скважина / куст";
type string;
is nullable;
}
attribute commissionedAt {
description "Дата ввода в эксплуатацию";
type date;
is nullable;
}
attribute totalEngineHours {
description "Общая наработка, моточасов";
type decimal;
is nullable;
}
attribute engineHoursSinceLastRepair {
description "Наработка с последнего ремонта, моточасов";
type decimal;
is nullable;
}
attribute lastRepairAt {
description "Дата последнего ремонта";
type date;
is nullable;
}
attribute notes {
description "Примечания";
type text;
is nullable;
}
}
// ─────────────────────────────────────────────
// Заявка на ремонт (RepairOrder)
// ─────────────────────────────────────────────
dto DTO.RepairOrderListItem {
description "Строка списка заявок на ремонт";
attribute id {
type uuid;
map RepairOrder.id;
}
attribute number {
type string;
map RepairOrder.number;
}
attribute equipmentId {
type uuid;
map RepairOrder.equipmentId;
}
attribute repairKind {
type RepairKind;
map RepairOrder.repairKind;
}
attribute status {
type RepairOrderStatus;
map RepairOrder.status;
}
attribute plannedAt {
type date;
map RepairOrder.plannedAt;
}
attribute contractor {
type string;
is nullable;
map RepairOrder.contractor;
}
}
dto DTO.RepairOrderDetail {
description "Полная информация о заявке на ремонт";
attribute id {
type uuid;
map RepairOrder.id;
}
attribute number {
type string;
map RepairOrder.number;
}
attribute equipmentId {
type uuid;
map RepairOrder.equipmentId;
}
attribute repairKind {
type RepairKind;
map RepairOrder.repairKind;
}
attribute status {
type RepairOrderStatus;
map RepairOrder.status;
}
attribute plannedAt {
type date;
map RepairOrder.plannedAt;
}
attribute startedAt {
type date;
is nullable;
map RepairOrder.startedAt;
}
attribute completedAt {
type date;
is nullable;
map RepairOrder.completedAt;
}
attribute contractor {
type string;
is nullable;
map RepairOrder.contractor;
}
attribute engineHoursAtRepair {
type decimal;
is nullable;
map RepairOrder.engineHoursAtRepair;
}
attribute description {
type text;
is nullable;
map RepairOrder.description;
}
attribute notes {
type text;
is nullable;
map RepairOrder.notes;
}
}
dto DTO.RepairOrderFilter {
description "Фильтры для списка заявок на ремонт";
attribute number {
description "Частичное совпадение по номеру заявки";
type string;
is nullable;
}
attribute equipmentId {
description "Точное совпадение по идентификатору оборудования";
type uuid;
is nullable;
}
attribute repairKind {
description "Фильтр по виду ремонта";
type RepairKind;
is nullable;
}
attribute status {
description "Фильтр по статусу заявки";
type RepairOrderStatus;
is nullable;
}
attribute plannedAtFrom {
description "Плановая дата начала ОТ";
type date;
is nullable;
}
attribute plannedAtTo {
description "Плановая дата начала ДО";
type date;
is nullable;
}
attribute contractor {
description "Частичное совпадение по подрядчику";
type string;
is nullable;
}
}
dto DTO.RepairOrderCreate {
description "Тело запроса на создание заявки на ремонт";
attribute number {
description "Номер заявки";
type string;
is required;
}
attribute equipmentId {
description "Идентификатор оборудования";
type uuid;
is required;
}
attribute repairKind {
description "Вид ремонта";
type RepairKind;
is required;
}
attribute status {
description "Статус заявки";
type RepairOrderStatus;
is nullable;
}
attribute plannedAt {
description "Плановая дата начала";
type date;
is required;
}
attribute startedAt {
description "Фактическая дата начала";
type date;
is nullable;
}
attribute completedAt {
description "Фактическая дата завершения";
type date;
is nullable;
}
attribute contractor {
description "Подрядная организация";
type string;
is nullable;
}
attribute engineHoursAtRepair {
description "Наработка на момент ремонта, моточасов";
type decimal;
is nullable;
}
attribute description {
description "Описание работ / дефекта";
type text;
is nullable;
}
attribute notes {
description "Примечания";
type text;
is nullable;
}
}
dto DTO.RepairOrderUpdate {
description "Тело запроса на обновление заявки на ремонт";
attribute number {
description "Номер заявки";
type string;
is nullable;
}
attribute equipmentId {
description "Идентификатор оборудования";
type uuid;
is nullable;
}
attribute repairKind {
description "Вид ремонта";
type RepairKind;
is nullable;
}
attribute status {
description "Статус заявки";
type RepairOrderStatus;
is nullable;
}
attribute plannedAt {
description "Плановая дата начала";
type date;
is nullable;
}
attribute startedAt {
description "Фактическая дата начала";
type date;
is nullable;
}
attribute completedAt {
description "Фактическая дата завершения";
type date;
is nullable;
}
attribute contractor {
description "Подрядная организация";
type string;
is nullable;
}
attribute engineHoursAtRepair {
description "Наработка на момент ремонта, моточасов";
type decimal;
is nullable;
}
attribute description {
description "Описание работ / дефекта";
type text;
is nullable;
}
attribute notes {
description "Примечания";
type text;
is nullable;
}
}

View File

@@ -9,7 +9,7 @@ Frontend stack:
- shadcn/ui
- Keycloak JS
The frontend is generated from the DSL and API specification.
The frontend is generated from `domain/*.dsl`.
Each entity becomes a React Admin resource.
@@ -17,6 +17,15 @@ 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/
@@ -55,6 +64,7 @@ The generated `App.tsx` must register:
- `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:
@@ -108,6 +118,7 @@ Rules:
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.
---

View File

@@ -40,6 +40,20 @@ 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:
@@ -117,6 +131,7 @@ React Admin requires every record in list and detail responses to contain a fiel
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:**
@@ -144,4 +159,6 @@ API response must include `id` so React Admin can identify the record:
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

@@ -28,7 +28,12 @@ You must read the project documentation in the following strict order:
domain/dsl-spec.md
examples/*.dsl
domain/*.dsl
If present, read optional overrides after the domain DSL:
overrides/api-overrides.dsl
overrides/ui-overrides.dsl
backend/architecture.md
@@ -68,9 +73,29 @@ 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 DSL-driven fullstack CRUD system with default Keycloak authentication and authorization.
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.
@@ -105,6 +130,7 @@ Keycloak JS
PROJECT STRUCTURE
Root
.gitignore
docker-compose.yml
root-level Keycloak realm import artifact (default example filename: `toir-realm.json`)
server/
@@ -118,11 +144,13 @@ config/
modules/{entity}/
prisma/schema.prisma
prisma/seed.ts
.gitignore
.env
.env.example
Frontend
client/
.gitignore
src/
auth/
config/
@@ -132,9 +160,9 @@ main.tsx
dataProvider.ts
.env.example
STEP 1 — Parse DSL
STEP 1 — Parse Domain DSL
Parse all DSL files and extract:
Parse domain/*.dsl and extract:
Entities
Attributes
@@ -142,6 +170,9 @@ 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
@@ -194,7 +225,7 @@ DTO mapping
decimal → string
date → ISO string
STEP 5 — Generate NestJS CRUD modules
STEP 5 — Generate NestJS CRUD modules and derived DTOs
Per entity generate:
@@ -311,6 +342,10 @@ Rules:
- 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
@@ -324,6 +359,9 @@ 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:
@@ -346,6 +384,17 @@ 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:
@@ -375,6 +424,8 @@ prisma.seed
STEP 12 — Generate React Admin resources
Generate React Admin resources automatically from the domain DSL.
For each entity generate:
Field mapping
@@ -389,6 +440,8 @@ 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
{
@@ -410,9 +463,14 @@ 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

View File

@@ -35,15 +35,47 @@ Follow:
---
# Step 1 — Parse DSL
# Input Contract
Read DSL inputs and extract:
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.
---
@@ -89,6 +121,12 @@ Generate backend source artifacts:
- 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`:
@@ -223,3 +261,4 @@ Run runtime, auth, and contract checks from `generation/post-generation-validati
- 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

@@ -2,6 +2,8 @@
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

View File

@@ -12,7 +12,35 @@ Follow:
---
# Step 1 — Parse DSL
# 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:
@@ -20,6 +48,10 @@ Extract:
- 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.
---
@@ -54,7 +86,7 @@ For each entity create:
- `EntityEdit.tsx`
- `EntityShow.tsx`
Resource generation must remain compatible with the auth-aware shared request seam.
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.
---
@@ -62,6 +94,14 @@ Resource generation must remain compatible with the auth-aware shared request se
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.
---
@@ -98,6 +138,11 @@ Generate Keycloak frontend integration with these required rules:
- 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

View File

@@ -6,6 +6,17 @@ After generating the backend or fullstack application, run these checks to ensur
# 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:
@@ -27,7 +38,22 @@ After generating the backend or fullstack application, run these checks to ensur
---
## 2. Keycloak realm artifact
## 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.
@@ -50,7 +76,7 @@ After generating the backend or fullstack application, run these checks to ensur
---
## 3. Frontend auth files and behavior
## 4. Frontend auth files and behavior
- [ ] Generated frontend includes:
- `client/src/config/env.ts`
@@ -65,6 +91,9 @@ After generating the backend or fullstack application, run these checks to ensur
- [ ] 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
@@ -77,7 +106,7 @@ After generating the backend or fullstack application, run these checks to ensur
---
## 4. Backend auth files and behavior
## 5. Backend auth files and behavior
- [ ] Generated backend includes:
- `server/src/auth/auth.module.ts`
@@ -100,7 +129,7 @@ After generating the backend or fullstack application, run these checks to ensur
---
## 5. CRUD protection and RBAC defaults
## 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.
@@ -115,7 +144,7 @@ After generating the backend or fullstack application, run these checks to ensur
---
## 6. PrismaService implementation
## 7. PrismaService implementation
- [ ] A `PrismaService` (or equivalent) class exists and extends `PrismaClient`.
- [ ] It implements `OnModuleInit` and calls `await this.$connect()` in `onModuleInit()`.
@@ -127,7 +156,7 @@ After generating the backend or fullstack application, run these checks to ensur
---
## 7. Prisma client lifecycle
## 8. Prisma client lifecycle
- [ ] `package.json` includes a script that runs Prisma client generation:
- either `"postinstall": "prisma generate"` (or `npx prisma generate`)
@@ -138,7 +167,7 @@ After generating the backend or fullstack application, run these checks to ensur
---
## 8. Database migration
## 9. Database migration
- [ ] Migration workflow is documented.
- [ ] Instruction to run `npx prisma migrate dev` exists after first generation or schema change.
@@ -148,7 +177,7 @@ After generating the backend or fullstack application, run these checks to ensur
---
## 9. REST route parameters
## 10. REST route parameters
- [ ] For each entity, path parameters use the correct primary key name from the DSL.
- [ ] Entity with PK `id` uses `/:id`.
@@ -160,18 +189,20 @@ After generating the backend or fullstack application, run these checks to ensur
---
## 10. DTO type mapping and React Admin ID compatibility
## 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.
---
## 11. Update payload sanitization
## 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()`.
@@ -180,7 +211,7 @@ After generating the backend or fullstack application, run these checks to ensur
---
## 12. Database runtime
## 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`.
@@ -191,7 +222,7 @@ After generating the backend or fullstack application, run these checks to ensur
---
## 13. Migrations, seed, and health endpoint
## 14. Migrations, seed, and health endpoint
- [ ] `npx prisma migrate dev` runs successfully from `server/`.
- [ ] Seed script exists at `server/prisma/seed.ts` (or equivalent).
@@ -209,9 +240,11 @@ After generating the backend or fullstack application, run these checks to ensur
| --- | --- |
| 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 |
@@ -224,6 +257,7 @@ After generating the backend or fullstack application, run these checks to ensur
| 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 |
---
@@ -231,6 +265,7 @@ After generating the backend or fullstack application, run these checks to ensur
# Integration with generation pipeline
1. Backend and frontend generation must produce artifacts that satisfy the above by default.
2. Runtime bootstrap must include Keycloak realm import/verification before app startup.
3. After generation, run this checklist manually or via an automated script.
4. If any check fails, update the generator context so future runs pass without manual repair.
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

@@ -9,6 +9,8 @@ The generator must produce a **runnable development environment** consisting of:
- 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.
---

View File

@@ -80,13 +80,35 @@ npm install keycloak-js
Generation pipeline order:
1. **Parse DSL** — Read domain, DTO, API, and UI DSL files.
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`, runtime config files, lifecycle scripts, and a root-level Keycloak realm import artifact (repository default example filename: `toir-realm.json`).
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 the DSL and the project context documents, including the auth-specific context in `auth/*.md`.
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

@@ -2,6 +2,9 @@
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`.
@@ -23,13 +26,17 @@ When the DSL changes, regeneration must preserve the default auth-enabled runtim
- `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 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.
9. Re-run post-generation validation, including:
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