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

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
This directory defines the AI generation context.
All code generation must follow the rules described in these documents.

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`. |

18
client/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

34
client/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# Dependencies
node_modules/
# Build
dist/
dist-ssr/
# Environment
.env
.env.local
.env.*.local
*.local
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.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?

30
client/README.md Normal file
View File

@@ -0,0 +1,30 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

13
client/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4871
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
client/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/material": "^7.3.9",
"ra-data-simple-rest": "^5.14.4",
"react": "^18.2.0",
"react-admin": "^5.14.4",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2",
"vite": "^5.1.0"
}
}

1
client/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

48
client/src/App.tsx Normal file
View File

@@ -0,0 +1,48 @@
import { Admin, Resource } from 'react-admin';
import dataProvider from './dataProvider';
import { EquipmentTypeList } from './resources/equipment-type/EquipmentTypeList';
import { EquipmentTypeCreate } from './resources/equipment-type/EquipmentTypeCreate';
import { EquipmentTypeEdit } from './resources/equipment-type/EquipmentTypeEdit';
import { EquipmentTypeShow } from './resources/equipment-type/EquipmentTypeShow';
import { EquipmentList } from './resources/equipment/EquipmentList';
import { EquipmentCreate } from './resources/equipment/EquipmentCreate';
import { EquipmentEdit } from './resources/equipment/EquipmentEdit';
import { EquipmentShow } from './resources/equipment/EquipmentShow';
import { RepairOrderList } from './resources/repair-order/RepairOrderList';
import { RepairOrderCreate } from './resources/repair-order/RepairOrderCreate';
import { RepairOrderEdit } from './resources/repair-order/RepairOrderEdit';
import { RepairOrderShow } from './resources/repair-order/RepairOrderShow';
const App = () => (
<Admin dataProvider={dataProvider}>
<Resource
name="equipment-types"
options={{ label: 'Виды оборудования' }}
list={EquipmentTypeList}
create={EquipmentTypeCreate}
edit={EquipmentTypeEdit}
show={EquipmentTypeShow}
/>
<Resource
name="equipment"
options={{ label: 'Оборудование' }}
list={EquipmentList}
create={EquipmentCreate}
edit={EquipmentEdit}
show={EquipmentShow}
/>
<Resource
name="repair-orders"
options={{ label: 'Заявки на ремонт' }}
list={RepairOrderList}
create={RepairOrderCreate}
edit={RepairOrderEdit}
show={RepairOrderShow}
/>
</Admin>
);
export default App;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

125
client/src/dataProvider.ts Normal file
View File

@@ -0,0 +1,125 @@
import { DataProvider, fetchUtils } from 'react-admin';
const apiUrl = 'http://localhost:3000';
const httpClient = fetchUtils.fetchJson;
const dataProvider: DataProvider = {
getList: async (resource, params) => {
const { page, perPage } = params.pagination!;
const { field, order } = params.sort!;
const start = (page - 1) * perPage;
const end = page * perPage;
const query: Record<string, string> = {
_start: String(start),
_end: String(end),
_sort: field,
_order: order,
};
if (params.filter) {
Object.keys(params.filter).forEach((key) => {
const val = params.filter[key];
if (val !== undefined && val !== null && val !== '') {
query[key] = String(val);
}
});
}
const queryString = new URLSearchParams(query).toString();
const url = `${apiUrl}/${resource}?${queryString}`;
const { json, headers } = await httpClient(url);
const contentRange = headers.get('Content-Range');
const total = contentRange
? parseInt(contentRange.split('/').pop() || '0', 10)
: json.length;
return { data: json, total };
},
getOne: async (resource, params) => {
const { json } = await httpClient(`${apiUrl}/${resource}/${params.id}`);
return { data: json };
},
getMany: async (resource, params) => {
const query = params.ids.map((id) => `id=${id}`).join('&');
const { json } = await httpClient(`${apiUrl}/${resource}?${query}`);
return { data: json };
},
getManyReference: async (resource, params) => {
const { page, perPage } = params.pagination!;
const { field, order } = params.sort!;
const start = (page - 1) * perPage;
const end = page * perPage;
const query: Record<string, string> = {
_start: String(start),
_end: String(end),
_sort: field,
_order: order,
[params.target]: String(params.id),
};
const queryString = new URLSearchParams(query).toString();
const url = `${apiUrl}/${resource}?${queryString}`;
const { json, headers } = await httpClient(url);
const contentRange = headers.get('Content-Range');
const total = contentRange
? parseInt(contentRange.split('/').pop() || '0', 10)
: json.length;
return { data: json, total };
},
create: async (resource, params) => {
const { json } = await httpClient(`${apiUrl}/${resource}`, {
method: 'POST',
body: JSON.stringify(params.data),
});
return { data: json };
},
update: async (resource, params) => {
const { json } = await httpClient(`${apiUrl}/${resource}/${params.id}`, {
method: 'PATCH',
body: JSON.stringify(params.data),
});
return { data: json };
},
updateMany: async (resource, params) => {
const responses = await Promise.all(
params.ids.map((id) =>
httpClient(`${apiUrl}/${resource}/${id}`, {
method: 'PATCH',
body: JSON.stringify(params.data),
})
)
);
return { data: responses.map(({ json }) => json.id) };
},
delete: async (resource, params) => {
const { json } = await httpClient(`${apiUrl}/${resource}/${params.id}`, {
method: 'DELETE',
});
return { data: json };
},
deleteMany: async (resource, params) => {
const responses = await Promise.all(
params.ids.map((id) =>
httpClient(`${apiUrl}/${resource}/${id}`, {
method: 'DELETE',
})
)
);
return { data: responses.map(({ json }) => json.id) };
},
};
export default dataProvider;

9
client/src/main.tsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View File

@@ -0,0 +1,13 @@
import { Create, SimpleForm, TextInput, NumberInput } from 'react-admin';
export const EquipmentTypeCreate = () => (
<Create>
<SimpleForm>
<TextInput source="code" label="Код" isRequired />
<TextInput source="name" label="Наименование" isRequired />
<TextInput source="manufacturer" label="Производитель" />
<NumberInput source="maintenanceIntervalHours" label="Периодичность ТО (ч)" />
<NumberInput source="overhaulIntervalHours" label="Периодичность КР (ч)" />
</SimpleForm>
</Create>
);

View File

@@ -0,0 +1,13 @@
import { Edit, SimpleForm, TextInput, NumberInput } from 'react-admin';
export const EquipmentTypeEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="code" label="Код" disabled />
<TextInput source="name" label="Наименование" isRequired />
<TextInput source="manufacturer" label="Производитель" />
<NumberInput source="maintenanceIntervalHours" label="Периодичность ТО (ч)" />
<NumberInput source="overhaulIntervalHours" label="Периодичность КР (ч)" />
</SimpleForm>
</Edit>
);

View File

@@ -0,0 +1,13 @@
import { List, Datagrid, TextField, NumberField } from 'react-admin';
export const EquipmentTypeList = () => (
<List>
<Datagrid rowClick="show">
<TextField source="code" label="Код" />
<TextField source="name" label="Наименование" />
<TextField source="manufacturer" label="Производитель" />
<NumberField source="maintenanceIntervalHours" label="Периодичность ТО (ч)" />
<NumberField source="overhaulIntervalHours" label="Периодичность КР (ч)" />
</Datagrid>
</List>
);

View File

@@ -0,0 +1,13 @@
import { Show, SimpleShowLayout, TextField, NumberField } from 'react-admin';
export const EquipmentTypeShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="code" label="Код" />
<TextField source="name" label="Наименование" />
<TextField source="manufacturer" label="Производитель" />
<NumberField source="maintenanceIntervalHours" label="Периодичность ТО (ч)" />
<NumberField source="overhaulIntervalHours" label="Периодичность КР (ч)" />
</SimpleShowLayout>
</Show>
);

View File

@@ -0,0 +1,36 @@
import {
Create,
SimpleForm,
TextInput,
NumberInput,
DateInput,
SelectInput,
ReferenceInput,
} from 'react-admin';
const statusChoices = [
{ id: 'Active', name: 'В эксплуатации' },
{ id: 'Repair', name: 'В ремонте' },
{ id: 'Reserve', name: 'В резерве' },
{ id: 'WriteOff', name: 'Списано' },
];
export const EquipmentCreate = () => (
<Create>
<SimpleForm>
<TextInput source="inventoryNumber" label="Инвентарный номер" isRequired />
<TextInput source="serialNumber" label="Заводской номер" />
<TextInput source="name" label="Наименование" isRequired />
<ReferenceInput source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования">
<SelectInput optionText="name" optionValue="code" isRequired />
</ReferenceInput>
<SelectInput source="status" label="Статус" choices={statusChoices} defaultValue="Active" />
<TextInput source="location" label="Место эксплуатации" />
<DateInput source="commissionedAt" label="Дата ввода в эксплуатацию" />
<NumberInput source="totalEngineHours" label="Общая наработка (ч)" />
<NumberInput source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта (ч)" />
<DateInput source="lastRepairAt" label="Дата последнего ремонта" />
<TextInput source="notes" label="Примечания" multiline />
</SimpleForm>
</Create>
);

View File

@@ -0,0 +1,36 @@
import {
Edit,
SimpleForm,
TextInput,
NumberInput,
DateInput,
SelectInput,
ReferenceInput,
} from 'react-admin';
const statusChoices = [
{ id: 'Active', name: 'В эксплуатации' },
{ id: 'Repair', name: 'В ремонте' },
{ id: 'Reserve', name: 'В резерве' },
{ id: 'WriteOff', name: 'Списано' },
];
export const EquipmentEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="inventoryNumber" label="Инвентарный номер" isRequired />
<TextInput source="serialNumber" label="Заводской номер" />
<TextInput source="name" label="Наименование" isRequired />
<ReferenceInput source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования">
<SelectInput optionText="name" optionValue="code" isRequired />
</ReferenceInput>
<SelectInput source="status" label="Статус" choices={statusChoices} />
<TextInput source="location" label="Место эксплуатации" />
<DateInput source="commissionedAt" label="Дата ввода в эксплуатацию" />
<NumberInput source="totalEngineHours" label="Общая наработка (ч)" />
<NumberInput source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта (ч)" />
<DateInput source="lastRepairAt" label="Дата последнего ремонта" />
<TextInput source="notes" label="Примечания" multiline />
</SimpleForm>
</Edit>
);

View File

@@ -0,0 +1,30 @@
import {
List,
Datagrid,
TextField,
NumberField,
SelectField,
ReferenceField,
} from 'react-admin';
const statusChoices = [
{ id: 'Active', name: 'В эксплуатации' },
{ id: 'Repair', name: 'В ремонте' },
{ id: 'Reserve', name: 'В резерве' },
{ id: 'WriteOff', name: 'Списано' },
];
export const EquipmentList = () => (
<List>
<Datagrid rowClick="show">
<TextField source="inventoryNumber" label="Инвентарный номер" />
<TextField source="name" label="Наименование" />
<ReferenceField source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования" link="show">
<TextField source="name" />
</ReferenceField>
<SelectField source="status" label="Статус" choices={statusChoices} />
<TextField source="location" label="Место эксплуатации" />
<NumberField source="totalEngineHours" label="Наработка (ч)" />
</Datagrid>
</List>
);

View File

@@ -0,0 +1,36 @@
import {
Show,
SimpleShowLayout,
TextField,
NumberField,
DateField,
SelectField,
ReferenceField,
} from 'react-admin';
const statusChoices = [
{ id: 'Active', name: 'В эксплуатации' },
{ id: 'Repair', name: 'В ремонте' },
{ id: 'Reserve', name: 'В резерве' },
{ id: 'WriteOff', name: 'Списано' },
];
export const EquipmentShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="inventoryNumber" label="Инвентарный номер" />
<TextField source="serialNumber" label="Заводской номер" />
<TextField source="name" label="Наименование" />
<ReferenceField source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования" link="show">
<TextField source="name" />
</ReferenceField>
<SelectField source="status" label="Статус" choices={statusChoices} />
<TextField source="location" label="Место эксплуатации" />
<DateField source="commissionedAt" label="Дата ввода в эксплуатацию" />
<NumberField source="totalEngineHours" label="Общая наработка (ч)" />
<NumberField source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта (ч)" />
<DateField source="lastRepairAt" label="Дата последнего ремонта" />
<TextField source="notes" label="Примечания" />
</SimpleShowLayout>
</Show>
);

View File

@@ -0,0 +1,46 @@
import {
Create,
SimpleForm,
TextInput,
NumberInput,
DateInput,
SelectInput,
ReferenceInput,
} from 'react-admin';
const repairKindChoices = [
{ id: 'TO', name: 'Техническое обслуживание' },
{ id: 'TR', name: 'Текущий ремонт' },
{ id: 'TRE', name: 'Текущий расширенный ремонт' },
{ id: 'KR', name: 'Капитальный ремонт' },
{ id: 'AR', name: 'Аварийный ремонт' },
{ id: 'MP', name: 'Метрологическая поверка' },
];
const statusChoices = [
{ id: 'Draft', name: 'Черновик' },
{ id: 'Approved', name: 'Утверждена' },
{ id: 'InWork', name: 'В работе' },
{ id: 'Done', name: 'Выполнена' },
{ id: 'Cancelled', name: 'Отменена' },
];
export const RepairOrderCreate = () => (
<Create>
<SimpleForm>
<TextInput source="number" label="Номер заявки" isRequired />
<ReferenceInput source="equipmentId" reference="equipment" label="Оборудование">
<SelectInput optionText="name" isRequired />
</ReferenceInput>
<SelectInput source="repairKind" label="Вид ремонта" choices={repairKindChoices} isRequired />
<SelectInput source="status" label="Статус" choices={statusChoices} defaultValue="Draft" />
<DateInput source="plannedAt" label="Плановая дата начала" isRequired />
<DateInput source="startedAt" label="Фактическая дата начала" />
<DateInput source="completedAt" label="Фактическая дата завершения" />
<TextInput source="contractor" label="Подрядная организация" />
<NumberInput source="engineHoursAtRepair" label="Наработка на момент ремонта (ч)" />
<TextInput source="description" label="Описание работ / дефекта" multiline />
<TextInput source="notes" label="Примечания" multiline />
</SimpleForm>
</Create>
);

View File

@@ -0,0 +1,46 @@
import {
Edit,
SimpleForm,
TextInput,
NumberInput,
DateInput,
SelectInput,
ReferenceInput,
} from 'react-admin';
const repairKindChoices = [
{ id: 'TO', name: 'Техническое обслуживание' },
{ id: 'TR', name: 'Текущий ремонт' },
{ id: 'TRE', name: 'Текущий расширенный ремонт' },
{ id: 'KR', name: 'Капитальный ремонт' },
{ id: 'AR', name: 'Аварийный ремонт' },
{ id: 'MP', name: 'Метрологическая поверка' },
];
const statusChoices = [
{ id: 'Draft', name: 'Черновик' },
{ id: 'Approved', name: 'Утверждена' },
{ id: 'InWork', name: 'В работе' },
{ id: 'Done', name: 'Выполнена' },
{ id: 'Cancelled', name: 'Отменена' },
];
export const RepairOrderEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="number" label="Номер заявки" isRequired />
<ReferenceInput source="equipmentId" reference="equipment" label="Оборудование">
<SelectInput optionText="name" isRequired />
</ReferenceInput>
<SelectInput source="repairKind" label="Вид ремонта" choices={repairKindChoices} isRequired />
<SelectInput source="status" label="Статус" choices={statusChoices} />
<DateInput source="plannedAt" label="Плановая дата начала" isRequired />
<DateInput source="startedAt" label="Фактическая дата начала" />
<DateInput source="completedAt" label="Фактическая дата завершения" />
<TextInput source="contractor" label="Подрядная организация" />
<NumberInput source="engineHoursAtRepair" label="Наработка на момент ремонта (ч)" />
<TextInput source="description" label="Описание работ / дефекта" multiline />
<TextInput source="notes" label="Примечания" multiline />
</SimpleForm>
</Edit>
);

View File

@@ -0,0 +1,40 @@
import {
List,
Datagrid,
TextField,
DateField,
SelectField,
ReferenceField,
} from 'react-admin';
const repairKindChoices = [
{ id: 'TO', name: 'Техническое обслуживание' },
{ id: 'TR', name: 'Текущий ремонт' },
{ id: 'TRE', name: 'Текущий расширенный ремонт' },
{ id: 'KR', name: 'Капитальный ремонт' },
{ id: 'AR', name: 'Аварийный ремонт' },
{ id: 'MP', name: 'Метрологическая поверка' },
];
const statusChoices = [
{ id: 'Draft', name: 'Черновик' },
{ id: 'Approved', name: 'Утверждена' },
{ id: 'InWork', name: 'В работе' },
{ id: 'Done', name: 'Выполнена' },
{ id: 'Cancelled', name: 'Отменена' },
];
export const RepairOrderList = () => (
<List>
<Datagrid rowClick="show">
<TextField source="number" label="Номер" />
<ReferenceField source="equipmentId" reference="equipment" label="Оборудование" link="show">
<TextField source="name" />
</ReferenceField>
<SelectField source="repairKind" label="Вид ремонта" choices={repairKindChoices} />
<SelectField source="status" label="Статус" choices={statusChoices} />
<DateField source="plannedAt" label="Плановая дата" />
<TextField source="contractor" label="Подрядчик" />
</Datagrid>
</List>
);

View File

@@ -0,0 +1,46 @@
import {
Show,
SimpleShowLayout,
TextField,
NumberField,
DateField,
SelectField,
ReferenceField,
} from 'react-admin';
const repairKindChoices = [
{ id: 'TO', name: 'Техническое обслуживание' },
{ id: 'TR', name: 'Текущий ремонт' },
{ id: 'TRE', name: 'Текущий расширенный ремонт' },
{ id: 'KR', name: 'Капитальный ремонт' },
{ id: 'AR', name: 'Аварийный ремонт' },
{ id: 'MP', name: 'Метрологическая поверка' },
];
const statusChoices = [
{ id: 'Draft', name: 'Черновик' },
{ id: 'Approved', name: 'Утверждена' },
{ id: 'InWork', name: 'В работе' },
{ id: 'Done', name: 'Выполнена' },
{ id: 'Cancelled', name: 'Отменена' },
];
export const RepairOrderShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="number" label="Номер заявки" />
<ReferenceField source="equipmentId" reference="equipment" label="Оборудование" link="show">
<TextField source="name" />
</ReferenceField>
<SelectField source="repairKind" label="Вид ремонта" choices={repairKindChoices} />
<SelectField source="status" label="Статус" choices={statusChoices} />
<DateField source="plannedAt" label="Плановая дата начала" />
<DateField source="startedAt" label="Фактическая дата начала" />
<DateField source="completedAt" label="Фактическая дата завершения" />
<TextField source="contractor" label="Подрядная организация" />
<NumberField source="engineHoursAtRepair" label="Наработка на момент ремонта (ч)" />
<TextField source="description" label="Описание работ / дефекта" />
<TextField source="notes" label="Примечания" />
</SimpleShowLayout>
</Show>
);

1
client/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

25
client/tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

11
client/tsconfig.node.json Normal file
View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

7
client/vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})

16
docker-compose.yml Normal file
View File

@@ -0,0 +1,16 @@
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:

213
domain/dsl-spec.md Normal file
View File

@@ -0,0 +1,213 @@
# 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.
---
# DSL Grammar Concepts
## entity
An **entity** is a domain object that becomes a database table and a first-class resource in the backend and frontend.
```
entity Equipment {
attribute id { type uuid; key primary; }
attribute name { type string; is required; }
}
```
- **Domain:** Defines the canonical model; one entity = one Prisma model, one NestJS module, one React Admin resource.
- **Naming:** PascalCase (e.g. `Equipment`, `EquipmentType`, `RepairOrder`).
---
## attribute
An **attribute** is a field of an entity. It has a type and optional modifiers.
```
attribute name {
description "Наименование";
type string;
is required;
is unique;
}
```
**Modifiers:**
- `type` — required; one of: `string`, `uuid`, `integer`, `decimal`, `date`, `text`, or an enum name.
- `key primary` — this attribute is the primary key.
- `key foreign { relates Entity.field }` — foreign key to another entity's field.
- `is required` — non-nullable.
- `is unique` — unique constraint.
- `default Value` — default value (for enums or literals).
- `description "..."` — human-readable description.
---
## enum
An **enum** defines a fixed set of values. Used for attributes that can only take one of these values.
```
enum EquipmentStatus {
value Active { label "В эксплуатации"; }
value Repair { label "В ремонте"; }
}
```
- **value** — identifier used in data and code.
- **label** — optional display label for UI.
---
## primary key
Exactly one attribute per entity must be marked as primary key.
```
attribute id {
type uuid;
key primary;
}
```
Or for a natural key:
```
attribute code {
type string;
key primary;
is required;
is unique;
}
```
---
## foreign key
A **foreign key** links to another entity's primary key. The attribute type must match the referenced primary key type.
```
attribute equipmentTypeCode {
type string;
key foreign {
relates EquipmentType.code;
}
is required;
}
```
- `relates Entity.attribute` — references `Entity`'s `attribute` (must be primary key).
- FK type must equal referenced PK type (e.g. `string``EquipmentType.code`, `uuid``Equipment.id`).
---
## required
- **is required** — attribute is non-nullable in domain and (unless overridden) in DTOs.
- Absence of `is required` means the attribute is optional (nullable).
---
## default
- **default Value** — applied when no value is provided (e.g. enum defaults like `default Active`).
- Value must exist in the enum when the attribute type is an enum.
---
## 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
| DSL Concept | Prisma Result |
| ------------ | --------------------------- |
| entity | model |
| attribute | field |
| enum | enum |
| key primary | @id or @id @default(uuid()) |
| key foreign | relation + references |
| type string | String |
| type uuid | String @id @default(uuid()) |
| type integer | Int |
| type decimal | Decimal |
| type date | DateTime |
| type text | String |
---
## DSL → NestJS
| DSL Concept | NestJS Result |
| -------------- | ------------------------------------- |
| 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 |
API paths are derived from entity name: PascalCase → kebab-case, pluralized (e.g. `Equipment``/equipment`, `RepairOrder``/repair-orders`).
---
## DSL → React Admin
| DSL Concept | React Admin Result |
| --------------------- | ----------------------------------- |
| entity | Resource (name = kebab-case plural) |
| attribute | Form field / column |
| type string | TextInput, TextField |
| type integer/decimal | NumberInput, NumberField |
| type date | DateInput, DateField |
| enum | SelectInput with choices |
| foreign key | ReferenceInput, ReferenceField |
| UI attribute with map | Field with correct source |
---
# DTO 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.

57
examples/TOiR-ui.dsl Normal file
View File

@@ -0,0 +1,57 @@
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;
}
}

811
examples/TOiR.api.dsl Normal file
View File

@@ -0,0 +1,811 @@
// ─────────────────────────────────────────────
// 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;
}
}
}

257
examples/TOiR.domain.dsl Normal file
View File

@@ -0,0 +1,257 @@
/*
КИС ТОиР — демонстрационная схема доменной модели
Сущности: Equipment (Оборудование), EquipmentType (Вид оборудования), RepairOrder (Заявка на ремонт)
*/
// ─────────────────────────────────────────────
// Перечисления
// ─────────────────────────────────────────────
enum EquipmentStatus {
value Active {
label "В эксплуатации";
}
value Repair {
label "В ремонте";
}
value Reserve {
label "В резерве";
}
value WriteOff {
label "Списано";
}
}
enum RepairKind {
value TO {
label "Техническое обслуживание";
}
value TR {
label "Текущий ремонт";
}
value TRE {
label "Текущий расширенный ремонт";
}
value KR {
label "Капитальный ремонт";
}
value AR {
label "Аварийный ремонт";
}
value MP {
label "Метрологическая поверка";
}
}
enum RepairOrderStatus {
value Draft {
label "Черновик";
}
value Approved {
label "Утверждена";
}
value InWork {
label "В работе";
}
value Done {
label "Выполнена";
}
value Cancelled {
label "Отменена";
}
}
// ─────────────────────────────────────────────
// Справочник: Вид оборудования
// ─────────────────────────────────────────────
entity EquipmentType {
description "Вид (марка) оборудования — нормативный справочник НСИ";
attribute code {
key primary;
description "Код вида оборудования";
type string;
is required;
is unique;
}
attribute name {
description "Наименование вида";
type string;
is required;
}
attribute manufacturer {
description "Производитель";
type string;
}
// Нормативный межремонтный ресурс (моточасы)
attribute maintenanceIntervalHours {
description "Периодичность ТО, моточасов";
type integer;
}
attribute overhaulIntervalHours {
description "Периодичность КР, моточасов";
type integer;
}
}
// ─────────────────────────────────────────────
// Основная сущность: Оборудование
// ─────────────────────────────────────────────
entity Equipment {
description "Единица оборудования — объект ремонта и технического обслуживания";
attribute id {
type uuid;
key primary;
}
attribute inventoryNumber {
description "Инвентарный номер";
type string;
is required;
is unique;
}
attribute serialNumber {
description "Заводской (серийный) номер";
type string;
}
attribute name {
description "Наименование единицы оборудования";
type string;
is required;
}
// Связь с видом оборудования (справочник НСИ)
attribute equipmentTypeCode {
type string;
key foreign {
relates EquipmentType.code;
}
is required;
}
attribute status {
description "Текущий статус";
type EquipmentStatus;
default Active;
is required;
}
attribute location {
description "Место эксплуатации / скважина / куст";
type string;
}
attribute commissionedAt {
description "Дата ввода в эксплуатацию";
type date;
}
// Наработка фиксируется вручную или из производственной программы
attribute totalEngineHours {
description "Общая наработка, моточасов";
type decimal;
}
attribute engineHoursSinceLastRepair {
description "Наработка с последнего ремонта, моточасов";
type decimal;
}
attribute lastRepairAt {
description "Дата последнего ремонта";
type date;
}
attribute notes {
description "Примечания";
type text;
}
}
// ─────────────────────────────────────────────
// Заявка на ремонт
// ─────────────────────────────────────────────
entity RepairOrder {
description "Заявка на ремонт — формируется по ППР или по факту обнаруженного дефекта";
attribute id {
type uuid;
key primary;
}
attribute number {
description "Номер заявки";
type string;
is required;
is unique;
}
attribute equipmentId {
type uuid;
key foreign {
relates Equipment.id;
}
is required;
}
attribute repairKind {
description "Вид ремонта";
type RepairKind;
is required;
}
attribute status {
type RepairOrderStatus;
default Draft;
is required;
}
attribute plannedAt {
description "Плановая дата начала";
type date;
is required;
}
attribute startedAt {
description "Фактическая дата начала";
type date;
}
attribute completedAt {
description "Фактическая дата завершения";
type date;
}
attribute contractor {
description "Подрядная организация (если внешний ремонт)";
type string;
}
attribute engineHoursAtRepair {
description "Наработка на момент ремонта, моточасов";
type decimal;
}
attribute description {
description "Описание работ / дефекта";
type text;
}
attribute notes {
description "Примечания";
type text;
}
}

753
examples/TOiR.dto.dsl Normal file
View File

@@ -0,0 +1,753 @@
/*
КИС ТОиР — 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;
}
}

123
frontend/architecture.md Normal file
View File

@@ -0,0 +1,123 @@
# Frontend Architecture
Frontend stack:
- React
- TypeScript
- Vite
- React Admin
- shadcn/ui
The frontend is generated from the DSL and API specification.
Each entity becomes a React Admin resource.
---
# Project Structure
client/
src/
App.tsx
resources/
{entity}/
{entity}List.tsx
{entity}Create.tsx
{entity}Edit.tsx
{entity}Show.tsx
---
# Resource Registration
Each resource must be registered in App.tsx.
Example:
<Resource
name="equipment"
list={EquipmentList}
create={EquipmentCreate}
edit={EquipmentEdit}
show={EquipmentShow}
/>
---
# Data Provider
React Admin uses the standard REST provider.
API format must follow:
GET /resource
GET /resource/:id
POST /resource
PATCH /resource/:id
DELETE /resource/:id
List response format:
{
data: [],
total: number
}
---
# 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

@@ -0,0 +1,98 @@
# DSL → React Admin Mapping
Entity attributes determine UI fields.
---
# 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).
**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`.
This rule ensures compatibility with React Admin resource identity handling.

