This commit is contained in:
MaKarin
2026-03-15 17:29:37 +03:00
commit 33521016d3
86 changed files with 20514 additions and 0 deletions

254
backend/architecture.md Normal file
View File

@@ -0,0 +1,254 @@
# Backend Architecture
Backend stack:
- Node.js
- TypeScript
- NestJS
- Prisma ORM
- PostgreSQL
The backend is generated from the DSL specification.
Each DSL entity becomes:
- Prisma model
- NestJS module
- CRUD controller
- Service
- DTO definitions
---
# Project Structure
server/
package.json
prisma/
schema.prisma
src/
main.ts
app.module.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
@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.
---
# 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
}
---
# Service Layer
Services implement CRUD operations using Prisma.
Example:
findAll()
findOne(id)
create(data)
update(id, data)
remove(id)
---
# DTO Rules
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 at least `DATABASE_URL`. See **backend/runtime-rules.md**.
- **.env:** Generated project must include a `.env` (and `.env.example`) with `DATABASE_URL` 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

@@ -0,0 +1,70 @@
# 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
```

166
backend/prisma-rules.md Normal file
View File

@@ -0,0 +1,166 @@
# 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`.

80
backend/prisma-service.md Normal file
View File

@@ -0,0 +1,80 @@
# 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`.

94
backend/runtime-rules.md Normal file
View File

@@ -0,0 +1,94 @@
# 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 |
| ------------ | -------- | --------------------------------------- |
| DATABASE_URL | Yes | PostgreSQL connection string for Prisma |
## Example
```env
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/toir"
```
## Generation requirement
When generating the backend, **always** create:
1. **`.env.example`** — with placeholder DATABASE_URL and instructions.
2. **`.env`** — with the same placeholder so the app can start; user replaces with real values.
If the generator does not create `.env`, the first run will fail with:
```
Environment variable not found: DATABASE_URL
```
---
# 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).
---
# Summary
| Requirement | Action |
| --------------- | ------------------------------------------------------------- |
| DATABASE_URL | Create `.env` and `.env.example` with DATABASE_URL |
| Prisma client | Run `npx prisma generate`; add `postinstall` script |
| Database schema | Document/run `npx prisma migrate dev` after schema generation |

82
backend/seed-rules.md Normal file
View File

@@ -0,0 +1,82 @@
# 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. |

97
backend/service-rules.md Normal file
View File

@@ -0,0 +1,97 @@
# 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`. |