365
general-prompt.md Normal file
View File

@@ -0,0 +1,365 @@
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
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
examples/\*.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
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.
GOAL
Generate a DSL-driven fullstack CRUD system.
Stack:
Backend
Node.js
NestJS
Prisma ORM
PostgreSQL
Frontend
React
Vite
React Admin
MUI
shadcn/ui
PROJECT STRUCTURE
Root
docker-compose.yml
server/
client/
Backend
server/
src/
modules/{entity}/
prisma/schema.prisma
prisma/seed.ts
.env
.env.example
Frontend
client/src/resources/{entity}/
client/src/App.tsx
client/src/dataProvider.ts
STEP 1 — Parse DSL
Parse all DSL files and extract:
Entities
Attributes
Primary keys
Foreign keys
Enums
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
Frontend
react-admin
ra-data-simple-rest
@mui/material
@emotion/react
@emotion/styled
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 modules
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 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 7 — Generate PrismaService
Requirements
extends PrismaClient
implements OnModuleInit
await this.$connect()
Do NOT use
beforeExit
STEP 8 — Generate runtime infrastructure
Create
server/.env
server/.env.example
DATABASE_URL example
postgresql://postgres:postgres@localhost:5432/toir
Add to package.json
postinstall: prisma generate
STEP 9 — Database runtime
Generate root
docker-compose.yml
PostgreSQL container
postgres:16
port 5432
STEP 10 — Generate seed
Create
server/prisma/seed.ts
Seed minimal data for
EquipmentType
Equipment
RepairOrder
Add to package.json
prisma.seed
STEP 11 — Generate React Admin
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.
Example
{
id: record.code,
code: record.code
}
STEP 12 — 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
OUTPUT
Provide
FULLSTACK GENERATION REPORT
Include
1 Parsed DSL
2 Prisma models
3 Backend modules
4 API endpoints
5 React Admin resources
6 Runtime configuration
7 Validation results
RUN INSTRUCTIONS
The generated application must run successfully with
docker compose up -d
cd server
npm install
npx prisma migrate dev
npm run start
cd client
npm install
npm run dev

View File

@@ -0,0 +1,138 @@
# Backend Generation Process
Backend generation follows a pipeline aligned with runtime and validation docs:
DSL
CLI scaffolding
code generation
runtime infrastructure
database runtime
migration
seed
validation
Follow **backend/runtime-rules.md**, **backend/prisma-rules.md**, **backend/prisma-service.md**, **backend/database-runtime.md**, **backend/seed-rules.md**, and **backend/service-rules.md**.
---
# Step 1 — Parse DSL
Read DSL inputs and extract:
- entities
- attributes (including primary key attribute name per entity)
- enums
- foreign keys
---
# 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`, and seed runner when needed)
---
# Step 3 — Code generation
Generate backend source artifacts:
1. **Prisma schema** (`server/prisma/schema.prisma`) from domain DSL:
- attributes
- primary keys
- relations
- enums
2. **NestJS modules** per entity:
- module
- controller (path params use actual PK name: `:id`, `:code`, etc.)
- service (must sanitize update payload before Prisma — see **backend/service-rules.md**)
3. **DTO files**:
- `create-entity.dto.ts`
- `update-entity.dto.ts`
- `entity.response.dto.ts` (or equivalent)
4. **PrismaService**:
- `OnModuleInit` + `await this.$connect()`
- no `beforeExit` hook
5. **Service update methods**: Sanitize update payload before passing to Prisma (remove `id`, primary key, and readonly attributes from `data`). Do not pass the raw request body as `data` to `prisma.*.update()`.
Use mapping rules from `backend/prisma-rules.md`:
- DSL `decimal` -> DTO `string`
- DSL `date` -> DTO `string` (ISO)
---
# Step 4 — Runtime infrastructure
Generate runtime config files:
- `server/.env`
- `server/.env.example`
- `server/package.json` lifecycle:
- `"postinstall": "prisma generate"`
- `server/package.json` Prisma seed:
- `"prisma": { "seed": "ts-node prisma/seed.ts" }` (or `tsx` variant by project standard)
Commands that must be supported/documented:
- `npx prisma generate`
- `npx prisma migrate dev`
- `npx prisma db seed`
---
# Step 5 — Database runtime
Generator must create `docker-compose.yml` at the **project root** with PostgreSQL.
Minimum required compose characteristics:
- `services.postgres`
- `image: postgres:16`
- `ports: ["5432:5432"]`
Credentials/database in compose must match `DATABASE_URL`.
---
# Step 6 — Migration
Apply schema to development database:
```bash
cd server
npx prisma migrate dev
```
---
# Step 7 — Seed
Run development seed:
```bash
cd server
npx prisma db seed
```
Seed file location: `server/prisma/seed.ts`.
---
# Step 8 — Validation
Run runtime 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
- React Admin receives `id` in every record

View File

@@ -0,0 +1,89 @@
# 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.
---
# Prerequisites
- **Node.js** (LTS, e.g. 18+)
- **npm**
- **Docker** and **Docker Compose** (for the development database)
---
# Workflow Steps
## 1. 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 (optional):
```bash
docker compose ps
```
---
## 2. 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` (for example `prisma generate`).
- `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 NestJS backend.
The API should be available at the configured port (e.g. `http://localhost:3000`). Verify with:
```bash
curl http://localhost:3000/health
```
Expected: `{ "status": "ok" }` (or equivalent).
---
## 3. 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.
- `npm run dev` — starts the Vite dev server (e.g. `http://localhost:5173`).
Open the Vite URL in a browser; the React Admin app should load and use the backend API.
---
# Summary
| Step | Command / location |
|------|---------------------|
| 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 (docker-compose, env, schema, migrations, seed, health endpoint) so that this workflow succeeds and the development environment is fully runnable.

View File

@@ -0,0 +1,36 @@
# Frontend Generation Process
Frontend generation uses the DSL and API specification.
---
# Step 1 — Parse DSL
Extract entities and attributes.
---
# Step 2 — Generate React Admin Resources
For each entity create:
EntityList.tsx
EntityCreate.tsx
EntityEdit.tsx
EntityShow.tsx
---
# Step 3 — Map Fields
Map DSL attributes to React Admin components.
---
# Step 4 — Register Resources
Register resources in App.tsx.
Example:
<Resource name="equipment" ... />

View File

@@ -0,0 +1,160 @@
# Post-Generation Validation
After generating the backend or fullstack application, run these checks to ensure the project will run without runtime errors.
---
# Validation Checklist
## 1. Environment file
- [ ] **`.env` exists** in the backend root (e.g. `server/.env`).
- [ ] **`.env.example` exists** with at least `DATABASE_URL` and a placeholder value.
- [ ] **`DATABASE_URL`** is present in `.env` (or documented in `.env.example` so the user can copy and set it).
**Failure symptom:** `Environment variable not found: DATABASE_URL` at startup.
---
## 2. 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`
---
## 3. 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 types at build/run.
---
## 4. Database migration
- [ ] **Migration workflow** is documented (e.g. in README or generation docs).
- [ ] Instruction to run **`npx prisma migrate dev`** (or `prisma migrate deploy` for production) after first generation or schema change.
- [ ] Generation pipeline (see `generation/backend-generation.md`) includes or documents the migration step.
**Failure symptom:** Tables do not exist; Prisma errors on first query.
---
## 5. REST route parameters
- [ ] For each entity, path parameters use the **correct primary key name** from the DSL.
- [ ] **Entity with PK `id` (uuid):** routes use **`/:id`** (e.g. `GET /equipment/:id`, `PATCH /equipment/:id`).
- [ ] **Entity with non-`id` primary key (e.g. `code`):** routes use **`/:code`** (or the actual PK attribute name), e.g. `GET /equipment-types/:code`, **not** `GET /equipment-types/:id`.
**Example:**
| Entity | Primary key | Correct path | Incorrect path |
| ------------- | ----------- | ---------------------- | -------------------- |
| Equipment | id | /equipment/:id | — |
| EquipmentType | code | /equipment-types/:code | /equipment-types/:id |
| RepairOrder | id | /repair-orders/:id | — |
**Failure symptom:** Controller expects `params.id` but route is defined with `:code`; or vice versa; 404 or wrong resource updated.
**Reference:** `backend/architecture.md` — API path rules for non-id primary keys.
---
## 6. DTO type mapping (serialization)
- [ ] **DSL `decimal`** → In DTO/API response, use **`string`** (or a type that serializes to string), not Prisma `Decimal`, to avoid JSON serialization issues.
- [ ] **DSL `date`** → In DTO/API response, use **`string`** (ISO 8601) or ensure DateTime is serialized to string, so React Admin and JSON consumers receive a string.
**Reference:** `backend/prisma-rules.md` — DTO type mapping table.
---
## 7. React Admin ID field in API responses
- [ ] **Every API response object** (list items and single-resource GET) contains a field named **`id`**.
- [ ] If the entity primary key is **not** named `id`, the response must **map** the primary key to `id`: e.g. `id: record.code` for EquipmentType, so the payload includes both `id` and `code` (or at least `id` with the PK value).
- [ ] The `id` field value must be the **primary key value** (string or uuid as appropriate).
**Failure symptom:** React Admin fails to identify records, breaks cache/references, or throws when expecting `record.id`.
**Reference:** `frontend/react-admin-rules.md` — React Admin ID Field Requirement.
---
## 8. Update payload sanitization (service layer)
- [ ] **Update endpoint must not pass `id` (or primary key) in Prisma `data`.** The service must sanitize the incoming DTO before calling `prisma.*.update({ where, data })`: remove `id`, remove the entity primary key field (e.g. `code`), and remove any readonly attributes. Only updatable fields should be passed as `data`.
- [ ] Generated update methods follow the pattern from **backend/service-rules.md** (e.g. `const { id, code, ...data } = dto` then pass `data` to Prisma).
**Failure symptom:** Prisma throws when `data` contains `id` or another field that is not on the model or not writable (e.g. entity with PK `code` receives body with `id` from React Admin).
**Reference:** `backend/service-rules.md`
---
## 9. Database runtime (docker-compose)
- [ ] **`docker-compose.yml` exists** at the project root (or documented location).
- [ ] It defines a **PostgreSQL** service with image (e.g. `postgres:16`), port `5432`, and credentials/DB name matching `DATABASE_URL` in `server/.env`.
- [ ] **Database container starts:** `docker compose up -d` runs without error and the container is reachable on the configured port.
**Failure symptom:** `PrismaClientInitializationError P1001: Can't reach database server at localhost:5432`.
**Reference:** `backend/database-runtime.md`
---
## 10. Migrations and seed
- [ ] **`npx prisma migrate dev`** runs successfully from `server/` when the database is up (schema is applied or created).
- [ ] **Seed script** exists at `server/prisma/seed.ts` (or equivalent) and creates minimal sample data (e.g. one EquipmentType, one Equipment, one RepairOrder).
- [ ] **`npx prisma db seed`** runs without error (package.json has `prisma.seed` configured and seed runner installed).
**Reference:** `backend/seed-rules.md`, `generation/runtime-bootstrap.md`
---
## 11. Health endpoint
- [ ] Backend exposes **GET /health** (or equivalent health route).
- [ ] **API responds to /health:** With backend running, `GET http://localhost:<port>/health` returns HTTP 200 and a body such as `{ "status": "ok" }`.
**Reference:** `backend/architecture.md` — Health Endpoint
---
# Summary Table
| Check | Required artifact / rule |
| ------------------- | ---------------------------------------------- |
| .env | File exists with DATABASE_URL |
| DATABASE_URL | Present in .env or .env.example |
| PrismaService | OnModuleInit + $connect(); no beforeExit |
| prisma generate | postinstall script or documented step |
| Migration | Documented step: prisma migrate dev |
| REST path params | Use entity PK name (:id or :code, etc.) |
| Decimal/Date in DTO | Map to string for serialization |
| API response `id` | Every record has `id`; if PK ≠ id, map PK → id |
| Update payload | Service strips `id`, PK, readonly from data before Prisma |
| docker-compose.yml | Exists at project root; PostgreSQL service |
| Database container | Starts with `docker compose up -d` |
| prisma migrate dev | Runs successfully from server/ |
| Seed script | Exists; prisma db seed runs |
| GET /health | Backend responds with 200 and status payload |
---
# Integration with generation pipeline
1. **Backend generation** (see `generation/backend-generation.md`) should produce artifacts that satisfy the above by default.
2. After generation, run this checklist manually or via a script that parses generated code and config.
3. If any check fails, the AI context or generator should be updated so that future runs pass.

View File

@@ -0,0 +1,64 @@
# Runtime Bootstrap
After project generation, the following commands must work in order so that the application runs without manual database provisioning or ad-hoc steps.
The generator must produce a **runnable development environment**: backend, frontend, and database must all be startable via a documented sequence.
---
# Bootstrap Sequence
## 1. Start the database
From the **project root**:
```bash
docker compose up -d
```
This starts the PostgreSQL container. The backend will connect to it using `DATABASE_URL` from `server/.env`.
---
## 2. 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` (e.g. `prisma generate`) if configured.
- `npx prisma generate` — ensures Prisma client is generated explicitly after install/schema changes.
- `npx prisma migrate dev` — creates/applies migrations and ensures the database schema exists.
- `npx prisma db seed` — populates minimal development data for immediate UI/API usage.
- `npm run start` — starts the NestJS server (default port e.g. 3000).
---
## 3. Frontend setup and start
In a separate terminal, from the **project root**:
```bash
cd client
npm install
npm run dev
```
- `npm run dev` — starts the Vite dev server (e.g. http://localhost:5173).
---
# Success Criteria
After running the above:
- Database container is running; Prisma can connect.
- Backend responds (e.g. `GET /health` returns `{ "status": "ok" }`).
- Frontend loads and can call the backend API.
The generator is responsible for producing all artifacts (docker-compose, schema, migrations, seed, env, health endpoint) so that this sequence succeeds without additional manual setup.

View File

@@ -0,0 +1,78 @@
# 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 (e.g. by writing all config files and folder structure by hand). Using the CLI reduces errors and ensures compatibility with current tool versions.
---
# 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 (default for Nest CLI).
- **npm** must be the package manager (`--package-manager npm`).
- **Git** initialization must be skipped (`--skip-git`).
## After scaffolding — install required dependencies
Run from the `server` directory:
```bash
npm install @prisma/client
npm install prisma --save-dev
npm install @nestjs/config
```
---
# 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 (`--template react-ts`).
## 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
```
---
# Scaffolding Strategy
Generation pipeline order:
1. **Parse DSL** — Read domain, DTO, API, and UI DSL files.
2. **Run CLI scaffolding** — Create `server` with NestJS CLI and `client` with Vite CLI; install dependencies as above.
3. **Code generation** — Generate Prisma schema, NestJS modules/DTOs/PrismaService, and React Admin resources.
4. **Runtime infrastructure** — Generate `.env`, `.env.example`, package lifecycle scripts, and runtime config files.
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`.
Scaffolding (steps 12) must be done with the CLI; steps 38 are generated from the DSL and project docs.

View File

@@ -0,0 +1,8 @@
# Update Strategy
When DSL changes:
1. Regenerate prisma.schema
2. Run prisma migrate dev
3. Regenerate Nest modules
4. Regenerate React Admin resources

1
server/.env.example Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/toir"

25
server/.eslintrc.js Normal file
View File

@@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

32
server/.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Dependencies
node_modules/
# Build
dist/
# Environment
.env
.env.local
.env.*.local
# Prisma
prisma/migrations/**/migration_lock.toml
# Logs
logs/
*.log
npm-debug.log*
# OS files
.DS_Store
Thumbs.db
# Editor / IDE
.vscode/*
!.vscode/extensions.json
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

4
server/.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

99
server/README.md Normal file
View File

@@ -0,0 +1,99 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

8
server/nest-cli.json Normal file
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

9805
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

76
server/package.json Normal file
View File

@@ -0,0 +1,76 @@
{
"name": "server",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"postinstall": "prisma generate"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^4.0.3",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@prisma/client": "^5.22.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^8.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"prisma": "^5.22.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@@ -0,0 +1,74 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum EquipmentStatus {
Active
Repair
Reserve
WriteOff
}
enum RepairKind {
TO
TR
TRE
KR
AR
MP
}
enum RepairOrderStatus {
Draft
Approved
InWork
Done
Cancelled
}
model EquipmentType {
code String @id
name String
manufacturer String?
maintenanceIntervalHours Int?
overhaulIntervalHours Int?
equipment Equipment[]
}
model Equipment {
id String @id @default(uuid())
inventoryNumber String @unique
serialNumber String?
name String
equipmentTypeCode String
equipmentType EquipmentType @relation(fields: [equipmentTypeCode], references: [code])
status EquipmentStatus @default(Active)
location String?
commissionedAt DateTime?
totalEngineHours Decimal?
engineHoursSinceLastRepair Decimal?
lastRepairAt DateTime?
notes String?
repairOrders RepairOrder[]
}
model RepairOrder {
id String @id @default(uuid())
number String @unique
equipmentId String
equipment Equipment @relation(fields: [equipmentId], references: [id])
repairKind RepairKind
status RepairOrderStatus @default(Draft)
plannedAt DateTime
startedAt DateTime?
completedAt DateTime?
contractor String?
engineHoursAtRepair Decimal?
description String?
notes String?
}

100
server/prisma/seed.ts Normal file
View File

@@ -0,0 +1,100 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const equipmentType = await prisma.equipmentType.upsert({
where: { code: 'pump' },
update: {},
create: {
code: 'pump',
name: 'Насосный агрегат',
manufacturer: 'АО НасосПром',
maintenanceIntervalHours: 2000,
overhaulIntervalHours: 16000,
},
});
const equipmentType2 = await prisma.equipmentType.upsert({
where: { code: 'compressor' },
update: {},
create: {
code: 'compressor',
name: 'Компрессорная установка',
manufacturer: 'ОАО Компрессормаш',
maintenanceIntervalHours: 1500,
overhaulIntervalHours: 12000,
},
});
const equipment = await prisma.equipment.upsert({
where: { inventoryNumber: 'INV-001' },
update: {},
create: {
inventoryNumber: 'INV-001',
serialNumber: 'SN-2024-0001',
name: 'Насос ЦНС 180-212',
equipmentTypeCode: 'pump',
status: 'Active',
location: 'Куст №5, скважина 42',
commissionedAt: new Date('2023-06-15'),
totalEngineHours: 4500,
engineHoursSinceLastRepair: 1200,
},
});
const equipment2 = await prisma.equipment.upsert({
where: { inventoryNumber: 'INV-002' },
update: {},
create: {
inventoryNumber: 'INV-002',
serialNumber: 'SN-2024-0002',
name: 'Компрессор 4ВМ10-120/9',
equipmentTypeCode: 'compressor',
status: 'Active',
location: 'ГКС-3',
commissionedAt: new Date('2022-03-10'),
totalEngineHours: 8200,
engineHoursSinceLastRepair: 800,
},
});
await prisma.repairOrder.upsert({
where: { number: 'RO-2026-001' },
update: {},
create: {
number: 'RO-2026-001',
equipmentId: equipment.id,
repairKind: 'TO',
status: 'Approved',
plannedAt: new Date('2026-04-01'),
contractor: 'ООО СервисРемонт',
engineHoursAtRepair: 4500,
description: 'Плановое техническое обслуживание насосного агрегата',
},
});
await prisma.repairOrder.upsert({
where: { number: 'RO-2026-002' },
update: {},
create: {
number: 'RO-2026-002',
equipmentId: equipment2.id,
repairKind: 'TR',
status: 'Draft',
plannedAt: new Date('2026-05-15'),
description: 'Текущий ремонт компрессорной установки',
},
});
console.log('Seed data created successfully');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

19
server/src/app.module.ts Normal file
View File

@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PrismaModule } from './prisma/prisma.module';
import { HealthModule } from './health/health.module';
import { EquipmentTypeModule } from './modules/equipment-type/equipment-type.module';
import { EquipmentModule } from './modules/equipment/equipment.module';
import { RepairOrderModule } from './modules/repair-order/repair-order.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
PrismaModule,
HealthModule,
EquipmentTypeModule,
EquipmentModule,
RepairOrderModule,
],
})
export class AppModule {}

View File

@@ -0,0 +1,9 @@
import { Controller, Get } from '@nestjs/common';
@Controller('health')
export class HealthController {
@Get()
getHealth() {
return { status: 'ok' };
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({
controllers: [HealthController],
})
export class HealthModule {}

12
server/src/main.ts Normal file
View File

@@ -0,0 +1,12 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: true,
exposedHeaders: ['Content-Range'],
});
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View File

@@ -0,0 +1,7 @@
export class CreateEquipmentTypeDto {
code: string;
name: string;
manufacturer?: string;
maintenanceIntervalHours?: number;
overhaulIntervalHours?: number;
}

View File

@@ -0,0 +1,6 @@
export class UpdateEquipmentTypeDto {
name?: string;
manufacturer?: string;
maintenanceIntervalHours?: number;
overhaulIntervalHours?: number;
}

View File

@@ -0,0 +1,38 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Res } from '@nestjs/common';
import { Response } from 'express';
import { EquipmentTypeService } from './equipment-type.service';
import { CreateEquipmentTypeDto } from './dto/create-equipment-type.dto';
import { UpdateEquipmentTypeDto } from './dto/update-equipment-type.dto';
@Controller('equipment-types')
export class EquipmentTypeController {
constructor(private readonly equipmentTypeService: EquipmentTypeService) {}
@Get()
async findAll(@Query() query: any, @Res() res: Response) {
const result = await this.equipmentTypeService.findAll(query);
res.set('Content-Range', `equipment-types ${query._start || 0}-${query._end || result.total}/${result.total}`);
res.set('Access-Control-Expose-Headers', 'Content-Range');
return res.json(result.data);
}
@Get(':code')
findOne(@Param('code') code: string) {
return this.equipmentTypeService.findOne(code);
}
@Post()
create(@Body() dto: CreateEquipmentTypeDto) {
return this.equipmentTypeService.create(dto);
}
@Patch(':code')
update(@Param('code') code: string, @Body() dto: UpdateEquipmentTypeDto) {
return this.equipmentTypeService.update(code, dto);
}
@Delete(':code')
remove(@Param('code') code: string) {
return this.equipmentTypeService.remove(code);
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { EquipmentTypeController } from './equipment-type.controller';
import { EquipmentTypeService } from './equipment-type.service';
@Module({
controllers: [EquipmentTypeController],
providers: [EquipmentTypeService],
})
export class EquipmentTypeModule {}

View File

@@ -0,0 +1,78 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateEquipmentTypeDto } from './dto/create-equipment-type.dto';
import { UpdateEquipmentTypeDto } from './dto/update-equipment-type.dto';
@Injectable()
export class EquipmentTypeService {
constructor(private readonly prisma: PrismaService) {}
async findAll(query: {
_start?: string;
_end?: string;
_sort?: string;
_order?: string;
[key: string]: any;
}) {
const start = parseInt(query._start) || 0;
const end = parseInt(query._end) || 10;
const take = end - start;
const skip = start;
const sortField = 'code';
const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';
const where: any = {};
if (query.code) where.code = { contains: query.code, mode: 'insensitive' };
if (query.name) where.name = { contains: query.name, mode: 'insensitive' };
if (query.manufacturer)
where.manufacturer = {
contains: query.manufacturer,
mode: 'insensitive',
};
if (query.id) {
const ids = Array.isArray(query.id) ? query.id : [query.id];
where.code = { in: ids };
}
const [data, total] = await Promise.all([
this.prisma.equipmentType.findMany({
where,
skip,
take,
orderBy: { [sortField]: sortOrder },
}),
this.prisma.equipmentType.count({ where }),
]);
return {
data: data.map((item) => ({ id: item.code, ...item })),
total,
};
}
async findOne(code: string) {
const record = await this.prisma.equipmentType.findUniqueOrThrow({
where: { code },
});
return { id: record.code, ...record };
}
async create(dto: CreateEquipmentTypeDto) {
const record = await this.prisma.equipmentType.create({ data: dto });
return { id: record.code, ...record };
}
async update(code: string, dto: UpdateEquipmentTypeDto) {
const { id, code: _pk, ...data } = dto as any;
const record = await this.prisma.equipmentType.update({
where: { code },
data,
});
return { id: record.code, ...record };
}
async remove(code: string) {
const record = await this.prisma.equipmentType.delete({ where: { code } });
return { id: record.code, ...record };
}
}

View File

@@ -0,0 +1,13 @@
export class CreateEquipmentDto {
inventoryNumber: string;
serialNumber?: string;
name: string;
equipmentTypeCode: string;
status?: string;
location?: string;
commissionedAt?: string;
totalEngineHours?: string;
engineHoursSinceLastRepair?: string;
lastRepairAt?: string;
notes?: string;
}

View File

@@ -0,0 +1,13 @@
export class UpdateEquipmentDto {
inventoryNumber?: string;
serialNumber?: string;
name?: string;
equipmentTypeCode?: string;
status?: string;
location?: string;
commissionedAt?: string;
totalEngineHours?: string;
engineHoursSinceLastRepair?: string;
lastRepairAt?: string;
notes?: string;
}

View File

@@ -0,0 +1,38 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Res } from '@nestjs/common';
import { Response } from 'express';
import { EquipmentService } from './equipment.service';
import { CreateEquipmentDto } from './dto/create-equipment.dto';
import { UpdateEquipmentDto } from './dto/update-equipment.dto';
@Controller('equipment')
export class EquipmentController {
constructor(private readonly equipmentService: EquipmentService) {}
@Get()
async findAll(@Query() query: any, @Res() res: Response) {
const result = await this.equipmentService.findAll(query);
res.set('Content-Range', `equipment ${query._start || 0}-${query._end || result.total}/${result.total}`);
res.set('Access-Control-Expose-Headers', 'Content-Range');
return res.json(result.data);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.equipmentService.findOne(id);
}
@Post()
create(@Body() dto: CreateEquipmentDto) {
return this.equipmentService.create(dto);
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateEquipmentDto) {
return this.equipmentService.update(id, dto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.equipmentService.remove(id);
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { EquipmentController } from './equipment.controller';
import { EquipmentService } from './equipment.service';
@Module({
controllers: [EquipmentController],
providers: [EquipmentService],
})
export class EquipmentModule {}

View File

@@ -0,0 +1,88 @@
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateEquipmentDto } from './dto/create-equipment.dto';
import { UpdateEquipmentDto } from './dto/update-equipment.dto';
function serializeRecord(record: any) {
return {
...record,
totalEngineHours: record.totalEngineHours?.toString() ?? null,
engineHoursSinceLastRepair: record.engineHoursSinceLastRepair?.toString() ?? null,
commissionedAt: record.commissionedAt?.toISOString() ?? null,
lastRepairAt: record.lastRepairAt?.toISOString() ?? null,
};
}
@Injectable()
export class EquipmentService {
constructor(private readonly prisma: PrismaService) {}
async findAll(query: { _start?: string; _end?: string; _sort?: string; _order?: string; [key: string]: any }) {
const start = parseInt(query._start) || 0;
const end = parseInt(query._end) || 10;
const take = end - start;
const skip = start;
const sortField = query._sort || 'id';
const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';
const where: any = {};
if (query.inventoryNumber) where.inventoryNumber = { contains: query.inventoryNumber, mode: 'insensitive' };
if (query.name) where.name = { contains: query.name, mode: 'insensitive' };
if (query.equipmentTypeCode) where.equipmentTypeCode = query.equipmentTypeCode;
if (query.status) where.status = query.status;
if (query.location) where.location = { contains: query.location, mode: 'insensitive' };
if (query.id) {
const ids = Array.isArray(query.id) ? query.id : [query.id];
where.id = { in: ids };
}
const [data, total] = await Promise.all([
this.prisma.equipment.findMany({
where,
skip,
take,
orderBy: { [sortField]: sortOrder },
}),
this.prisma.equipment.count({ where }),
]);
return {
data: data.map(serializeRecord),
total,
};
}
async findOne(id: string) {
const record = await this.prisma.equipment.findUniqueOrThrow({ where: { id } });
return serializeRecord(record);
}
async create(dto: CreateEquipmentDto) {
const data: any = { ...dto };
if (dto.commissionedAt) data.commissionedAt = new Date(dto.commissionedAt);
if (dto.lastRepairAt) data.lastRepairAt = new Date(dto.lastRepairAt);
if (dto.totalEngineHours) data.totalEngineHours = new Prisma.Decimal(dto.totalEngineHours);
if (dto.engineHoursSinceLastRepair) data.engineHoursSinceLastRepair = new Prisma.Decimal(dto.engineHoursSinceLastRepair);
const record = await this.prisma.equipment.create({ data });
return serializeRecord(record);
}
async update(id: string, dto: UpdateEquipmentDto) {
const { id: _pk, ...rest } = dto as any;
const data: any = { ...rest };
if (data.commissionedAt) data.commissionedAt = new Date(data.commissionedAt);
if (data.lastRepairAt) data.lastRepairAt = new Date(data.lastRepairAt);
if (data.totalEngineHours !== undefined && data.totalEngineHours !== null) data.totalEngineHours = new Prisma.Decimal(data.totalEngineHours);
if (data.engineHoursSinceLastRepair !== undefined && data.engineHoursSinceLastRepair !== null) data.engineHoursSinceLastRepair = new Prisma.Decimal(data.engineHoursSinceLastRepair);
const record = await this.prisma.equipment.update({ where: { id }, data });
return serializeRecord(record);
}
async remove(id: string) {
const record = await this.prisma.equipment.delete({ where: { id } });
return serializeRecord(record);
}
}

View File

@@ -0,0 +1,13 @@
export class CreateRepairOrderDto {
number: string;
equipmentId: string;
repairKind: string;
status?: string;
plannedAt: string;
startedAt?: string;
completedAt?: string;
contractor?: string;
engineHoursAtRepair?: string;
description?: string;
notes?: string;
}

View File

@@ -0,0 +1,13 @@
export class UpdateRepairOrderDto {
number?: string;
equipmentId?: string;
repairKind?: string;
status?: string;
plannedAt?: string;
startedAt?: string;
completedAt?: string;
contractor?: string;
engineHoursAtRepair?: string;
description?: string;
notes?: string;
}

View File

@@ -0,0 +1,38 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Res } from '@nestjs/common';
import { Response } from 'express';
import { RepairOrderService } from './repair-order.service';
import { CreateRepairOrderDto } from './dto/create-repair-order.dto';
import { UpdateRepairOrderDto } from './dto/update-repair-order.dto';
@Controller('repair-orders')
export class RepairOrderController {
constructor(private readonly repairOrderService: RepairOrderService) {}
@Get()
async findAll(@Query() query: any, @Res() res: Response) {
const result = await this.repairOrderService.findAll(query);
res.set('Content-Range', `repair-orders ${query._start || 0}-${query._end || result.total}/${result.total}`);
res.set('Access-Control-Expose-Headers', 'Content-Range');
return res.json(result.data);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.repairOrderService.findOne(id);
}
@Post()
create(@Body() dto: CreateRepairOrderDto) {
return this.repairOrderService.create(dto);
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateRepairOrderDto) {
return this.repairOrderService.update(id, dto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.repairOrderService.remove(id);
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { RepairOrderController } from './repair-order.controller';
import { RepairOrderService } from './repair-order.service';
@Module({
controllers: [RepairOrderController],
providers: [RepairOrderService],
})
export class RepairOrderModule {}

View File

@@ -0,0 +1,88 @@
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateRepairOrderDto } from './dto/create-repair-order.dto';
import { UpdateRepairOrderDto } from './dto/update-repair-order.dto';
function serializeRecord(record: any) {
return {
...record,
engineHoursAtRepair: record.engineHoursAtRepair?.toString() ?? null,
plannedAt: record.plannedAt?.toISOString() ?? null,
startedAt: record.startedAt?.toISOString() ?? null,
completedAt: record.completedAt?.toISOString() ?? null,
};
}
@Injectable()
export class RepairOrderService {
constructor(private readonly prisma: PrismaService) {}
async findAll(query: { _start?: string; _end?: string; _sort?: string; _order?: string; [key: string]: any }) {
const start = parseInt(query._start) || 0;
const end = parseInt(query._end) || 10;
const take = end - start;
const skip = start;
const sortField = query._sort || 'id';
const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';
const where: any = {};
if (query.number) where.number = { contains: query.number, mode: 'insensitive' };
if (query.equipmentId) where.equipmentId = query.equipmentId;
if (query.repairKind) where.repairKind = query.repairKind;
if (query.status) where.status = query.status;
if (query.contractor) where.contractor = { contains: query.contractor, mode: 'insensitive' };
if (query.id) {
const ids = Array.isArray(query.id) ? query.id : [query.id];
where.id = { in: ids };
}
const [data, total] = await Promise.all([
this.prisma.repairOrder.findMany({
where,
skip,
take,
orderBy: { [sortField]: sortOrder },
}),
this.prisma.repairOrder.count({ where }),
]);
return {
data: data.map(serializeRecord),
total,
};
}
async findOne(id: string) {
const record = await this.prisma.repairOrder.findUniqueOrThrow({ where: { id } });
return serializeRecord(record);
}
async create(dto: CreateRepairOrderDto) {
const data: any = { ...dto };
if (dto.plannedAt) data.plannedAt = new Date(dto.plannedAt);
if (dto.startedAt) data.startedAt = new Date(dto.startedAt);
if (dto.completedAt) data.completedAt = new Date(dto.completedAt);
if (dto.engineHoursAtRepair) data.engineHoursAtRepair = new Prisma.Decimal(dto.engineHoursAtRepair);
const record = await this.prisma.repairOrder.create({ data });
return serializeRecord(record);
}
async update(id: string, dto: UpdateRepairOrderDto) {
const { id: _pk, ...rest } = dto as any;
const data: any = { ...rest };
if (data.plannedAt) data.plannedAt = new Date(data.plannedAt);
if (data.startedAt) data.startedAt = new Date(data.startedAt);
if (data.completedAt) data.completedAt = new Date(data.completedAt);
if (data.engineHoursAtRepair !== undefined && data.engineHoursAtRepair !== null) data.engineHoursAtRepair = new Prisma.Decimal(data.engineHoursAtRepair);
const record = await this.prisma.repairOrder.update({ where: { id }, data });
return serializeRecord(record);
}
async remove(id: string) {
const record = await this.prisma.repairOrder.delete({ where: { id } });
return serializeRecord(record);
}
}

View File

@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@@ -0,0 +1,9 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
}

View File

@@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View File

@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

21
server/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}