Compare commits
2 Commits
chore/arch
...
5ef01c2282
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ef01c2282 | ||
|
|
51a5e1b5c1 |
108
.codex/AGENTS.md
108
.codex/AGENTS.md
@@ -1,108 +0,0 @@
|
|||||||
# Codex CLI — KIS-TOiR workspace supplement
|
|
||||||
|
|
||||||
This file supplements the repository root `AGENTS.md` with Codex-specific
|
|
||||||
operational notes. The root `AGENTS.md` is the authoritative contract —
|
|
||||||
if anything here contradicts root, root wins.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Agent role summary
|
|
||||||
|
|
||||||
| Role | Config file | Sandbox | Write boundary |
|
|
||||||
|------|-------------|---------|----------------|
|
|
||||||
| `generator` | `agents/generator.toml` | workspace-write | Tier 3 generation zones |
|
|
||||||
| `explorer` | `agents/explorer.toml` | read-only | None |
|
|
||||||
| `reviewer` | `agents/reviewer.toml` | read-only | Proposes patches only |
|
|
||||||
| `docs_researcher` | `agents/docs-researcher.toml` | read-only | None |
|
|
||||||
|
|
||||||
Use `/agent generator` for implementation work. Use `/agent explorer` first for
|
|
||||||
discovery, `/agent docs_researcher` when framework or prompt patterns need
|
|
||||||
verification, and `/agent reviewer` before claiming a generation run is complete.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mutation boundary map
|
|
||||||
|
|
||||||
```
|
|
||||||
Tier 1 — Source of truth (NEVER written by any agent)
|
|
||||||
domain/*.api.dsl — single source of truth for all generation
|
|
||||||
prompts/*.md — generation spec / rules
|
|
||||||
AGENTS.md — agent operating rules
|
|
||||||
.codex/AGENTS.md (this file) — Codex-specific supplement
|
|
||||||
|
|
||||||
Tier 2 — Deterministic derivatives (written only by npm scripts, not by agents)
|
|
||||||
api-summary.json ← npm run generate:api-summary
|
|
||||||
openapi.json ← npm run generate:openapi (auxiliary)
|
|
||||||
|
|
||||||
Tier 3 — LLM-generated artifacts (written ONLY by generator agent)
|
|
||||||
server/src/modules/<entity>/
|
|
||||||
client/src/resources/<entity>/
|
|
||||||
server/src/app.module.ts
|
|
||||||
client/src/App.tsx
|
|
||||||
server/prisma/schema.prisma ← LLM-generated per prompts/prisma-rules.md
|
|
||||||
server/src/auth/
|
|
||||||
client/src/auth/
|
|
||||||
client/src/dataProvider.ts
|
|
||||||
toir-realm.json
|
|
||||||
docker-compose.yml
|
|
||||||
server/.env.example
|
|
||||||
client/.env.example
|
|
||||||
|
|
||||||
Tier 4 — Handwritten / framework-managed support files
|
|
||||||
framework scaffold and other manual support files outside prompt-governed outputs
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Standard generation invocation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Read AGENTS.md + prompts/general-prompt.md
|
|
||||||
# 2. Read the entity-scoped DSL block from domain/toir.api.dsl
|
|
||||||
# 3. Load only the stage-specific companion rules you need
|
|
||||||
# 4. Run generation or repair with the appropriate agent
|
|
||||||
# 5. Refresh api-summary.json only if validator/tooling expects the auxiliary freshness artifact
|
|
||||||
# 6. Verify (both stages must pass)
|
|
||||||
node tools/validate-generation.mjs --artifacts-only
|
|
||||||
npm run eval:generation
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MCP servers (project-local)
|
|
||||||
|
|
||||||
Defined in `.codex/config.toml`:
|
|
||||||
|
|
||||||
- **github** — repository access
|
|
||||||
- **context7** — library documentation lookup (use for framework questions)
|
|
||||||
- **exa** — web search
|
|
||||||
- **memory** — persistent cross-session context
|
|
||||||
- **playwright** — browser automation for smoke tests
|
|
||||||
- **sequential-thinking** — structured multi-step reasoning
|
|
||||||
|
|
||||||
Add heavier or credential-backed servers in `~/.codex/config.toml`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Validation gate
|
|
||||||
|
|
||||||
Run before every commit and after every generation:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Stage 1 — structural gate
|
|
||||||
node tools/validate-generation.mjs --artifacts-only
|
|
||||||
|
|
||||||
# Stage 2 — eval harness
|
|
||||||
npm run eval:generation
|
|
||||||
```
|
|
||||||
|
|
||||||
The pre-commit hook (`tools/hooks/pre-commit`) runs both stages automatically
|
|
||||||
after `npm run install-hooks`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security notes
|
|
||||||
|
|
||||||
- Never commit secrets. Use environment variables from `.env.example` templates.
|
|
||||||
- Run `npm audit` when adding new dependencies to `server/` or `client/`.
|
|
||||||
- Auth contracts live in `prompts/auth-rules.md`. Do not deviate from them.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
model = "gpt-5.4-mini"
|
|
||||||
model_reasoning_effort = "low"
|
|
||||||
sandbox_mode = "read-only"
|
|
||||||
|
|
||||||
developer_instructions = """
|
|
||||||
Verify APIs, framework behavior, and release-note claims against primary documentation before changes land.
|
|
||||||
Cite the exact docs or file paths that support each claim.
|
|
||||||
Do not invent undocumented behavior.
|
|
||||||
Use Context7 and official docs first when the current environment exposes them.
|
|
||||||
"""
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
model = "gpt-5.4-mini"
|
|
||||||
model_reasoning_effort = "medium"
|
|
||||||
sandbox_mode = "read-only"
|
|
||||||
|
|
||||||
developer_instructions = """
|
|
||||||
Stay in exploration mode. Read files freely; write nothing.
|
|
||||||
|
|
||||||
Trace the real execution path, cite files and symbols, and avoid proposing
|
|
||||||
fixes unless the parent agent asks for them.
|
|
||||||
Prefer targeted search and file reads over broad scans.
|
|
||||||
|
|
||||||
KIS-TOiR source-of-truth tier reference (read-only for this agent):
|
|
||||||
Tier 1: domain/*.api.dsl, prompts/*.md, AGENTS.md
|
|
||||||
Tier 2: api-summary.json (deterministic auxiliary derivative; never authoritative)
|
|
||||||
Tier 3: server/src/modules/, client/src/resources/, server/src/app.module.ts,
|
|
||||||
client/src/App.tsx, server/prisma/schema.prisma, server/src/auth/,
|
|
||||||
client/src/auth/, client/src/dataProvider.ts, toir-realm.json,
|
|
||||||
docker-compose.yml, server/.env.example, client/.env.example
|
|
||||||
Tier 4: framework scaffold and other handwritten support files
|
|
||||||
|
|
||||||
When asked about generation output, always trace it back to its Tier 1 DSL source
|
|
||||||
and do not recommend api-summary.json as the primary input when the DSL is available.
|
|
||||||
"""
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
model = "gpt-5.4"
|
|
||||||
model_reasoning_effort = "high"
|
|
||||||
sandbox_mode = "workspace-write"
|
|
||||||
approval_policy = "on-request"
|
|
||||||
|
|
||||||
developer_instructions = """
|
|
||||||
You are the LLM generation agent for KIS-TOiR.
|
|
||||||
|
|
||||||
PERMITTED write zones (Tier 3 — LLM-generated artifacts):
|
|
||||||
server/src/modules/<entity>/ — NestJS modules, controllers, services, DTOs
|
|
||||||
client/src/resources/<entity>/ — React Admin List/Create/Edit/Show
|
|
||||||
server/src/app.module.ts — module registration section only
|
|
||||||
client/src/App.tsx — resource registration section only
|
|
||||||
server/prisma/schema.prisma — LLM-generated per prompts/prisma-rules.md
|
|
||||||
server/src/auth/ — auth artifacts per prompts/auth-rules.md
|
|
||||||
client/src/auth/ — auth artifacts per prompts/auth-rules.md
|
|
||||||
client/src/dataProvider.ts — authenticated data provider seam per prompts/auth-rules.md
|
|
||||||
toir-realm.json — realm artifact per prompts/auth-rules.md
|
|
||||||
docker-compose.yml — runtime artifact per prompts/runtime-rules.md
|
|
||||||
server/.env.example — runtime defaults per prompts/runtime-rules.md
|
|
||||||
client/.env.example — runtime defaults per prompts/runtime-rules.md
|
|
||||||
|
|
||||||
FORBIDDEN write zones — never modify these files:
|
|
||||||
domain/*.api.dsl — source of truth (Tier 1)
|
|
||||||
prompts/*.md — generation spec (Tier 1)
|
|
||||||
AGENTS.md — workflow contract (Tier 1)
|
|
||||||
api-summary.json — deterministic derivative (Tier 2)
|
|
||||||
tools/ — deterministic tooling, not generated artifacts
|
|
||||||
|
|
||||||
CONTEXT BUDGET (mandatory):
|
|
||||||
1. Read prompts/general-prompt.md first.
|
|
||||||
2. Read ONLY the entity-scoped api.dsl block (api API.<EntityName> + its DTOs + enums)
|
|
||||||
from domain/toir.api.dsl. Do NOT inject the full api.dsl as a blob.
|
|
||||||
3. Read ONLY the relevant companion rule file for the active stage.
|
|
||||||
4. Before generating any DTO or component, quote the relevant DSL field definitions
|
|
||||||
verbatim, then generate from those quotes. This prevents training-data contamination.
|
|
||||||
5. Use api-summary.json only as an auxiliary inventory or validator-related artifact,
|
|
||||||
never as the source of truth or default starting point.
|
|
||||||
|
|
||||||
GENERATION WORKFLOW:
|
|
||||||
1. Read prompts/general-prompt.md.
|
|
||||||
2. Read the entity-scoped block from domain/toir.api.dsl.
|
|
||||||
3. Read the relevant stage rule docs.
|
|
||||||
4. Generate or update Tier 3 artifacts.
|
|
||||||
5. Refresh api-summary.json only if the validator/tooling requires it.
|
|
||||||
6. Run: node tools/validate-generation.mjs --artifacts-only
|
|
||||||
7. Run: npm run eval:generation
|
|
||||||
8. Fix all failures before reporting complete.
|
|
||||||
|
|
||||||
NEVER report generation complete if either validation gate fails.
|
|
||||||
"""
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
model = "gpt-5.4"
|
|
||||||
model_reasoning_effort = "medium"
|
|
||||||
sandbox_mode = "read-only"
|
|
||||||
|
|
||||||
developer_instructions = """
|
|
||||||
Review mode. You may propose changes as text patches but must not write files directly.
|
|
||||||
|
|
||||||
Focus on:
|
|
||||||
- Correctness: does generated code match the api.dsl and prompt contracts?
|
|
||||||
- Security: auth guard placement, CORS, env variable handling.
|
|
||||||
- Regression: do both verification gates pass?
|
|
||||||
node tools/validate-generation.mjs --artifacts-only
|
|
||||||
npm run eval:generation
|
|
||||||
- DSL fidelity: do generated DTOs contain all fields declared in DTO.<Entity>Create/Update?
|
|
||||||
- Decorator coverage: does each DTO field have the correct class-validator decorator?
|
|
||||||
- Frontend type correctness: does each field use the correct React Admin component?
|
|
||||||
- Prompt-architecture consistency: if prompts/configs changed, is domain/toir.api.dsl still clearly authoritative and api-summary.json still clearly auxiliary?
|
|
||||||
|
|
||||||
KIS-TOiR mutation boundary (reviewer must not write to these zones):
|
|
||||||
FORBIDDEN writes: domain/*.api.dsl, prompts/*.md, AGENTS.md,
|
|
||||||
api-summary.json, tools/, server/prisma/schema.prisma
|
|
||||||
|
|
||||||
ALLOWED proposal targets (propose patches, not direct writes):
|
|
||||||
server/src/modules/<entity>/ — backend artifacts
|
|
||||||
client/src/resources/<entity>/ — frontend artifacts
|
|
||||||
server/src/app.module.ts, client/src/App.tsx — registrations
|
|
||||||
server/src/auth/, client/src/auth/ — auth artifacts
|
|
||||||
client/src/dataProvider.ts — authenticated data provider seam
|
|
||||||
toir-realm.json, docker-compose.yml — runtime/realm artifacts
|
|
||||||
server/.env.example, client/.env.example — runtime defaults
|
|
||||||
docs/ — documentation updates
|
|
||||||
"""
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
#:schema https://developers.openai.com/codex/config-schema.json
|
|
||||||
|
|
||||||
# Everything Claude Code (ECC) — Codex Reference Configuration
|
|
||||||
#
|
|
||||||
# Copy this file to ~/.codex/config.toml for global defaults, or keep it in
|
|
||||||
# the project root as .codex/config.toml for project-local settings.
|
|
||||||
#
|
|
||||||
# Official docs:
|
|
||||||
# - https://developers.openai.com/codex/config-reference
|
|
||||||
# - https://developers.openai.com/codex/multi-agent
|
|
||||||
|
|
||||||
# Model selection
|
|
||||||
# Leave `model` and `model_provider` unset so Codex CLI uses its current
|
|
||||||
# built-in defaults. Uncomment and pin them only if you intentionally want
|
|
||||||
# repo-local or global model overrides.
|
|
||||||
|
|
||||||
# Top-level runtime settings (current Codex schema)
|
|
||||||
approval_policy = "on-request"
|
|
||||||
sandbox_mode = "workspace-write"
|
|
||||||
web_search = "live"
|
|
||||||
|
|
||||||
# External notifications receive a JSON payload on stdin.
|
|
||||||
# macOS example (uncomment on Mac if `terminal-notifier` is installed):
|
|
||||||
# notify = [
|
|
||||||
# "terminal-notifier",
|
|
||||||
# "-title", "Codex KIS",
|
|
||||||
# "-message", "Task completed!",
|
|
||||||
# "-sound", "default",
|
|
||||||
# ]
|
|
||||||
|
|
||||||
# Persistent instructions are appended to every prompt (additive, unlike
|
|
||||||
# model_instructions_file which replaces AGENTS.md).
|
|
||||||
persistent_instructions = "Follow project AGENTS.md guidelines. Use available MCP servers when they can help."
|
|
||||||
|
|
||||||
# model_instructions_file replaces built-in instructions instead of AGENTS.md,
|
|
||||||
# so leave it unset unless you intentionally want a single override file.
|
|
||||||
# model_instructions_file = "/absolute/path/to/instructions.md"
|
|
||||||
|
|
||||||
# MCP servers
|
|
||||||
# Keep the default project set lean. API-backed servers inherit credentials from
|
|
||||||
# the launching environment or can be supplied by a user-level ~/.codex/config.toml.
|
|
||||||
[mcp_servers.github]
|
|
||||||
command = "npx"
|
|
||||||
args = ["-y", "@modelcontextprotocol/server-github"]
|
|
||||||
startup_timeout_sec = 30
|
|
||||||
|
|
||||||
[mcp_servers.context7]
|
|
||||||
command = "npx"
|
|
||||||
# Canonical Codex section name is `context7`; the package itself remains
|
|
||||||
# `@upstash/context7-mcp`.
|
|
||||||
args = ["-y", "@upstash/context7-mcp@latest"]
|
|
||||||
startup_timeout_sec = 30
|
|
||||||
|
|
||||||
[mcp_servers.exa]
|
|
||||||
url = "https://mcp.exa.ai/mcp"
|
|
||||||
|
|
||||||
[mcp_servers.memory]
|
|
||||||
command = "npx"
|
|
||||||
args = ["-y", "@modelcontextprotocol/server-memory"]
|
|
||||||
startup_timeout_sec = 30
|
|
||||||
|
|
||||||
[mcp_servers.playwright]
|
|
||||||
command = "npx"
|
|
||||||
args = ["-y", "@playwright/mcp@latest", "--extension"]
|
|
||||||
startup_timeout_sec = 30
|
|
||||||
|
|
||||||
[mcp_servers.sequential-thinking]
|
|
||||||
command = "npx"
|
|
||||||
args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]
|
|
||||||
startup_timeout_sec = 30
|
|
||||||
|
|
||||||
# Additional MCP servers (uncomment as needed):
|
|
||||||
# [mcp_servers.supabase]
|
|
||||||
# command = "npx"
|
|
||||||
# args = ["-y", "supabase-mcp-server@latest", "--read-only"]
|
|
||||||
#
|
|
||||||
# [mcp_servers.firecrawl]
|
|
||||||
# command = "npx"
|
|
||||||
# args = ["-y", "firecrawl-mcp"]
|
|
||||||
#
|
|
||||||
# [mcp_servers.fal-ai]
|
|
||||||
# command = "npx"
|
|
||||||
# args = ["-y", "fal-ai-mcp-server"]
|
|
||||||
#
|
|
||||||
# [mcp_servers.cloudflare]
|
|
||||||
# command = "npx"
|
|
||||||
# args = ["-y", "@cloudflare/mcp-server-cloudflare"]
|
|
||||||
|
|
||||||
[features]
|
|
||||||
# Codex multi-agent collaboration is stable and on by default in current builds.
|
|
||||||
# Keep the explicit toggle here so the repo documents its expectation clearly.
|
|
||||||
multi_agent = true
|
|
||||||
|
|
||||||
# Profiles — switch with `codex -p <name>`
|
|
||||||
[profiles.strict]
|
|
||||||
approval_policy = "on-request"
|
|
||||||
sandbox_mode = "read-only"
|
|
||||||
web_search = "cached"
|
|
||||||
|
|
||||||
[profiles.yolo]
|
|
||||||
approval_policy = "never"
|
|
||||||
sandbox_mode = "workspace-write"
|
|
||||||
web_search = "live"
|
|
||||||
|
|
||||||
[agents]
|
|
||||||
# Multi-agent role limits and local role definitions.
|
|
||||||
# These map to `.codex/agents/*.toml` and mirror the repo's explorer/reviewer/docs workflow.
|
|
||||||
max_threads = 6
|
|
||||||
max_depth = 1
|
|
||||||
|
|
||||||
[agents.explorer]
|
|
||||||
description = "Read-only codebase explorer for gathering evidence before changes are proposed."
|
|
||||||
config_file = "agents/explorer.toml"
|
|
||||||
|
|
||||||
[agents.reviewer]
|
|
||||||
description = "PR reviewer focused on correctness, security, and DSL fidelity. Proposes patches; writes nothing directly."
|
|
||||||
config_file = "agents/reviewer.toml"
|
|
||||||
|
|
||||||
[agents.docs_researcher]
|
|
||||||
description = "Documentation specialist that verifies APIs, framework behavior, and release notes."
|
|
||||||
config_file = "agents/docs-researcher.toml"
|
|
||||||
|
|
||||||
[agents.generator]
|
|
||||||
description = "LLM generation agent: writes server/src/modules/, client/src/resources/, app.module.ts, App.tsx only. Never touches DSL, prompts, or deterministic tooling."
|
|
||||||
config_file = "agents/generator.toml"
|
|
||||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
|||||||
*.sh text eol=lf
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -39,5 +39,3 @@ Thumbs.db
|
|||||||
openapi.generated.json
|
openapi.generated.json
|
||||||
openapi.llm.json
|
openapi.llm.json
|
||||||
tools/api-format-to-openapi/demo-output/
|
tools/api-format-to-openapi/demo-output/
|
||||||
|
|
||||||
.cursor/
|
|
||||||
216
AGENTS.md
216
AGENTS.md
@@ -1,216 +0,0 @@
|
|||||||
# KIS-TOiR — Agent Operating Rules
|
|
||||||
|
|
||||||
Read this file at the start of every session before reading any other file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What this repository is
|
|
||||||
|
|
||||||
KIS-TOiR is a fullstack CRUD application (NestJS backend + React Admin frontend)
|
|
||||||
for equipment maintenance management (Техническое обслуживание и ремонт).
|
|
||||||
|
|
||||||
Generation is driven by a single authoritative source file:
|
|
||||||
|
|
||||||
- `domain/toir.api.dsl` — the complete API contract: enums, DTOs, endpoints, HTTP methods, pagination
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Source-of-truth hierarchy
|
|
||||||
|
|
||||||
### Tier 1 — authoritative (hand-authored; never overwritten by generation)
|
|
||||||
|
|
||||||
| File | Authoritative for |
|
|
||||||
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `domain/*.api.dsl` | Enums, DTO shapes per operation, nullability, HTTP verb+path per endpoint, endpoint names, pagination contracts. Single source of truth. Drives: NestJS modules + React Admin resources + Prisma schema. |
|
|
||||||
| `prompts/*.md` | Auth rules, runtime rules, framework scaffold rules, Prisma rules, validation rules, generation policy, naming conventions. |
|
|
||||||
| `AGENTS.md` (this file) | Agent workflow, mutation boundaries, verification contract. |
|
|
||||||
|
|
||||||
### Tier 2 — deterministic derivative (generated by script; never edited manually)
|
|
||||||
|
|
||||||
| File | Generated from | Command |
|
|
||||||
| ------------------ | ------------------ | ------------------------------ |
|
|
||||||
| `api-summary.json` | `domain/*.api.dsl` | `npm run generate:api-summary` |
|
|
||||||
|
|
||||||
### Tier 3 — LLM-generated artifacts (never edit manually after generation)
|
|
||||||
|
|
||||||
| Zone | Generated from |
|
|
||||||
| -------------------------------- | ------------------------------------------------------ |
|
|
||||||
| `server/src/modules/<entity>/` | `domain/*.api.dsl` + `prompts/backend-rules.md` |
|
|
||||||
| `client/src/resources/<entity>/` | `domain/*.api.dsl` + `prompts/frontend-rules.md` |
|
|
||||||
| `server/src/app.module.ts` | Module registrations derived from api.dsl api blocks |
|
|
||||||
| `client/src/App.tsx` | Resource registrations derived from api.dsl api blocks |
|
|
||||||
| `server/prisma/schema.prisma` | `domain/*.api.dsl` + `prompts/prisma-rules.md` |
|
|
||||||
| `server/src/auth/` | `prompts/auth-rules.md` |
|
|
||||||
| `client/src/auth/` | `prompts/auth-rules.md` |
|
|
||||||
| `client/src/dataProvider.ts` | `prompts/auth-rules.md` |
|
|
||||||
| `toir-realm.json` | `prompts/auth-rules.md` |
|
|
||||||
| `docker-compose.yml` | `prompts/runtime-rules.md` |
|
|
||||||
| `server/.env.example` | `prompts/runtime-rules.md` |
|
|
||||||
| `client/.env.example` | `prompts/runtime-rules.md` |
|
|
||||||
|
|
||||||
### Tier 4 — handwritten / framework-managed support files
|
|
||||||
|
|
||||||
- Framework scaffold: `server/nest-cli.json`, `server/tsconfig*.json`, `client/vite.config.*`, etc.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Forbidden mutations during any generation run
|
|
||||||
|
|
||||||
**NEVER write to `*.dsl` files.**
|
|
||||||
They are read-only inputs. To change the API contract or domain model, edit `domain/*.api.dsl`
|
|
||||||
as a separate explicit task.
|
|
||||||
|
|
||||||
**NEVER manually edit files under `server/src/modules/` or `client/src/resources/`.**
|
|
||||||
To change generated code: update `domain/*.api.dsl` and regenerate.
|
|
||||||
|
|
||||||
**NEVER edit `server/prisma/schema.prisma` directly.**
|
|
||||||
It is LLM-generated from `domain/*.api.dsl` following `prompts/prisma-rules.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Generation workflow (required sequence)
|
|
||||||
|
|
||||||
1. Read `AGENTS.md` and `prompts/general-prompt.md`.
|
|
||||||
2. Read the active `api API.<EntityName>` block + its DTOs + its enums from `domain/toir.api.dsl` (entity-scoped — do not inject the full DSL as a blob).
|
|
||||||
3. Load the companion rule files required for the active stage:
|
|
||||||
- `prompts/prisma-rules.md`
|
|
||||||
- `prompts/backend-rules.md`
|
|
||||||
- `prompts/frontend-rules.md`
|
|
||||||
- `prompts/auth-rules.md`
|
|
||||||
- `prompts/runtime-rules.md`
|
|
||||||
- `prompts/validation-rules.md`
|
|
||||||
4. Verify or repair framework scaffolds before domain generation:
|
|
||||||
- official Nest CLI for backend workspace creation/repair
|
|
||||||
- official Vite React TypeScript CLI for frontend workspace creation/repair
|
|
||||||
- official Prisma CLI when Prisma initialization or repair is required
|
|
||||||
5. Generate `server/prisma/schema.prisma` per `prompts/prisma-rules.md`.
|
|
||||||
6. Generate backend modules and registrations per `prompts/backend-rules.md`.
|
|
||||||
7. Generate frontend resources and registrations per `prompts/frontend-rules.md`.
|
|
||||||
8. Generate auth/runtime/realm artifacts per `prompts/auth-rules.md` and `prompts/runtime-rules.md`.
|
|
||||||
9. Refresh `api-summary.json` only when validation/tooling requires the auxiliary freshness artifact: `npm run generate:api-summary`.
|
|
||||||
10. Run: `node tools/validate-generation.mjs --artifacts-only`
|
|
||||||
11. Run: `npm run eval:generation`
|
|
||||||
12. Fix all failures before considering the task complete.
|
|
||||||
|
|
||||||
**Context budget rule:** Before generating any DTO or component, quote the field
|
|
||||||
definitions from the DSL api block verbatim, then generate from those quotes. This
|
|
||||||
prevents training-data contamination. See `prompts/general-prompt.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Type mappings
|
|
||||||
|
|
||||||
| DSL type | Prisma type | TS DTO type | React Admin component |
|
|
||||||
| --------- | ----------------------------- | ----------- | ----------------------------- |
|
|
||||||
| `uuid` | `String @id @default(uuid())` | `string` | `TextInput` / `TextField` |
|
|
||||||
| `string` | `String` | `string` | `TextInput` / `TextField` |
|
|
||||||
| `text` | `String` | `string` | `TextInput` / `TextField` |
|
|
||||||
| `integer` | `Int` | `number` | `NumberInput` / `NumberField` |
|
|
||||||
| `number` | `Float` | `number` | `NumberInput` / `NumberField` |
|
|
||||||
| `decimal` | `Decimal` | `string` | `NumberInput` / `NumberField` |
|
|
||||||
| `date` | `DateTime` | `string` | `DateInput` / `DateField` |
|
|
||||||
| `boolean` | `Boolean` | `boolean` | (appropriate boolean input) |
|
|
||||||
| enum name | enum name | `string` | `SelectInput` / `SelectField` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Naming conventions
|
|
||||||
|
|
||||||
Resource name (plural, kebab-case):
|
|
||||||
|
|
||||||
- `Equipment` → `equipment` (irregular — stays as-is)
|
|
||||||
- `EquipmentType` → `equipment-types`
|
|
||||||
- `RepairOrder` → `repair-orders`
|
|
||||||
- General: PascalCase → kebab-case → append `s` (or `es` if ends in `s`; irregular cases explicit)
|
|
||||||
|
|
||||||
Default sort field (when not declared in api.dsl):
|
|
||||||
Priority: `inventoryNumber` > `number` > `code` > `name` > primary key
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification gate (two-stage)
|
|
||||||
|
|
||||||
### Stage 1 — Structural gate
|
|
||||||
|
|
||||||
```
|
|
||||||
node tools/validate-generation.mjs --artifacts-only
|
|
||||||
```
|
|
||||||
|
|
||||||
Checks: file existence, api-summary freshness, auth seam contracts, natural-key handling, realm structure, runtime contract, api.dsl coverage (backend + frontend files per entity), DTO field name coverage, **DTO class-validator decorator coverage**, **@UseGuards presence on controllers**, **frontend component type correctness**.
|
|
||||||
|
|
||||||
Full structural verification (requires installed `node_modules`):
|
|
||||||
|
|
||||||
```
|
|
||||||
node tools/validate-generation.mjs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stage 2 — Eval harness (Rule 6)
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run eval:generation
|
|
||||||
```
|
|
||||||
|
|
||||||
Fixture-based semantic checks from `tools/eval/fixtures/`. Checks: correct HTTP method decorators, Content-Range header, enum filter patterns, FK reference wiring, component type correctness, class-validator decorator patterns.
|
|
||||||
|
|
||||||
See `tools/eval/README.md` for fixture authoring and eval-driven development workflow.
|
|
||||||
|
|
||||||
**Generation is incomplete unless both stages pass.**
|
|
||||||
|
|
||||||
## Pre-commit hook
|
|
||||||
|
|
||||||
Install with `npm run install-hooks`. The hook runs **both** the structural gate and
|
|
||||||
the eval harness before every commit. Commits are blocked when either fails.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Auth and runtime defaults
|
|
||||||
|
|
||||||
Full auth contract: `prompts/auth-rules.md`
|
|
||||||
|
|
||||||
Working defaults (do not regress to localhost):
|
|
||||||
|
|
||||||
- Keycloak base: `https://sso.greact.ru`
|
|
||||||
- Realm/client: `toir` / `toir-frontend` / `toir-backend`
|
|
||||||
- Production frontend: `https://toir-frontend.greact.ru`
|
|
||||||
- CORS: `http://localhost:5173,https://toir-frontend.greact.ru`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## OpenRouter configuration
|
|
||||||
|
|
||||||
```
|
|
||||||
OPENAI_API_KEY=<openrouter-key>
|
|
||||||
OPENAI_BASE_URL=https://openrouter.ai/api/v1
|
|
||||||
OPENAI_MODEL=<model-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
These variables are used by `tools/api-format-to-openapi/convert.mjs --mode llm`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Reading order for generation tasks
|
|
||||||
|
|
||||||
**Critical zone (load first, never drop):**
|
|
||||||
|
|
||||||
1. `AGENTS.md` (this file) — project governance, mutation boundaries, tier hierarchy
|
|
||||||
2. `prompts/general-prompt.md` — master orchestration prompt: mission, stage ownership, delegation model, completion criteria
|
|
||||||
3. `domain/toir.api.dsl §API.<EntityName>` — active api block only, plus its referenced DTOs and enums
|
|
||||||
|
|
||||||
**Companion zone (load when the matching stage is active):**
|
|
||||||
|
|
||||||
4. `prompts/prisma-rules.md` — Prisma schema generation details
|
|
||||||
5. `prompts/backend-rules.md` — NestJS generation details
|
|
||||||
6. `prompts/frontend-rules.md` — React Admin generation details
|
|
||||||
7. `prompts/auth-rules.md` — auth seam and realm requirements
|
|
||||||
8. `prompts/runtime-rules.md` — scaffold, env, and bootstrap requirements
|
|
||||||
9. `prompts/validation-rules.md` — success gate requirements
|
|
||||||
|
|
||||||
**Auxiliary zone (never authoritative):**
|
|
||||||
|
|
||||||
10. `api-summary.json` — optional inventory/freshness artifact for validators and supporting tooling; do not use it instead of the DSL
|
|
||||||
|
|
||||||
**Reference only (do not load proactively):**
|
|
||||||
|
|
||||||
- `domain/dsl-spec.md` — DSL syntax reference; load only if DSL is ambiguous
|
|
||||||
- `docs/generation-playbook.md` — step-by-step workflow reference
|
|
||||||
- `docs/future-work.md` — deferred items (Rules 7 and 8)
|
|
||||||
40
README.md
40
README.md
@@ -8,29 +8,25 @@ It is not a new generator engine and it is not a compiler platform. The reposito
|
|||||||
- `client/` as the active frontend target output path
|
- `client/` as the active frontend target output path
|
||||||
- an LLM-first orchestration baseline with CLI-first framework bootstrap
|
- an LLM-first orchestration baseline with CLI-first framework bootstrap
|
||||||
- a compact rule set that strengthens the existing pipeline with:
|
- a compact rule set that strengthens the existing pipeline with:
|
||||||
- `api-summary.json` (deterministic intermediate context)
|
- `domain-summary.json`
|
||||||
- a physical root-level realm artifact
|
- a physical root-level realm artifact
|
||||||
- a lightweight automated validation gate
|
- a lightweight automated validation gate
|
||||||
|
|
||||||
## Active knowledge blocks
|
## Active knowledge blocks
|
||||||
|
|
||||||
The master generation prompt is `prompts/general-prompt.md`. It contains the complete
|
The active prompt corpus is intentionally normalized to six stable blocks:
|
||||||
generation workflow, type mappings, naming conventions, and all core rules.
|
|
||||||
|
|
||||||
Companion rule files for artifact-specific details:
|
1. [prompts/general-prompt.md](prompts/general-prompt.md)
|
||||||
|
2. [prompts/auth-rules.md](prompts/auth-rules.md)
|
||||||
1. [prompts/general-prompt.md](prompts/general-prompt.md) — master generation prompt
|
3. [prompts/backend-rules.md](prompts/backend-rules.md)
|
||||||
2. [prompts/auth-rules.md](prompts/auth-rules.md) — auth seam / realm spec
|
4. [prompts/frontend-rules.md](prompts/frontend-rules.md)
|
||||||
3. [prompts/backend-rules.md](prompts/backend-rules.md) — backend reference
|
5. [prompts/runtime-rules.md](prompts/runtime-rules.md)
|
||||||
4. [prompts/frontend-rules.md](prompts/frontend-rules.md) — frontend reference
|
6. [prompts/validation-rules.md](prompts/validation-rules.md)
|
||||||
5. [prompts/prisma-rules.md](prompts/prisma-rules.md) — Prisma schema rules
|
|
||||||
6. [prompts/runtime-rules.md](prompts/runtime-rules.md) — runtime / bootstrap
|
|
||||||
7. [prompts/validation-rules.md](prompts/validation-rules.md) — validation gate
|
|
||||||
|
|
||||||
## Baseline contracts
|
## Baseline contracts
|
||||||
|
|
||||||
- `domain/*.api.dsl` is the single source of truth for the domain model and API contract.
|
- `domain/*.dsl` is the source of truth for the domain model.
|
||||||
- [api-summary.json](api-summary.json) is a derived artifact for LLM stabilization and validation.
|
- [domain-summary.json](domain-summary.json) is a derived artifact for LLM stabilization and validation.
|
||||||
- [toir-realm.json](toir-realm.json) is the physical Keycloak bootstrap artifact baseline.
|
- [toir-realm.json](toir-realm.json) is the physical Keycloak bootstrap artifact baseline.
|
||||||
- `server/` and `client/` are the active target output paths for this repository.
|
- `server/` and `client/` are the active target output paths for this repository.
|
||||||
- `server/` must remain a valid NestJS workspace baseline.
|
- `server/` must remain a valid NestJS workspace baseline.
|
||||||
@@ -49,7 +45,7 @@ Companion rule files for artifact-specific details:
|
|||||||
|
|
||||||
- The active prompts define forbidden generation patterns, required invariants, and recovery rules for future agents.
|
- The active prompts define forbidden generation patterns, required invariants, and recovery rules for future agents.
|
||||||
- Buildability is part of the baseline contract, not an optional follow-up.
|
- Buildability is part of the baseline contract, not an optional follow-up.
|
||||||
- Validation targets `domain/*.api.dsl` as reusable source inputs, while TOiR names remain project defaults/examples.
|
- Validation targets `domain/*.dsl` as reusable source inputs, while TOiR names remain project defaults/examples.
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
@@ -60,19 +56,13 @@ Companion rule files for artifact-specific details:
|
|||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run generate:api-summary
|
npm run generate:domain-summary
|
||||||
npm run validate:generation
|
npm run validate:generation
|
||||||
npm run validate:generation:runtime
|
npm run validate:generation:runtime
|
||||||
npm run eval:generation
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`npm run validate:generation` checks both contract shape and workspace validity. When dependencies are installed, it also verifies `npm run build` in `server/` and `client/`. If dependencies are missing, it reports build verification as skipped instead of pretending the baseline is fully green.
|
`npm run validate:generation` now checks both contract shape and workspace validity. When dependencies are installed, it also verifies `npm run build` in `server/` and `client/`. If dependencies are missing, it reports build verification as skipped instead of pretending the baseline is fully green.
|
||||||
|
|
||||||
## AID export (OpenAPI)
|
## AID export (OpenAPI + app generator)
|
||||||
|
|
||||||
HTTP-экспортёр для интеграции с AID: **`POST /aid/export/openapi`** (api-format → OpenAPI 3.0). Подробно: **[docs/AID_EXPORT_README.md](docs/AID_EXPORT_README.md)**.
|
HTTP-экспортёры для интеграции с AID: **`POST /aid/export/openapi`** (api-format → OpenAPI 3.0) и **`POST /aid/export/app`** (DSL → бандл файлов или `--apply`). Подробно: **[docs/AID_EXPORT_README.md](docs/AID_EXPORT_README.md)**.
|
||||||
|
|
||||||
> **Note:** The `POST /aid/export/app` endpoint (DSL → generated app bundle) is currently
|
|
||||||
> non-operative because its backing script (`generation/generate.mjs`) was removed during
|
|
||||||
> the architecture migration to api.dsl-first generation. See `docs/AID_EXPORT_README.md`
|
|
||||||
> for details.
|
|
||||||
|
|||||||
2185
api-summary.json
2185
api-summary.json
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
.git
|
.git
|
||||||
*.md
|
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
npm-debug.log*
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ VITE_API_URL=http://localhost:3000
|
|||||||
VITE_KEYCLOAK_URL=https://sso.greact.ru
|
VITE_KEYCLOAK_URL=https://sso.greact.ru
|
||||||
VITE_KEYCLOAK_REALM=toir
|
VITE_KEYCLOAK_REALM=toir
|
||||||
VITE_KEYCLOAK_CLIENT_ID=toir-frontend
|
VITE_KEYCLOAK_CLIENT_ID=toir-frontend
|
||||||
|
|
||||||
|
|||||||
18
client/.eslintrc.cjs
Normal file
18
client/.eslintrc.cjs
Normal 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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
28
client/.gitignore
vendored
28
client/.gitignore
vendored
@@ -1,22 +1,32 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
*.local
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs/
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
# OS files
|
||||||
dist
|
.DS_Store
|
||||||
dist-ssr
|
Thumbs.db
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor / IDE
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea/
|
||||||
.DS_Store
|
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-bookworm-slim AS build
|
FROM node:20-alpine AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -21,11 +21,7 @@ RUN npm run build
|
|||||||
|
|
||||||
FROM nginx:1.27-alpine AS runtime
|
FROM nginx:1.27-alpine AS runtime
|
||||||
|
|
||||||
RUN apk add --no-cache wget
|
|
||||||
|
|
||||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
|
|||||||
@@ -4,70 +4,27 @@ This template provides a minimal setup to get React working in Vite with HMR and
|
|||||||
|
|
||||||
Currently, two official plugins are available:
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
- [@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/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
## React Compiler
|
|
||||||
|
|
||||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
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
|
```js
|
||||||
export default defineConfig([
|
export default {
|
||||||
globalIgnores(['dist']),
|
// other rules...
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
// Other configs...
|
|
||||||
|
|
||||||
// Remove tseslint.configs.recommended and replace with this
|
|
||||||
tseslint.configs.recommendedTypeChecked,
|
|
||||||
// Alternatively, use this for stricter rules
|
|
||||||
tseslint.configs.strictTypeChecked,
|
|
||||||
// Optionally, add this for stylistic rules
|
|
||||||
tseslint.configs.stylisticTypeChecked,
|
|
||||||
|
|
||||||
// Other configs...
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
ecmaVersion: 'latest',
|
||||||
tsconfigRootDir: import.meta.dirname,
|
sourceType: 'module',
|
||||||
|
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
},
|
},
|
||||||
// other options...
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
- 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`
|
||||||
```js
|
- 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
|
||||||
// eslint.config.js
|
|
||||||
import reactX from 'eslint-plugin-react-x'
|
|
||||||
import reactDom from 'eslint-plugin-react-dom'
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
// Other configs...
|
|
||||||
// Enable lint rules for React
|
|
||||||
reactX.configs['recommended-typescript'],
|
|
||||||
// Enable lint rules for React DOM
|
|
||||||
reactDom.configs.recommended,
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
|
||||||
// other options...
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import js from '@eslint/js'
|
|
||||||
import globals from 'globals'
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
import tseslint from 'typescript-eslint'
|
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
js.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
reactHooks.configs.flat.recommended,
|
|
||||||
reactRefresh.configs.vite,
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>client</title>
|
<title>Vite + React + TS</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
|
||||||
|
|
||||||
location = /healthz {
|
|
||||||
access_log off;
|
|
||||||
default_type text/plain;
|
|
||||||
return 200 'ok';
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://server:3000/;
|
proxy_pass http://toir-server:3000/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /healthz {
|
||||||
|
access_log off;
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
return 200 'ok';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2931
client/package-lock.json
generated
2931
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,31 +6,29 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^7.3.5",
|
"@mui/material": "^7.3.9",
|
||||||
"@mui/material": "^7.3.5",
|
|
||||||
"keycloak-js": "^26.2.3",
|
"keycloak-js": "^26.2.3",
|
||||||
"react": "^19.2.4",
|
"ra-data-simple-rest": "^5.14.4",
|
||||||
"react-admin": "^5.14.5",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^19.2.4"
|
"react-admin": "^5.14.4",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@types/react": "^18.2.55",
|
||||||
"@types/node": "^24.12.0",
|
"@types/react-dom": "^18.2.19",
|
||||||
"@types/react": "^19.2.14",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"globals": "^17.4.0",
|
"typescript": "^5.2.2",
|
||||||
"typescript": "~5.9.3",
|
"vite": "^5.1.0"
|
||||||
"typescript-eslint": "^8.57.0",
|
|
||||||
"vite": "^8.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB |
@@ -1,24 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
|
||||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
|
||||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
|
||||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
|
||||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
|
||||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
|
||||||
</symbol>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.9 KiB |
1
client/public/vite.svg
Normal file
1
client/public/vite.svg
Normal 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 |
@@ -1,184 +0,0 @@
|
|||||||
.counter {
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: var(--accent);
|
|
||||||
background: var(--accent-bg);
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--accent-border);
|
|
||||||
}
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid var(--accent);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.base,
|
|
||||||
.framework,
|
|
||||||
.vite {
|
|
||||||
inset-inline: 0;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.base {
|
|
||||||
width: 170px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.framework,
|
|
||||||
.vite {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.framework {
|
|
||||||
z-index: 1;
|
|
||||||
top: 34px;
|
|
||||||
height: 28px;
|
|
||||||
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
|
||||||
scale(1.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vite {
|
|
||||||
z-index: 0;
|
|
||||||
top: 107px;
|
|
||||||
height: 26px;
|
|
||||||
width: auto;
|
|
||||||
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
|
||||||
scale(0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#center {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 25px;
|
|
||||||
place-content: center;
|
|
||||||
place-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
padding: 32px 20px 24px;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#next-steps {
|
|
||||||
display: flex;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
flex: 1 1 0;
|
|
||||||
padding: 32px;
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
padding: 24px 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#docs {
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#next-steps ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin: 32px 0 0;
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--text-h);
|
|
||||||
font-size: 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--social-bg);
|
|
||||||
display: flex;
|
|
||||||
padding: 6px 12px;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: box-shadow 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
.button-icon {
|
|
||||||
height: 18px;
|
|
||||||
width: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
margin-top: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
li {
|
|
||||||
flex: 1 1 calc(50% - 8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#spacer {
|
|
||||||
height: 88px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
height: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticks {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&::before,
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: -4.5px;
|
|
||||||
border: 5px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
left: 0;
|
|
||||||
border-left-color: var(--border);
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
right: 0;
|
|
||||||
border-right-color: var(--border);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +1,55 @@
|
|||||||
import { Admin, Resource } from "react-admin";
|
import { Admin, Resource } from 'react-admin';
|
||||||
import { authProvider } from "./auth/authProvider";
|
import dataProvider from './dataProvider';
|
||||||
import { dataProvider } from "./dataProvider";
|
import authProvider from './auth/authProvider';
|
||||||
import { CategoryResourceCreate } from "./resources/category-resource/CategoryResourceCreate";
|
import { AppNotification } from './AppNotification';
|
||||||
import { CategoryResourceEdit } from "./resources/category-resource/CategoryResourceEdit";
|
|
||||||
import { CategoryResourceList } from "./resources/category-resource/CategoryResourceList";
|
|
||||||
import { CategoryResourceShow } from "./resources/category-resource/CategoryResourceShow";
|
|
||||||
import { EmployeeCreate } from "./resources/employee/EmployeeCreate";
|
|
||||||
import { EmployeeEdit } from "./resources/employee/EmployeeEdit";
|
|
||||||
import { EmployeeList } from "./resources/employee/EmployeeList";
|
|
||||||
import { EmployeeShow } from "./resources/employee/EmployeeShow";
|
|
||||||
import { EquipmentCreate } from "./resources/equipment/EquipmentCreate";
|
|
||||||
import { EquipmentEdit } from "./resources/equipment/EquipmentEdit";
|
|
||||||
import { EquipmentList } from "./resources/equipment/EquipmentList";
|
|
||||||
import { EquipmentShow } from "./resources/equipment/EquipmentShow";
|
|
||||||
import { PartCreate } from "./resources/part/PartCreate";
|
|
||||||
import { PartEdit } from "./resources/part/PartEdit";
|
|
||||||
import { PartList } from "./resources/part/PartList";
|
|
||||||
import { PartShow } from "./resources/part/PartShow";
|
|
||||||
import { PriceListCreate } from "./resources/price-list/PriceListCreate";
|
|
||||||
import { PriceListEdit } from "./resources/price-list/PriceListEdit";
|
|
||||||
import { PriceListList } from "./resources/price-list/PriceListList";
|
|
||||||
import { PriceListShow } from "./resources/price-list/PriceListShow";
|
|
||||||
import "./App.css";
|
|
||||||
|
|
||||||
export default function App() {
|
import { EquipmentTypeList } from './resources/equipment-type/EquipmentTypeList';
|
||||||
return (
|
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
|
<Admin
|
||||||
dataProvider={dataProvider}
|
dataProvider={dataProvider}
|
||||||
authProvider={authProvider}
|
authProvider={authProvider}
|
||||||
disableTelemetry
|
notification={AppNotification}
|
||||||
|
requireAuth
|
||||||
>
|
>
|
||||||
|
<Resource
|
||||||
|
name="equipment-types"
|
||||||
|
options={{ label: 'Виды оборудования' }}
|
||||||
|
list={EquipmentTypeList}
|
||||||
|
create={EquipmentTypeCreate}
|
||||||
|
edit={EquipmentTypeEdit}
|
||||||
|
show={EquipmentTypeShow}
|
||||||
|
/>
|
||||||
<Resource
|
<Resource
|
||||||
name="equipment"
|
name="equipment"
|
||||||
|
options={{ label: 'Оборудование' }}
|
||||||
list={EquipmentList}
|
list={EquipmentList}
|
||||||
create={EquipmentCreate}
|
create={EquipmentCreate}
|
||||||
edit={EquipmentEdit}
|
edit={EquipmentEdit}
|
||||||
show={EquipmentShow}
|
show={EquipmentShow}
|
||||||
/>
|
/>
|
||||||
<Resource
|
<Resource
|
||||||
name="employees"
|
name="repair-orders"
|
||||||
list={EmployeeList}
|
options={{ label: 'Заявки на ремонт' }}
|
||||||
create={EmployeeCreate}
|
list={RepairOrderList}
|
||||||
edit={EmployeeEdit}
|
create={RepairOrderCreate}
|
||||||
show={EmployeeShow}
|
edit={RepairOrderEdit}
|
||||||
/>
|
show={RepairOrderShow}
|
||||||
<Resource
|
|
||||||
name="parts"
|
|
||||||
list={PartList}
|
|
||||||
create={PartCreate}
|
|
||||||
edit={PartEdit}
|
|
||||||
show={PartShow}
|
|
||||||
/>
|
|
||||||
<Resource
|
|
||||||
name="category-resources"
|
|
||||||
list={CategoryResourceList}
|
|
||||||
create={CategoryResourceCreate}
|
|
||||||
edit={CategoryResourceEdit}
|
|
||||||
show={CategoryResourceShow}
|
|
||||||
/>
|
|
||||||
<Resource
|
|
||||||
name="price-list"
|
|
||||||
list={PriceListList}
|
|
||||||
create={PriceListCreate}
|
|
||||||
edit={PriceListEdit}
|
|
||||||
show={PriceListShow}
|
|
||||||
/>
|
/>
|
||||||
</Admin>
|
</Admin>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
export default App;
|
||||||
|
|||||||
16
client/src/AppNotification.tsx
Normal file
16
client/src/AppNotification.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Notification, NotificationProps } from 'react-admin';
|
||||||
|
|
||||||
|
export const AppNotification = (props: NotificationProps) => (
|
||||||
|
<Notification
|
||||||
|
{...props}
|
||||||
|
sx={{
|
||||||
|
whiteSpace: 'pre-line',
|
||||||
|
'& .MuiAlert-message': {
|
||||||
|
whiteSpace: 'pre-line',
|
||||||
|
},
|
||||||
|
'& .MuiSnackbarContent-message': {
|
||||||
|
whiteSpace: 'pre-line',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 8.5 KiB |
@@ -1,44 +1,45 @@
|
|||||||
import type { AuthProvider } from 'react-admin';
|
import { AuthProvider } from 'react-admin';
|
||||||
import { getKeycloak, initKeycloak } from './keycloak';
|
import {
|
||||||
|
forceReauthentication,
|
||||||
|
getIdentity,
|
||||||
|
getRealmRoles,
|
||||||
|
getValidAccessToken,
|
||||||
|
initKeycloak,
|
||||||
|
logoutFromKeycloak,
|
||||||
|
} from './keycloak';
|
||||||
|
|
||||||
export const authProvider: AuthProvider = {
|
const authProvider: AuthProvider = {
|
||||||
login: async () => {
|
login: async () => {
|
||||||
await initKeycloak();
|
await initKeycloak();
|
||||||
await getKeycloak().login();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
await getKeycloak().logout({ redirectUri: window.location.origin });
|
await logoutFromKeycloak();
|
||||||
},
|
},
|
||||||
|
|
||||||
checkAuth: async () => {
|
checkAuth: async () => {
|
||||||
await initKeycloak();
|
await getValidAccessToken();
|
||||||
if (!getKeycloak().authenticated) {
|
|
||||||
await getKeycloak().login();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
checkError: async (error) => {
|
checkError: async (error) => {
|
||||||
const status = error?.status ?? error?.response?.status;
|
const status = error?.status;
|
||||||
|
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
getKeycloak().clearToken();
|
await forceReauthentication();
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 403) {
|
if (status === 403) {
|
||||||
return Promise.reject(error);
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
getIdentity: async () => {
|
|
||||||
await initKeycloak();
|
getIdentity: async () => getIdentity(),
|
||||||
const tokenParsed = getKeycloak().tokenParsed as Record<string, unknown> | undefined;
|
|
||||||
return {
|
getPermissions: async () => getRealmRoles(),
|
||||||
id: String(tokenParsed?.sub ?? 'anonymous'),
|
|
||||||
fullName: typeof tokenParsed?.name === 'string' ? tokenParsed.name : (typeof tokenParsed?.preferred_username === 'string' ? tokenParsed.preferred_username : 'User'),
|
|
||||||
avatar: undefined,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getPermissions: async () => {
|
|
||||||
await initKeycloak();
|
|
||||||
const tokenParsed = getKeycloak().tokenParsed as { realm_access?: { roles?: unknown } } | undefined;
|
|
||||||
const roles = tokenParsed?.realm_access?.roles;
|
|
||||||
return Array.isArray(roles) ? roles.filter((role): role is string => typeof role === 'string') : [];
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default authProvider;
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +1,96 @@
|
|||||||
import Keycloak from 'keycloak-js';
|
import Keycloak, { KeycloakTokenParsed } from 'keycloak-js';
|
||||||
import { env } from '../config/env';
|
import { env } from '../config/env';
|
||||||
|
|
||||||
|
interface RealmAccessTokenParsed extends KeycloakTokenParsed {
|
||||||
|
realm_access?: {
|
||||||
|
roles: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const keycloak = new Keycloak({
|
const keycloak = new Keycloak({
|
||||||
url: env.keycloakUrl,
|
url: env.keycloakUrl,
|
||||||
realm: env.keycloakRealm,
|
realm: env.keycloakRealm,
|
||||||
clientId: env.keycloakClientId,
|
clientId: env.keycloakClientId,
|
||||||
});
|
});
|
||||||
|
|
||||||
let initPromise: Promise<boolean> | null = null;
|
let keycloakInitPromise: Promise<void> | null = null;
|
||||||
let refreshPromise: Promise<string | null> | null = null;
|
let refreshInFlight: Promise<void> | null = null;
|
||||||
|
|
||||||
export async function initKeycloak(): Promise<boolean> {
|
|
||||||
if (!initPromise) {
|
|
||||||
initPromise = keycloak.init({
|
|
||||||
onLoad: 'login-required',
|
|
||||||
pkceMethod: 'S256',
|
|
||||||
checkLoginIframe: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return initPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAccessToken(): Promise<string | null> {
|
|
||||||
await initKeycloak();
|
|
||||||
if (!keycloak.authenticated) return null;
|
|
||||||
|
|
||||||
if (!refreshPromise) {
|
|
||||||
refreshPromise = keycloak
|
|
||||||
.updateToken(30)
|
|
||||||
.then(() => keycloak.token ?? null)
|
|
||||||
.finally(() => {
|
|
||||||
refreshPromise = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return refreshPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getKeycloak() {
|
export function getKeycloak() {
|
||||||
return keycloak;
|
return keycloak;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function initKeycloak() {
|
||||||
|
if (!keycloakInitPromise) {
|
||||||
|
keycloakInitPromise = keycloak
|
||||||
|
.init({
|
||||||
|
onLoad: 'login-required',
|
||||||
|
pkceMethod: 'S256',
|
||||||
|
checkLoginIframe: false,
|
||||||
|
})
|
||||||
|
.then((authenticated) => {
|
||||||
|
if (!authenticated) {
|
||||||
|
return keycloak.login({ redirectUri: window.location.href });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await keycloakInitPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAccessToken(minValiditySeconds = 30) {
|
||||||
|
if (!refreshInFlight) {
|
||||||
|
refreshInFlight = keycloak
|
||||||
|
.updateToken(minValiditySeconds)
|
||||||
|
.then(() => undefined)
|
||||||
|
.finally(() => {
|
||||||
|
refreshInFlight = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getValidAccessToken(minValiditySeconds = 30): Promise<string> {
|
||||||
|
await initKeycloak();
|
||||||
|
|
||||||
|
if (!keycloak.authenticated) {
|
||||||
|
await keycloak.login({ redirectUri: window.location.href });
|
||||||
|
throw new Error('User is not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshAccessToken(minValiditySeconds);
|
||||||
|
|
||||||
|
if (!keycloak.token) {
|
||||||
|
throw new Error('Missing access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return keycloak.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function forceReauthentication() {
|
||||||
|
keycloak.clearToken();
|
||||||
|
await keycloak.login({ redirectUri: window.location.href });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logoutFromKeycloak() {
|
||||||
|
await keycloak.logout({ redirectUri: window.location.origin });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRealmRoles(): string[] {
|
||||||
|
const parsed = keycloak.tokenParsed as RealmAccessTokenParsed | undefined;
|
||||||
|
const roles = parsed?.realm_access?.roles;
|
||||||
|
return Array.isArray(roles) ? roles : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIdentity() {
|
||||||
|
const parsed = keycloak.tokenParsed as RealmAccessTokenParsed | undefined;
|
||||||
|
const id = parsed?.sub ?? 'unknown';
|
||||||
|
const fullName =
|
||||||
|
parsed?.name ??
|
||||||
|
parsed?.preferred_username ??
|
||||||
|
parsed?.email ??
|
||||||
|
'Unknown User';
|
||||||
|
|
||||||
|
return { id, fullName };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,24 @@
|
|||||||
|
const REQUIRED_ENV_KEYS = [
|
||||||
|
'VITE_API_URL',
|
||||||
|
'VITE_KEYCLOAK_URL',
|
||||||
|
'VITE_KEYCLOAK_REALM',
|
||||||
|
'VITE_KEYCLOAK_CLIENT_ID',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type RequiredEnvKey = (typeof REQUIRED_ENV_KEYS)[number];
|
||||||
|
|
||||||
|
function readRequiredEnv(key: RequiredEnvKey): string {
|
||||||
|
const value = import.meta.env[key];
|
||||||
|
if (!value || !value.trim()) {
|
||||||
|
throw new Error(`Missing required environment variable: ${key}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
export const env = {
|
export const env = {
|
||||||
apiUrl: import.meta.env.VITE_API_URL ?? 'http://localhost:3000',
|
apiUrl: readRequiredEnv('VITE_API_URL'),
|
||||||
keycloakUrl: import.meta.env.VITE_KEYCLOAK_URL ?? 'https://sso.greact.ru',
|
keycloakUrl: readRequiredEnv('VITE_KEYCLOAK_URL'),
|
||||||
keycloakRealm: import.meta.env.VITE_KEYCLOAK_REALM ?? 'toir',
|
keycloakRealm: readRequiredEnv('VITE_KEYCLOAK_REALM'),
|
||||||
keycloakClientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID ?? 'toir-frontend',
|
keycloakClientId: readRequiredEnv('VITE_KEYCLOAK_CLIENT_ID'),
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -1,194 +1,169 @@
|
|||||||
import type { DataProvider } from "react-admin";
|
import { DataProvider, fetchUtils, HttpError } from 'react-admin';
|
||||||
import { env } from "./config/env";
|
import { getValidAccessToken } from './auth/keycloak';
|
||||||
import { getAccessToken } from "./auth/keycloak";
|
import { env } from './config/env';
|
||||||
|
|
||||||
async function fetchJson(
|
const apiUrl = env.apiUrl;
|
||||||
url: string,
|
|
||||||
options: RequestInit = {},
|
const httpClient = async (url: string, options: fetchUtils.Options = {}) => {
|
||||||
): Promise<{ json: any; headers: Headers; status: number }> {
|
const token = await getValidAccessToken();
|
||||||
const headers = new Headers(
|
const headers = new Headers(options.headers ?? { Accept: 'application/json' });
|
||||||
options.headers ?? { Accept: "application/json" },
|
|
||||||
);
|
|
||||||
const token = await getAccessToken();
|
|
||||||
if (token) {
|
|
||||||
headers.set('Authorization', `Bearer ${token}`);
|
headers.set('Authorization', `Bearer ${token}`);
|
||||||
}
|
|
||||||
if (!headers.has("Content-Type") && options.body) {
|
|
||||||
headers.set("Content-Type", "application/json");
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, { ...options, headers });
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = new Error(
|
|
||||||
"Request failed with status " + response.status,
|
|
||||||
) as Error & { status?: number; body?: unknown };
|
|
||||||
error.status = response.status;
|
|
||||||
try {
|
try {
|
||||||
error.body = await response.json();
|
return await fetchUtils.fetchJson(url, {
|
||||||
} catch {
|
...options,
|
||||||
error.body = null;
|
headers,
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 204) {
|
|
||||||
return { json: null, headers: response.headers, status: response.status };
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = await response.json();
|
|
||||||
return { json, headers: response.headers, status: response.status };
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendSearchParam(
|
|
||||||
searchParams: URLSearchParams,
|
|
||||||
key: string,
|
|
||||||
value: unknown,
|
|
||||||
): void {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value.forEach((entry) => appendSearchParam(searchParams, key, entry));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value === undefined || value === null || value === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
searchParams.append(key, String(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseListBody(json: unknown): { rows: unknown[]; totalHint?: number } {
|
|
||||||
if (Array.isArray(json)) {
|
|
||||||
return { rows: json };
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
json !== null &&
|
|
||||||
typeof json === "object" &&
|
|
||||||
"data" in json &&
|
|
||||||
Array.isArray((json as { data: unknown }).data)
|
|
||||||
) {
|
|
||||||
const body = json as { data: unknown[]; total?: unknown };
|
|
||||||
const totalHint =
|
|
||||||
typeof body.total === "number" && Number.isFinite(body.total)
|
|
||||||
? body.total
|
|
||||||
: undefined;
|
|
||||||
return { rows: body.data, totalHint };
|
|
||||||
}
|
|
||||||
return { rows: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildListUrl(resource: string, params: any): string {
|
|
||||||
const resourcePath = resource === "equipment" ? "equipments" : resource;
|
|
||||||
const searchParams = new URLSearchParams();
|
|
||||||
searchParams.set(
|
|
||||||
"_start",
|
|
||||||
String((params.pagination.page - 1) * params.pagination.perPage),
|
|
||||||
);
|
|
||||||
searchParams.set(
|
|
||||||
"_end",
|
|
||||||
String(params.pagination.page * params.pagination.perPage),
|
|
||||||
);
|
|
||||||
searchParams.set("_sort", params.sort.field);
|
|
||||||
searchParams.set("_order", params.sort.order);
|
|
||||||
Object.entries(params.filter ?? {}).forEach(([key, value]) => {
|
|
||||||
appendSearchParam(searchParams, key, value);
|
|
||||||
});
|
});
|
||||||
const queryString = searchParams.toString();
|
} catch (error: unknown) {
|
||||||
return (
|
const e = error as {
|
||||||
env.apiUrl + "/" + resourcePath + (queryString ? "?" + queryString : "")
|
status?: number;
|
||||||
|
body?: {
|
||||||
|
message?: string | string[];
|
||||||
|
details?: unknown;
|
||||||
|
};
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
const messageFromBody = e?.body?.message;
|
||||||
|
const normalizedMessage = Array.isArray(messageFromBody)
|
||||||
|
? messageFromBody.join('\n')
|
||||||
|
: typeof messageFromBody === 'string'
|
||||||
|
? messageFromBody.split(', ').join('\n')
|
||||||
|
: messageFromBody;
|
||||||
|
|
||||||
|
throw new HttpError(
|
||||||
|
normalizedMessage || e?.message || 'Request failed',
|
||||||
|
e?.status ?? 500,
|
||||||
|
e?.body,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildQueryString(query: Record<string, unknown>) {
|
||||||
|
const search = new URLSearchParams();
|
||||||
|
Object.entries(query).forEach(([key, val]) => {
|
||||||
|
if (val === undefined || val === null || val === '') return;
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
val.forEach((v) => {
|
||||||
|
if (v === undefined || v === null || v === '') return;
|
||||||
|
search.append(key, String(v));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
search.set(key, String(val));
|
||||||
|
});
|
||||||
|
return search.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dataProvider: DataProvider = {
|
const dataProvider: DataProvider = {
|
||||||
getList: async (resource, params) => {
|
getList: async (resource, params) => {
|
||||||
if (resource === "price-list") {
|
const { page, perPage } = params.pagination!;
|
||||||
const { json } = await fetchJson(env.apiUrl + "/price-list");
|
const { field, order } = params.sort!;
|
||||||
return { data: [json], total: 1 };
|
const start = (page - 1) * perPage;
|
||||||
}
|
const end = page * perPage;
|
||||||
const { json, headers } = await fetchJson(buildListUrl(resource, params));
|
|
||||||
const { rows, totalHint } = parseListBody(json);
|
const query: Record<string, unknown> = {
|
||||||
const contentRange = headers.get("Content-Range");
|
_start: start,
|
||||||
|
_end: end,
|
||||||
|
_sort: field,
|
||||||
|
_order: order,
|
||||||
|
...(params.filter ?? {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryString = buildQueryString(query);
|
||||||
|
const url = `${apiUrl}/${resource}?${queryString}`;
|
||||||
|
const { json, headers } = await httpClient(url);
|
||||||
|
|
||||||
|
const contentRange = headers.get('Content-Range');
|
||||||
const total = contentRange
|
const total = contentRange
|
||||||
? Number(
|
? parseInt(contentRange.split('/').pop() || '0', 10)
|
||||||
contentRange.split("/").pop() ??
|
: json.length;
|
||||||
totalHint ??
|
|
||||||
rows.length,
|
return { data: json, total };
|
||||||
)
|
|
||||||
: (totalHint ?? rows.length);
|
|
||||||
return { data: rows as any[], total };
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getOne: async (resource, params) => {
|
getOne: async (resource, params) => {
|
||||||
const resourcePath = resource === "equipment" ? "equipments" : resource;
|
const { json } = await httpClient(`${apiUrl}/${resource}/${params.id}`);
|
||||||
const url =
|
|
||||||
resource === "price-list"
|
|
||||||
? env.apiUrl + "/price-list"
|
|
||||||
: env.apiUrl + "/" + resourcePath + "/" + params.id;
|
|
||||||
const { json } = await fetchJson(url);
|
|
||||||
return { data: json };
|
return { data: json };
|
||||||
},
|
},
|
||||||
|
|
||||||
getMany: async (resource, params) => {
|
getMany: async (resource, params) => {
|
||||||
if (resource === "price-list") {
|
const query = params.ids.map((id) => `id=${id}`).join('&');
|
||||||
const { json } = await fetchJson(env.apiUrl + "/price-list");
|
const { json } = await httpClient(`${apiUrl}/${resource}?${query}`);
|
||||||
return { data: params.ids.includes("price-list") ? [json] : [] };
|
return { data: json };
|
||||||
}
|
|
||||||
const records = await Promise.all(
|
|
||||||
params.ids.map((id) =>
|
|
||||||
dataProvider.getOne(resource, { id, meta: params.meta } as any),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return { data: records.map((result) => result.data) };
|
|
||||||
},
|
},
|
||||||
getManyReference: async (resource, params) =>
|
|
||||||
dataProvider.getList(resource, {
|
getManyReference: async (resource, params) => {
|
||||||
pagination: params.pagination,
|
const { page, perPage } = params.pagination!;
|
||||||
sort: params.sort,
|
const { field, order } = params.sort!;
|
||||||
filter: { ...(params.filter ?? {}), [params.target]: params.id },
|
const start = (page - 1) * perPage;
|
||||||
meta: params.meta,
|
const end = page * perPage;
|
||||||
} as any),
|
|
||||||
|
const query: Record<string, unknown> = {
|
||||||
|
_start: start,
|
||||||
|
_end: end,
|
||||||
|
_sort: field,
|
||||||
|
_order: order,
|
||||||
|
[params.target]: params.id,
|
||||||
|
...(params.filter ?? {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryString = buildQueryString(query);
|
||||||
|
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) => {
|
create: async (resource, params) => {
|
||||||
const resourcePath = resource === "equipment" ? "equipments" : resource;
|
const { json } = await httpClient(`${apiUrl}/${resource}`, {
|
||||||
const { json } = await fetchJson(env.apiUrl + "/" + resourcePath, {
|
method: 'POST',
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(params.data),
|
body: JSON.stringify(params.data),
|
||||||
});
|
});
|
||||||
return { data: json };
|
return { data: json };
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async (resource, params) => {
|
update: async (resource, params) => {
|
||||||
const resourcePath = resource === "equipment" ? "equipments" : resource;
|
const { json } = await httpClient(`${apiUrl}/${resource}/${params.id}`, {
|
||||||
const { json } = await fetchJson(
|
method: 'PATCH',
|
||||||
env.apiUrl + "/" + resourcePath + "/" + params.id,
|
body: JSON.stringify(params.data),
|
||||||
{ method: "PATCH", body: JSON.stringify(params.data) },
|
});
|
||||||
);
|
|
||||||
return { data: json };
|
return { data: json };
|
||||||
},
|
},
|
||||||
|
|
||||||
updateMany: async (resource, params) => {
|
updateMany: async (resource, params) => {
|
||||||
const results = await Promise.all(
|
const responses = await Promise.all(
|
||||||
params.ids.map((id) =>
|
params.ids.map((id) =>
|
||||||
dataProvider.update(resource, {
|
httpClient(`${apiUrl}/${resource}/${id}`, {
|
||||||
id,
|
method: 'PATCH',
|
||||||
data: params.data,
|
body: JSON.stringify(params.data),
|
||||||
previousData: {},
|
})
|
||||||
meta: params.meta,
|
)
|
||||||
} as any),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return { data: results.map((result) => result.data.id) };
|
return { data: responses.map(({ json }) => json.id) };
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async (resource, params) => {
|
delete: async (resource, params) => {
|
||||||
const resourcePath = resource === "equipment" ? "equipments" : resource;
|
const { json } = await httpClient(`${apiUrl}/${resource}/${params.id}`, {
|
||||||
const { json } = await fetchJson(
|
method: 'DELETE',
|
||||||
env.apiUrl + "/" + resourcePath + "/" + params.id,
|
});
|
||||||
{ method: "DELETE" },
|
return { data: json };
|
||||||
);
|
|
||||||
return { data: json ?? { id: params.id } };
|
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteMany: async (resource, params) => {
|
deleteMany: async (resource, params) => {
|
||||||
const results = await Promise.all(
|
const responses = await Promise.all(
|
||||||
params.ids.map((id) =>
|
params.ids.map((id) =>
|
||||||
dataProvider.delete(resource, {
|
httpClient(`${apiUrl}/${resource}/${id}`, {
|
||||||
id,
|
method: 'DELETE',
|
||||||
previousData: {},
|
})
|
||||||
meta: params.meta,
|
)
|
||||||
} as any),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return { data: results.map((result) => result.data.id) };
|
return { data: responses.map(({ json }) => json.id) };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default dataProvider;
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
:root {
|
|
||||||
--text: #6b6375;
|
|
||||||
--text-h: #08060d;
|
|
||||||
--bg: #fff;
|
|
||||||
--border: #e5e4e7;
|
|
||||||
--code-bg: #f4f3ec;
|
|
||||||
--accent: #aa3bff;
|
|
||||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
|
||||||
--accent-border: rgba(170, 59, 255, 0.5);
|
|
||||||
--social-bg: rgba(244, 243, 236, 0.5);
|
|
||||||
--shadow:
|
|
||||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
|
||||||
|
|
||||||
--sans: system-ui, "Segoe UI", Roboto, sans-serif;
|
|
||||||
--heading: system-ui, "Segoe UI", Roboto, sans-serif;
|
|
||||||
--mono: ui-monospace, Consolas, monospace;
|
|
||||||
|
|
||||||
font: 18px/145% var(--sans);
|
|
||||||
letter-spacing: 0.18px;
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: var(--text);
|
|
||||||
background: var(--bg);
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--text: #9ca3af;
|
|
||||||
--text-h: #f3f4f6;
|
|
||||||
--bg: #16171d;
|
|
||||||
--border: #2e303a;
|
|
||||||
--code-bg: #1f2028;
|
|
||||||
--accent: #c084fc;
|
|
||||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
|
||||||
--accent-border: rgba(192, 132, 252, 0.5);
|
|
||||||
--social-bg: rgba(47, 48, 58, 0.5);
|
|
||||||
--shadow:
|
|
||||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#social .button-icon {
|
|
||||||
filter: invert(1) brightness(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
border-inline: 1px solid var(--border);
|
|
||||||
min-height: 100svh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2 {
|
|
||||||
font-family: var(--heading);
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-h);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 56px;
|
|
||||||
letter-spacing: -1.68px;
|
|
||||||
margin: 32px 0;
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 36px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 118%;
|
|
||||||
letter-spacing: -0.24px;
|
|
||||||
margin: 0 0 8px;
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
code,
|
|
||||||
.counter {
|
|
||||||
font-family: var(--mono);
|
|
||||||
display: inline-flex;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--text-h);
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 135%;
|
|
||||||
padding: 4px 8px;
|
|
||||||
background: var(--code-bg);
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,26 @@
|
|||||||
import { StrictMode } from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import './index.css';
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { initKeycloak } from './auth/keycloak';
|
import { initKeycloak } from './auth/keycloak';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root')!);
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
await initKeycloak();
|
await initKeycloak();
|
||||||
createRoot(document.getElementById('root')!).render(
|
|
||||||
<StrictMode>
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void bootstrap();
|
bootstrap().catch((error) => {
|
||||||
|
console.error('Failed to initialize authentication', error);
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<div>Authentication initialization failed. Check your environment variables.</div>
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import {
|
|
||||||
AutocompleteInput,
|
|
||||||
Create,
|
|
||||||
ReferenceInput,
|
|
||||||
SimpleForm,
|
|
||||||
} from "react-admin";
|
|
||||||
import { employeeOptionText, partOptionText } from "../shared/enums";
|
|
||||||
export const CategoryResourceCreate = () => (
|
|
||||||
<Create>
|
|
||||||
<SimpleForm>
|
|
||||||
<ReferenceInput source="partId" reference="parts">
|
|
||||||
<AutocompleteInput
|
|
||||||
optionText={partOptionText}
|
|
||||||
filterToQuery={(searchText) => ({ q: searchText })}
|
|
||||||
/>
|
|
||||||
</ReferenceInput>
|
|
||||||
<ReferenceInput source="employeeCode" reference="employees">
|
|
||||||
<AutocompleteInput
|
|
||||||
optionText={employeeOptionText}
|
|
||||||
filterToQuery={(searchText) => ({ q: searchText })}
|
|
||||||
/>
|
|
||||||
</ReferenceInput>
|
|
||||||
</SimpleForm>
|
|
||||||
</Create>
|
|
||||||
);
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import {
|
|
||||||
AutocompleteInput,
|
|
||||||
Edit,
|
|
||||||
ReferenceInput,
|
|
||||||
SimpleForm,
|
|
||||||
} from "react-admin";
|
|
||||||
import { employeeOptionText, partOptionText } from "../shared/enums";
|
|
||||||
export const CategoryResourceEdit = () => (
|
|
||||||
<Edit>
|
|
||||||
<SimpleForm>
|
|
||||||
<ReferenceInput source="partId" reference="parts">
|
|
||||||
<AutocompleteInput
|
|
||||||
optionText={partOptionText}
|
|
||||||
filterToQuery={(searchText) => ({ q: searchText })}
|
|
||||||
/>
|
|
||||||
</ReferenceInput>
|
|
||||||
<ReferenceInput source="employeeCode" reference="employees">
|
|
||||||
<AutocompleteInput
|
|
||||||
optionText={employeeOptionText}
|
|
||||||
filterToQuery={(searchText) => ({ q: searchText })}
|
|
||||||
/>
|
|
||||||
</ReferenceInput>
|
|
||||||
</SimpleForm>
|
|
||||||
</Edit>
|
|
||||||
);
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import {
|
|
||||||
AutocompleteInput,
|
|
||||||
CreateButton,
|
|
||||||
Datagrid,
|
|
||||||
FilterButton,
|
|
||||||
List,
|
|
||||||
ReferenceField,
|
|
||||||
ReferenceInput,
|
|
||||||
TextField,
|
|
||||||
TextInput,
|
|
||||||
TopToolbar,
|
|
||||||
} from "react-admin";
|
|
||||||
import { employeeOptionText, partOptionText } from "../shared/enums";
|
|
||||||
const categoryResourceFilters = [
|
|
||||||
<TextInput key="q" source="q" label="Search" alwaysOn />,
|
|
||||||
<ReferenceInput key="partId" source="partId" reference="parts">
|
|
||||||
<AutocompleteInput
|
|
||||||
optionText={partOptionText}
|
|
||||||
filterToQuery={(searchText) => ({ q: searchText })}
|
|
||||||
/>
|
|
||||||
</ReferenceInput>,
|
|
||||||
<ReferenceInput
|
|
||||||
key="employeeCode"
|
|
||||||
source="employeeCode"
|
|
||||||
reference="employees"
|
|
||||||
>
|
|
||||||
<AutocompleteInput
|
|
||||||
optionText={employeeOptionText}
|
|
||||||
filterToQuery={(searchText) => ({ q: searchText })}
|
|
||||||
/>
|
|
||||||
</ReferenceInput>,
|
|
||||||
];
|
|
||||||
export const CategoryResourceList = () => (
|
|
||||||
<List
|
|
||||||
filters={categoryResourceFilters}
|
|
||||||
actions={
|
|
||||||
<TopToolbar>
|
|
||||||
<FilterButton />
|
|
||||||
<CreateButton />
|
|
||||||
</TopToolbar>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Datagrid rowClick="show">
|
|
||||||
<TextField source="id" />
|
|
||||||
<ReferenceField source="partId" reference="parts" link="show">
|
|
||||||
<TextField source="name" />
|
|
||||||
</ReferenceField>
|
|
||||||
<ReferenceField source="employeeCode" reference="employees" link="show">
|
|
||||||
<TextField source="fullName" />
|
|
||||||
</ReferenceField>
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { ReferenceField, Show, SimpleShowLayout, TextField } from "react-admin";
|
|
||||||
export const CategoryResourceShow = () => (
|
|
||||||
<Show>
|
|
||||||
<SimpleShowLayout>
|
|
||||||
<TextField source="id" />
|
|
||||||
<ReferenceField source="partId" reference="parts" link="show">
|
|
||||||
<TextField source="name" />
|
|
||||||
</ReferenceField>
|
|
||||||
<ReferenceField source="employeeCode" reference="employees" link="show">
|
|
||||||
<TextField source="fullName" />
|
|
||||||
</ReferenceField>
|
|
||||||
</SimpleShowLayout>
|
|
||||||
</Show>
|
|
||||||
);
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import {
|
|
||||||
AutocompleteInput,
|
|
||||||
Create,
|
|
||||||
NumberInput,
|
|
||||||
ReferenceInput,
|
|
||||||
SelectInput,
|
|
||||||
SimpleForm,
|
|
||||||
TextInput,
|
|
||||||
} from "react-admin";
|
|
||||||
import { employeeOptionText, roleChoices } from "../shared/enums";
|
|
||||||
export const EmployeeCreate = () => (
|
|
||||||
<Create>
|
|
||||||
<SimpleForm>
|
|
||||||
<TextInput source="code" required />
|
|
||||||
<TextInput source="fullName" required />
|
|
||||||
<SelectInput source="role" choices={roleChoices} required />
|
|
||||||
<TextInput source="position" required />
|
|
||||||
<ReferenceInput source="boss" reference="employees">
|
|
||||||
<AutocompleteInput
|
|
||||||
optionText={employeeOptionText}
|
|
||||||
filterToQuery={(searchText) => ({ q: searchText })}
|
|
||||||
/>
|
|
||||||
</ReferenceInput>
|
|
||||||
<NumberInput source="price" />
|
|
||||||
<NumberInput source="phoneNumber" />
|
|
||||||
</SimpleForm>
|
|
||||||
</Create>
|
|
||||||
);
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import {
|
|
||||||
AutocompleteInput,
|
|
||||||
Edit,
|
|
||||||
NumberInput,
|
|
||||||
ReferenceInput,
|
|
||||||
SelectInput,
|
|
||||||
SimpleForm,
|
|
||||||
TextInput,
|
|
||||||
} from "react-admin";
|
|
||||||
import { employeeOptionText, roleChoices } from "../shared/enums";
|
|
||||||
export const EmployeeEdit = () => (
|
|
||||||
<Edit>
|
|
||||||
<SimpleForm>
|
|
||||||
<TextInput source="fullName" />
|
|
||||||
<SelectInput source="role" choices={roleChoices} />
|
|
||||||
<TextInput source="position" />
|
|
||||||
<ReferenceInput source="boss" reference="employees">
|
|
||||||
<AutocompleteInput
|
|
||||||
optionText={employeeOptionText}
|
|
||||||
filterToQuery={(searchText) => ({ q: searchText })}
|
|
||||||
/>
|
|
||||||
</ReferenceInput>
|
|
||||||
<NumberInput source="price" />
|
|
||||||
<NumberInput source="phoneNumber" />
|
|
||||||
</SimpleForm>
|
|
||||||
</Edit>
|
|
||||||
);
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import {
|
|
||||||
Datagrid,
|
|
||||||
List,
|
|
||||||
ReferenceField,
|
|
||||||
SelectArrayInput,
|
|
||||||
SelectField,
|
|
||||||
TextField,
|
|
||||||
TextInput,
|
|
||||||
} from "react-admin";
|
|
||||||
import { ResourceListActions } from "../shared/ListActions";
|
|
||||||
import { roleChoices } from "../shared/enums";
|
|
||||||
const employeeFilters = [
|
|
||||||
<TextInput key="q" source="q" label="Search" alwaysOn />,
|
|
||||||
<SelectArrayInput
|
|
||||||
key="role"
|
|
||||||
source="role"
|
|
||||||
label="Role"
|
|
||||||
choices={roleChoices}
|
|
||||||
/>,
|
|
||||||
<TextInput key="position" source="position" label="Position" />,
|
|
||||||
];
|
|
||||||
export const EmployeeList = () => (
|
|
||||||
<List
|
|
||||||
filters={employeeFilters}
|
|
||||||
actions={<ResourceListActions filters={employeeFilters} />}
|
|
||||||
>
|
|
||||||
<Datagrid rowClick="show">
|
|
||||||
<TextField source="code" />
|
|
||||||
<TextField source="fullName" />
|
|
||||||
<SelectField source="role" choices={roleChoices} />
|
|
||||||
<TextField source="position" />
|
|
||||||
<ReferenceField source="bossCode" reference="employees" link="show">
|
|
||||||
<TextField source="fullName" />
|
|
||||||
</ReferenceField>
|
|
||||||
<TextField source="phoneNumber" />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import {
|
|
||||||
ReferenceField,
|
|
||||||
SelectField,
|
|
||||||
Show,
|
|
||||||
SimpleShowLayout,
|
|
||||||
TextField,
|
|
||||||
} from "react-admin";
|
|
||||||
import { roleChoices } from "../shared/enums";
|
|
||||||
export const EmployeeShow = () => (
|
|
||||||
<Show>
|
|
||||||
<SimpleShowLayout>
|
|
||||||
<TextField source="code" />
|
|
||||||
<TextField source="fullName" />
|
|
||||||
<SelectField source="role" choices={roleChoices} />
|
|
||||||
<TextField source="position" />
|
|
||||||
<ReferenceField source="bossCode" reference="employees" link="show">
|
|
||||||
<TextField source="fullName" />
|
|
||||||
</ReferenceField>
|
|
||||||
<TextField source="phoneNumber" />
|
|
||||||
</SimpleShowLayout>
|
|
||||||
</Show>
|
|
||||||
);
|
|
||||||
14
client/src/resources/equipment-type/EquipmentTypeCreate.tsx
Normal file
14
client/src/resources/equipment-type/EquipmentTypeCreate.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
14
client/src/resources/equipment-type/EquipmentTypeEdit.tsx
Normal file
14
client/src/resources/equipment-type/EquipmentTypeEdit.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
38
client/src/resources/equipment-type/EquipmentTypeList.tsx
Normal file
38
client/src/resources/equipment-type/EquipmentTypeList.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
List,
|
||||||
|
Datagrid,
|
||||||
|
TextField,
|
||||||
|
TextInput,
|
||||||
|
TopToolbar,
|
||||||
|
FilterButton,
|
||||||
|
CreateButton,
|
||||||
|
ExportButton,
|
||||||
|
NumberField
|
||||||
|
} from 'react-admin';
|
||||||
|
|
||||||
|
|
||||||
|
const equipmentTypeFilters = [
|
||||||
|
<TextInput key="q" source="q" label="Поиск" alwaysOn />,
|
||||||
|
<TextInput key="name" source="name" label="Наименование вида" />,
|
||||||
|
<TextInput key="manufacturer" source="manufacturer" label="Производитель" />
|
||||||
|
];
|
||||||
|
|
||||||
|
const EquipmentTypeListActions = () => (
|
||||||
|
<TopToolbar>
|
||||||
|
<FilterButton filters={equipmentTypeFilters} />
|
||||||
|
<CreateButton />
|
||||||
|
<ExportButton />
|
||||||
|
</TopToolbar>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const EquipmentTypeList = () => (
|
||||||
|
<List actions={<EquipmentTypeListActions />} filters={equipmentTypeFilters} sort={{ field: 'code', order: 'ASC' }}>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
13
client/src/resources/equipment-type/EquipmentTypeShow.tsx
Normal file
13
client/src/resources/equipment-type/EquipmentTypeShow.tsx
Normal 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>
|
||||||
|
);
|
||||||
@@ -1,44 +1,28 @@
|
|||||||
import {
|
import { Create, SimpleForm, TextInput, NumberInput, DateInput, SelectInput, ReferenceInput, AutocompleteInput } from 'react-admin';
|
||||||
Create,
|
|
||||||
DateInput,
|
const statusChoices = [
|
||||||
NumberInput,
|
{ id: 'Active', name: 'В эксплуатации' },
|
||||||
SelectInput,
|
{ id: 'Repair', name: 'В ремонте' },
|
||||||
SimpleForm,
|
{ id: 'Reserve', name: 'В резерве' },
|
||||||
} from "react-admin";
|
{ id: 'WriteOff', name: 'Списано' },
|
||||||
import {
|
];
|
||||||
equipmentStatusChoices,
|
|
||||||
equipmentTypeChoices,
|
|
||||||
laborOperationChoices,
|
|
||||||
periodicityChoices,
|
|
||||||
} from "../shared/enums";
|
|
||||||
import { PlainInput } from "../shared/inputs";
|
|
||||||
|
|
||||||
export const EquipmentCreate = () => (
|
export const EquipmentCreate = () => (
|
||||||
<Create>
|
<Create>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<PlainInput source="name" required />
|
<TextInput source="inventoryNumber" label="Инвентарный номер" isRequired />
|
||||||
<PlainInput source="serialNumber" required />
|
<TextInput source="serialNumber" label="Заводской (серийный) номер" />
|
||||||
<PlainInput source="inventoryNumber" required />
|
<TextInput source="name" label="Наименование единицы оборудования" isRequired />
|
||||||
<SelectInput
|
<ReferenceInput source="equipmentTypeCode" reference="equipment-types">
|
||||||
source="equipmentType"
|
<AutocompleteInput label="Вид оборудования" optionText={(record) => record.code ? `${record.code} — ${record.name ?? record.code}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />
|
||||||
choices={equipmentTypeChoices}
|
</ReferenceInput>
|
||||||
required
|
<SelectInput source="status" label="Текущий статус" choices={statusChoices} emptyText="Не выбрано" />
|
||||||
/>
|
<TextInput source="location" label="Место эксплуатации / скважина / куст" />
|
||||||
<DateInput source="dateOfInspection" />
|
<DateInput source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
||||||
<SelectInput
|
<NumberInput source="totalEngineHours" label="Общая наработка, моточасов" />
|
||||||
source="periodicityTO"
|
<NumberInput source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта, моточасов" />
|
||||||
choices={periodicityChoices}
|
<DateInput source="lastRepairAt" label="Дата последнего ремонта" />
|
||||||
required
|
<TextInput source="notes" label="Примечания" />
|
||||||
/>
|
|
||||||
<PlainInput source="location" />
|
|
||||||
<SelectInput source="status" choices={equipmentStatusChoices} required />
|
|
||||||
<DateInput source="commissionedAt" />
|
|
||||||
<NumberInput source="totalEngineHours" />
|
|
||||||
<NumberInput source="engineHoursSinceLastRepair" />
|
|
||||||
<DateInput source="lastRepairAt" />
|
|
||||||
<PlainInput source="notes" multiline />
|
|
||||||
<SelectInput source="workAsPartOf" choices={laborOperationChoices} />
|
|
||||||
<NumberInput source="fuelConsumed" />
|
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</Create>
|
</Create>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +1,29 @@
|
|||||||
import {
|
import { Edit, SimpleForm, TextInput, NumberInput, DateInput, SelectInput, ReferenceInput, AutocompleteInput } from 'react-admin';
|
||||||
DateInput,
|
|
||||||
Edit,
|
const statusChoices = [
|
||||||
NumberInput,
|
{ id: 'Active', name: 'В эксплуатации' },
|
||||||
SelectInput,
|
{ id: 'Repair', name: 'В ремонте' },
|
||||||
SimpleForm,
|
{ id: 'Reserve', name: 'В резерве' },
|
||||||
} from "react-admin";
|
{ id: 'WriteOff', name: 'Списано' },
|
||||||
import {
|
];
|
||||||
equipmentStatusChoices,
|
|
||||||
equipmentTypeChoices,
|
|
||||||
laborOperationChoices,
|
|
||||||
periodicityChoices,
|
|
||||||
} from "../shared/enums";
|
|
||||||
import { PlainInput } from "../shared/inputs";
|
|
||||||
|
|
||||||
export const EquipmentEdit = () => (
|
export const EquipmentEdit = () => (
|
||||||
<Edit>
|
<Edit>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<PlainInput source="name" />
|
<TextInput source="id" label="id" disabled />
|
||||||
<PlainInput source="serialNumber" />
|
<TextInput source="inventoryNumber" label="Инвентарный номер" isRequired />
|
||||||
<PlainInput source="inventoryNumber" />
|
<TextInput source="serialNumber" label="Заводской (серийный) номер" />
|
||||||
<SelectInput source="equipmentType" choices={equipmentTypeChoices} />
|
<TextInput source="name" label="Наименование единицы оборудования" isRequired />
|
||||||
<DateInput source="dateOfInspection" />
|
<ReferenceInput source="equipmentTypeCode" reference="equipment-types">
|
||||||
<SelectInput source="periodicityTO" choices={periodicityChoices} />
|
<AutocompleteInput label="Вид оборудования" optionText={(record) => record.code ? `${record.code} — ${record.name ?? record.code}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />
|
||||||
<PlainInput source="location" />
|
</ReferenceInput>
|
||||||
<SelectInput source="status" choices={equipmentStatusChoices} />
|
<SelectInput source="status" label="Текущий статус" choices={statusChoices} emptyText="Не выбрано" />
|
||||||
<DateInput source="commissionedAt" />
|
<TextInput source="location" label="Место эксплуатации / скважина / куст" />
|
||||||
<NumberInput source="totalEngineHours" />
|
<DateInput source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
||||||
<NumberInput source="engineHoursSinceLastRepair" />
|
<NumberInput source="totalEngineHours" label="Общая наработка, моточасов" />
|
||||||
<DateInput source="lastRepairAt" />
|
<NumberInput source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта, моточасов" />
|
||||||
<PlainInput source="notes" multiline />
|
<DateInput source="lastRepairAt" label="Дата последнего ремонта" />
|
||||||
<SelectInput source="workAsPartOf" choices={laborOperationChoices} />
|
<TextInput source="notes" label="Примечания" />
|
||||||
<NumberInput source="fuelConsumed" />
|
|
||||||
</SimpleForm>
|
</SimpleForm>
|
||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,78 +1,66 @@
|
|||||||
import {
|
import {
|
||||||
CreateButton,
|
|
||||||
Datagrid,
|
|
||||||
DateField,
|
|
||||||
FilterButton,
|
|
||||||
List,
|
List,
|
||||||
NumberField,
|
Datagrid,
|
||||||
SelectArrayInput,
|
|
||||||
SelectField,
|
|
||||||
TextField,
|
TextField,
|
||||||
TextInput,
|
TextInput,
|
||||||
TopToolbar,
|
TopToolbar,
|
||||||
} from "react-admin";
|
FilterButton,
|
||||||
import {
|
CreateButton,
|
||||||
equipmentStatusChoices,
|
ExportButton,
|
||||||
equipmentTypeChoices,
|
NumberField,
|
||||||
laborOperationChoices,
|
DateField,
|
||||||
periodicityChoices,
|
SelectField,
|
||||||
} from "../shared/enums";
|
ReferenceField,
|
||||||
|
SelectArrayInput,
|
||||||
|
ReferenceInput,
|
||||||
|
AutocompleteInput
|
||||||
|
} from 'react-admin';
|
||||||
|
|
||||||
const equipmentFilters = [
|
const statusChoices = [
|
||||||
<TextInput key="q" source="q" label="Search" alwaysOn />,
|
{ id: 'Active', name: 'В эксплуатации' },
|
||||||
<TextInput
|
{ id: 'Repair', name: 'В ремонте' },
|
||||||
key="inventoryNumber"
|
{ id: 'Reserve', name: 'В резерве' },
|
||||||
source="inventoryNumber"
|
{ id: 'WriteOff', name: 'Списано' },
|
||||||
label="Inventory number"
|
|
||||||
/>,
|
|
||||||
<TextInput key="serialNumber" source="serialNumber" label="Serial number" />,
|
|
||||||
<TextInput key="name" source="name" label="Name" />,
|
|
||||||
<SelectArrayInput
|
|
||||||
key="equipmentType"
|
|
||||||
source="equipmentType"
|
|
||||||
label="Type"
|
|
||||||
choices={equipmentTypeChoices}
|
|
||||||
/>,
|
|
||||||
<SelectArrayInput
|
|
||||||
key="periodicityTO"
|
|
||||||
source="periodicityTO"
|
|
||||||
label="Periodicity"
|
|
||||||
choices={periodicityChoices}
|
|
||||||
/>,
|
|
||||||
<SelectArrayInput
|
|
||||||
key="status"
|
|
||||||
source="status"
|
|
||||||
label="Status"
|
|
||||||
choices={equipmentStatusChoices}
|
|
||||||
/>,
|
|
||||||
<TextInput key="location" source="location" label="Location" />,
|
|
||||||
<SelectArrayInput
|
|
||||||
key="workAsPartOf"
|
|
||||||
source="workAsPartOf"
|
|
||||||
label="Operation"
|
|
||||||
choices={laborOperationChoices}
|
|
||||||
/>,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const EquipmentList = () => (
|
const equipmentFilters = [
|
||||||
<List
|
<TextInput key="q" source="q" label="Поиск" alwaysOn />,
|
||||||
filters={equipmentFilters}
|
<TextInput key="inventoryNumber" source="inventoryNumber" label="Инвентарный номер" />,
|
||||||
actions={
|
<TextInput key="serialNumber" source="serialNumber" label="Заводской (серийный) номер" />,
|
||||||
|
<TextInput key="name" source="name" label="Наименование единицы оборудования" />,
|
||||||
|
<ReferenceInput key="equipmentTypeCode" source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования">
|
||||||
|
<AutocompleteInput optionText={(record) => record.code ? `${record.code} — ${record.name ?? record.code}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />
|
||||||
|
</ReferenceInput>,
|
||||||
|
<SelectArrayInput key="status" source="status" label="Текущий статус" choices={statusChoices} />,
|
||||||
|
<TextInput key="location" source="location" label="Место эксплуатации / скважина / куст" />,
|
||||||
|
<TextInput key="notes" source="notes" label="Примечания" />
|
||||||
|
];
|
||||||
|
|
||||||
|
const EquipmentListActions = () => (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
<FilterButton />
|
<FilterButton filters={equipmentFilters} />
|
||||||
<CreateButton />
|
<CreateButton />
|
||||||
|
<ExportButton />
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
}
|
);
|
||||||
>
|
|
||||||
|
export const EquipmentList = () => (
|
||||||
|
<List actions={<EquipmentListActions />} filters={equipmentFilters} sort={{ field: 'inventoryNumber', order: 'ASC' }}>
|
||||||
<Datagrid rowClick="show">
|
<Datagrid rowClick="show">
|
||||||
<TextField source="inventoryNumber" />
|
<TextField source="id" label="id" />
|
||||||
<TextField source="name" />
|
<TextField source="inventoryNumber" label="Инвентарный номер" />
|
||||||
<TextField source="serialNumber" />
|
<TextField source="serialNumber" label="Заводской (серийный) номер" />
|
||||||
<SelectField source="equipmentType" choices={equipmentTypeChoices} />
|
<TextField source="name" label="Наименование единицы оборудования" />
|
||||||
<SelectField source="status" choices={equipmentStatusChoices} />
|
<ReferenceField source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования" link="show">
|
||||||
<DateField source="dateOfInspection" />
|
<TextField source="code" />
|
||||||
<NumberField source="totalEngineHours" />
|
</ReferenceField>
|
||||||
<TextField source="location" />
|
<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="Примечания" />
|
||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,35 +1,28 @@
|
|||||||
import {
|
import { Show, SimpleShowLayout, TextField, NumberField, DateField, SelectField, ReferenceField } from 'react-admin';
|
||||||
DateField,
|
|
||||||
NumberField,
|
const statusChoices = [
|
||||||
SelectField,
|
{ id: 'Active', name: 'В эксплуатации' },
|
||||||
Show,
|
{ id: 'Repair', name: 'В ремонте' },
|
||||||
SimpleShowLayout,
|
{ id: 'Reserve', name: 'В резерве' },
|
||||||
TextField,
|
{ id: 'WriteOff', name: 'Списано' },
|
||||||
} from "react-admin";
|
];
|
||||||
import {
|
|
||||||
equipmentStatusChoices,
|
|
||||||
equipmentTypeChoices,
|
|
||||||
laborOperationChoices,
|
|
||||||
periodicityChoices,
|
|
||||||
} from "../shared/enums";
|
|
||||||
export const EquipmentShow = () => (
|
export const EquipmentShow = () => (
|
||||||
<Show>
|
<Show>
|
||||||
<SimpleShowLayout>
|
<SimpleShowLayout>
|
||||||
<TextField source="inventoryNumber" />
|
<TextField source="id" label="id" />
|
||||||
<TextField source="name" />
|
<TextField source="inventoryNumber" label="Инвентарный номер" />
|
||||||
<TextField source="serialNumber" />
|
<TextField source="serialNumber" label="Заводской (серийный) номер" />
|
||||||
<SelectField source="equipmentType" choices={equipmentTypeChoices} />
|
<TextField source="name" label="Наименование единицы оборудования" />
|
||||||
<DateField source="dateOfInspection" />
|
<ReferenceField source="equipmentTypeCode" reference="equipment-types" label="Вид оборудования" link="show">
|
||||||
<SelectField source="periodicityTO" choices={periodicityChoices} />
|
<TextField source="code" />
|
||||||
<TextField source="location" />
|
</ReferenceField>
|
||||||
<SelectField source="status" choices={equipmentStatusChoices} />
|
<SelectField source="status" label="Текущий статус" choices={statusChoices} />
|
||||||
<DateField source="commissionedAt" />
|
<TextField source="location" label="Место эксплуатации / скважина / куст" />
|
||||||
<NumberField source="totalEngineHours" />
|
<DateField source="commissionedAt" label="Дата ввода в эксплуатацию" />
|
||||||
<NumberField source="engineHoursSinceLastRepair" />
|
<NumberField source="totalEngineHours" label="Общая наработка, моточасов" />
|
||||||
<DateField source="lastRepairAt" />
|
<NumberField source="engineHoursSinceLastRepair" label="Наработка с последнего ремонта, моточасов" />
|
||||||
<TextField source="notes" />
|
<DateField source="lastRepairAt" label="Дата последнего ремонта" />
|
||||||
<SelectField source="workAsPartOf" choices={laborOperationChoices} />
|
<TextField source="notes" label="Примечания" />
|
||||||
<NumberField source="fuelConsumed" />
|
|
||||||
</SimpleShowLayout>
|
</SimpleShowLayout>
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Create, NumberInput, SelectInput, SimpleForm } from "react-admin";
|
|
||||||
import { categoryPartChoices } from "../shared/enums";
|
|
||||||
import { PlainInput } from "../shared/inputs";
|
|
||||||
|
|
||||||
export const PartCreate = () => (
|
|
||||||
<Create>
|
|
||||||
<SimpleForm>
|
|
||||||
<PlainInput source="name" required />
|
|
||||||
<SelectInput source="categories" choices={categoryPartChoices} />
|
|
||||||
<NumberInput source="price" />
|
|
||||||
<PlainInput source="description" multiline />
|
|
||||||
<PlainInput source="serialNumber" />
|
|
||||||
</SimpleForm>
|
|
||||||
</Create>
|
|
||||||
);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Edit, NumberInput, SelectInput, SimpleForm } from "react-admin";
|
|
||||||
import { categoryPartChoices } from "../shared/enums";
|
|
||||||
import { PlainInput } from "../shared/inputs";
|
|
||||||
|
|
||||||
export const PartEdit = () => (
|
|
||||||
<Edit>
|
|
||||||
<SimpleForm>
|
|
||||||
<PlainInput source="name" />
|
|
||||||
<SelectInput source="categories" choices={categoryPartChoices} />
|
|
||||||
<NumberInput source="price" />
|
|
||||||
<PlainInput source="description" multiline />
|
|
||||||
<PlainInput source="serialNumber" />
|
|
||||||
</SimpleForm>
|
|
||||||
</Edit>
|
|
||||||
);
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import {
|
|
||||||
Datagrid,
|
|
||||||
List,
|
|
||||||
NumberField,
|
|
||||||
SelectArrayInput,
|
|
||||||
SelectField,
|
|
||||||
TextField,
|
|
||||||
TextInput,
|
|
||||||
} from "react-admin";
|
|
||||||
import { ResourceListActions } from "../shared/ListActions";
|
|
||||||
import { categoryPartChoices } from "../shared/enums";
|
|
||||||
const partFilters = [
|
|
||||||
<TextInput key="q" source="q" label="Search" alwaysOn />,
|
|
||||||
<SelectArrayInput
|
|
||||||
key="categories"
|
|
||||||
source="categories"
|
|
||||||
label="Category"
|
|
||||||
choices={categoryPartChoices}
|
|
||||||
/>,
|
|
||||||
<TextInput key="serialNumber" source="serialNumber" label="Serial number" />,
|
|
||||||
];
|
|
||||||
export const PartList = () => (
|
|
||||||
<List
|
|
||||||
filters={partFilters}
|
|
||||||
actions={<ResourceListActions filters={partFilters} />}
|
|
||||||
>
|
|
||||||
<Datagrid rowClick="show">
|
|
||||||
<TextField source="name" />
|
|
||||||
<SelectField source="categories" choices={categoryPartChoices} />
|
|
||||||
<NumberField source="price" />
|
|
||||||
<TextField source="serialNumber" />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import {
|
|
||||||
NumberField,
|
|
||||||
SelectField,
|
|
||||||
Show,
|
|
||||||
SimpleShowLayout,
|
|
||||||
TextField,
|
|
||||||
} from "react-admin";
|
|
||||||
import { categoryPartChoices } from "../shared/enums";
|
|
||||||
export const PartShow = () => (
|
|
||||||
<Show>
|
|
||||||
<SimpleShowLayout>
|
|
||||||
<TextField source="name" />
|
|
||||||
<SelectField source="categories" choices={categoryPartChoices} />
|
|
||||||
<NumberField source="price" />
|
|
||||||
<TextField source="description" />
|
|
||||||
<TextField source="serialNumber" />
|
|
||||||
</SimpleShowLayout>
|
|
||||||
</Show>
|
|
||||||
);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Create, NumberInput, SimpleForm } from "react-admin";
|
|
||||||
export const PriceListCreate = () => (
|
|
||||||
<Create>
|
|
||||||
<SimpleForm toolbar={false}>
|
|
||||||
<NumberInput source="costOfWorkingHours" disabled />
|
|
||||||
<NumberInput source="partPrice" disabled />
|
|
||||||
</SimpleForm>
|
|
||||||
</Create>
|
|
||||||
);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Edit, NumberInput, SimpleForm } from "react-admin";
|
|
||||||
export const PriceListEdit = () => (
|
|
||||||
<Edit>
|
|
||||||
<SimpleForm toolbar={false}>
|
|
||||||
<NumberInput source="costOfWorkingHours" disabled />
|
|
||||||
<NumberInput source="partPrice" disabled />
|
|
||||||
</SimpleForm>
|
|
||||||
</Edit>
|
|
||||||
);
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Datagrid, List, NumberField, TextField, TextInput } from "react-admin";
|
|
||||||
import { ResourceListActions } from "../shared/ListActions";
|
|
||||||
const priceListFilters = [
|
|
||||||
<TextInput key="q" source="q" label="Search" alwaysOn />,
|
|
||||||
];
|
|
||||||
export const PriceListList = () => (
|
|
||||||
<List
|
|
||||||
filters={priceListFilters}
|
|
||||||
actions={
|
|
||||||
<ResourceListActions filters={priceListFilters} hasCreate={false} />
|
|
||||||
}
|
|
||||||
perPage={1}
|
|
||||||
>
|
|
||||||
<Datagrid rowClick="show">
|
|
||||||
<TextField source="id" />
|
|
||||||
<NumberField source="costOfWorkingHours" />
|
|
||||||
<NumberField source="partPrice" />
|
|
||||||
</Datagrid>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { NumberField, Show, SimpleShowLayout, TextField } from "react-admin";
|
|
||||||
export const PriceListShow = () => (
|
|
||||||
<Show>
|
|
||||||
<SimpleShowLayout>
|
|
||||||
<TextField source="id" />
|
|
||||||
<NumberField source="costOfWorkingHours" />
|
|
||||||
<NumberField source="partPrice" />
|
|
||||||
</SimpleShowLayout>
|
|
||||||
</Show>
|
|
||||||
);
|
|
||||||
38
client/src/resources/repair-order/RepairOrderCreate.tsx
Normal file
38
client/src/resources/repair-order/RepairOrderCreate.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Create, SimpleForm, TextInput, NumberInput, DateInput, SelectInput, ReferenceInput, AutocompleteInput } 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">
|
||||||
|
<AutocompleteInput label="Оборудование" optionText={(record) => record.inventoryNumber ? `${record.inventoryNumber} — ${record.name ?? record.inventoryNumber}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />
|
||||||
|
</ReferenceInput>
|
||||||
|
<SelectInput source="repairKind" label="Вид ремонта" choices={repairKindChoices} emptyText="Не выбрано" />
|
||||||
|
<SelectInput source="status" label="Статус" choices={statusChoices} emptyText="Не выбрано" />
|
||||||
|
<DateInput source="plannedAt" label="Плановая дата начала" />
|
||||||
|
<DateInput source="startedAt" label="Фактическая дата начала" />
|
||||||
|
<DateInput source="completedAt" label="Фактическая дата завершения" />
|
||||||
|
<TextInput source="contractor" label="Подрядная организация (если внешний ремонт)" />
|
||||||
|
<NumberInput source="engineHoursAtRepair" label="Наработка на момент ремонта, моточасов" />
|
||||||
|
<TextInput source="description" label="Описание работ / дефекта" />
|
||||||
|
<TextInput source="notes" label="Примечания" />
|
||||||
|
</SimpleForm>
|
||||||
|
</Create>
|
||||||
|
);
|
||||||
39
client/src/resources/repair-order/RepairOrderEdit.tsx
Normal file
39
client/src/resources/repair-order/RepairOrderEdit.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Edit, SimpleForm, TextInput, NumberInput, DateInput, SelectInput, ReferenceInput, AutocompleteInput } 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="id" label="id" disabled />
|
||||||
|
<TextInput source="number" label="Номер заявки" isRequired />
|
||||||
|
<ReferenceInput source="equipmentId" reference="equipment">
|
||||||
|
<AutocompleteInput label="Оборудование" optionText={(record) => record.inventoryNumber ? `${record.inventoryNumber} — ${record.name ?? record.inventoryNumber}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />
|
||||||
|
</ReferenceInput>
|
||||||
|
<SelectInput source="repairKind" label="Вид ремонта" choices={repairKindChoices} emptyText="Не выбрано" />
|
||||||
|
<SelectInput source="status" label="Статус" choices={statusChoices} emptyText="Не выбрано" />
|
||||||
|
<DateInput source="plannedAt" label="Плановая дата начала" />
|
||||||
|
<DateInput source="startedAt" label="Фактическая дата начала" />
|
||||||
|
<DateInput source="completedAt" label="Фактическая дата завершения" />
|
||||||
|
<TextInput source="contractor" label="Подрядная организация (если внешний ремонт)" />
|
||||||
|
<NumberInput source="engineHoursAtRepair" label="Наработка на момент ремонта, моточасов" />
|
||||||
|
<TextInput source="description" label="Описание работ / дефекта" />
|
||||||
|
<TextInput source="notes" label="Примечания" />
|
||||||
|
</SimpleForm>
|
||||||
|
</Edit>
|
||||||
|
);
|
||||||
77
client/src/resources/repair-order/RepairOrderList.tsx
Normal file
77
client/src/resources/repair-order/RepairOrderList.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import {
|
||||||
|
List,
|
||||||
|
Datagrid,
|
||||||
|
TextField,
|
||||||
|
TextInput,
|
||||||
|
TopToolbar,
|
||||||
|
FilterButton,
|
||||||
|
CreateButton,
|
||||||
|
ExportButton,
|
||||||
|
NumberField,
|
||||||
|
DateField,
|
||||||
|
SelectField,
|
||||||
|
ReferenceField,
|
||||||
|
SelectArrayInput,
|
||||||
|
SelectInput,
|
||||||
|
ReferenceInput,
|
||||||
|
AutocompleteInput
|
||||||
|
} 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: 'Отменена' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const repairOrderFilters = [
|
||||||
|
<TextInput key="q" source="q" label="Поиск" alwaysOn />,
|
||||||
|
<TextInput key="number" source="number" label="Номер заявки" />,
|
||||||
|
<ReferenceInput key="equipmentId" source="equipmentId" reference="equipment" label="Оборудование">
|
||||||
|
<AutocompleteInput optionText={(record) => record.inventoryNumber ? `${record.inventoryNumber} — ${record.name ?? record.inventoryNumber}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />
|
||||||
|
</ReferenceInput>,
|
||||||
|
<SelectInput key="repairKind" source="repairKind" label="Вид ремонта" choices={repairKindChoices} emptyText="Все" />,
|
||||||
|
<SelectArrayInput key="status" source="status" label="Статус" choices={statusChoices} />,
|
||||||
|
<TextInput key="contractor" source="contractor" label="Подрядная организация (если внешний ремонт)" />,
|
||||||
|
<TextInput key="description" source="description" label="Описание работ / дефекта" />,
|
||||||
|
<TextInput key="notes" source="notes" label="Примечания" />
|
||||||
|
];
|
||||||
|
|
||||||
|
const RepairOrderListActions = () => (
|
||||||
|
<TopToolbar>
|
||||||
|
<FilterButton filters={repairOrderFilters} />
|
||||||
|
<CreateButton />
|
||||||
|
<ExportButton />
|
||||||
|
</TopToolbar>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const RepairOrderList = () => (
|
||||||
|
<List actions={<RepairOrderListActions />} filters={repairOrderFilters} sort={{ field: 'number', order: 'ASC' }}>
|
||||||
|
<Datagrid rowClick="show">
|
||||||
|
<TextField source="id" label="id" />
|
||||||
|
<TextField source="number" label="Номер заявки" />
|
||||||
|
<ReferenceField source="equipmentId" reference="equipment" label="Оборудование" link="show">
|
||||||
|
<TextField source="inventoryNumber" />
|
||||||
|
</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="Примечания" />
|
||||||
|
</Datagrid>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
38
client/src/resources/repair-order/RepairOrderShow.tsx
Normal file
38
client/src/resources/repair-order/RepairOrderShow.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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="id" label="id" />
|
||||||
|
<TextField source="number" label="Номер заявки" />
|
||||||
|
<ReferenceField source="equipmentId" reference="equipment" label="Оборудование" link="show">
|
||||||
|
<TextField source="inventoryNumber" />
|
||||||
|
</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,17 +0,0 @@
|
|||||||
import type { ReactElement } from "react";
|
|
||||||
import { CreateButton, FilterButton, TopToolbar } from "react-admin";
|
|
||||||
|
|
||||||
interface ListActionsProps {
|
|
||||||
filters?: ReactElement[];
|
|
||||||
hasCreate?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ResourceListActions = ({
|
|
||||||
filters,
|
|
||||||
hasCreate = true,
|
|
||||||
}: ListActionsProps) => (
|
|
||||||
<TopToolbar>
|
|
||||||
{filters ? <FilterButton filters={filters} /> : null}
|
|
||||||
{hasCreate ? <CreateButton /> : null}
|
|
||||||
</TopToolbar>
|
|
||||||
);
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
export const equipmentStatusChoices = [
|
|
||||||
"Active",
|
|
||||||
"Repair",
|
|
||||||
"Reserve",
|
|
||||||
"WriteOff",
|
|
||||||
].map((value) => ({ id: value, name: value }));
|
|
||||||
export const laborOperationChoices = ["Manual", "MachineManual", "Machine"].map(
|
|
||||||
(value) => ({ id: value, name: value }),
|
|
||||||
);
|
|
||||||
|
|
||||||
/** id = Prisma/API wire value; name = human label (matches DB enum @map). */
|
|
||||||
export const periodicityChoices = [
|
|
||||||
{ id: "EZHEDNEVNOE", name: "Ежедневное" },
|
|
||||||
{ id: "EZHENEDELNOE", name: "Еженедельное" },
|
|
||||||
{ id: "EZHEMESYACHNOE", name: "Ежемесячное" },
|
|
||||||
{ id: "POLUGODOVOE", name: "Полугодовое" },
|
|
||||||
{ id: "GODOVOE", name: "Годовое" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const roleChoices = [
|
|
||||||
{ id: "ISPOLNITEL", name: "Исполнитель" },
|
|
||||||
{ id: "PODPISANT", name: "Подписант" },
|
|
||||||
{ id: "POLZOVATEL", name: "Пользователь" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const categoryPartChoices = [
|
|
||||||
{ id: "RASKHODNIK", name: "Расходник" },
|
|
||||||
{ id: "ZAPCHAST", name: "Запчасть" },
|
|
||||||
{ id: "INSTRUMENT", name: "Инструмент" },
|
|
||||||
{ id: "SPETSODEZHDA", name: "Спецодежда" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const equipmentTypeChoices = [
|
|
||||||
{ id: "PROIZVODSTVENNOE", name: "Производственное" },
|
|
||||||
{ id: "ENERGETICHESKOE", name: "Энергетическое" },
|
|
||||||
{ id: "NASOSNOE", name: "Насосное" },
|
|
||||||
{ id: "KOMPRESSORNOE", name: "Компрессорное" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const equipmentOptionText = (
|
|
||||||
record?: Record<string, unknown> | null,
|
|
||||||
): string => {
|
|
||||||
if (!record) return "";
|
|
||||||
const inventoryNumber =
|
|
||||||
typeof record.inventoryNumber === "string" ? record.inventoryNumber : "";
|
|
||||||
const name = typeof record.name === "string" ? record.name : inventoryNumber;
|
|
||||||
return inventoryNumber
|
|
||||||
? inventoryNumber + " — " + (name || inventoryNumber)
|
|
||||||
: typeof record.name === "string"
|
|
||||||
? record.name
|
|
||||||
: String(record.id ?? "");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const employeeOptionText = (
|
|
||||||
record?: Record<string, unknown> | null,
|
|
||||||
): string => {
|
|
||||||
if (!record) return "";
|
|
||||||
if (typeof record.code === "string") {
|
|
||||||
const fallback =
|
|
||||||
typeof record.fullName === "string" ? record.fullName : record.code;
|
|
||||||
return record.code + " — " + fallback;
|
|
||||||
}
|
|
||||||
if (typeof record.fullName === "string") return record.fullName;
|
|
||||||
return String(record.id ?? "");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const partOptionText = (
|
|
||||||
record?: Record<string, unknown> | null,
|
|
||||||
): string => {
|
|
||||||
if (!record) return "";
|
|
||||||
if (typeof record.name === "string") return record.name;
|
|
||||||
return String(record.id ?? "");
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { TextInput } from "react-admin";
|
|
||||||
|
|
||||||
export const PlainInput = TextInput;
|
|
||||||
11
client/src/vite-env.d.ts
vendored
11
client/src/vite-env.d.ts
vendored
@@ -1 +1,12 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_URL: string;
|
||||||
|
readonly VITE_KEYCLOAK_URL: string;
|
||||||
|
readonly VITE_KEYCLOAK_REALM: string;
|
||||||
|
readonly VITE_KEYCLOAK_CLIENT_ID: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
||||||
"target": "ES2023",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"types": ["vite/client"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,25 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"compilerOptions": {
|
||||||
"references": [
|
"target": "ES2020",
|
||||||
{ "path": "./tsconfig.app.json" },
|
"useDefineForClassFields": true,
|
||||||
{ "path": "./tsconfig.node.json" }
|
"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" }]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
"composite": true,
|
||||||
"target": "ES2023",
|
|
||||||
"lib": ["ES2023"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"types": ["node"],
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"verbatimModuleSyntax": true,
|
"strict": true
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ services:
|
|||||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me}
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-toir}
|
POSTGRES_DB: ${POSTGRES_DB:-toir}
|
||||||
# UTF-8 cluster (applies only on first volume init) — migrations use Cyrillic enum labels
|
|
||||||
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C.UTF-8"
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
[
|
[
|
||||||
@@ -19,10 +17,12 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_PORT:-5432}:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres-data:/var/lib/postgresql/data
|
- postgres-data:/var/lib/postgresql/data
|
||||||
networks:
|
networks:
|
||||||
- toir
|
- app
|
||||||
|
|
||||||
server:
|
server:
|
||||||
build:
|
build:
|
||||||
@@ -42,15 +42,20 @@ services:
|
|||||||
KEYCLOAK_JWKS_URL: ${KEYCLOAK_JWKS_URL:-}
|
KEYCLOAK_JWKS_URL: ${KEYCLOAK_JWKS_URL:-}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
["CMD", "curl", "-fsS", "http://127.0.0.1:3000/health"]
|
[
|
||||||
|
"CMD",
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
|
||||||
|
]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 90s
|
start_period: 20s
|
||||||
expose:
|
expose:
|
||||||
- 3000
|
- "3000"
|
||||||
networks:
|
networks:
|
||||||
- toir
|
- app
|
||||||
- proxy
|
- proxy
|
||||||
|
|
||||||
client:
|
client:
|
||||||
@@ -66,25 +71,26 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
server:
|
server:
|
||||||
# Do not wait for healthy: if migrations fail, stack stays up so you can read server logs.
|
condition: service_healthy
|
||||||
condition: service_started
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/healthz >/dev/null 2>&1 || exit 1"]
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/healthz >/dev/null 2>&1 || exit 1"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
ports:
|
||||||
|
- "${CLIENT_PORT:-8080}:80"
|
||||||
expose:
|
expose:
|
||||||
- 80
|
- "80"
|
||||||
networks:
|
networks:
|
||||||
- toir
|
- app
|
||||||
- proxy
|
- proxy
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
toir:
|
app:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
proxy:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
# AID: экспорт OpenAPI и генератор приложения
|
# AID: экспорт OpenAPI и генератор приложения
|
||||||
|
|
||||||
В репозитории добавлены **сервисы-экспортёры** для интеграции с **AID** (или любым другим клиентом по HTTP): автоматическое получение **OpenAPI 3.0** из доменного **api-format**.
|
В репозитории добавлены **сервисы-экспортёры** для интеграции с **AID** (или любым другим клиентом по HTTP): автоматическое получение **OpenAPI 3.0** из доменного **api-format** и выдача **сгенерированного fullstack-приложения** из **DSL** без ручного копирования файлов.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Что работает
|
## Что сделано
|
||||||
|
|
||||||
| Компонент | Назначение |
|
| Компонент | Назначение |
|
||||||
|-----------|------------|
|
|-----------|------------|
|
||||||
| **`POST /aid/export/openapi`** (NestJS) | На вход JSON **api-format** → на выход документ **OpenAPI 3.0** в поле `openapi`. |
|
| **`POST /aid/export/openapi`** (NestJS) | На вход JSON **api-format** → на выход документ **OpenAPI 3.0** в поле `openapi`. |
|
||||||
|
| **`POST /aid/export/app`** (NestJS) | На вход текст **DSL** → либо JSON-бандл всех сгенерированных файлов (`files`), либо запись в рабочую копию репозитория (`apply: true`, опционально). |
|
||||||
| **`tools/api-format-to-openapi/`** | CLI и промпт для LLM: тот же конвертер, что вызывает Nest. |
|
| **`tools/api-format-to-openapi/`** | CLI и промпт для LLM: тот же конвертер, что вызывает Nest. |
|
||||||
|
| **`generation/generate.mjs`** | Новый флаг **`--print-bundle-json`**: вывод в stdout JSON с `entityCount`, `enumCount`, `files` — без записи на диск (аналог «сухого» экспорта для AID). |
|
||||||
| **`server/src/aid-export/`** | Модуль Nest: контроллер, сервис, краткая справка в `README.md` рядом с кодом. |
|
| **`server/src/aid-export/`** | Модуль Nest: контроллер, сервис, краткая справка в `README.md` рядом с кодом. |
|
||||||
|
|
||||||
## Что временно не работает
|
Ветка с этими изменениями: **`add_aid_exporters`**.
|
||||||
|
|
||||||
| Компонент | Статус |
|
|
||||||
|-----------|--------|
|
|
||||||
| **`POST /aid/export/app`** (DSL → бандл/apply) | **Non-operative.** The backing script (`generation/generate.mjs`) was removed during the architecture migration to api.dsl-first LLM generation. The endpoint returns 500 with a descriptive error. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Требования к запуску
|
## Требования к запуску
|
||||||
|
|
||||||
1. Репозиторий клонирован целиком (есть `tools/`, `server/`, `client/`).
|
1. Репозиторий клонирован целиком (есть `generation/`, `tools/`, `server/`, `client/`).
|
||||||
2. Backend запускается из каталога **`server/`** (`npm run start` / `start:dev`), чтобы относительные пути `../tools/api-format-to-openapi/convert.mjs` были корректны.
|
2. Backend запускается из каталога **`server/`** (`npm run start` / `start:dev`), чтобы относительные пути `../generation/generate.mjs` и `../tools/api-format-to-openapi/convert.mjs` были корректны.
|
||||||
3. Для режима OpenAPI через LLM на сервере нужны **`OPENAI_API_KEY`** (и при необходимости `OPENAI_MODEL`, `OPENAI_BASE_URL`).
|
3. Для режима OpenAPI через LLM на сервере нужны **`OPENAI_API_KEY`** (и при необходимости `OPENAI_MODEL`, `OPENAI_BASE_URL`).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -33,6 +31,7 @@
|
|||||||
| Переменная | Зачем |
|
| Переменная | Зачем |
|
||||||
|------------|--------|
|
|------------|--------|
|
||||||
| `AID_EXPORT_API_KEY` | Если задана, к **`/aid/export/*`** нужен заголовок **`X-AID-Export-Key`** с тем же значением. |
|
| `AID_EXPORT_API_KEY` | Если задана, к **`/aid/export/*`** нужен заголовок **`X-AID-Export-Key`** с тем же значением. |
|
||||||
|
| `AID_GENERATOR_ALLOW_APPLY` | Должна быть **`1`** или **`true`**, иначе **`POST /aid/export/app`** с **`apply: true`** вернёт **403** (защита от случайной перезаписи репозитория на сервере). |
|
||||||
| `OPENAI_API_KEY` | Для `POST /aid/export/openapi` с **`"mode": "llm"`**. |
|
| `OPENAI_API_KEY` | Для `POST /aid/export/openapi` с **`"mode": "llm"`**. |
|
||||||
|
|
||||||
Остальное как для обычного бэкенда (`DATABASE_URL`, `PORT` и т.д.).
|
Остальное как для обычного бэкенда (`DATABASE_URL`, `PORT` и т.д.).
|
||||||
@@ -70,19 +69,40 @@ X-AID-Export-Key: <если задан AID_EXPORT_API_KEY>
|
|||||||
|
|
||||||
Пример входа для теста: `tools/api-format-to-openapi/examples/api-format.example.json` (подставьте как значение `apiFormat`).
|
Пример входа для теста: `tools/api-format-to-openapi/examples/api-format.example.json` (подставьте как значение `apiFormat`).
|
||||||
|
|
||||||
### 2. Генератор приложения из DSL (non-operative)
|
### 2. Генератор приложения из DSL
|
||||||
|
|
||||||
**`POST /aid/export/app`**
|
**`POST /aid/export/app`**
|
||||||
|
|
||||||
> **Non-operative.** This endpoint depended on `generation/generate.mjs` which was removed
|
```json
|
||||||
> during the migration to api.dsl-first LLM generation. It currently returns HTTP 500
|
{
|
||||||
> with a descriptive error message. Restoring this endpoint requires implementing a new
|
"dsl": "domain TOiR {\n ...\n}\n",
|
||||||
> backing script compatible with the current `domain/*.api.dsl` pipeline.
|
"apply": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`apply: false`** (рекомендуется для AID): в ответе **`files`** — объект «путь от корня репо → текст файла». Диск на сервере не меняется.
|
||||||
|
- **`apply: true`**: выполняется запись файлов как у `npm run generate:from-dsl` с `--apply`; нужен **`AID_GENERATOR_ALLOW_APPLY=1`**.
|
||||||
|
|
||||||
|
**Ответ (бандл):** `{ "applied": false, "entityCount": N, "enumCount": M, "files": { ... } }`
|
||||||
|
**Ответ (apply):** `{ "applied": true, "message": "Generated ..." }`
|
||||||
|
|
||||||
|
Эталон DSL: `examples/TOiR.domain.dsl`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## CLI (без Nest)
|
## CLI (без Nest)
|
||||||
|
|
||||||
|
### Пошаговая демонстрация в терминале
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tools/api-format-to-openapi
|
||||||
|
npm run demo
|
||||||
|
# или с паузой после каждого шага (Enter):
|
||||||
|
npm run demo:pause
|
||||||
|
```
|
||||||
|
|
||||||
|
Показывает входной **api-format**, логику маппинга, запуск конвертера и структуру **OpenAPI**; результат — `demo-output/openapi.json`.
|
||||||
|
|
||||||
### api-format → OpenAPI
|
### api-format → OpenAPI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -99,22 +119,46 @@ node convert.mjs --mode llm --in your-api-format.json --out ../../openapi.llm.js
|
|||||||
|
|
||||||
Подробнее: **`tools/api-format-to-openapi/README.md`**.
|
Подробнее: **`tools/api-format-to-openapi/README.md`**.
|
||||||
|
|
||||||
|
### DSL → JSON-бандл
|
||||||
|
|
||||||
|
Из **корня репозитория**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node generation/generate.mjs --print-bundle-json --dsl examples/TOiR.domain.dsl > bundle.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Из **`server/`**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run generate:bundle-json > ../bundle.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Применить генератор к файлам на диске (как раньше):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
npm run generate:from-dsl
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Типичный сценарий для AID
|
## Типичный сценарий для AID
|
||||||
|
|
||||||
1. AID уже сформировал **api-format** (как у вас принято после DTO).
|
1. AID уже сформировал **api-format** (как у вас принято после DTO).
|
||||||
2. AID вызывает **`POST /aid/export/openapi`** → получает **OpenAPI 3.0** → сохраняет в проект / отдаёт в Swagger / в реестр.
|
2. AID вызывает **`POST /aid/export/openapi`** → получает **OpenAPI 3.0** → сохраняет в проект / отдаёт в Swagger / в реестр.
|
||||||
|
3. Для кода: AID передаёт **DSL** в **`POST /aid/export/app`** с **`apply: false`** → забирает **`files`** → применяет у себя (git apply, распаковка, PR).
|
||||||
|
4. Запись **`apply: true`** на общем сервере используйте только в доверенной среде и с **`AID_GENERATOR_ALLOW_APPLY`**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Где смотреть код и короткую справку
|
## Где смотреть код и короткую справку
|
||||||
|
|
||||||
- Полное описание эндпоинтов рядом с реализацией: **`server/src/aid-export/README.md`**
|
- Полное описание эндпоинтов рядом с реализацией: **`server/src/aid-export/README.md`**
|
||||||
|
- Общий dev-workflow (в т.ч. упоминание AID): **`generation/dev-workflow.md`**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Ограничения и дальнейшие шаги
|
## Ограничения и дальнейшие шаги
|
||||||
|
|
||||||
- Пример **api-format** в репозитории — **учебный**; под ваш продакшен-формат может понадобиться расширить маппинг в `convert.mjs` или отточить промпт **`llm-system.md`**.
|
- Пример **api-format** в репозитории — **учебный**; под ваш продакшен-формат может понадобиться расширить маппинг в `convert.mjs` или отточить промпт **`llm-system.md`**.
|
||||||
- **`/aid/export/app`** requires a new backing implementation compatible with the `domain/*.api.dsl` pipeline to be restored. See `docs/future-work.md` for planned future work.
|
- Ответ **`/aid/export/app`** с большим числом сущностей может быть объёмным; при необходимости добавьте сжатие, отдельное хранилище артефактов или пагинацию по файлам — контракт с AID лучше зафиксировать отдельно.
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
# api.dsl Conventions
|
|
||||||
|
|
||||||
Grammar and authoring conventions for `domain/*.api.dsl` files.
|
|
||||||
See `domain/toir.api.dsl` as the canonical example.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File location and naming
|
|
||||||
|
|
||||||
- All api.dsl files live in `domain/`.
|
|
||||||
- Naming: `<project>.api.dsl` (e.g. `toir.api.dsl`).
|
|
||||||
- Multiple api.dsl files are allowed but not required.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Two top-level block types
|
|
||||||
|
|
||||||
An api.dsl file contains two types of top-level blocks:
|
|
||||||
|
|
||||||
1. `dto` — defines the shape of a data transfer object
|
|
||||||
2. `api` — declares a group of API endpoints for one resource
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## dto block
|
|
||||||
|
|
||||||
```
|
|
||||||
dto DTO.<Name> {
|
|
||||||
description "Human-readable description";
|
|
||||||
|
|
||||||
attribute <fieldName> {
|
|
||||||
description "Field description";
|
|
||||||
type <type>;
|
|
||||||
is required; // or: is nullable;
|
|
||||||
map <Entity>.<field>; // links to a field in domain/*.api.dsl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### DTO naming convention
|
|
||||||
|
|
||||||
| DTO name | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| `DTO.<Entity>` | Full response shape (GET by id, list items) |
|
|
||||||
| `DTO.<Entity>Create` | Create request body |
|
|
||||||
| `DTO.<Entity>Update` | Update request body (partial — all fields nullable) |
|
|
||||||
| `DTO.<Entity>ListRequest` | Paginated list request (filters + page) |
|
|
||||||
| `DTO.<Entity>ListResponse` | Paginated list response (content + page info) |
|
|
||||||
|
|
||||||
### Types
|
|
||||||
|
|
||||||
Same scalar types as the domain DSL:
|
|
||||||
`uuid`, `string`, `text`, `integer`, `decimal`, `date`, `boolean`, or an enum name from
|
|
||||||
`domain/*.api.dsl`.
|
|
||||||
|
|
||||||
Cross-DTO references: `DTO.<Name>` or `DTO.<Name>[]`.
|
|
||||||
|
|
||||||
Standard pagination types (not entity-specific; treated as well-known):
|
|
||||||
`DTO.Filter[]`, `DTO.PageRequest`, `DTO.PageInfo`.
|
|
||||||
|
|
||||||
### is required vs is nullable
|
|
||||||
|
|
||||||
Unlike the domain DSL (where these drive DB constraints), in api.dsl these drive the
|
|
||||||
TypeScript DTO property modifier:
|
|
||||||
|
|
||||||
- `is required` → `field!: type` in the generated class
|
|
||||||
- `is nullable` → `field?: type` in the generated class
|
|
||||||
- Neither modifier → treated as optional (`field?: type`)
|
|
||||||
|
|
||||||
### map directive
|
|
||||||
|
|
||||||
`map Entity.field` links the DTO attribute to a domain entity field.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
- The entity and field must exist in `domain/*.api.dsl`.
|
|
||||||
- The scalar type must match the domain DSL field type (nullability may differ).
|
|
||||||
- Omit `map` only for pagination helper types (`DTO.Filter[]`, `DTO.PageRequest`, etc.)
|
|
||||||
that have no direct entity field counterpart.
|
|
||||||
|
|
||||||
### Conflict resolution
|
|
||||||
|
|
||||||
If the type declared in a `dto` attribute conflicts with the domain DSL field type,
|
|
||||||
the domain DSL is correct. Fix the api.dsl.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## api block
|
|
||||||
|
|
||||||
```
|
|
||||||
api API.<Name> {
|
|
||||||
description "API group description";
|
|
||||||
|
|
||||||
endpoint <endpointName> {
|
|
||||||
label "METHOD /path";
|
|
||||||
description "Endpoint description";
|
|
||||||
|
|
||||||
// For endpoints with a request body:
|
|
||||||
attribute request {
|
|
||||||
type DTO.<RequestDto>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For endpoints with a response body:
|
|
||||||
attribute response {
|
|
||||||
type DTO.<ResponseDto>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For path parameters:
|
|
||||||
attribute <paramName> {
|
|
||||||
type <type>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### api block naming
|
|
||||||
|
|
||||||
`API.<EntityName>` (e.g. `API.Equipment`, `API.RepairOrder`)
|
|
||||||
|
|
||||||
### endpoint label
|
|
||||||
|
|
||||||
`label "METHOD /path"` is the authoritative declaration of HTTP verb and route.
|
|
||||||
|
|
||||||
| Label pattern | NestJS decorator | Notes |
|
|
||||||
|---------------|-----------------|-------|
|
|
||||||
| `"GET /resource/{id}"` | `@Get(':id')` | |
|
|
||||||
| `"POST /resource"` | `@Post()` | Create |
|
|
||||||
| `"POST /resource/page"` | `@Post('page')` | Paginated list (body-based filter) |
|
|
||||||
| `"PUT /resource/{id}"` | `@Put(':id')` | Full or partial update |
|
|
||||||
| `"DELETE /resource/{id}"` | `@Delete(':id')` | |
|
|
||||||
|
|
||||||
Do not infer HTTP verbs from endpoint names. Always read the `label`.
|
|
||||||
|
|
||||||
### Path parameters
|
|
||||||
|
|
||||||
Declared as a plain `attribute` inside the endpoint block (not wrapped in `request`
|
|
||||||
or `response`):
|
|
||||||
|
|
||||||
```
|
|
||||||
attribute id {
|
|
||||||
type uuid;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Standard 5-endpoint CRUD pattern
|
|
||||||
|
|
||||||
| Endpoint name | Label | Body |
|
|
||||||
|---------------|-------|------|
|
|
||||||
| `list<Entity>s` | `"POST /<path>/page"` | request: `DTO.<Entity>ListRequest`, response: `DTO.<Entity>ListResponse` |
|
|
||||||
| `get<Entity>` | `"GET /<path>/{id}"` | path param `id`, response: `DTO.<Entity>` |
|
|
||||||
| `create<Entity>` | `"POST /<path>"` | request: `DTO.<Entity>Create` |
|
|
||||||
| `update<Entity>` | `"PUT /<path>/{id}"` | path param `id`, request: `DTO.<Entity>Update` |
|
|
||||||
| `delete<Entity>` | `"DELETE /<path>/{id}"` | path param `id` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Constraints
|
|
||||||
|
|
||||||
1. Every `map Entity.field` must resolve to an existing entity + field in `domain/*.api.dsl`.
|
|
||||||
2. Types in api.dsl must be compatible with domain DSL field types (same scalar type).
|
|
||||||
3. api.dsl must not define entities or enums. Those belong in `domain/*.api.dsl`.
|
|
||||||
4. An api.dsl may omit domain entity fields from a DTO (e.g. no PK in Create DTO).
|
|
||||||
It must not add fields that don't exist in the domain model.
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
# Future Work — Deferred Items
|
|
||||||
|
|
||||||
This file tracks engineering improvements that are deliberately deferred due to the
|
|
||||||
current stage of the project. They are not forgotten — they are acknowledged technical
|
|
||||||
debt that should be addressed before scaling.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rule 7 — Tracing, Telemetry, Cost/Latency Observability
|
|
||||||
|
|
||||||
**Status:** Deferred. No LLM calls are instrumented.
|
|
||||||
|
|
||||||
**Why it matters (Anthropic / Google / Microsoft guidance):**
|
|
||||||
Without observability, you cannot:
|
|
||||||
- Know which prompts are expensive (token count, latency)
|
|
||||||
- Detect prompt regressions via cost drift
|
|
||||||
- Attribute generation failures to specific prompt versions
|
|
||||||
- Track improvement over time
|
|
||||||
|
|
||||||
**What needs to be built:**
|
|
||||||
|
|
||||||
### 7.1 — Generation log
|
|
||||||
|
|
||||||
Create `tools/generation-log.mjs` that wraps any LLM generation call and writes a
|
|
||||||
structured JSON entry to `logs/generation.jsonl`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"timestamp": "2026-04-03T10:00:00.000Z",
|
|
||||||
"entity": "Equipment",
|
|
||||||
"artifact": "backend",
|
|
||||||
"prompt_version": "1.0",
|
|
||||||
"model": "...",
|
|
||||||
"input_tokens": 4200,
|
|
||||||
"output_tokens": 1800,
|
|
||||||
"latency_ms": 3200,
|
|
||||||
"validation_passed": true,
|
|
||||||
"eval_passed": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 — Cost budget alerts
|
|
||||||
|
|
||||||
Add a threshold check (e.g., warn if input_tokens > 8000 for a single entity generation).
|
|
||||||
This enforces the context budget from `prompts/general-prompt.md §CONTEXT BUDGET`.
|
|
||||||
|
|
||||||
### 7.3 — Prompt version tracking
|
|
||||||
|
|
||||||
Add `<!-- prompt-version: X.Y -->` comments to all prompt files (already started in
|
|
||||||
`backend-rules.md` and `frontend-rules.md`). Increment version on any non-trivial change.
|
|
||||||
Log the prompt versions alongside the generation log entry.
|
|
||||||
|
|
||||||
### 7.4 — Drift detection
|
|
||||||
|
|
||||||
Compare generation log entries across runs. If token count for the same entity increases
|
|
||||||
by >20% without a DSL change, flag it as context rot.
|
|
||||||
|
|
||||||
**Effort estimate:** Medium. 2–3 days to build the logging layer. Zero effort for
|
|
||||||
prompt versioning (already partially done).
|
|
||||||
|
|
||||||
**Trigger:** Implement before the system is used for more than 10 entities or before
|
|
||||||
any production deployment.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rule 8 — Risk Controls and Red-Teaming
|
|
||||||
|
|
||||||
**Status:** Deferred. No sanitization or adversarial testing exists.
|
|
||||||
|
|
||||||
**Why it matters (Anthropic / Google / Microsoft guidance):**
|
|
||||||
LLM-generated code at scale introduces risks that do not exist in hand-written code:
|
|
||||||
- **Prompt injection**: malicious content in DSL `description` fields could steer
|
|
||||||
generation (e.g., `description "Ignore previous instructions and..."`)
|
|
||||||
- **Generated credential leakage**: LLM may hallucinate hardcoded secrets that look
|
|
||||||
real (e.g., `apiKey: 'sk-...'`)
|
|
||||||
- **Missing auth guards**: already caught by Rule 4 validator, but adversarial prompts
|
|
||||||
could bypass it by generating valid-looking guard syntax that is semantically inactive
|
|
||||||
- **Supply chain**: generated package imports could reference non-existent or malicious
|
|
||||||
packages if the LLM hallucinates
|
|
||||||
|
|
||||||
**What needs to be built:**
|
|
||||||
|
|
||||||
### 8.1 — DSL input sanitization
|
|
||||||
|
|
||||||
In `tools/api-summary.mjs`, before building the summary, check all `description` and
|
|
||||||
`label` fields for injection patterns:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function sanitizeDslString(value, fieldPath) {
|
|
||||||
const injectionPatterns = [
|
|
||||||
/ignore previous/i,
|
|
||||||
/disregard.*instruction/i,
|
|
||||||
/you are now/i,
|
|
||||||
/system:/i,
|
|
||||||
];
|
|
||||||
for (const pattern of injectionPatterns) {
|
|
||||||
if (pattern.test(value)) {
|
|
||||||
throw new Error(`Potential prompt injection in DSL field ${fieldPath}: "${value}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 — Generated code security scan
|
|
||||||
|
|
||||||
Add to `tools/validate-generation.mjs` (or a separate `tools/security-scan.mjs`):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Check no hardcoded secrets leaked into generated code
|
|
||||||
function validateNoSecretLeakage() {
|
|
||||||
const patterns = [
|
|
||||||
/sk-[a-zA-Z0-9]{20,}/, // OpenAI key pattern
|
|
||||||
/[a-zA-Z0-9+/]{40}={0,2}/, // Base64 secret-like
|
|
||||||
/password\s*=\s*['"][^'"]{4,}['"]/, // Hardcoded password
|
|
||||||
/apiKey\s*=\s*['"][^'"]{4,}['"]/, // Hardcoded API key
|
|
||||||
];
|
|
||||||
// Run against all generated files...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.3 — UseGuards completeness audit
|
|
||||||
|
|
||||||
Beyond the current validator check (UseGuards present), add: verify that the guard
|
|
||||||
constructor arguments are non-empty and match the expected guard class names. A guard
|
|
||||||
call like `@UseGuards()` (empty) passes the current regex but provides no protection.
|
|
||||||
|
|
||||||
### 8.4 — Red-team fixture
|
|
||||||
|
|
||||||
Create `tools/eval/fixtures/_adversarial/` with a fixture that includes a DSL snippet
|
|
||||||
containing a benign injection attempt (e.g., a `description` field with "ignore format
|
|
||||||
rules") and verifies the generation still produces spec-compliant output.
|
|
||||||
|
|
||||||
### 8.5 — Generated import allowlist
|
|
||||||
|
|
||||||
Maintain a list of approved npm packages that generated code may import. Flag any
|
|
||||||
import not on the allowlist as a manual review item.
|
|
||||||
|
|
||||||
**Effort estimate:** Medium-High. 3–5 days. Security scan and sanitization are low
|
|
||||||
effort; red-team fixtures and import allowlisting are higher effort.
|
|
||||||
|
|
||||||
**Trigger:** Implement before any external user can influence `domain/*.api.dsl` content
|
|
||||||
(i.e., before a UI or API to edit the DSL is exposed).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tracking
|
|
||||||
|
|
||||||
| Rule | Status | Priority | Trigger |
|
|
||||||
|------|--------|----------|---------|
|
|
||||||
| Rule 7 — Telemetry | Deferred | Medium | Before >10 entities or production deployment |
|
|
||||||
| Rule 8 — Risk controls | Deferred | High | Before DSL editing is exposed to external users |
|
|
||||||
|
|
||||||
Last updated: 2026-04-03
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
# Generation Playbook
|
|
||||||
|
|
||||||
Step-by-step instructions for generating and regenerating artifacts in this repository.
|
|
||||||
Read `AGENTS.md` and `docs/source-of-truth.md` first.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pipeline overview
|
|
||||||
|
|
||||||
```
|
|
||||||
Tier 1 — Single Source of Truth (hand-authored, never generated)
|
|
||||||
domain/toir.api.dsl ──┐ enums, DTOs, endpoints, HTTP methods,
|
|
||||||
│ entity field mappings, primary keys
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Tier 2 — Deterministic Preprocessing (npm scripts, no LLM)
|
|
||||||
api-summary.json ←─ npm run generate:api-summary
|
|
||||||
openapi.json ←─ npm run generate:openapi (auxiliary)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Tier 1 (api.dsl) + Tier 2 (context) + prompts/*.md ──► LLM Generation
|
|
||||||
prompts/general-prompt.md
|
|
||||||
prompts/backend-rules.md ──► server/src/modules/<entity>/
|
|
||||||
prompts/frontend-rules.md ──► client/src/resources/<entity>/
|
|
||||||
prompts/prisma-rules.md ──► server/prisma/schema.prisma
|
|
||||||
prompts/auth-rules.md ──► (auth seam reference)
|
|
||||||
prompts/runtime-rules.md ──► (env/docker reference)
|
|
||||||
prompts/validation-rules.md ──► (validation gate reference)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Tier 4 — Validation Gate
|
|
||||||
node tools/validate-generation.mjs --artifacts-only
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Before any generation run:
|
|
||||||
|
|
||||||
1. `domain/*.api.dsl` is current and valid.
|
|
||||||
2. Refresh the Tier 2 intermediate context:
|
|
||||||
```bash
|
|
||||||
npm run generate:api-summary
|
|
||||||
```
|
|
||||||
3. Run `node tools/validate-generation.mjs --artifacts-only` to confirm the baseline passes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Standard generation workflow
|
|
||||||
|
|
||||||
### Step 1 — Refresh Tier 2 derived artifacts
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From repo root
|
|
||||||
npm run generate:api-summary
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2 — Read generation inputs (context budget)
|
|
||||||
|
|
||||||
> **`prompts/general-prompt.md` is the master generation prompt.** It contains all core
|
|
||||||
> type mappings, naming conventions, DTO/controller/service/frontend rules, mutation
|
|
||||||
> boundaries, and the complete generation workflow. Load it as the single entrypoint.
|
|
||||||
>
|
|
||||||
> For artifact-specific details (Prisma FK rules, auth JWKS chain, detailed validation
|
|
||||||
> groups), load the relevant companion file: `prompts/prisma-rules.md`,
|
|
||||||
> `prompts/auth-rules.md`, `prompts/validation-rules.md`, or `prompts/runtime-rules.md`.
|
|
||||||
>
|
|
||||||
> See `prompts/general-prompt.md §CONTEXT BUDGET` for the full budget model.
|
|
||||||
|
|
||||||
1. `prompts/general-prompt.md` — master generation prompt (always load)
|
|
||||||
2. `api-summary.json §<entity>` — compact entity index (fast-path context anchor)
|
|
||||||
3. `domain/*.api.dsl §API.<EntityName>` — **only the api block + its referenced DTOs + enums** (entity-scoped)
|
|
||||||
4. **If needed:** `prompts/prisma-rules.md` (Prisma) or `prompts/auth-rules.md` (auth seams)
|
|
||||||
|
|
||||||
Before generating any DTO or component: **quote the relevant DSL field definitions verbatim first**, then generate from those quotes. This prevents training-data contamination.
|
|
||||||
|
|
||||||
### Step 3 — Generate Prisma schema
|
|
||||||
|
|
||||||
Generate `server/prisma/schema.prisma` from `domain/*.api.dsl` following `prompts/prisma-rules.md`.
|
|
||||||
|
|
||||||
If the schema changed, run Prisma migration in `server/`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd server
|
|
||||||
npx prisma migrate dev --name <description>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4 — Generate backend modules
|
|
||||||
|
|
||||||
For each `api` block in `domain/*.api.dsl`, generate:
|
|
||||||
|
|
||||||
1. `server/src/modules/<kebab>/<entity>.module.ts`
|
|
||||||
2. `server/src/modules/<kebab>/dto/create-<kebab>.dto.ts`
|
|
||||||
— fields from the `DTO.<Entity>Create` block in api.dsl
|
|
||||||
3. `server/src/modules/<kebab>/dto/update-<kebab>.dto.ts`
|
|
||||||
— fields from the `DTO.<Entity>Update` block in api.dsl
|
|
||||||
4. `server/src/modules/<kebab>/<entity>.service.ts`
|
|
||||||
— CRUD operations using Prisma; respect type mappings from `prompts/backend-rules.md`
|
|
||||||
5. `server/src/modules/<kebab>/<entity>.controller.ts`
|
|
||||||
— one method per `endpoint` in the `api` block; HTTP verb and path from the `label`
|
|
||||||
|
|
||||||
Register the module in `server/src/app.module.ts`.
|
|
||||||
|
|
||||||
### Step 5 — Generate frontend resources
|
|
||||||
|
|
||||||
For each `api` block in `domain/*.api.dsl`, generate:
|
|
||||||
|
|
||||||
1. `client/src/resources/<kebab>/<Entity>List.tsx`
|
|
||||||
— columns from `DTO.<Entity>` (response shape)
|
|
||||||
2. `client/src/resources/<kebab>/<Entity>Create.tsx`
|
|
||||||
— fields from `DTO.<Entity>Create`
|
|
||||||
3. `client/src/resources/<kebab>/<Entity>Edit.tsx`
|
|
||||||
— fields from `DTO.<Entity>Update`
|
|
||||||
4. `client/src/resources/<kebab>/<Entity>Show.tsx`
|
|
||||||
— fields from `DTO.<Entity>`
|
|
||||||
|
|
||||||
Register the resource in `client/src/App.tsx`.
|
|
||||||
|
|
||||||
### Step 6 — Verify (two-stage gate)
|
|
||||||
|
|
||||||
**Stage 1 — Structural gate:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node tools/validate-generation.mjs --artifacts-only
|
|
||||||
```
|
|
||||||
|
|
||||||
Checks file existence, field names, class-validator decorators, auth guards, and RA component types.
|
|
||||||
|
|
||||||
Full structural verification (requires installed deps):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node tools/validate-generation.mjs
|
|
||||||
```
|
|
||||||
|
|
||||||
**Stage 2 — Eval harness:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run eval:generation
|
|
||||||
```
|
|
||||||
|
|
||||||
Fixture-based semantic checks. See `tools/eval/README.md`.
|
|
||||||
|
|
||||||
Both must pass before the task is complete.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new entity
|
|
||||||
|
|
||||||
1. Add the entity's enums, DTOs, and `api` block to `domain/toir.api.dsl`.
|
|
||||||
2. Run `npm run generate:api-summary`.
|
|
||||||
3. **Before generating:** create `tools/eval/fixtures/<kebab>/meta.json`, `backend.assertions.json`, and `frontend.assertions.json` with expected patterns.
|
|
||||||
4. Run `npm run eval:generation` — it will fail (entity files don't exist yet). That's expected.
|
|
||||||
5. Generate backend and frontend artifacts (Steps 3–5).
|
|
||||||
6. Run `npm run eval:generation` again — now it should pass.
|
|
||||||
7. Run `node tools/validate-generation.mjs --artifacts-only` — both gates must pass.
|
|
||||||
|
|
||||||
> This is **eval-driven development**: write the failing eval first (step 3), then generate to make it pass (step 6). A passing eval confirms the LLM followed the rules.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Changing an existing entity
|
|
||||||
|
|
||||||
**If the domain model changes** (new field, changed type, new FK, new enum):
|
|
||||||
|
|
||||||
1. Update `domain/toir.api.dsl` (enums, DTO attributes, `map` references).
|
|
||||||
2. Run `npm run generate:api-summary`.
|
|
||||||
3. Regenerate `server/prisma/schema.prisma`, run migration.
|
|
||||||
4. Regenerate the affected modules and resources (Steps 4–6).
|
|
||||||
|
|
||||||
**If only the API contract changes** (different nullability, new endpoint, different HTTP method):
|
|
||||||
|
|
||||||
1. Update `domain/toir.api.dsl` only.
|
|
||||||
2. Run `npm run generate:api-summary` to refresh `api-summary.json`.
|
|
||||||
3. Run Steps 4–6. No Prisma migration needed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Artifact traceability matrix
|
|
||||||
|
|
||||||
| Artifact | DSL source | Prompt rule | Validator check |
|
|
||||||
| ---------------------------------------------- | ---------------------- | ---------------------------------------------- | --------------------------------------- |
|
|
||||||
| `server/prisma/schema.prisma` | `domain/*.api.dsl` | `prompts/prisma-rules.md` | `§validateBuildChecks` (file exists) |
|
|
||||||
| `api-summary.json` | `domain/*.api.dsl` | — (deterministic) | `§validateBuildChecks` (freshness) |
|
|
||||||
| `server/src/modules/<e>/*.ts` | `domain/*.api.dsl` | `prompts/backend-rules.md` | `§validateApiDslCoverage` |
|
|
||||||
| `server/src/modules/<e>/dto/create-<e>.dto.ts` | `DTO.<E>Create` fields | `prompts/backend-rules.md §DTO-field-coverage` | `§validateApiDslCoverage` (field names) |
|
|
||||||
| `server/src/modules/<e>/dto/update-<e>.dto.ts` | `DTO.<E>Update` fields | `prompts/backend-rules.md §DTO-field-coverage` | `§validateApiDslCoverage` (field names) |
|
|
||||||
| `client/src/resources/<e>/<E>*.tsx` | `domain/*.api.dsl` | `prompts/frontend-rules.md` | `§validateApiDslCoverage` |
|
|
||||||
| `client/src/auth/keycloak.ts` | — (Tier 4 handwritten) | `prompts/auth-rules.md` | `§validateAuthChecks` |
|
|
||||||
| `toir-realm.json` | — (Tier 4 handwritten) | `prompts/auth-rules.md` | `§validateRealmChecks` |
|
|
||||||
| `docker-compose.yml` | — (Tier 4 handwritten) | `prompts/runtime-rules.md` | `§validateRuntimeContractChecks` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification commands reference
|
|
||||||
|
|
||||||
| Command | When to use |
|
|
||||||
| ----------------------------------------------------- | ------------------------------------------ |
|
|
||||||
| `npm run generate:api-summary` | After any change to `domain/*.api.dsl` |
|
|
||||||
| `npm run generate:openapi` | To regenerate OpenAPI 3.0.3 documentation |
|
|
||||||
| `node tools/validate-generation.mjs --artifacts-only` | After every generation run (required) |
|
|
||||||
| `node tools/validate-generation.mjs` | Before committing; requires installed deps |
|
|
||||||
| `node tools/validate-generation.mjs --run-runtime` | End-to-end; requires running DB |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## OpenRouter invocation
|
|
||||||
|
|
||||||
Set environment variables before any LLM-mode tool call:
|
|
||||||
|
|
||||||
```
|
|
||||||
OPENAI_API_KEY=<openrouter-key>
|
|
||||||
OPENAI_BASE_URL=https://openrouter.ai/api/v1
|
|
||||||
OPENAI_MODEL=<model-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
The `OPENAI_BASE_URL` variable is consumed by `tools/api-format-to-openapi/convert.mjs --mode llm`.
|
|
||||||
For agent-driven generation via Cursor or direct API calls, the standard workflow above
|
|
||||||
applies regardless of model provider.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Auxiliary tools
|
|
||||||
|
|
||||||
### `tools/api-summary-to-openapi.mjs`
|
|
||||||
|
|
||||||
Generates `openapi.json` (OpenAPI 3.0.3) from `api-summary.json` deterministically.
|
|
||||||
This is a documentation/integration artifact, not part of the core generation pipeline.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run generate:openapi
|
|
||||||
```
|
|
||||||
|
|
||||||
### `tools/api-format-to-openapi/`
|
|
||||||
|
|
||||||
Auxiliary integration tool for external consumers using the `api-format` JSON schema.
|
|
||||||
Not connected to the main DSL pipeline. See `docs/AID_EXPORT_README.md` for details.
|
|
||||||
@@ -28,7 +28,7 @@ Root-level files stay limited to repository-level artifacts such as:
|
|||||||
- `README.md`
|
- `README.md`
|
||||||
- `package.json`
|
- `package.json`
|
||||||
- `docker-compose.yml`
|
- `docker-compose.yml`
|
||||||
- `api-summary.json`
|
- `domain-summary.json`
|
||||||
- `toir-realm.json`
|
- `toir-realm.json`
|
||||||
- `.gitignore`
|
- `.gitignore`
|
||||||
|
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
# Source-of-Truth Hierarchy
|
|
||||||
|
|
||||||
This document is the authoritative reference for which files own which decisions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tier 1: Authoritative sources (hand-authored; never generated)
|
|
||||||
|
|
||||||
### `domain/*.api.dsl`
|
|
||||||
|
|
||||||
**Single source of truth for the entire domain and API contract:**
|
|
||||||
|
|
||||||
- Entity names and structure
|
|
||||||
- Attribute names, scalar types, descriptions
|
|
||||||
- Primary keys (including natural string keys)
|
|
||||||
- Foreign keys and relations
|
|
||||||
- Enum definitions and their values
|
|
||||||
- Database-level constraints: `is required`, `is unique`, `default`
|
|
||||||
- DTO shapes per operation (Create, Update, Read, ListRequest, ListResponse)
|
|
||||||
- Which fields appear in each DTO and with what TypeScript modifier (`!` or `?`)
|
|
||||||
- HTTP method and path for each endpoint (via `label "METHOD /path"`)
|
|
||||||
- Endpoint names (camelCase identifiers)
|
|
||||||
- Pagination request/response contract
|
|
||||||
|
|
||||||
**Drives:** `server/prisma/schema.prisma`, `server/src/modules/`, `client/src/resources/`,
|
|
||||||
`server/src/app.module.ts`, `client/src/App.tsx`.
|
|
||||||
|
|
||||||
### `prompts/*.md` and `AGENTS.md`
|
|
||||||
|
|
||||||
**Authoritative for:**
|
|
||||||
|
|
||||||
- Agent generation workflow and reading order
|
|
||||||
- Auth seam patterns (Keycloak, JWT, PKCE S256, JWKS resolution chain)
|
|
||||||
- Runtime conventions (env examples, docker-compose topology)
|
|
||||||
- Framework scaffold baseline requirements (NestJS CLI, Vite React TypeScript)
|
|
||||||
- Filtering and sorting contract
|
|
||||||
- Naming conventions and implicit rules (pluralization, sort field priority, type mappings)
|
|
||||||
- Mutation boundaries (what agents must not overwrite)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tier 2: Deterministic derivatives (never edit manually)
|
|
||||||
|
|
||||||
| File | Generated from | Command |
|
|
||||||
| ----------------- | ------------------ | ------------------------------ |
|
|
||||||
| `api-summary.json` | `domain/*.api.dsl` | `npm run generate:api-summary` |
|
|
||||||
|
|
||||||
These files are regenerated from their sources. Manual edits are overwritten on the
|
|
||||||
next generation run.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tier 3: LLM-generated artifacts (never edit manually after generation)
|
|
||||||
|
|
||||||
| Zone | Generated from |
|
|
||||||
| -------------------------------- | ------------------------------------------------ |
|
|
||||||
| `server/prisma/schema.prisma` | `domain/*.api.dsl` + `prompts/prisma-rules.md` |
|
|
||||||
| `server/src/modules/<entity>/` | `domain/*.api.dsl` + `prompts/backend-rules.md` |
|
|
||||||
| `client/src/resources/<entity>/` | `domain/*.api.dsl` + `prompts/frontend-rules.md` |
|
|
||||||
| `server/src/app.module.ts` | Module list derived from api.dsl `api` blocks |
|
|
||||||
| `client/src/App.tsx` | Resource list derived from api.dsl `api` blocks |
|
|
||||||
|
|
||||||
To change these files: update `domain/*.api.dsl` and regenerate.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tier 4: Handwritten (not generated; not derived)
|
|
||||||
|
|
||||||
- Auth seams: `client/src/auth/`, `server/src/auth/`
|
|
||||||
- `toir-realm.json`
|
|
||||||
- `docker-compose.yml`
|
|
||||||
- `server/.env.example`, `client/.env.example`
|
|
||||||
- Framework config: `nest-cli.json`, `tsconfig*.json`, `vite.config.*`, etc.
|
|
||||||
- All `prompts/*.md`, `AGENTS.md`, `domain/dsl-spec.md`, `domain/*.api.dsl`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Validation gate
|
|
||||||
|
|
||||||
### Stage 1 — Structural gate
|
|
||||||
|
|
||||||
```
|
|
||||||
node tools/validate-generation.mjs --artifacts-only
|
|
||||||
```
|
|
||||||
|
|
||||||
Verifies that generated artifacts satisfy the contracts declared in Tier 1 sources.
|
|
||||||
|
|
||||||
### Stage 2 — Eval harness
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run eval:generation
|
|
||||||
```
|
|
||||||
|
|
||||||
Fixture-based semantic checks from `tools/eval/fixtures/`.
|
|
||||||
|
|
||||||
Both stages must pass before any generation task is considered complete.
|
|
||||||
324
domain-summary.json
Normal file
324
domain-summary.json
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
{
|
||||||
|
"sourceFiles": [
|
||||||
|
"domain/TOiR.domain.dsl"
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"name": "EquipmentType",
|
||||||
|
"primaryKey": "code",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "code",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"unique": true,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "manufacturer",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maintenanceIntervalHours",
|
||||||
|
"type": "integer",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "overhaulIntervalHours",
|
||||||
|
"type": "integer",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Equipment",
|
||||||
|
"primaryKey": "id",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "inventoryNumber",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"unique": true,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "serialNumber",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "equipmentTypeCode",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "EquipmentStatus",
|
||||||
|
"required": true,
|
||||||
|
"unique": false,
|
||||||
|
"default": "Active"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "location",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "commissionedAt",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "totalEngineHours",
|
||||||
|
"type": "decimal",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "engineHoursSinceLastRepair",
|
||||||
|
"type": "decimal",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lastRepairAt",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "notes",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"field": "equipmentTypeCode",
|
||||||
|
"references": {
|
||||||
|
"entity": "EquipmentType",
|
||||||
|
"field": "code"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RepairOrder",
|
||||||
|
"primaryKey": "id",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "number",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"unique": true,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "equipmentId",
|
||||||
|
"type": "uuid",
|
||||||
|
"required": true,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "repairKind",
|
||||||
|
"type": "RepairKind",
|
||||||
|
"required": true,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": "RepairOrderStatus",
|
||||||
|
"required": true,
|
||||||
|
"unique": false,
|
||||||
|
"default": "Draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "plannedAt",
|
||||||
|
"type": "date",
|
||||||
|
"required": true,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "startedAt",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "completedAt",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "contractor",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "engineHoursAtRepair",
|
||||||
|
"type": "decimal",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "notes",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"unique": false,
|
||||||
|
"default": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"field": "equipmentId",
|
||||||
|
"references": {
|
||||||
|
"entity": "Equipment",
|
||||||
|
"field": "id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enums": [
|
||||||
|
{
|
||||||
|
"name": "EquipmentStatus",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"name": "Active",
|
||||||
|
"label": "В эксплуатации"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Repair",
|
||||||
|
"label": "В ремонте"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reserve",
|
||||||
|
"label": "В резерве"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WriteOff",
|
||||||
|
"label": "Списано"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RepairKind",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"name": "TO",
|
||||||
|
"label": "Техническое обслуживание"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TR",
|
||||||
|
"label": "Текущий ремонт"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TRE",
|
||||||
|
"label": "Текущий расширенный ремонт"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "KR",
|
||||||
|
"label": "Капитальный ремонт"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AR",
|
||||||
|
"label": "Аварийный ремонт"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MP",
|
||||||
|
"label": "Метрологическая поверка"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RepairOrderStatus",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"name": "Draft",
|
||||||
|
"label": "Черновик"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Approved",
|
||||||
|
"label": "Утверждена"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "InWork",
|
||||||
|
"label": "В работе"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Done",
|
||||||
|
"label": "Выполнена"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cancelled",
|
||||||
|
"label": "Отменена"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
236
domain/TOiR.domain.dsl
Normal file
236
domain/TOiR.domain.dsl
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
/*
|
||||||
|
КИС ТОиР — демонстрационная схема доменной модели
|
||||||
|
Сущности: 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,42 +1,34 @@
|
|||||||
# DSL Language Specification
|
# DSL Language Specification
|
||||||
|
|
||||||
This document describes the DSL system used to specify fullstack CRUD applications.
|
This document describes the single DSL (Domain Specific Language) used to specify fullstack CRUD applications. The only required DSL input is `domain/*.dsl`.
|
||||||
|
|
||||||
`domain/*.api.dsl` is the single source of truth for the entire domain model and API
|
`domain-summary.json` is a derived artifact generated from this DSL to stabilize LLM-first generation and feed the lightweight validation gate. It must never replace the DSL as the source of truth. The active prompt corpus that consumes this contract lives in `prompts/`.
|
||||||
contract. It drives Prisma schema generation, NestJS module generation, and React Admin
|
|
||||||
resource generation.
|
|
||||||
|
|
||||||
`api-summary.json` is a derived artifact generated from the api.dsl to stabilize
|
|
||||||
LLM-first generation and feed the lightweight validation gate. It must never replace the
|
|
||||||
DSL as the source of truth. The active prompt corpus that consumes this contract lives in
|
|
||||||
`prompts/`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# DSL Architecture
|
# DSL Responsibility
|
||||||
|
|
||||||
## `domain/*.api.dsl`
|
The domain DSL defines only:
|
||||||
|
|
||||||
The api.dsl is the authoritative source of truth for:
|
- domain model
|
||||||
|
- relations
|
||||||
|
- enums
|
||||||
|
|
||||||
- entities and their attributes
|
The domain DSL is the single source of truth for:
|
||||||
- scalar types and enums
|
|
||||||
- primary keys and foreign keys
|
|
||||||
- database-level constraints (required, unique, default)
|
|
||||||
- relations between entities
|
|
||||||
- DTO shapes per operation (Create, Update, Read, List)
|
|
||||||
- nullability and requiredness of each DTO attribute per operation
|
|
||||||
- HTTP methods and paths for each endpoint
|
|
||||||
- endpoint names and groupings
|
|
||||||
- pagination request/response contracts
|
|
||||||
|
|
||||||
The api.dsl drives: Prisma schema, NestJS controller/service/DTO generation,
|
- entities
|
||||||
and React Admin resource generation.
|
- attributes
|
||||||
|
- primary keys
|
||||||
|
- foreign keys
|
||||||
|
- enums
|
||||||
|
|
||||||
Constraint: every `map Entity.field` or `sync Entity.field` reference in `domain/*.api.dsl`
|
The following layers are always derived from the domain DSL and must not be authored as standalone authoritative DSL inputs:
|
||||||
must resolve to an entity and field defined within the same api.dsl file.
|
|
||||||
|
|
||||||
## Optional extension mechanism
|
- DTO
|
||||||
|
- API
|
||||||
|
- UI
|
||||||
|
|
||||||
|
Optional extension mechanism:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
overrides/
|
overrides/
|
||||||
@@ -48,8 +40,7 @@ Override rules:
|
|||||||
|
|
||||||
- Overrides are optional.
|
- Overrides are optional.
|
||||||
- The generator must work without them.
|
- The generator must work without them.
|
||||||
- Overrides may refine derived API or UI behavior, but they must not duplicate or redefine
|
- Overrides may refine derived API or UI behavior, but they must not duplicate or redefine entities, attributes, primary keys, foreign keys, relations, or enums.
|
||||||
entities, attributes, primary keys, foreign keys, relations, or enums.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -86,15 +77,13 @@ attribute name {
|
|||||||
|
|
||||||
**Modifiers:**
|
**Modifiers:**
|
||||||
|
|
||||||
- `type` — required; one of: `string`, `uuid`, `integer`, `decimal`, `date`, `text`, `boolean`, `number`, or an enum name.
|
- `type` — required; one of: `string`, `uuid`, `integer`, `decimal`, `date`, `text`, or an enum name.
|
||||||
- `key primary` — this attribute is the primary key.
|
- `key primary` — this attribute is the primary key.
|
||||||
- `key foreign { relates Entity.field }` — foreign key to another entity's field.
|
- `key foreign { relates Entity.field }` — foreign key to another entity's field.
|
||||||
- `is required` — non-nullable.
|
- `is required` — non-nullable.
|
||||||
- `is unique` — unique constraint.
|
- `is unique` — unique constraint.
|
||||||
- `is nullable` — explicitly nullable.
|
|
||||||
- `default Value` — default value (for enums or literals).
|
- `default Value` — default value (for enums or literals).
|
||||||
- `description "..."` — human-readable description.
|
- `description "..."` — human-readable description.
|
||||||
- `label "..."` — display label for UI.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -176,7 +165,7 @@ attribute equipmentTypeCode {
|
|||||||
## DSL → Prisma
|
## DSL → Prisma
|
||||||
|
|
||||||
| DSL Concept | Prisma Result |
|
| DSL Concept | Prisma Result |
|
||||||
| ------------- | --------------------------- |
|
| ------------ | --------------------------- |
|
||||||
| entity | model |
|
| entity | model |
|
||||||
| attribute | field |
|
| attribute | field |
|
||||||
| enum | enum |
|
| enum | enum |
|
||||||
@@ -185,11 +174,9 @@ attribute equipmentTypeCode {
|
|||||||
| type string | String |
|
| type string | String |
|
||||||
| type uuid | String @id @default(uuid()) |
|
| type uuid | String @id @default(uuid()) |
|
||||||
| type integer | Int |
|
| type integer | Int |
|
||||||
| type number | Float |
|
|
||||||
| type decimal | Decimal |
|
| type decimal | Decimal |
|
||||||
| type date | DateTime |
|
| type date | DateTime |
|
||||||
| type text | String |
|
| type text | String |
|
||||||
| type boolean | Boolean |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -216,26 +203,15 @@ API paths are derived from entity name: PascalCase → kebab-case, pluralized (e
|
|||||||
| attribute | Form field / column |
|
| attribute | Form field / column |
|
||||||
| type string | TextInput, TextField |
|
| type string | TextInput, TextField |
|
||||||
| type integer/decimal | NumberInput, NumberField |
|
| type integer/decimal | NumberInput, NumberField |
|
||||||
| type number | NumberInput, NumberField |
|
|
||||||
| type date | DateInput, DateField |
|
| type date | DateInput, DateField |
|
||||||
| type boolean | BooleanInput, BooleanField |
|
|
||||||
| enum | SelectInput with choices |
|
| enum | SelectInput with choices |
|
||||||
| foreign key | ReferenceInput, ReferenceField |
|
| foreign key | ReferenceInput, ReferenceField |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# API DSL Layer Mapping
|
# Derived Layer Mapping
|
||||||
|
|
||||||
DTO shapes and endpoint contracts are declared in `domain/*.api.dsl`. The api.dsl
|
- **Create DTO** — derived from domain attributes and must not include generated primary keys (for example no `id` for uuid PKs).
|
||||||
is the authoritative source for:
|
- **Update DTO** — derived from the same domain attributes with all fields optional for partial updates.
|
||||||
|
- **API response shape** — derived from domain attributes and must expose React Admin-compatible identifiers when needed.
|
||||||
- **Create DTO** — declared as `dto DTO.<Entity>Create` in api.dsl. Must not include
|
- **UI field mapping** — derived from attribute types, descriptions, enums, and foreign keys without a separate UI DSL.
|
||||||
server-generated primary keys (e.g. no `id` for uuid PKs). Required/nullable per field
|
|
||||||
is explicit in the api.dsl, not inferred.
|
|
||||||
- **Update DTO** — declared as `dto DTO.<Entity>Update` in api.dsl. All fields are
|
|
||||||
typically nullable for partial update semantics.
|
|
||||||
- **API response shape** — declared as `dto DTO.<Entity>` in api.dsl. Must expose
|
|
||||||
React Admin-compatible `id` field.
|
|
||||||
- **UI field mapping** — derived from the DTO shapes in api.dsl, not from domain
|
|
||||||
attributes directly. The Create form uses `DTO.<Entity>Create` fields; the Edit form
|
|
||||||
uses `DTO.<Entity>Update` fields; List and Show use `DTO.<Entity>` fields.
|
|
||||||
|
|||||||
1291
domain/toir.api.dsl
1291
domain/toir.api.dsl
File diff suppressed because it is too large
Load Diff
815
generation/generate.mjs
Normal file
815
generation/generate.mjs
Normal file
@@ -0,0 +1,815 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
// Always resolve repo root relative to this script location
|
||||||
|
// <repo>/generation/generate.mjs -> root is parent folder of generation/
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
function readFile(p) {
|
||||||
|
return fs.readFileSync(p, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFile(p, content) {
|
||||||
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||||
|
fs.writeFileSync(p, content, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toKebab(s) {
|
||||||
|
return s
|
||||||
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
||||||
|
.replace(/_/g, '-')
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pluralize(resource) {
|
||||||
|
// Minimal heuristic; can be improved later.
|
||||||
|
if (resource === 'equipment') return 'equipment';
|
||||||
|
if (resource.endsWith('s')) return `${resource}es`;
|
||||||
|
return `${resource}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function upperFirst(s) {
|
||||||
|
return s ? s[0].toUpperCase() + s.slice(1) : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lowerFirst(s) {
|
||||||
|
return s ? s[0].toLowerCase() + s.slice(1) : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIdentifierFromKebab(kebab) {
|
||||||
|
// "repair-order" -> "repairOrder"
|
||||||
|
return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBlocks(text, kind) {
|
||||||
|
// kind: 'enum' | 'entity'
|
||||||
|
const blocks = [];
|
||||||
|
const lines = text.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const m = line.match(new RegExp(`^\\s*${kind}\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\{\\s*$`));
|
||||||
|
if (!m) continue;
|
||||||
|
const name = m[1];
|
||||||
|
let depth = 0;
|
||||||
|
const start = i;
|
||||||
|
let end = i;
|
||||||
|
for (let j = i; j < lines.length; j++) {
|
||||||
|
const l = lines[j];
|
||||||
|
if (l.includes('{')) depth += (l.match(/\{/g) || []).length;
|
||||||
|
if (l.includes('}')) depth -= (l.match(/\}/g) || []).length;
|
||||||
|
if (depth === 0 && j > i) {
|
||||||
|
end = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const body = lines.slice(start + 1, end).join('\n');
|
||||||
|
blocks.push({ name, body, startLine: start, endLine: end });
|
||||||
|
i = end;
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEnum(body) {
|
||||||
|
const values = [];
|
||||||
|
const labels = {};
|
||||||
|
const re = /value\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{([\s\S]*?)\}/gm;
|
||||||
|
let m;
|
||||||
|
while ((m = re.exec(body))) {
|
||||||
|
values.push(m[1]);
|
||||||
|
const label = (m[2].match(/label\s+"([^"]+)"/m) || [])[1];
|
||||||
|
labels[m[1]] = label || m[1];
|
||||||
|
}
|
||||||
|
return { values, labels };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEntity(body) {
|
||||||
|
const attrs = [];
|
||||||
|
const entityLabel = (body.match(/description\s+"([^"]+)"/m) || [])[1];
|
||||||
|
const lines = body.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const m = lines[i].match(/^\s*attribute\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{\s*$/);
|
||||||
|
if (!m) continue;
|
||||||
|
const name = m[1];
|
||||||
|
let depth = 0;
|
||||||
|
const start = i;
|
||||||
|
let end = i;
|
||||||
|
for (let j = i; j < lines.length; j++) {
|
||||||
|
const l = lines[j];
|
||||||
|
if (l.includes('{')) depth += (l.match(/\{/g) || []).length;
|
||||||
|
if (l.includes('}')) depth -= (l.match(/\}/g) || []).length;
|
||||||
|
if (depth === 0 && j > i) {
|
||||||
|
end = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const abody = lines.slice(start + 1, end).join('\n');
|
||||||
|
const type = (abody.match(/^\s*type\s+([A-Za-z_][A-Za-z0-9_]*)\s*;/m) || [])[1];
|
||||||
|
const isRequired = /^\s*is required\s*;/m.test(abody);
|
||||||
|
const isUnique = /^\s*is unique\s*;/m.test(abody);
|
||||||
|
const isPrimary = /^\s*key primary\s*;/m.test(abody);
|
||||||
|
const defaultValue = (abody.match(/^\s*default\s+([A-Za-z_][A-Za-z0-9_]*)\s*;/m) || [])[1];
|
||||||
|
const foreignRel = (abody.match(/relates\s+([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\s*;/m) || []).slice(1);
|
||||||
|
const description = (abody.match(/description\s+"([^"]+)"/m) || [])[1];
|
||||||
|
|
||||||
|
attrs.push({
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
label: description || name,
|
||||||
|
isRequired,
|
||||||
|
isUnique,
|
||||||
|
isPrimary,
|
||||||
|
defaultValue,
|
||||||
|
foreign: foreignRel.length ? { entity: foreignRel[0], field: foreignRel[1] } : null,
|
||||||
|
});
|
||||||
|
i = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pk = attrs.find((a) => a.isPrimary);
|
||||||
|
if (!pk) throw new Error('Entity missing primary key attribute');
|
||||||
|
|
||||||
|
return { attributes: attrs, primaryKey: pk.name, label: entityLabel };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDomainDSL(dslText) {
|
||||||
|
const enums = {};
|
||||||
|
const entities = {};
|
||||||
|
|
||||||
|
for (const b of parseBlocks(dslText, 'enum')) {
|
||||||
|
enums[b.name] = parseEnum(b.body);
|
||||||
|
}
|
||||||
|
for (const b of parseBlocks(dslText, 'entity')) {
|
||||||
|
entities[b.name] = parseEntity(b.body);
|
||||||
|
}
|
||||||
|
return { enums, entities };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntityAttrNames(entity) {
|
||||||
|
return new Set(entity.attributes.map((a) => a.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBestSortField(entity, pk) {
|
||||||
|
const attrs = getEntityAttrNames(entity);
|
||||||
|
if (attrs.has('inventoryNumber')) return 'inventoryNumber';
|
||||||
|
if (attrs.has('number')) return 'number';
|
||||||
|
if (attrs.has('code')) return 'code';
|
||||||
|
if (attrs.has('name')) return 'name';
|
||||||
|
return pk;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReferenceDisplayExpr(foreignEntity) {
|
||||||
|
const attrs = getEntityAttrNames(foreignEntity);
|
||||||
|
if (attrs.has('inventoryNumber')) {
|
||||||
|
return "(record) => record.inventoryNumber ? `${record.inventoryNumber} — ${record.name ?? record.inventoryNumber}` : (record.name ?? record.id)";
|
||||||
|
}
|
||||||
|
if (attrs.has('code')) {
|
||||||
|
return "(record) => record.code ? `${record.code} — ${record.name ?? record.code}` : (record.name ?? record.id)";
|
||||||
|
}
|
||||||
|
if (attrs.has('number')) {
|
||||||
|
return "(record) => record.number ? `${record.number} — ${record.name ?? record.number}` : (record.name ?? record.id)";
|
||||||
|
}
|
||||||
|
if (attrs.has('name')) {
|
||||||
|
return "(record) => record.name ?? record.id";
|
||||||
|
}
|
||||||
|
return "(record) => record.id";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAttributeLabel(attr, allEntities) {
|
||||||
|
if (attr.label && attr.label !== attr.name) return attr.label;
|
||||||
|
if (attr.name === 'status') return 'Статус';
|
||||||
|
if (attr.name === 'equipmentId') return 'Оборудование';
|
||||||
|
if (attr.name === 'equipmentTypeCode') return 'Вид оборудования';
|
||||||
|
if (attr.foreign) return allEntities[attr.foreign.entity]?.label || attr.name;
|
||||||
|
return attr.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prismaScalarType(dslType) {
|
||||||
|
switch (dslType) {
|
||||||
|
case 'string':
|
||||||
|
case 'text':
|
||||||
|
return 'String';
|
||||||
|
case 'uuid':
|
||||||
|
return 'String';
|
||||||
|
case 'integer':
|
||||||
|
return 'Int';
|
||||||
|
case 'decimal':
|
||||||
|
return 'Decimal';
|
||||||
|
case 'date':
|
||||||
|
return 'DateTime';
|
||||||
|
default:
|
||||||
|
// enum type name
|
||||||
|
return dslType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePrismaEnum(name, values) {
|
||||||
|
return `enum ${name} {\n${values.map((v) => ` ${v}`).join('\n')}\n}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePrismaModel(name, entity, allEntities) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push(`model ${name} {`);
|
||||||
|
for (const attr of entity.attributes) {
|
||||||
|
if (attr.foreign) {
|
||||||
|
// Keep scalar FK field, relation field added below
|
||||||
|
}
|
||||||
|
const scalar = prismaScalarType(attr.type);
|
||||||
|
const optional = attr.isRequired || attr.isPrimary ? '' : '?';
|
||||||
|
const parts = [` ${attr.name} ${scalar}${optional}`];
|
||||||
|
|
||||||
|
if (attr.isPrimary) {
|
||||||
|
if (attr.type === 'uuid' || attr.name === 'id') parts.push('@id @default(uuid())');
|
||||||
|
else parts.push('@id');
|
||||||
|
}
|
||||||
|
if (attr.isUnique && !attr.isPrimary) parts.push('@unique');
|
||||||
|
if (attr.defaultValue) parts.push(`@default(${attr.defaultValue})`);
|
||||||
|
|
||||||
|
lines.push(`${parts.join(' ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add relations for foreign keys
|
||||||
|
for (const attr of entity.attributes.filter((a) => a.foreign)) {
|
||||||
|
const relEntity = attr.foreign.entity;
|
||||||
|
const relField = attr.foreign.field;
|
||||||
|
const relName = lowerFirst(relEntity);
|
||||||
|
// relation field must not collide; fallback to relEntity name if needed
|
||||||
|
const relationFieldName = entity.attributes.some((a) => a.name === relName) ? `${relName}Ref` : relName;
|
||||||
|
lines.push(
|
||||||
|
` ${relationFieldName} ${relEntity} @relation(fields: [${attr.name}], references: [${relField}])`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add back-relations (required by Prisma when a relation field exists)
|
||||||
|
// For each other entity that has a FK pointing to this model, create a list field.
|
||||||
|
for (const [otherName, otherEntity] of Object.entries(allEntities)) {
|
||||||
|
for (const fk of otherEntity.attributes.filter((a) => a.foreign)) {
|
||||||
|
if (fk.foreign.entity !== name) continue;
|
||||||
|
const candidate = pluralize(lowerFirst(otherName));
|
||||||
|
const fieldName = lines.some((l) => l.startsWith(` ${candidate} `))
|
||||||
|
? `${candidate}List`
|
||||||
|
: candidate;
|
||||||
|
// Avoid duplicates if multiple FKs exist (basic de-dupe)
|
||||||
|
if (lines.some((l) => l.startsWith(` ${fieldName} `))) continue;
|
||||||
|
lines.push(` ${fieldName} ${otherName}[]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('}');
|
||||||
|
return `${lines.join('\n')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePrismaSchema({ enums, entities }, prismaPath, apply) {
|
||||||
|
const existing = fs.existsSync(prismaPath) ? readFile(prismaPath) : '';
|
||||||
|
const hasGenerator = /generator\s+client\s*\{/m.test(existing);
|
||||||
|
const header = hasGenerator
|
||||||
|
? existing.split(/\n(?=enum|model)\b/)[0].trimEnd() + '\n\n'
|
||||||
|
: `generator client {\n provider = "prisma-client-js"\n}\n\ndatasource db {\n provider = "postgresql"\n url = env("DATABASE_URL")\n}\n\n`;
|
||||||
|
|
||||||
|
const out = [header];
|
||||||
|
|
||||||
|
// Preserve existing enum/model blocks not in DSL? For now, regenerate from DSL only.
|
||||||
|
for (const [name, e] of Object.entries(enums)) out.push(generatePrismaEnum(name, e.values) + '\n');
|
||||||
|
for (const [name, ent] of Object.entries(entities)) out.push(generatePrismaModel(name, ent, entities) + '\n');
|
||||||
|
|
||||||
|
const next = out.join('').trimEnd() + '\n';
|
||||||
|
if (!apply) return { changed: next !== existing, content: next };
|
||||||
|
writeFile(prismaPath, next);
|
||||||
|
return { changed: true, content: next };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBackendModule(entityName, entity, resourceName, pk) {
|
||||||
|
const className = entityName;
|
||||||
|
const moduleName = `${className}Module`;
|
||||||
|
const serviceName = `${className}Service`;
|
||||||
|
const controllerName = `${className}Controller`;
|
||||||
|
const folder = toKebab(entityName);
|
||||||
|
|
||||||
|
// DTOs
|
||||||
|
const dtoType = (attr) => {
|
||||||
|
switch (attr.type) {
|
||||||
|
case 'uuid':
|
||||||
|
case 'string':
|
||||||
|
case 'text':
|
||||||
|
return 'string';
|
||||||
|
case 'integer':
|
||||||
|
return 'number';
|
||||||
|
case 'decimal':
|
||||||
|
return 'string';
|
||||||
|
case 'date':
|
||||||
|
return 'string';
|
||||||
|
default:
|
||||||
|
// enum
|
||||||
|
return 'string';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getValidationDecorators = (attr, isUpdate) => {
|
||||||
|
const decorators = [];
|
||||||
|
const imports = new Set();
|
||||||
|
let needsTypeImport = false;
|
||||||
|
const field = attr.name;
|
||||||
|
if (isUpdate) {
|
||||||
|
decorators.push('@IsOptional()');
|
||||||
|
imports.add('IsOptional');
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (attr.type) {
|
||||||
|
case 'uuid':
|
||||||
|
decorators.push(`@IsUUID(undefined, { message: '${field}: должно быть UUID' })`);
|
||||||
|
imports.add('IsUUID');
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
case 'text':
|
||||||
|
decorators.push(`@IsString({ message: '${field}: должно быть строкой' })`);
|
||||||
|
imports.add('IsString');
|
||||||
|
break;
|
||||||
|
case 'integer':
|
||||||
|
decorators.push('@Type(() => Number)');
|
||||||
|
decorators.push(`@IsInt({ message: '${field}: должно быть целым числом' })`);
|
||||||
|
imports.add('IsInt');
|
||||||
|
needsTypeImport = true;
|
||||||
|
break;
|
||||||
|
case 'decimal':
|
||||||
|
decorators.push(`@IsNumberString({}, { message: '${field}: должно быть числом' })`);
|
||||||
|
imports.add('IsNumberString');
|
||||||
|
break;
|
||||||
|
case 'date':
|
||||||
|
decorators.push(`@IsISO8601({}, { message: '${field}: должно содержать корректную дату' })`);
|
||||||
|
imports.add('IsISO8601');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// enum (kept as string in generated DTOs)
|
||||||
|
decorators.push(`@IsString({ message: '${field}: должно быть строкой' })`);
|
||||||
|
imports.add('IsString');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUpdate && attr.isRequired && !(attr.isPrimary && attr.type === 'uuid')) {
|
||||||
|
decorators.push(`@IsNotEmpty({ message: '${field}: обязательное поле' })`);
|
||||||
|
imports.add('IsNotEmpty');
|
||||||
|
}
|
||||||
|
return { decorators, imports, needsTypeImport };
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDecorators = new Set();
|
||||||
|
const updateDecorators = new Set(['IsOptional']);
|
||||||
|
let createNeedsTypeImport = false;
|
||||||
|
let updateNeedsTypeImport = false;
|
||||||
|
|
||||||
|
const createDtoLines = [];
|
||||||
|
for (const a of entity.attributes) {
|
||||||
|
if (a.isPrimary && a.type === 'uuid') continue; // generated
|
||||||
|
const { decorators, imports, needsTypeImport } = getValidationDecorators(a, false);
|
||||||
|
imports.forEach((i) => createDecorators.add(i));
|
||||||
|
if (needsTypeImport) createNeedsTypeImport = true;
|
||||||
|
decorators.forEach((d) => createDtoLines.push(` ${d}`));
|
||||||
|
const opt = a.isRequired && !(a.isPrimary && a.type !== 'uuid') ? '!' : '?';
|
||||||
|
createDtoLines.push(` ${a.name}${opt}: ${dtoType(a)};`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDtoLines = [];
|
||||||
|
if (pk !== 'id') {
|
||||||
|
updateDtoLines.push(' @IsOptional()');
|
||||||
|
updateDtoLines.push(` @IsString({ message: 'id: должно быть строкой' })`);
|
||||||
|
updateDtoLines.push(' id?: string;');
|
||||||
|
updateDecorators.add('IsString');
|
||||||
|
}
|
||||||
|
for (const a of entity.attributes) {
|
||||||
|
if (pk !== 'id' && a.name === 'id') continue;
|
||||||
|
const { decorators, imports, needsTypeImport } = getValidationDecorators(a, true);
|
||||||
|
imports.forEach((i) => updateDecorators.add(i));
|
||||||
|
if (needsTypeImport) updateNeedsTypeImport = true;
|
||||||
|
decorators.forEach((d) => updateDtoLines.push(` ${d}`));
|
||||||
|
updateDtoLines.push(` ${a.name}?: ${dtoType(a)};`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createImports = Array.from(createDecorators)
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort()
|
||||||
|
.join(', ');
|
||||||
|
const updateImports = Array.from(updateDecorators)
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort()
|
||||||
|
.join(', ');
|
||||||
|
const createDto = `import { ${createImports} } from 'class-validator';\n${createNeedsTypeImport ? "import { Type } from 'class-transformer';\n" : ''}\nexport class Create${className}Dto {\n${createDtoLines.join('\n')}\n}\n`;
|
||||||
|
const updateDto = `import { ${updateImports} } from 'class-validator';\n${updateNeedsTypeImport ? "import { Type } from 'class-transformer';\n" : ''}\nexport class Update${className}Dto {\n${updateDtoLines.join('\n')}\n}\n`;
|
||||||
|
|
||||||
|
const controller = `import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Res } from '@nestjs/common';\nimport { Response } from 'express';\nimport { Roles } from '../../auth/decorators/roles.decorator';\nimport { RealmRole } from '../../auth/roles/realm-role.enum';\nimport { ${serviceName} } from './${folder}.service';\nimport { Create${className}Dto } from './dto/create-${folder}.dto';\nimport { Update${className}Dto } from './dto/update-${folder}.dto';\n\n@Controller('${resourceName}')\nexport class ${controllerName} {\n constructor(private readonly service: ${serviceName}) {}\n\n @Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)\n @Get()\n async findAll(@Query() query: any, @Res() res: Response) {\n const result = await this.service.findAll(query);\n res.set('Content-Range', \`${resourceName} \${query._start || 0}-\${query._end || result.total}/\${result.total}\`);\n res.set('Access-Control-Expose-Headers', 'Content-Range');\n return res.json(result.data);\n }\n\n @Roles(RealmRole.Viewer, RealmRole.Editor, RealmRole.Admin)\n @Get(':${pk}')\n findOne(@Param('${pk}') id: string) {\n return this.service.findOne(id);\n }\n\n @Roles(RealmRole.Editor, RealmRole.Admin)\n @Post()\n create(@Body() dto: Create${className}Dto) {\n return this.service.create(dto);\n }\n\n @Roles(RealmRole.Editor, RealmRole.Admin)\n @Patch(':${pk}')\n update(@Param('${pk}') id: string, @Body() dto: Update${className}Dto) {\n return this.service.update(id, dto);\n }\n\n @Roles(RealmRole.Admin)\n @Delete(':${pk}')\n remove(@Param('${pk}') id: string) {\n return this.service.remove(id);\n }\n}\n`;
|
||||||
|
|
||||||
|
const service = `import { Injectable } from '@nestjs/common';\nimport { Prisma } from '@prisma/client';\nimport { PrismaService } from '../../prisma/prisma.service';\nimport { Create${className}Dto } from './dto/create-${folder}.dto';\nimport { Update${className}Dto } from './dto/update-${folder}.dto';\n\nfunction serializeRecord(record: any) {\n return {\n ...record,\n${entity.attributes
|
||||||
|
.filter((a) => a.type === 'decimal')
|
||||||
|
.map((a) => ` ${a.name}: record.${a.name}?.toString() ?? null,`)
|
||||||
|
.join('\n')}\n${entity.attributes
|
||||||
|
.filter((a) => a.type === 'date')
|
||||||
|
.map((a) => ` ${a.name}: record.${a.name}?.toISOString() ?? null,`)
|
||||||
|
.join('\n')}\n };\n}\n\n@Injectable()\nexport class ${serviceName} {\n constructor(private readonly prisma: PrismaService) {}\n\n async findAll(query: { _start?: string; _end?: string; _sort?: string; _order?: string; [key: string]: any }) {\n const start = parseInt(query._start) || 0;\n const end = parseInt(query._end) || 10;\n const take = end - start;\n const skip = start;\n const sortField = query._sort || '${getBestSortField(entity, pk)}';\n const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';\n\n const where: any = {};\n\n if (query.q) {\n const q = String(query.q);\n const ors: any[] = [];\n ${entity.attributes
|
||||||
|
.filter((a) => ['string', 'text'].includes(a.type))
|
||||||
|
.slice(0, 6)
|
||||||
|
.map((a) => `ors.push({ ${a.name}: { contains: q, mode: 'insensitive' } });`)
|
||||||
|
.join('\n ')}\n if (ors.length) where.OR = ors;\n }\n\n ${entity.attributes
|
||||||
|
.filter((a) => ['string', 'text'].includes(a.type) && !a.foreign)
|
||||||
|
.map((a) => `if (query.${a.name}) where.${a.name} = { contains: query.${a.name}, mode: 'insensitive' };`)
|
||||||
|
.join('\n ')}\n\n ${entity.attributes
|
||||||
|
.filter((a) => a.foreign)
|
||||||
|
.map((a) => `if (query.${a.name}) where.${a.name} = query.${a.name};`)
|
||||||
|
.join('\n ')}\n\n // Enum multi-value support (e.g. status=A&status=B)\n ${entity.attributes
|
||||||
|
.filter((a) => !['string', 'text', 'uuid', 'integer', 'decimal', 'date'].includes(a.type))
|
||||||
|
.map((a) => `if (query.${a.name}) { const vals = Array.isArray(query.${a.name}) ? query.${a.name} : [query.${a.name}]; where.${a.name} = vals.length > 1 ? { in: vals } : vals[0]; }`)
|
||||||
|
.join('\n ')}\n\n if (query.id) {\n const ids = Array.isArray(query.id) ? query.id : [query.id];\n where.${pk} = { in: ids };\n }\n\n const [data, total] = await Promise.all([\n this.prisma.${lowerFirst(className)}.findMany({ where, skip, take, orderBy: { [sortField]: sortOrder } }),\n this.prisma.${lowerFirst(className)}.count({ where }),\n ]);\n\n const mapped = ${pk === 'id' ? 'data.map(serializeRecord)' : `data.map((r: any) => ({ id: r.${pk}, ...serializeRecord(r) }))`};\n return { data: mapped, total };\n }\n\n async findOne(id: string) {\n const record = await this.prisma.${lowerFirst(className)}.findUniqueOrThrow({ where: { ${pk}: id } as any });\n return ${pk === 'id' ? 'serializeRecord(record)' : `{ id: (record as any).${pk}, ...serializeRecord(record) }`};\n }\n\n async create(dto: Create${className}Dto) {\n const data: any = { ...(dto as any) };\n${entity.attributes
|
||||||
|
.filter((a) => a.type === 'date')
|
||||||
|
.map((a) => ` if (data.${a.name}) data.${a.name} = new Date(data.${a.name});`)
|
||||||
|
.join('\n')}\n${entity.attributes
|
||||||
|
.filter((a) => a.type === 'decimal')
|
||||||
|
.map((a) => ` if (data.${a.name}) data.${a.name} = new Prisma.Decimal(data.${a.name});`)
|
||||||
|
.join('\n')}\n\n const record = await this.prisma.${lowerFirst(className)}.create({ data });\n return ${pk === 'id' ? 'serializeRecord(record)' : `{ id: (record as any).${pk}, ...serializeRecord(record) }`};\n }\n\n async update(id: string, dto: Update${className}Dto) {\n const data: any = { ...(dto as any) };\n delete data.id;\n delete data.${pk};\n${entity.attributes
|
||||||
|
.filter((a) => a.type === 'date')
|
||||||
|
.map((a) => ` if (data.${a.name}) data.${a.name} = new Date(data.${a.name});`)
|
||||||
|
.join('\n')}\n${entity.attributes
|
||||||
|
.filter((a) => a.type === 'decimal')
|
||||||
|
.map((a) => ` if (data.${a.name} !== undefined && data.${a.name} !== null) data.${a.name} = new Prisma.Decimal(data.${a.name});`)
|
||||||
|
.join('\n')}\n\n const record = await this.prisma.${lowerFirst(className)}.update({ where: { ${pk}: id } as any, data });\n return ${pk === 'id' ? 'serializeRecord(record)' : `{ id: (record as any).${pk}, ...serializeRecord(record) }`};\n }\n\n async remove(id: string) {\n const record = await this.prisma.${lowerFirst(className)}.delete({ where: { ${pk}: id } as any });\n return ${pk === 'id' ? 'serializeRecord(record)' : `{ id: (record as any).${pk}, ...serializeRecord(record) }`};\n }\n}\n`;
|
||||||
|
|
||||||
|
let serviceContent = service
|
||||||
|
.replace(
|
||||||
|
`const sortField = query._sort || '${getBestSortField(entity, pk)}';\n const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';`,
|
||||||
|
`const sortField = query._sort || '${getBestSortField(entity, pk)}';\n const prismaSortField = sortField === 'id' ? '${pk}' : sortField;\n const sortOrder = (query._order || 'ASC').toLowerCase() as 'asc' | 'desc';`
|
||||||
|
)
|
||||||
|
.replace('orderBy: { [sortField]: sortOrder }', 'orderBy: { [prismaSortField]: sortOrder }')
|
||||||
|
.replace(
|
||||||
|
`data.map((r: any) => ({ id: r.${pk}, ...serializeRecord(r) }))`,
|
||||||
|
`data.map((item: any) => ({ id: item.${pk}, ...serializeRecord(item) }))`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pk !== 'id') {
|
||||||
|
serviceContent = serviceContent.replace(
|
||||||
|
`const data: any = { ...(dto as any) };\n delete data.id;\n delete data.${pk};`,
|
||||||
|
`const { id: _pk, ${pk}, ...rest } = (dto as any);\n const data: any = { ...rest };`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mod = `import { Module } from '@nestjs/common';\nimport { ${controllerName} } from './${folder}.controller';\nimport { ${serviceName} } from './${folder}.service';\n\n@Module({\n controllers: [${controllerName}],\n providers: [${serviceName}],\n})\nexport class ${moduleName} {}\n`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
folder,
|
||||||
|
files: {
|
||||||
|
[`server/src/modules/${folder}/${folder}.controller.ts`]: controller,
|
||||||
|
[`server/src/modules/${folder}/${folder}.service.ts`]: serviceContent,
|
||||||
|
[`server/src/modules/${folder}/${folder}.module.ts`]: mod,
|
||||||
|
[`server/src/modules/${folder}/dto/create-${folder}.dto.ts`]: createDto,
|
||||||
|
[`server/src/modules/${folder}/dto/update-${folder}.dto.ts`]: updateDto,
|
||||||
|
},
|
||||||
|
moduleName,
|
||||||
|
importPath: `./modules/${folder}/${folder}.module`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFrontendResource(entityName, entity, resourceName, pk, enums, allEntities) {
|
||||||
|
const folder = toKebab(entityName);
|
||||||
|
const className = entityName;
|
||||||
|
const enumAttrs = entity.attributes.filter(
|
||||||
|
(a) => !['string', 'text', 'uuid', 'integer', 'decimal', 'date'].includes(a.type)
|
||||||
|
);
|
||||||
|
const statusEnumAttr = enumAttrs.find((a) => a.name === 'status');
|
||||||
|
|
||||||
|
const identBase = toIdentifierFromKebab(folder);
|
||||||
|
const filtersIdent = `${identBase}Filters`;
|
||||||
|
const sortField = getBestSortField(entity, pk);
|
||||||
|
|
||||||
|
const hasNumber = entity.attributes.some((a) => ['integer', 'decimal'].includes(a.type));
|
||||||
|
const hasDate = entity.attributes.some((a) => a.type === 'date');
|
||||||
|
const hasFK = entity.attributes.some((a) => a.foreign);
|
||||||
|
const hasNonStatusEnum = enumAttrs.some((a) => a.name !== 'status');
|
||||||
|
|
||||||
|
const listImportSet = new Set([
|
||||||
|
'List',
|
||||||
|
'Datagrid',
|
||||||
|
'TextField',
|
||||||
|
'TextInput',
|
||||||
|
'TopToolbar',
|
||||||
|
'FilterButton',
|
||||||
|
'CreateButton',
|
||||||
|
'ExportButton',
|
||||||
|
]);
|
||||||
|
if (hasNumber) listImportSet.add('NumberField');
|
||||||
|
if (hasDate) listImportSet.add('DateField');
|
||||||
|
if (enumAttrs.length) listImportSet.add('SelectField');
|
||||||
|
if (hasFK) listImportSet.add('ReferenceField');
|
||||||
|
if (statusEnumAttr) listImportSet.add('SelectArrayInput');
|
||||||
|
if (hasNonStatusEnum) listImportSet.add('SelectInput');
|
||||||
|
if (hasFK) {
|
||||||
|
listImportSet.add('ReferenceInput');
|
||||||
|
listImportSet.add('AutocompleteInput');
|
||||||
|
}
|
||||||
|
const listImports = Array.from(listImportSet);
|
||||||
|
|
||||||
|
const choiceConsts = [];
|
||||||
|
for (const a of enumAttrs) {
|
||||||
|
const enumName = a.type;
|
||||||
|
const values = enums?.[enumName]?.values ?? [];
|
||||||
|
const labels = enums?.[enumName]?.labels ?? {};
|
||||||
|
const constName = `${a.name}Choices`;
|
||||||
|
if (a.name === 'status') {
|
||||||
|
choiceConsts.push(
|
||||||
|
`const statusChoices = [\n${values.map((v) => ` { id: '${v}', name: '${labels[v] ?? v}' },`).join('\n')}\n];\n`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
choiceConsts.push(
|
||||||
|
`const ${constName} = [\n${values.map((v) => ` { id: '${v}', name: '${labels[v] ?? v}' },`).join('\n')}\n];\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterInputs = [];
|
||||||
|
// Always include q if any string fields
|
||||||
|
if (entity.attributes.some((a) => ['string', 'text'].includes(a.type))) {
|
||||||
|
filterInputs.push(`<TextInput key="q" source="q" label="Поиск" alwaysOn />`);
|
||||||
|
}
|
||||||
|
for (const a of entity.attributes) {
|
||||||
|
const label = getAttributeLabel(a, allEntities);
|
||||||
|
if (a.name === pk) continue;
|
||||||
|
if (a.foreign) {
|
||||||
|
const referenceDisplay = getReferenceDisplayExpr(allEntities[a.foreign.entity]);
|
||||||
|
filterInputs.push(
|
||||||
|
`<ReferenceInput key="${a.name}" source="${a.name}" reference="${pluralize(toKebab(a.foreign.entity))}" label="${label}">\n <AutocompleteInput optionText={${referenceDisplay}} filterToQuery={(searchText) => ({ q: searchText })} />\n </ReferenceInput>`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (['string', 'text', 'uuid'].includes(a.type)) {
|
||||||
|
filterInputs.push(`<TextInput key="${a.name}" source="${a.name}" label="${label}" />`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (['integer', 'decimal'].includes(a.type)) continue;
|
||||||
|
if (a.type === 'date') continue;
|
||||||
|
// enum
|
||||||
|
if (a.name === 'status') {
|
||||||
|
filterInputs.push(`<SelectArrayInput key="${a.name}" source="${a.name}" label="${label}" choices={statusChoices} />`);
|
||||||
|
} else {
|
||||||
|
filterInputs.push(`<SelectInput key="${a.name}" source="${a.name}" label="${label}" choices={${a.name}Choices} emptyText="Все" />`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listFields = [];
|
||||||
|
for (const a of entity.attributes) {
|
||||||
|
const label = getAttributeLabel(a, allEntities);
|
||||||
|
if (a.foreign) {
|
||||||
|
const referenceEntity = allEntities[a.foreign.entity];
|
||||||
|
const referenceAttrs = getEntityAttrNames(referenceEntity);
|
||||||
|
const fieldSource = referenceAttrs.has('inventoryNumber')
|
||||||
|
? 'inventoryNumber'
|
||||||
|
: referenceAttrs.has('code')
|
||||||
|
? 'code'
|
||||||
|
: referenceAttrs.has('number')
|
||||||
|
? 'number'
|
||||||
|
: 'name';
|
||||||
|
listFields.push(
|
||||||
|
`<ReferenceField source="${a.name}" reference="${pluralize(toKebab(a.foreign.entity))}" label="${label}" link="show">\n <TextField source="${fieldSource}" />\n </ReferenceField>`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a.type === 'date') {
|
||||||
|
listFields.push(`<DateField source="${a.name}" label="${label}" />`);
|
||||||
|
} else if (['integer', 'decimal'].includes(a.type)) {
|
||||||
|
listFields.push(`<NumberField source="${a.name}" label="${label}" />`);
|
||||||
|
} else if (!['string', 'text', 'uuid', 'integer', 'decimal', 'date'].includes(a.type)) {
|
||||||
|
listFields.push(`<SelectField source="${a.name}" label="${label}" choices={${a.name === 'status' ? 'statusChoices' : `${a.name}Choices`}} />`);
|
||||||
|
} else {
|
||||||
|
listFields.push(`<TextField source="${a.name}" label="${label}" />`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = `import {\n ${listImports.join(',\n ')}\n} from 'react-admin';\n\n${choiceConsts.join('\n')}\nconst ${filtersIdent} = [\n ${filterInputs.join(',\n ')}\n];\n\nconst ${className}ListActions = () => (\n <TopToolbar>\n <FilterButton filters={${filtersIdent}} />\n <CreateButton />\n <ExportButton />\n </TopToolbar>\n);\n\nexport const ${className}List = () => (\n <List actions={<${className}ListActions />} filters={${filtersIdent}} sort={{ field: '${sortField}', order: 'ASC' }}>\n <Datagrid rowClick=\"show\">\n ${listFields.join('\n ')}\n </Datagrid>\n </List>\n);\n`;
|
||||||
|
|
||||||
|
const formField = (a, mode) => {
|
||||||
|
const label = getAttributeLabel(a, allEntities);
|
||||||
|
if (a.isPrimary && mode === 'create' && a.type === 'uuid') return null;
|
||||||
|
if (a.isPrimary && mode === 'edit') {
|
||||||
|
return `<TextInput source="${a.name}" label="${label}" disabled />`;
|
||||||
|
}
|
||||||
|
if (a.foreign) {
|
||||||
|
const referenceDisplay = getReferenceDisplayExpr(allEntities[a.foreign.entity]);
|
||||||
|
return `<ReferenceInput source="${a.name}" reference="${pluralize(toKebab(a.foreign.entity))}">\n <AutocompleteInput label="${label}" optionText={${referenceDisplay}} filterToQuery={(searchText) => ({ q: searchText })} />\n </ReferenceInput>`;
|
||||||
|
}
|
||||||
|
if (a.type === 'date') return `<DateInput source="${a.name}" label="${label}" />`;
|
||||||
|
if (['integer', 'decimal'].includes(a.type)) return `<NumberInput source="${a.name}" label="${label}" />`;
|
||||||
|
if (!['string', 'text', 'uuid', 'integer', 'decimal', 'date'].includes(a.type)) {
|
||||||
|
if (a.name === 'status' && statusEnumAttr) return `<SelectInput source="${a.name}" label="${label}" choices={statusChoices} emptyText="Не выбрано" />`;
|
||||||
|
return `<SelectInput source="${a.name}" label="${label}" choices={${a.name}Choices} emptyText="Не выбрано" />`;
|
||||||
|
}
|
||||||
|
return `<TextInput source="${a.name}" label="${label}" ${a.isRequired ? 'isRequired' : ''} />`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formImportSet = new Set(['SimpleForm', 'TextInput']);
|
||||||
|
if (hasNumber) formImportSet.add('NumberInput');
|
||||||
|
if (hasDate) formImportSet.add('DateInput');
|
||||||
|
if (enumAttrs.length) formImportSet.add('SelectInput');
|
||||||
|
if (hasFK) {
|
||||||
|
formImportSet.add('ReferenceInput');
|
||||||
|
formImportSet.add('AutocompleteInput');
|
||||||
|
}
|
||||||
|
const createImports = ['Create', ...Array.from(formImportSet)].join(', ');
|
||||||
|
const create = `import { ${createImports} } from 'react-admin';\n\n${choiceConsts.join('\n')}\nexport const ${className}Create = () => (\n <Create>\n <SimpleForm>\n ${entity.attributes.map((a) => formField(a, 'create')).filter(Boolean).join('\n ')}\n </SimpleForm>\n </Create>\n);\n`;
|
||||||
|
|
||||||
|
const editImports = ['Edit', ...Array.from(formImportSet)].join(', ');
|
||||||
|
const edit = `import { ${editImports} } from 'react-admin';\n\n${choiceConsts.join('\n')}\nexport const ${className}Edit = () => (\n <Edit>\n <SimpleForm>\n ${entity.attributes.map((a) => formField(a, 'edit')).filter(Boolean).join('\n ')}\n </SimpleForm>\n </Edit>\n);\n`;
|
||||||
|
|
||||||
|
const showImportSet = new Set(['Show', 'SimpleShowLayout', 'TextField']);
|
||||||
|
if (hasNumber) showImportSet.add('NumberField');
|
||||||
|
if (hasDate) showImportSet.add('DateField');
|
||||||
|
if (enumAttrs.length) showImportSet.add('SelectField');
|
||||||
|
if (hasFK) showImportSet.add('ReferenceField');
|
||||||
|
|
||||||
|
const showFields = [];
|
||||||
|
for (const a of entity.attributes) {
|
||||||
|
const label = getAttributeLabel(a, allEntities);
|
||||||
|
if (a.foreign) {
|
||||||
|
const referenceEntity = allEntities[a.foreign.entity];
|
||||||
|
const referenceAttrs = getEntityAttrNames(referenceEntity);
|
||||||
|
const fieldSource = referenceAttrs.has('inventoryNumber')
|
||||||
|
? 'inventoryNumber'
|
||||||
|
: referenceAttrs.has('code')
|
||||||
|
? 'code'
|
||||||
|
: referenceAttrs.has('number')
|
||||||
|
? 'number'
|
||||||
|
: 'name';
|
||||||
|
showFields.push(
|
||||||
|
`<ReferenceField source="${a.name}" reference="${pluralize(toKebab(a.foreign.entity))}" label="${label}" link="show">\n <TextField source="${fieldSource}" />\n </ReferenceField>`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a.type === 'date') {
|
||||||
|
showFields.push(`<DateField source="${a.name}" label="${label}" />`);
|
||||||
|
} else if (['integer', 'decimal'].includes(a.type)) {
|
||||||
|
showFields.push(`<NumberField source="${a.name}" label="${label}" />`);
|
||||||
|
} else if (!['string', 'text', 'uuid', 'integer', 'decimal', 'date'].includes(a.type)) {
|
||||||
|
showFields.push(`<SelectField source="${a.name}" label="${label}" choices={${a.name === 'status' ? 'statusChoices' : `${a.name}Choices`}} />`);
|
||||||
|
} else {
|
||||||
|
showFields.push(`<TextField source="${a.name}" label="${label}" />`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const show = `import { ${Array.from(showImportSet).join(', ')} } from 'react-admin';\n\n${choiceConsts.join('\n')}export const ${className}Show = () => (\n <Show>\n <SimpleShowLayout>\n ${showFields.join('\n ')}\n </SimpleShowLayout>\n </Show>\n);\n`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: {
|
||||||
|
[`client/src/resources/${folder}/${className}List.tsx`]: list,
|
||||||
|
[`client/src/resources/${folder}/${className}Create.tsx`]: create,
|
||||||
|
[`client/src/resources/${folder}/${className}Edit.tsx`]: edit,
|
||||||
|
[`client/src/resources/${folder}/${className}Show.tsx`]: show,
|
||||||
|
},
|
||||||
|
resourceName,
|
||||||
|
className,
|
||||||
|
folder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertInFile(filePath, apply, updater) {
|
||||||
|
const abs = path.join(ROOT, filePath);
|
||||||
|
const existing = fs.existsSync(abs) ? readFile(abs) : '';
|
||||||
|
const next = updater(existing);
|
||||||
|
if (apply) writeFile(abs, next);
|
||||||
|
return { changed: next !== existing, content: next };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureAppModule(apply, backendModules) {
|
||||||
|
return upsertInFile('server/src/app.module.ts', apply, (src) => {
|
||||||
|
let out = src;
|
||||||
|
for (const m of backendModules) {
|
||||||
|
if (!out.includes(`import { ${m.moduleName} }`)) {
|
||||||
|
const importLine = `import { ${m.moduleName} } from '${m.importPath}';`;
|
||||||
|
const importMatches = [...out.matchAll(/^import\s+.*;$/gm)];
|
||||||
|
if (importMatches.length) {
|
||||||
|
const lastImport = importMatches[importMatches.length - 1];
|
||||||
|
const insertAt = lastImport.index + lastImport[0].length;
|
||||||
|
out = `${out.slice(0, insertAt)}\n${importLine}${out.slice(insertAt)}`;
|
||||||
|
} else {
|
||||||
|
out = `${importLine}\n${out}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = out.replace(/imports:\s*\[\s*([\s\S]*?)\s*\],/m, (match, inner) => {
|
||||||
|
let block = inner;
|
||||||
|
for (const m of backendModules) {
|
||||||
|
if (!block.includes(m.moduleName)) block = block.replace(/\s*\],?\s*$/m, '') + `\n ${m.moduleName},`;
|
||||||
|
}
|
||||||
|
// normalize trailing comma/indent by reusing original replacement style
|
||||||
|
return `imports: [${block}\n ],`;
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureClientApp(apply, frontendResources) {
|
||||||
|
return upsertInFile('client/src/App.tsx', apply, (src) => {
|
||||||
|
let out = src;
|
||||||
|
for (const r of frontendResources) {
|
||||||
|
const imports = [
|
||||||
|
`import { ${r.className}List } from './resources/${r.folder}/${r.className}List';`,
|
||||||
|
`import { ${r.className}Create } from './resources/${r.folder}/${r.className}Create';`,
|
||||||
|
`import { ${r.className}Edit } from './resources/${r.folder}/${r.className}Edit';`,
|
||||||
|
`import { ${r.className}Show } from './resources/${r.folder}/${r.className}Show';`,
|
||||||
|
];
|
||||||
|
for (const imp of imports) {
|
||||||
|
if (!out.includes(imp)) {
|
||||||
|
const importMatches = [...out.matchAll(/^import\s+.*;$/gm)];
|
||||||
|
if (importMatches.length) {
|
||||||
|
const lastImport = importMatches[importMatches.length - 1];
|
||||||
|
const insertAt = lastImport.index + lastImport[0].length;
|
||||||
|
out = `${out.slice(0, insertAt)}\n${imports.join('\n')}${out.slice(insertAt)}`;
|
||||||
|
} else {
|
||||||
|
out = `${imports.join('\n')}\n${out}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!out.includes(`name="${r.resourceName}"`)) {
|
||||||
|
out = out.replace(
|
||||||
|
/<\/Admin>/m,
|
||||||
|
` <Resource\n name="${r.resourceName}"\n options={{ label: '${r.className}' }}\n list={${r.className}List}\n create={${r.className}Create}\n edit={${r.className}Edit}\n show={${r.className}Show}\n />\n </Admin>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Собирает файлы как при --apply, без записи. Учитывает текущие app.module.ts и App.tsx на диске. */
|
||||||
|
function collectGeneratedBundle(parsed) {
|
||||||
|
const files = {};
|
||||||
|
const prismaPath = path.join(ROOT, 'server/prisma/schema.prisma');
|
||||||
|
const pr = ensurePrismaSchema(parsed, prismaPath, false);
|
||||||
|
files['server/prisma/schema.prisma'] = pr.content;
|
||||||
|
|
||||||
|
const backendModules = [];
|
||||||
|
const frontendResources = [];
|
||||||
|
for (const [entityName, ent] of Object.entries(parsed.entities)) {
|
||||||
|
const pk = ent.primaryKey;
|
||||||
|
const resource = pluralize(toKebab(entityName));
|
||||||
|
const be = renderBackendModule(entityName, ent, resource, pk);
|
||||||
|
const fe = renderFrontendResource(entityName, ent, resource, pk, parsed.enums);
|
||||||
|
backendModules.push(be);
|
||||||
|
frontendResources.push(fe);
|
||||||
|
Object.assign(files, be.files, fe.files);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appMod = ensureAppModule(false, backendModules);
|
||||||
|
files['server/src/app.module.ts'] = appMod.content;
|
||||||
|
const clientApp = ensureClientApp(false, frontendResources);
|
||||||
|
files['client/src/App.tsx'] = clientApp.content;
|
||||||
|
|
||||||
|
return {
|
||||||
|
entityCount: Object.keys(parsed.entities).length,
|
||||||
|
enumCount: Object.keys(parsed.enums).length,
|
||||||
|
files,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const apply = args.includes('--apply');
|
||||||
|
const printBundleJson = args.includes('--print-bundle-json');
|
||||||
|
const dslArgIdx = args.indexOf('--dsl');
|
||||||
|
const dslPath = dslArgIdx >= 0 ? args[dslArgIdx + 1] : 'domain/TOiR.domain.dsl';
|
||||||
|
|
||||||
|
const absDsl = path.resolve(ROOT, dslPath);
|
||||||
|
const dslText = readFile(absDsl);
|
||||||
|
const parsed = parseDomainDSL(dslText);
|
||||||
|
|
||||||
|
if (printBundleJson) {
|
||||||
|
const bundle = collectGeneratedBundle(parsed);
|
||||||
|
process.stdout.write(JSON.stringify(bundle));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prisma schema
|
||||||
|
const prismaPath = path.join(ROOT, 'server/prisma/schema.prisma');
|
||||||
|
ensurePrismaSchema(parsed, prismaPath, apply);
|
||||||
|
|
||||||
|
// Backend modules + frontend resources
|
||||||
|
const backendModules = [];
|
||||||
|
const frontendResources = [];
|
||||||
|
for (const [entityName, ent] of Object.entries(parsed.entities)) {
|
||||||
|
const pk = ent.primaryKey;
|
||||||
|
const resource = pluralize(toKebab(entityName));
|
||||||
|
const be = renderBackendModule(entityName, ent, resource, pk);
|
||||||
|
const fe = renderFrontendResource(entityName, ent, resource, pk, parsed.enums, parsed.entities);
|
||||||
|
backendModules.push(be);
|
||||||
|
frontendResources.push(fe);
|
||||||
|
|
||||||
|
if (apply) {
|
||||||
|
for (const [rel, content] of Object.entries(be.files)) writeFile(path.join(ROOT, rel), content);
|
||||||
|
for (const [rel, content] of Object.entries(fe.files)) writeFile(path.join(ROOT, rel), content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureAppModule(apply, backendModules);
|
||||||
|
ensureClientApp(apply, frontendResources);
|
||||||
|
|
||||||
|
process.stdout.write(
|
||||||
|
`${apply ? 'Generated' : 'Planned'} ${Object.keys(parsed.entities).length} entities from ${dslPath}\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
||||||
931
openapi.json
931
openapi.json
@@ -1,931 +0,0 @@
|
|||||||
{
|
|
||||||
"openapi": "3.0.3",
|
|
||||||
"info": {
|
|
||||||
"title": "KIS-TOiR API",
|
|
||||||
"description": "Equipment maintenance management system. Generated from domain/toir.api.dsl via tools/api-summary-to-openapi.mjs.",
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"url": "/api",
|
|
||||||
"description": "Default server"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"components": {
|
|
||||||
"securitySchemes": {
|
|
||||||
"bearerAuth": {
|
|
||||||
"type": "http",
|
|
||||||
"scheme": "bearer",
|
|
||||||
"bearerFormat": "JWT"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schemas": {
|
|
||||||
"Equipment": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
},
|
|
||||||
"inventoryNumber": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Инвентарный номер"
|
|
||||||
},
|
|
||||||
"serialNumber": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Заводской (серийный) номер"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Наименование единицы оборудования"
|
|
||||||
},
|
|
||||||
"equipmentTypeCode": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Код вида оборудования"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/EquipmentStatus"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Текущий статус"
|
|
||||||
},
|
|
||||||
"location": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Место эксплуатации / скважина / куст"
|
|
||||||
},
|
|
||||||
"commissionedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Дата ввода в эксплуатацию"
|
|
||||||
},
|
|
||||||
"totalEngineHours": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "decimal",
|
|
||||||
"description": "Общая наработка, моточасов"
|
|
||||||
},
|
|
||||||
"engineHoursSinceLastRepair": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "decimal",
|
|
||||||
"description": "Наработка с последнего ремонта, моточасов"
|
|
||||||
},
|
|
||||||
"lastRepairAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Дата последнего ремонта"
|
|
||||||
},
|
|
||||||
"notes": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Примечания"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Оборудование — полный объект ответа"
|
|
||||||
},
|
|
||||||
"EquipmentCreate": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"inventoryNumber": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Инвентарный номер"
|
|
||||||
},
|
|
||||||
"serialNumber": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Заводской (серийный) номер"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Наименование единицы оборудования"
|
|
||||||
},
|
|
||||||
"equipmentTypeCode": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Код вида оборудования"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/EquipmentStatus"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Текущий статус"
|
|
||||||
},
|
|
||||||
"location": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Место эксплуатации / скважина / куст"
|
|
||||||
},
|
|
||||||
"commissionedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Дата ввода в эксплуатацию"
|
|
||||||
},
|
|
||||||
"totalEngineHours": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "decimal",
|
|
||||||
"description": "Общая наработка, моточасов"
|
|
||||||
},
|
|
||||||
"engineHoursSinceLastRepair": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "decimal",
|
|
||||||
"description": "Наработка с последнего ремонта, моточасов"
|
|
||||||
},
|
|
||||||
"lastRepairAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Дата последнего ремонта"
|
|
||||||
},
|
|
||||||
"notes": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Примечания"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Оборудование — тело запроса на создание",
|
|
||||||
"required": [
|
|
||||||
"inventoryNumber",
|
|
||||||
"name",
|
|
||||||
"equipmentTypeCode"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"EquipmentUpdate": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"inventoryNumber": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Инвентарный номер"
|
|
||||||
},
|
|
||||||
"serialNumber": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Заводской (серийный) номер"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Наименование единицы оборудования"
|
|
||||||
},
|
|
||||||
"equipmentTypeCode": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Код вида оборудования"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/EquipmentStatus"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Текущий статус"
|
|
||||||
},
|
|
||||||
"location": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Место эксплуатации / скважина / куст"
|
|
||||||
},
|
|
||||||
"commissionedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Дата ввода в эксплуатацию"
|
|
||||||
},
|
|
||||||
"totalEngineHours": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "decimal",
|
|
||||||
"description": "Общая наработка, моточасов"
|
|
||||||
},
|
|
||||||
"engineHoursSinceLastRepair": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "decimal",
|
|
||||||
"description": "Наработка с последнего ремонта, моточасов"
|
|
||||||
},
|
|
||||||
"lastRepairAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Дата последнего ремонта"
|
|
||||||
},
|
|
||||||
"notes": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Примечания"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Оборудование — тело запроса на обновление (частичное)"
|
|
||||||
},
|
|
||||||
"EquipmentListRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"filters": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/DTO.Filter"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"page": {
|
|
||||||
"$ref": "#/components/schemas/DTO.PageRequest"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Оборудование — запрос постраничного списка с фильтрацией"
|
|
||||||
},
|
|
||||||
"EquipmentListResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"content": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Equipment"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"page": {
|
|
||||||
"$ref": "#/components/schemas/DTO.PageInfo"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Оборудование — постраничный результат"
|
|
||||||
},
|
|
||||||
"RepairOrder": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
},
|
|
||||||
"number": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Номер заявки"
|
|
||||||
},
|
|
||||||
"equipmentId": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid",
|
|
||||||
"description": "Идентификатор оборудования"
|
|
||||||
},
|
|
||||||
"repairKind": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/RepairKind"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Вид ремонта"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/RepairOrderStatus"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Статус заявки"
|
|
||||||
},
|
|
||||||
"plannedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Плановая дата начала"
|
|
||||||
},
|
|
||||||
"startedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Фактическая дата начала"
|
|
||||||
},
|
|
||||||
"completedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Фактическая дата завершения"
|
|
||||||
},
|
|
||||||
"contractor": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Подрядная организация (если внешний ремонт)"
|
|
||||||
},
|
|
||||||
"engineHoursAtRepair": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "decimal",
|
|
||||||
"description": "Наработка на момент ремонта, моточасов"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Описание работ / дефекта"
|
|
||||||
},
|
|
||||||
"notes": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Примечания"
|
|
||||||
},
|
|
||||||
"confirmed": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Согласовано/Не согласовано"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Заявка на ремонт — полный объект ответа"
|
|
||||||
},
|
|
||||||
"RepairOrderCreate": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"number": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Номер заявки"
|
|
||||||
},
|
|
||||||
"equipmentId": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid",
|
|
||||||
"description": "Идентификатор оборудования"
|
|
||||||
},
|
|
||||||
"repairKind": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/RepairKind"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Вид ремонта"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/RepairOrderStatus"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Статус заявки"
|
|
||||||
},
|
|
||||||
"plannedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Плановая дата начала"
|
|
||||||
},
|
|
||||||
"startedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Фактическая дата начала"
|
|
||||||
},
|
|
||||||
"completedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Фактическая дата завершения"
|
|
||||||
},
|
|
||||||
"contractor": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Подрядная организация (если внешний ремонт)"
|
|
||||||
},
|
|
||||||
"engineHoursAtRepair": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "decimal",
|
|
||||||
"description": "Наработка на момент ремонта, моточасов"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Описание работ / дефекта"
|
|
||||||
},
|
|
||||||
"notes": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Примечания"
|
|
||||||
},
|
|
||||||
"confirmed": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Согласовано/Не согласовано"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Заявка на ремонт — тело запроса на создание",
|
|
||||||
"required": [
|
|
||||||
"number",
|
|
||||||
"equipmentId",
|
|
||||||
"repairKind",
|
|
||||||
"plannedAt"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"RepairOrderUpdate": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"number": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Номер заявки"
|
|
||||||
},
|
|
||||||
"equipmentId": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid",
|
|
||||||
"description": "Идентификатор оборудования"
|
|
||||||
},
|
|
||||||
"repairKind": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/RepairKind"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Вид ремонта"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/RepairOrderStatus"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Статус заявки"
|
|
||||||
},
|
|
||||||
"plannedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Плановая дата начала"
|
|
||||||
},
|
|
||||||
"startedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Фактическая дата начала"
|
|
||||||
},
|
|
||||||
"completedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Фактическая дата завершения"
|
|
||||||
},
|
|
||||||
"contractor": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Подрядная организация (если внешний ремонт)"
|
|
||||||
},
|
|
||||||
"engineHoursAtRepair": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "decimal",
|
|
||||||
"description": "Наработка на момент ремонта, моточасов"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Описание работ / дефекта"
|
|
||||||
},
|
|
||||||
"notes": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Примечания"
|
|
||||||
},
|
|
||||||
"confirmed": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Согласовано/Не согласовано"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Заявка на ремонт — тело запроса на обновление (частичное)"
|
|
||||||
},
|
|
||||||
"RepairOrderListRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"filters": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/DTO.Filter"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"page": {
|
|
||||||
"$ref": "#/components/schemas/DTO.PageRequest"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Заявка на ремонт — запрос постраничного списка с фильтрацией"
|
|
||||||
},
|
|
||||||
"RepairOrderListResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"content": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/RepairOrder"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"page": {
|
|
||||||
"$ref": "#/components/schemas/DTO.PageInfo"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Заявка на ремонт — постраничный результат"
|
|
||||||
},
|
|
||||||
"EquipmentStatus": {
|
|
||||||
"type": "string",
|
|
||||||
"x-dsl-enum": "EquipmentStatus",
|
|
||||||
"description": "Enum: EquipmentStatus (values defined in domain/*.api.dsl)"
|
|
||||||
},
|
|
||||||
"DTO.Filter": {
|
|
||||||
"type": "string",
|
|
||||||
"x-dsl-enum": "DTO.Filter",
|
|
||||||
"description": "Enum: DTO.Filter (values defined in domain/*.api.dsl)"
|
|
||||||
},
|
|
||||||
"DTO.PageRequest": {
|
|
||||||
"type": "string",
|
|
||||||
"x-dsl-enum": "DTO.PageRequest",
|
|
||||||
"description": "Enum: DTO.PageRequest (values defined in domain/*.api.dsl)"
|
|
||||||
},
|
|
||||||
"DTO.PageInfo": {
|
|
||||||
"type": "string",
|
|
||||||
"x-dsl-enum": "DTO.PageInfo",
|
|
||||||
"description": "Enum: DTO.PageInfo (values defined in domain/*.api.dsl)"
|
|
||||||
},
|
|
||||||
"RepairKind": {
|
|
||||||
"type": "string",
|
|
||||||
"x-dsl-enum": "RepairKind",
|
|
||||||
"description": "Enum: RepairKind (values defined in domain/*.api.dsl)"
|
|
||||||
},
|
|
||||||
"RepairOrderStatus": {
|
|
||||||
"type": "string",
|
|
||||||
"x-dsl-enum": "RepairOrderStatus",
|
|
||||||
"description": "Enum: RepairOrderStatus (values defined in domain/*.api.dsl)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"paths": {
|
|
||||||
"/equipment/page": {
|
|
||||||
"post": {
|
|
||||||
"summary": "Постраничный список оборудования с фильтрацией",
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"оборудованием"
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/EquipmentListRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Success",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/EquipmentListResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/equipment/{id}": {
|
|
||||||
"get": {
|
|
||||||
"summary": "Получить оборудование по идентификатору",
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"оборудованием"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Success",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/Equipment"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"summary": "Обновить единицу оборудования",
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"оборудованием"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/EquipmentUpdate"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Success",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"summary": "Удалить единицу оборудования",
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"оборудованием"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"204": {
|
|
||||||
"description": "No content"
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not found"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/equipment": {
|
|
||||||
"post": {
|
|
||||||
"summary": "Создать единицу оборудования",
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"оборудованием"
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/EquipmentCreate"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "Success",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/repair-orders/page": {
|
|
||||||
"post": {
|
|
||||||
"summary": "Постраничный список заявок на ремонт с фильтрацией",
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"заявками на ремонт"
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/RepairOrderListRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Success",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/RepairOrderListResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/repair-orders/{id}": {
|
|
||||||
"get": {
|
|
||||||
"summary": "Получить заявку на ремонт по идентификатору",
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"заявками на ремонт"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Success",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/RepairOrder"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"summary": "Обновить заявку на ремонт",
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"заявками на ремонт"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/RepairOrderUpdate"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Success",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"summary": "Удалить заявку на ремонт",
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"заявками на ремонт"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"204": {
|
|
||||||
"description": "No content"
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not found"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/repair-orders": {
|
|
||||||
"post": {
|
|
||||||
"summary": "Создать заявку на ремонт",
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"заявками на ремонт"
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/RepairOrderCreate"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "Success",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
overrides/api-overrides.dsl
Normal file
2
overrides/api-overrides.dsl
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Optional overrides:
|
||||||
|
// resource EquipmentType path "equipment-types";
|
||||||
2
overrides/ui-overrides.dsl
Normal file
2
overrides/ui-overrides.dsl
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Optional overrides:
|
||||||
|
// field EquipmentType.code widget "text";
|
||||||
@@ -2,12 +2,9 @@
|
|||||||
"name": "toir-generation-context",
|
"name": "toir-generation-context",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate:api-summary": "node tools/generate-api-summary.mjs",
|
"generate:domain-summary": "node tools/generate-domain-summary.mjs",
|
||||||
"generate:openapi": "node tools/api-summary-to-openapi.mjs --out openapi.json",
|
|
||||||
"validate:generation": "node tools/validate-generation.mjs",
|
"validate:generation": "node tools/validate-generation.mjs",
|
||||||
"validate:generation:runtime": "node tools/validate-generation.mjs --run-runtime",
|
"validate:generation:runtime": "node tools/validate-generation.mjs --run-runtime",
|
||||||
"validate:generation:artifacts": "node tools/validate-generation.mjs --artifacts-only",
|
"validate:generation:artifacts": "node tools/validate-generation.mjs --artifacts-only"
|
||||||
"eval:generation": "node tools/eval/run-evals.mjs",
|
|
||||||
"install-hooks": "node tools/install-hooks.mjs"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,95 +1,79 @@
|
|||||||
# Auth Rules
|
# Auth Rules
|
||||||
|
|
||||||
<!-- prompt-version: 2.0 -->
|
This repository keeps the current LLM-first CRUD generation architecture as the primary working baseline.
|
||||||
<!-- applies-to: client/src/auth/, server/src/auth/, toir-realm.json -->
|
|
||||||
<!-- validated-by: tools/validate-generation.mjs §validateAuthChecks §validateRealmChecks -->
|
|
||||||
|
|
||||||
Use this document during the **Auth / Runtime / Realm** stage defined in `prompts/general-prompt.md`.
|
- Auth is part of the default generation path, not a post-generation addon.
|
||||||
|
- `server/` is the active backend target output path.
|
||||||
|
- `client/` is the active frontend target output path.
|
||||||
|
- The generated runtime stays SPA + API + external Keycloak + PostgreSQL only.
|
||||||
|
|
||||||
## Purpose
|
## Frontend auth invariants
|
||||||
|
|
||||||
Generate and preserve the auth contracts that let the CRUD app run as a React Admin SPA backed by a NestJS API protected by external Keycloak.
|
- Use `keycloak-js` with redirect-based login only.
|
||||||
|
- Initialize Keycloak before rendering the SPA.
|
||||||
|
- Use Authorization Code Flow + PKCE (`S256`).
|
||||||
|
- Keep `authProvider`, `dataProvider`, `getIdentity()`, `getPermissions()`, and `checkError()` as stable provider seams.
|
||||||
|
- Derive identity from token claims already present in the parsed token.
|
||||||
|
- Do not call `loadUserProfile()` and do not depend on `/account` for the baseline app.
|
||||||
|
- `401` must force re-authentication; `403` must stay an authorization error.
|
||||||
|
- Keep token handling in memory and refresh through one shared in-flight operation.
|
||||||
|
|
||||||
## Mandatory Inputs
|
## Working runtime defaults
|
||||||
|
|
||||||
- `prompts/general-prompt.md`
|
Use the already working project defaults unless a prompt explicitly overrides them.
|
||||||
- `prompts/runtime-rules.md`
|
|
||||||
- current repository auth/runtime defaults
|
|
||||||
|
|
||||||
## Expected Outputs
|
- Frontend Keycloak base URL example:
|
||||||
|
- `VITE_KEYCLOAK_URL=https://sso.greact.ru`
|
||||||
- `client/src/auth/`
|
- Frontend realm and client example:
|
||||||
- `client/src/dataProvider.ts`
|
- `VITE_KEYCLOAK_REALM=toir`
|
||||||
- `server/src/auth/`
|
- `VITE_KEYCLOAK_CLIENT_ID=toir-frontend`
|
||||||
- `toir-realm.json`
|
- Backend issuer and audience example:
|
||||||
|
- `KEYCLOAK_ISSUER_URL=https://sso.greact.ru/realms/toir`
|
||||||
## Frontend Auth Invariants
|
- `KEYCLOAK_AUDIENCE=toir-backend`
|
||||||
|
- CORS example:
|
||||||
- use `keycloak-js` with redirect-based login only
|
- `CORS_ALLOWED_ORIGINS=http://localhost:5173,https://toir-frontend.greact.ru`
|
||||||
- initialize Keycloak before rendering the SPA
|
|
||||||
- use Authorization Code Flow + PKCE (`S256`)
|
|
||||||
- keep `authProvider`, `dataProvider`, `getIdentity()`, `getPermissions()`, and `checkError()` as stable seams
|
|
||||||
- derive identity from token claims already present in the token
|
|
||||||
- do not call `loadUserProfile()`
|
|
||||||
- `401` forces re-authentication; `403` remains an authorization error
|
|
||||||
- keep token handling in memory with one shared in-flight refresh path
|
|
||||||
|
|
||||||
## Backend Auth Invariants
|
|
||||||
|
|
||||||
- verify JWTs with `jose`
|
|
||||||
- validate issuer, audience, and signature via JWKS
|
|
||||||
- resolve JWKS in this order:
|
|
||||||
1. `KEYCLOAK_JWKS_URL`
|
|
||||||
2. OIDC discovery at `/.well-known/openid-configuration`
|
|
||||||
3. `${issuer}/protocol/openid-connect/certs`
|
|
||||||
- extract roles only from `realm_access.roles`
|
|
||||||
- keep `/health` public
|
|
||||||
- generated CRUD routes stay protected by default
|
|
||||||
|
|
||||||
## Working Runtime Defaults
|
|
||||||
|
|
||||||
Keep these defaults unless a task explicitly overrides them:
|
|
||||||
|
|
||||||
- `VITE_KEYCLOAK_URL=https://sso.greact.ru`
|
|
||||||
- `VITE_KEYCLOAK_REALM=toir`
|
|
||||||
- `VITE_KEYCLOAK_CLIENT_ID=toir-frontend`
|
|
||||||
- `KEYCLOAK_ISSUER_URL=https://sso.greact.ru/realms/toir`
|
|
||||||
- `KEYCLOAK_AUDIENCE=toir-backend`
|
|
||||||
- `CORS_ALLOWED_ORIGINS=http://localhost:5173,https://toir-frontend.greact.ru`
|
|
||||||
|
|
||||||
Anti-regression rule:
|
Anti-regression rule:
|
||||||
|
|
||||||
- do not revert shared examples to localhost Keycloak defaults unless a task explicitly requests a local Keycloak baseline
|
- Do not switch the baseline Keycloak example back to `http://localhost:8080` in generated `.env.example` files unless the prompt explicitly asks for a local Keycloak runtime.
|
||||||
|
- Localhost Keycloak values may exist in private `.env.local` or `.env` overrides, but they are not the default project examples.
|
||||||
|
|
||||||
## Realm Artifact Contract
|
## Backend auth invariants
|
||||||
|
|
||||||
The root realm artifact is mandatory and must:
|
- Verify JWTs with `jose`.
|
||||||
|
- Validate issuer + audience + signature via JWKS.
|
||||||
|
- Resolve JWKS in this order:
|
||||||
|
1. `KEYCLOAK_JWKS_URL`
|
||||||
|
2. OIDC discovery at `/.well-known/openid-configuration`
|
||||||
|
3. `${issuer}/protocol/openid-connect/certs`
|
||||||
|
- Extract roles only from `realm_access.roles`.
|
||||||
|
- Keep `/health` public and all generated CRUD routes protected by default.
|
||||||
|
|
||||||
- be importable and versioned
|
## Auth anti-regression invariants
|
||||||
- align with generated frontend/backend env contracts
|
|
||||||
- parameterize:
|
- The accepted JWKS resolution chain above is the only baseline truth path. Do not document one order and implement another.
|
||||||
|
- If auth implementation changes, `prompts/auth-rules.md` and `prompts/validation-rules.md` must be updated in the same change.
|
||||||
|
- Do not skip OIDC discovery when no explicit `KEYCLOAK_JWKS_URL` is provided.
|
||||||
|
- Do not switch role extraction to alternative claims unless the prompt explicitly changes the baseline contract.
|
||||||
|
- Do not reintroduce localhost Keycloak defaults into shared baseline examples.
|
||||||
|
|
||||||
|
## Realm artifact contract
|
||||||
|
|
||||||
|
- A physical root-level `*-realm.json` artifact is mandatory output.
|
||||||
|
- The artifact must be importable, versioned, and aligned with generated backend/frontend env contracts.
|
||||||
|
- It must parameterize:
|
||||||
- realm name
|
- realm name
|
||||||
- frontend client id
|
- frontend client id
|
||||||
- backend client id / audience
|
- backend client id / audience
|
||||||
- local and production frontend URLs
|
- local and production frontend URLs
|
||||||
- artifact filename
|
- artifact filename
|
||||||
- explicitly deliver:
|
- It must explicitly deliver:
|
||||||
- `sub`
|
- `sub`
|
||||||
- `aud`
|
- `aud`
|
||||||
- `realm_access.roles`
|
- `realm_access.roles`
|
||||||
- define:
|
- It must define:
|
||||||
- realm roles `admin`, `editor`, `viewer`
|
- realm roles `admin`, `editor`, `viewer`
|
||||||
- a public SPA client with PKCE S256
|
- a public SPA client with PKCE S256
|
||||||
- a bearer-only backend client
|
- a bearer-only backend client
|
||||||
- an explicit audience client scope
|
- an explicit audience client scope
|
||||||
- protocol mappers for baseline identity and role claims
|
- explicit protocol mappers for baseline identity and role claims
|
||||||
|
|
||||||
## Completion Expectations
|
|
||||||
|
|
||||||
Auth/runtime generation is incomplete if any of the following is true:
|
|
||||||
|
|
||||||
- frontend and backend auth seams drift from each other
|
|
||||||
- JWKS resolution order changes
|
|
||||||
- `/health` stops being public
|
|
||||||
- shared Keycloak defaults regress to localhost examples
|
|
||||||
- the realm artifact no longer matches backend/frontend expectations
|
|
||||||
|
|||||||
@@ -1,147 +1,88 @@
|
|||||||
# Backend Rules
|
# Backend Rules
|
||||||
|
|
||||||
<!-- prompt-version: 2.0 -->
|
The backend remains derived from `domain/*.dsl` inside the existing LLM-first pipeline. No compiler platform or generator engine is introduced.
|
||||||
<!-- applies-to: server/src/modules/, server/src/app.module.ts -->
|
|
||||||
<!-- validated-by: tools/validate-generation.mjs §validateApiDslCoverage §validateNaturalKeyChecks -->
|
|
||||||
|
|
||||||
Use this document during the **Backend** stage defined in `prompts/general-prompt.md`.
|
## Backend scaffold baseline
|
||||||
|
|
||||||
## Purpose
|
- Start backend initialization from the official NestJS CLI workspace, not from manually created files.
|
||||||
|
- The backend must remain compatible with standard Nest workspace tooling such as `nest build` and `nest start`.
|
||||||
Generate NestJS CRUD artifacts that match the DSL contract exactly and remain compatible with a standard NestJS workspace.
|
- Preserve the core Nest workspace files generated by the CLI, especially:
|
||||||
|
|
||||||
## Mandatory Inputs
|
|
||||||
|
|
||||||
- `prompts/general-prompt.md`
|
|
||||||
- the active `api API.<Entity>` block from `domain/toir.api.dsl`
|
|
||||||
- referenced DTOs and enums from `domain/toir.api.dsl`
|
|
||||||
- an intact or repaired official NestJS scaffold under `server/`
|
|
||||||
|
|
||||||
`api-summary.json` may be consulted only as an auxiliary inventory or validator-related artifact. It must never replace the DSL as the backend source of truth.
|
|
||||||
|
|
||||||
## Expected Outputs
|
|
||||||
|
|
||||||
Per entity:
|
|
||||||
|
|
||||||
- `server/src/modules/<kebab>/<kebab>.module.ts`
|
|
||||||
- `server/src/modules/<kebab>/<kebab>.controller.ts`
|
|
||||||
- `server/src/modules/<kebab>/<kebab>.service.ts`
|
|
||||||
- `server/src/modules/<kebab>/dto/create-<kebab>.dto.ts`
|
|
||||||
- `server/src/modules/<kebab>/dto/update-<kebab>.dto.ts`
|
|
||||||
|
|
||||||
Repository-wide:
|
|
||||||
|
|
||||||
- `server/src/app.module.ts` registrations
|
|
||||||
|
|
||||||
## Scaffold Baseline
|
|
||||||
|
|
||||||
- Start backend initialization and repair from the official NestJS CLI workspace, not from hand-written files.
|
|
||||||
- Preserve Nest workspace essentials:
|
|
||||||
- `server/tsconfig.json`
|
- `server/tsconfig.json`
|
||||||
- `server/tsconfig.build.json`
|
- `server/tsconfig.build.json`
|
||||||
- `server/nest-cli.json`
|
- `server/nest-cli.json`
|
||||||
- `server/src/main.ts`
|
- `server/src/main.ts`
|
||||||
- `server/src/app.module.ts`
|
- `server/src/app.module.ts`
|
||||||
- If the workspace is degraded, repair it before generating domain code.
|
- For domain resources, prefer official Nest CLI generation patterns for modules/controllers/services/resources and then adapt the generated code to Prisma and auth requirements.
|
||||||
|
- Do not delete required Nest workspace files just because the LLM can inline a smaller custom structure.
|
||||||
|
|
||||||
Forbidden patterns:
|
## Forbidden backend generation patterns
|
||||||
|
|
||||||
- hand-written pseudo-Nest scaffolds
|
- Do not bootstrap `server/` by hand-writing a pseudo-Nest project from memory.
|
||||||
- deleting required Nest config files after generation
|
- Do not remove `tsconfig.json`, `tsconfig.build.json`, or `nest-cli.json` after generation.
|
||||||
- replacing normal Nest build/start behavior with ad hoc scripts
|
- Do not replace standard Nest package scripts with ad hoc commands that break `nest build` or `nest start`.
|
||||||
|
- Do not continue CRUD generation on top of a degraded backend workspace without repairing the workspace first.
|
||||||
|
|
||||||
## Route And Resource Contract
|
## Domain-derived output
|
||||||
|
|
||||||
|
- `domain/*.dsl` is the source of truth for entities, fields, primary keys, foreign keys, and enums.
|
||||||
|
- `domain-summary.json` is a derived artifact used to stabilize LLM generation and validation. It must never replace the DSL as the source of truth.
|
||||||
|
- Each entity becomes:
|
||||||
|
- a Prisma model
|
||||||
|
- a NestJS module
|
||||||
|
- a controller
|
||||||
|
- a service
|
||||||
|
- create/update DTOs
|
||||||
|
|
||||||
|
## DTO and Prisma mapping
|
||||||
|
|
||||||
|
- `decimal` -> Prisma `Decimal`, DTO/API `string`
|
||||||
|
- `date` -> Prisma `DateTime`, DTO/API `string`
|
||||||
|
- Enums remain string-valued in DTO/API contracts
|
||||||
|
|
||||||
|
## CRUD and natural-key invariants
|
||||||
|
|
||||||
- Use the shared entity-to-resource naming convention from `prompts/general-prompt.md`.
|
|
||||||
- Each entity becomes a NestJS module, controller, service, and create/update DTO pair.
|
|
||||||
- CRUD routes use the real primary key name in the path.
|
- CRUD routes use the real primary key name in the path.
|
||||||
- Every API record returned to React Admin must include `id`.
|
- Every API record returned to React Admin must include `id`.
|
||||||
- For natural-key entities, map the real primary key to `id` in responses and sort translation.
|
- For entities whose primary key is not `id`, the backend must map the real key to `id`.
|
||||||
|
- Natural-key list/sort logic must never build ORM `orderBy` against a fake physical `id`.
|
||||||
|
|
||||||
## DTO Contract
|
## Service invariants
|
||||||
|
|
||||||
- `DTO.<Entity>Create` defines `Create<Entity>Dto`.
|
- Never pass raw update DTOs into Prisma update `data`.
|
||||||
- `DTO.<Entity>Update` defines `Update<Entity>Dto`.
|
- Remove `id`, the real primary key, and readonly fields from update payloads before calling Prisma.
|
||||||
- Do not invent fields or pull field lists from memory.
|
- Keep PrismaService lightweight:
|
||||||
- Never include `id` in Create/Update DTOs.
|
|
||||||
|
|
||||||
Type and decorator rules:
|
|
||||||
|
|
||||||
| DSL type | TS DTO type | class-validator decorator | Notes |
|
|
||||||
|-----------|-------------|---------------------------|-------|
|
|
||||||
| `uuid` | `string` | `@IsUUID()` | |
|
|
||||||
| `string` | `string` | `@IsString()` | |
|
|
||||||
| `text` | `string` | `@IsString()` | |
|
|
||||||
| `integer` | `number` | `@IsInt()` | |
|
|
||||||
| `number` | `number` | `@IsNumber()` | |
|
|
||||||
| `decimal` | `string` | `@IsString()` | serialize with Prisma Decimal |
|
|
||||||
| `date` | `string` | `@IsString()` | serialize as ISO string |
|
|
||||||
| `boolean` | `boolean` | `@IsBoolean()` | |
|
|
||||||
| enum name | `string` | `@IsEnum(EnumName)` | |
|
|
||||||
|
|
||||||
Nullability rules:
|
|
||||||
|
|
||||||
- every field that is not `is required` gets `@IsOptional()` before the type decorator
|
|
||||||
- every generated DTO imports from `'class-validator'`
|
|
||||||
|
|
||||||
## Controller Contract
|
|
||||||
|
|
||||||
- Apply `@UseGuards(JwtAuthGuard, RolesGuard)` at controller class level.
|
|
||||||
- Roles per verb:
|
|
||||||
- `GET` -> `viewer | editor | admin`
|
|
||||||
- `POST`, `PATCH`, `PUT` -> `editor | admin`
|
|
||||||
- `DELETE` -> `admin`
|
|
||||||
- Reconcile DSL HTTP shapes for repository compatibility:
|
|
||||||
- list endpoints declared as `POST .../page` generate as `@Get()` with React Admin query params
|
|
||||||
- update endpoints declared as `PUT` generate as `@Patch(':<pk>')`
|
|
||||||
- Path parameters are taken from the DSL endpoint contract, not invented from generic CRUD memory.
|
|
||||||
|
|
||||||
## Service Contract
|
|
||||||
|
|
||||||
- Never pass raw update DTOs directly into Prisma update `data`.
|
|
||||||
- Strip `id`, the real primary key, and readonly fields before writes.
|
|
||||||
- Keep `PrismaService` lightweight:
|
|
||||||
- extend `PrismaClient`
|
- extend `PrismaClient`
|
||||||
- implement `OnModuleInit`
|
- implement `OnModuleInit`
|
||||||
- call `$connect()`
|
- call `$connect()`
|
||||||
- do not add `beforeExit`
|
- do not use `beforeExit`
|
||||||
|
|
||||||
List endpoint requirements:
|
## Filtering contract
|
||||||
|
|
||||||
- accept React Admin query params: `_start`, `_end`, `_sort`, `_order`, `q`
|
- List endpoints must support React Admin query parameters:
|
||||||
- set `Content-Range`
|
- `_start`, `_end`, `_sort`, `_order`
|
||||||
- set `Access-Control-Expose-Headers: Content-Range`
|
- arbitrary field filters from query string
|
||||||
|
- `q` for reference autocomplete search
|
||||||
|
- String/text search filters may use `contains` with case-insensitive mode.
|
||||||
|
- Foreign key filters must use exact-match semantics (no `contains` for FK scalar keys).
|
||||||
|
- Enum filters must support both single and repeated query params:
|
||||||
|
- `status=Draft`
|
||||||
|
- `status=Draft&status=Approved`
|
||||||
|
- Repeated enum params must map to Prisma `{ in: [...] }`.
|
||||||
|
- Sorting must use real model scalar fields only; natural-key entities must not fallback to fake physical `id`.
|
||||||
|
|
||||||
Filtering rules:
|
## Reproducibility invariants
|
||||||
|
|
||||||
- string/text filters may use case-insensitive `contains`
|
- A freshly generated backend must be bootstrappable with ordinary Nest + Prisma commands from `prompts/runtime-rules.md`.
|
||||||
- foreign-key scalar filters must use exact-match semantics
|
- Missing TypeScript or Nest workspace config is a generation failure, not an acceptable simplification.
|
||||||
- enum filters must support both single and repeated params
|
- The baseline backend should fail only on missing runtime dependencies or env values, not because the Nest workspace itself is incomplete.
|
||||||
- repeated enum params must map to Prisma `{ in: [...] }`
|
|
||||||
- `_sort=id` must map to the real primary key for natural-key entities
|
|
||||||
|
|
||||||
Decimal and date handling:
|
## Recovery rule if backend workspace degraded
|
||||||
|
|
||||||
- `decimal` writes: `new Prisma.Decimal(value)`
|
- If required Nest scaffold files are missing or broken, restore the official workspace baseline before editing Prisma models, modules, controllers, services, or DTOs.
|
||||||
- `decimal` reads: `.toString()`
|
- Treat workspace repair as higher priority than feature generation, because generated domain code on top of a broken workspace is invalid baseline output.
|
||||||
- `date` writes: `new Date(value)`
|
|
||||||
- `date` reads: `.toISOString()`
|
|
||||||
|
|
||||||
## Natural-Key Rules
|
## Backend auth defaults
|
||||||
|
|
||||||
For entities whose physical primary key is not `id`:
|
- `GET` -> `viewer | editor | admin`
|
||||||
|
- `POST`, `PATCH`, `PUT` -> `editor | admin`
|
||||||
- route params use the real primary key name
|
- `DELETE` -> `admin`
|
||||||
- responses expose `id` mapped from that primary key
|
|
||||||
- sort/update behavior never targets a fake physical `id`
|
|
||||||
- update payload sanitization removes both `id` and the real primary key
|
|
||||||
|
|
||||||
## Completion Expectations
|
|
||||||
|
|
||||||
Backend generation is incomplete if any of the following is true:
|
|
||||||
|
|
||||||
- required Nest scaffold files are missing
|
|
||||||
- DTO decorators are incomplete or type-incorrect
|
|
||||||
- controllers are missing guards or role decorators
|
|
||||||
- natural-key handling regresses to a fake physical `id`
|
|
||||||
- list/filter behavior is incompatible with React Admin expectations
|
|
||||||
|
|||||||
@@ -1,118 +1,65 @@
|
|||||||
# Frontend Rules
|
# Frontend Rules
|
||||||
|
|
||||||
<!-- prompt-version: 2.0 -->
|
The frontend stays a React Admin SPA generated from `domain/*.dsl` and anchored to the existing auth seams.
|
||||||
<!-- applies-to: client/src/resources/, client/src/App.tsx -->
|
|
||||||
<!-- validated-by: tools/validate-generation.mjs §validateApiDslCoverage -->
|
|
||||||
|
|
||||||
Use this document during the **Frontend** stage defined in `prompts/general-prompt.md`.
|
## Frontend scaffold baseline
|
||||||
|
|
||||||
## Purpose
|
- Start frontend initialization from the official Vite React TypeScript scaffold, not from manually assembled files.
|
||||||
|
- Preserve a valid Vite workspace baseline, including:
|
||||||
Generate React Admin resources that stay aligned with the DSL contract, the backend contract, and the repository auth/data provider seams.
|
|
||||||
|
|
||||||
## Mandatory Inputs
|
|
||||||
|
|
||||||
- `prompts/general-prompt.md`
|
|
||||||
- the active `api API.<Entity>` block from `domain/toir.api.dsl`
|
|
||||||
- referenced DTOs and enums from `domain/toir.api.dsl`
|
|
||||||
- an intact or repaired official Vite React TypeScript scaffold under `client/`
|
|
||||||
|
|
||||||
## Expected Outputs
|
|
||||||
|
|
||||||
Per entity:
|
|
||||||
|
|
||||||
- `client/src/resources/<kebab>/<Entity>List.tsx`
|
|
||||||
- `client/src/resources/<kebab>/<Entity>Create.tsx`
|
|
||||||
- `client/src/resources/<kebab>/<Entity>Edit.tsx`
|
|
||||||
- `client/src/resources/<kebab>/<Entity>Show.tsx`
|
|
||||||
|
|
||||||
Repository-wide:
|
|
||||||
|
|
||||||
- `client/src/App.tsx` resource registrations
|
|
||||||
|
|
||||||
## Scaffold Baseline
|
|
||||||
|
|
||||||
- Start frontend initialization and repair from the official Vite React TypeScript scaffold, not from a hand-written shell.
|
|
||||||
- Preserve workspace essentials:
|
|
||||||
- `client/index.html`
|
- `client/index.html`
|
||||||
- `client/tsconfig.json`
|
- `client/tsconfig.json`
|
||||||
- `client/vite.config.*`
|
- `client/vite.config.*`
|
||||||
- `client/src/main.tsx`
|
- `client/src/main.tsx`
|
||||||
- Repair the scaffold before generating resources if it is degraded.
|
- Add React Admin and auth seams on top of that baseline instead of replacing the workspace with a hand-written minimal shell.
|
||||||
|
- Do not delete required Vite entry/config files just because the LLM can write a shorter custom setup.
|
||||||
|
|
||||||
## Resource Contract
|
## Forbidden frontend generation patterns
|
||||||
|
|
||||||
- Use the shared entity-to-resource naming convention from `prompts/general-prompt.md`.
|
- Do not bootstrap `client/` by hand-writing a pseudo-Vite project from memory.
|
||||||
- Every entity becomes a React Admin resource with `list`, `create`, `edit`, and `show`.
|
- Do not remove `index.html`, `tsconfig*`, or `vite.config.*` after generation.
|
||||||
- `Resource` registration in `client/src/App.tsx` must include `show={...}`.
|
- Do not replace standard Vite package scripts with ad hoc commands that break `vite build`, `vite dev`, or `vite preview`.
|
||||||
- Every frontend record must work with React Admin's `id` contract, including natural-key entities.
|
- Do not continue React Admin resource generation on top of a degraded frontend workspace without repairing the workspace first.
|
||||||
|
|
||||||
DTO-driven view rules:
|
## Resource generation
|
||||||
|
|
||||||
- List and Show views use fields from `DTO.<Entity>`
|
- Each entity becomes a React Admin resource with list/create/edit/show views.
|
||||||
- Create view uses fields from `DTO.<Entity>Create`
|
- Resource names must stay aligned with backend path segments.
|
||||||
- Edit view uses fields from `DTO.<Entity>Update`
|
- Foreign keys must use `ReferenceInput` / `ReferenceField`.
|
||||||
- Do not derive form fields directly from model attributes when the DTO contract is narrower
|
- Foreign keys shown in list/show views must stay clickable via `ReferenceField link="show"` to open full details of the related resource.
|
||||||
|
- Lists must expose filters through `List` `filters` and an actions toolbar with `FilterButton`.
|
||||||
|
- For enum fields where multi-select is required (for example `status`), use `SelectArrayInput` in list filters.
|
||||||
|
- For foreign key filters and form selection use `ReferenceInput` + `AutocompleteInput` with `filterToQuery={(searchText) => ({ q: searchText })}`.
|
||||||
|
- Form mapping must stay type-safe:
|
||||||
|
- `integer` / `decimal` -> `NumberInput`
|
||||||
|
- `date` -> `DateInput`
|
||||||
|
|
||||||
## Input And Field Mapping
|
## Provider seams
|
||||||
|
|
||||||
Form inputs:
|
- `client/src/dataProvider.ts` is the single authenticated request seam.
|
||||||
|
- `client/src/auth/authProvider.ts` is the single React Admin auth seam.
|
||||||
|
- Auth logic must not leak into resource components.
|
||||||
|
|
||||||
- `integer`, `number`, `decimal` -> `NumberInput`
|
## Identity and permissions
|
||||||
- `date` -> `DateInput`
|
|
||||||
- required `boolean` -> `BooleanInput`
|
|
||||||
- nullable `boolean` -> `NullableBooleanInput`
|
|
||||||
- enum -> `SelectInput`
|
|
||||||
- FK reference -> `ReferenceInput` + `AutocompleteInput`
|
|
||||||
|
|
||||||
Display fields:
|
- `getIdentity()` must resolve from parsed token claims.
|
||||||
|
- `getPermissions()` may expose realm roles for UI awareness.
|
||||||
|
- Backend enforcement remains authoritative.
|
||||||
|
|
||||||
- `integer`, `number`, `decimal` -> `NumberField`
|
## React Admin compatibility
|
||||||
- `date` -> `DateField`
|
|
||||||
- `boolean` -> `BooleanField`
|
|
||||||
- enum -> `SelectField`
|
|
||||||
- FK reference -> `ReferenceField`
|
|
||||||
|
|
||||||
Hard failure rule:
|
- Every resource record must include `id`.
|
||||||
|
- Natural-key resources must preserve route, update, and sort compatibility with React Admin contracts.
|
||||||
|
- Frontend requests must continue to work when the real primary key is not named `id`.
|
||||||
|
- `dataProvider` query serialization must preserve repeated query params for array filters (for example enum multi-select).
|
||||||
|
- `Resource` wiring in `App.tsx` must keep `show={...}` registration for all generated resources.
|
||||||
|
|
||||||
- using plain `TextInput` for `integer`, `number`, `decimal`, `date`, or `boolean` is a generation failure
|
## Reproducibility invariants
|
||||||
|
|
||||||
## Filter And Reference Contract
|
- A freshly generated frontend must remain compatible with standard Vite commands such as `npm run dev` and `npm run build`.
|
||||||
|
- Missing Vite workspace files or missing local Vite executable wiring is a generation failure, not an acceptable simplification.
|
||||||
|
- The generated frontend should fail only on missing installation/env/runtime backend availability, not because the Vite app structure itself is incomplete.
|
||||||
|
|
||||||
- Lists must expose filters and include a toolbar with `FilterButton`.
|
## Recovery rule if frontend workspace degraded
|
||||||
- Enum multi-select filters use `SelectArrayInput`.
|
|
||||||
- Reference filters and form selectors use `ReferenceInput` + `AutocompleteInput` with `filterToQuery={(searchText) => ({ q: searchText })}`.
|
|
||||||
- FK list/show rendering must use `ReferenceField link=\"show\"`.
|
|
||||||
- `dataProvider` query serialization must preserve repeated params for array filters.
|
|
||||||
|
|
||||||
Reference display expression priority:
|
- If required Vite scaffold files are missing or broken, restore the official workspace baseline before editing resources, auth seams, or UI code.
|
||||||
|
- Treat workspace repair as higher priority than feature generation, because generated React Admin code on top of a broken Vite workspace is invalid baseline output.
|
||||||
1. if `inventoryNumber` exists: ``(record) => `${record.inventoryNumber} — ${record.name ?? record.inventoryNumber}``
|
|
||||||
2. else if `code` exists: ``(record) => `${record.code} — ${record.name ?? record.code}``
|
|
||||||
3. else if `number` exists: ``(record) => `${record.number} — ${record.name ?? record.number}``
|
|
||||||
4. else if `name` exists: `(record) => record.name ?? record.id`
|
|
||||||
5. else: `(record) => record.id`
|
|
||||||
|
|
||||||
## Auth And Provider Seams
|
|
||||||
|
|
||||||
- `client/src/dataProvider.ts` remains the single authenticated request seam.
|
|
||||||
- `client/src/auth/authProvider.ts` remains the single React Admin auth seam.
|
|
||||||
- Resource components must not embed auth logic.
|
|
||||||
- `getIdentity()` resolves from token claims.
|
|
||||||
- `getPermissions()` may expose realm roles for UI awareness, but backend enforcement stays authoritative.
|
|
||||||
|
|
||||||
## Natural-Key Compatibility
|
|
||||||
|
|
||||||
- Frontend requests and routes must continue to work when the real primary key is not named `id`.
|
|
||||||
- Edit/show/delete flows must preserve compatibility with backend natural-key handling.
|
|
||||||
- Sorting and filtering assumptions must not regress to a fake physical `id`.
|
|
||||||
|
|
||||||
## Completion Expectations
|
|
||||||
|
|
||||||
Frontend generation is incomplete if any of the following is true:
|
|
||||||
|
|
||||||
- required Vite scaffold files are missing
|
|
||||||
- Create/Edit inputs are type-incorrect
|
|
||||||
- filter UI is missing or incomplete
|
|
||||||
- reference fields stop linking to `show`
|
|
||||||
- resource registration omits `show={...}`
|
|
||||||
|
|||||||
@@ -1,298 +1,146 @@
|
|||||||
<!-- prompt-version: 3.0 -->
|
ROLE
|
||||||
<!-- applies-to: all generation tasks (master orchestration prompt) -->
|
|
||||||
<!-- validated-by: tools/validate-generation.mjs + npm run eval:generation -->
|
|
||||||
|
|
||||||
# Role
|
You are a Staff-level Fullstack Platform Engineer working inside the established LLM-first CRUD generation baseline.
|
||||||
|
|
||||||
You are the master orchestrator of the KIS-TOiR generation pipeline.
|
Use context7 when official framework guidance is needed.
|
||||||
|
|
||||||
Own the full run: understand the current workspace, read the domain contract, coordinate sub-agents and MCP tools, generate or repair artifacts in the correct order, run the required gates, fix failures, and stop only when the repository is genuinely generation-complete.
|
This repository is not a new generator engine. Do not redesign it into a planner/emitter/runtime platform.
|
||||||
|
|
||||||
# Project Description
|
GOAL
|
||||||
|
|
||||||
KIS-TOiR is an LLM-first fullstack CRUD generation project for equipment maintenance management.
|
Strengthen and use the existing LLM-first CRUD generation pipeline.
|
||||||
|
|
||||||
- Backend: NestJS + Prisma
|
- Keep `domain/*.dsl` as the source of truth for the domain model.
|
||||||
- Frontend: Vite React TypeScript + React Admin
|
- Keep `server/` as the active backend target output path.
|
||||||
- Auth/runtime: external Keycloak + PostgreSQL + repository-managed env/runtime artifacts
|
- Keep `client/` as the active frontend target output path.
|
||||||
|
- Keep Keycloak auth in the default generation path.
|
||||||
|
- Keep PostgreSQL as the only Dockerized runtime dependency.
|
||||||
|
- Generate and maintain:
|
||||||
|
- `domain-summary.json`
|
||||||
|
- a root-level `*-realm.json`
|
||||||
|
- backend/frontend auth seams
|
||||||
|
- runtime/env/bootstrap artifacts
|
||||||
|
- validation-gate-compatible output
|
||||||
|
|
||||||
The repository is intentionally prompt-driven. `prompts/*.md` define generation policy; generated code lives under `server/` and `client/`.
|
Use the already working runtime defaults for this project unless the prompt explicitly overrides them:
|
||||||
|
|
||||||
# Mission
|
- frontend Keycloak URL example: `https://sso.greact.ru`
|
||||||
|
- frontend realm/client examples: `toir`, `toir-frontend`
|
||||||
|
- backend issuer/audience examples: `https://sso.greact.ru/realms/toir`, `toir-backend`
|
||||||
|
- production frontend origin example: `https://toir-frontend.greact.ru`
|
||||||
|
|
||||||
Turn the repository source contract into a buildable, validated workspace by:
|
Do not silently regress these examples to localhost Keycloak defaults.
|
||||||
|
|
||||||
1. starting from official framework scaffolding when a workspace is missing or degraded
|
ACTIVE KNOWLEDGE BLOCKS
|
||||||
2. generating Prisma, backend, frontend, auth, runtime, and realm artifacts from the DSL
|
|
||||||
3. using sub-agents intentionally instead of carrying every concern in one context window
|
|
||||||
4. proving completion with builds and repository validation gates
|
|
||||||
|
|
||||||
# Source Of Truth
|
Read in this order:
|
||||||
|
|
||||||
`domain/toir.api.dsl` is the operative source of truth for generation runs.
|
1. `domain/dsl-spec.md`
|
||||||
|
2. `domain/*.dsl`
|
||||||
|
3. `domain-summary.json` if present
|
||||||
|
4. `prompts/auth-rules.md`
|
||||||
|
5. `prompts/backend-rules.md`
|
||||||
|
6. `prompts/frontend-rules.md`
|
||||||
|
7. `prompts/runtime-rules.md`
|
||||||
|
8. `prompts/validation-rules.md`
|
||||||
|
|
||||||
It is authoritative for:
|
Interpretation rules:
|
||||||
|
|
||||||
- entities and enums
|
- `domain/*.dsl` is authoritative.
|
||||||
- DTO shapes per operation
|
- `domain-summary.json` is derived. Regenerate or validate it against the DSL; never treat it as the source of truth.
|
||||||
- nullability and requiredness
|
INPUT CONTRACT
|
||||||
- primary and foreign keys
|
|
||||||
- HTTP methods, endpoint paths, and pagination contracts
|
Required:
|
||||||
|
|
||||||
|
- `domain/*.dsl`
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
|
||||||
|
- `overrides/api-overrides.dsl`
|
||||||
|
- `overrides/ui-overrides.dsl`
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- Read the DSL directly. Do not substitute `api-summary.json` for `domain/toir.api.dsl`.
|
- Do not require DTO/API/UI DSL files.
|
||||||
- Work from entity-scoped slices: the active `api API.<Entity>` block plus its referenced DTOs and enums.
|
- Do not resurrect multi-DSL source-of-truth behavior.
|
||||||
- Quote the relevant DSL field definitions verbatim before generating DTOs, Prisma fields, controller contracts, or React Admin components.
|
- Optional overrides may refine derived API/UI behavior but must not redefine the domain model.
|
||||||
- Treat `api-summary.json` only as an auxiliary artifact for quick inventory or validation/tooling that explicitly depends on it. It is never the authoritative generation input.
|
|
||||||
|
|
||||||
# Orchestration Model
|
PIPELINE CONTRACT
|
||||||
|
|
||||||
Use a manager-first, agent-as-tool architecture.
|
1. Parse `domain/*.dsl`.
|
||||||
|
2. Generate or refresh `domain-summary.json`.
|
||||||
|
3. Scaffold with official framework CLI conventions where scaffolding is needed.
|
||||||
|
4. Generate or maintain backend/frontend code in `server/` and `client/`.
|
||||||
|
5. Generate or maintain the root-level realm artifact.
|
||||||
|
6. Generate or maintain env/bootstrap/runtime artifacts.
|
||||||
|
7. Run the lightweight validation gate.
|
||||||
|
|
||||||
- Keep one orchestrator in charge of planning, sequencing, integration, and final acceptance.
|
REPAIR-BEFORE-GENERATE ORDER
|
||||||
- Delegate bounded work to specialists; do not let sub-agents redefine the source hierarchy or completion criteria.
|
|
||||||
- Delegate by stage or artifact family, and by entity when parallelism helps.
|
|
||||||
- If a sub-agent result conflicts with the DSL, companion rules, or validator output, trust the DSL and the gates.
|
|
||||||
|
|
||||||
Mandatory delegation pattern for future runs:
|
1. Inspect whether `server/` and `client/` are still valid framework workspaces.
|
||||||
|
2. If either workspace is degraded, repair the official scaffold baseline first.
|
||||||
|
3. Only after workspace repair, generate or update domain-derived feature code.
|
||||||
|
4. Only after generation, run validation and buildability checks.
|
||||||
|
|
||||||
- `explorer`
|
CLI-FIRST SCAFFOLDING CONTRACT
|
||||||
Use first for repo discovery, scaffold inspection, locating entity-scoped DSL context, and finding existing registrations/seams.
|
|
||||||
- `docs_researcher`
|
|
||||||
Use when framework behavior, CLI scaffolding, or prompt/orchestration patterns need verification against official docs or Context7.
|
|
||||||
- stage worker / generator
|
|
||||||
Use for bounded Prisma, backend, frontend, or auth/runtime implementation work after the orchestrator has assembled the right inputs.
|
|
||||||
- `reviewer`
|
|
||||||
Use before declaring completion. Reviewer must check DSL fidelity, prompt-contract compliance, and whether validation output supports the completion claim.
|
|
||||||
|
|
||||||
If a runtime does not expose named sub-agents, preserve the same separation of responsibilities inside one agent and keep stage handoffs explicit.
|
- Never hand-write a fresh framework workspace from scratch when an official CLI exists for that framework.
|
||||||
|
- For `server/`, initialize the workspace from the official NestJS CLI baseline first, then generate/adapt modules/resources inside that workspace.
|
||||||
|
- For `client/`, initialize the workspace from the official Vite React TypeScript baseline first, then add React Admin, auth seams, and generated resources.
|
||||||
|
- Treat CLI scaffolding as the required baseline for compiler config, package scripts, workspace metadata, and default entry files.
|
||||||
|
- Manual creation is allowed only for domain-derived feature code after the official workspace exists.
|
||||||
|
- If a workspace already exists but is missing required CLI baseline files, repair it back to a valid official-style workspace before adding more generated code.
|
||||||
|
|
||||||
# MCP Usage Model
|
ANTI-REGRESSION FAILURES
|
||||||
|
|
||||||
Use MCP/tools deliberately, not reflexively.
|
Treat the following as baseline violations, not acceptable improvisations:
|
||||||
|
|
||||||
- Filesystem/search tools: gather exact local context before making decisions.
|
- creating a NestJS workspace by manually writing `package.json`, `tsconfig*`, `nest-cli.json`, and `src/*` from memory instead of starting from official CLI conventions
|
||||||
- Shell/runtime tools: run official CLI scaffolding, Prisma commands, builds, validators, and evals. Do not simulate command results from memory.
|
- creating a Vite React TypeScript workspace by manually writing `package.json`, `index.html`, `tsconfig*`, `vite.config.*`, and `src/*` from memory instead of starting from official scaffold conventions
|
||||||
- Context7: verify current NestJS, Prisma, React Admin, Vite, Keycloak, or prompt/orchestration guidance when repository docs are not enough.
|
- deleting required framework scaffold files after generation because the app appears to work with a smaller custom structure
|
||||||
- Web research: only after local files and Context7 are insufficient; prefer primary sources.
|
- declaring generation successful when workspace validity or buildability is broken or unverified
|
||||||
- Diff/validation tools: use before edits, after edits, and always at the end.
|
- letting prompts promise an auth/runtime contract that validation does not enforce
|
||||||
|
- treating one project-specific DSL filename as the only allowed source instead of supporting `domain/*.dsl`
|
||||||
|
|
||||||
Tool-order policy:
|
OUTPUT CONTRACT
|
||||||
|
|
||||||
1. local authoritative files
|
The baseline output must include:
|
||||||
2. Context7 / official docs
|
|
||||||
3. web fallback
|
|
||||||
4. validation gates
|
|
||||||
|
|
||||||
# Generation Roadmap
|
|
||||||
|
|
||||||
## 1. Preparation / Discovery
|
|
||||||
|
|
||||||
Purpose:
|
|
||||||
|
|
||||||
- establish the active scope
|
|
||||||
- verify scaffold health
|
|
||||||
- load only the context needed for the next stage
|
|
||||||
|
|
||||||
Responsible:
|
|
||||||
|
|
||||||
- orchestrator
|
|
||||||
- `explorer` first
|
|
||||||
- `docs_researcher` if scaffold conventions or framework behavior are uncertain
|
|
||||||
|
|
||||||
Mandatory inputs:
|
|
||||||
|
|
||||||
- `AGENTS.md`
|
|
||||||
- `prompts/general-prompt.md`
|
|
||||||
- `domain/toir.api.dsl`
|
|
||||||
- `prompts/runtime-rules.md`
|
|
||||||
- `.codex/AGENTS.md` and `.codex/agents/*.toml` when the runtime supports those agents
|
|
||||||
|
|
||||||
Expected outputs:
|
|
||||||
|
|
||||||
- entity-scoped DSL quotes for the active work
|
|
||||||
- a clean stage plan
|
|
||||||
- `server/` and `client/` confirmed healthy or repaired from official scaffolding
|
|
||||||
|
|
||||||
Handoff:
|
|
||||||
|
|
||||||
- proceed to Prisma only after the repository has a valid NestJS workspace, Vite React TypeScript workspace, or a documented repair plan using official CLIs
|
|
||||||
|
|
||||||
Stage rules:
|
|
||||||
|
|
||||||
- Use official Nest CLI for initial backend workspace creation or repair.
|
|
||||||
- Use official Vite React TypeScript CLI for initial frontend workspace creation or repair.
|
|
||||||
- Use Prisma CLI for Prisma initialization when relevant.
|
|
||||||
- Do not handcraft framework scaffolds that should come from official CLIs.
|
|
||||||
|
|
||||||
## 2. Prisma
|
|
||||||
|
|
||||||
Purpose:
|
|
||||||
|
|
||||||
- generate the repository schema that reflects the DSL exactly
|
|
||||||
|
|
||||||
Responsible:
|
|
||||||
|
|
||||||
- orchestrator
|
|
||||||
- Prisma-focused stage worker
|
|
||||||
- `docs_researcher` when Prisma behavior is uncertain
|
|
||||||
|
|
||||||
Mandatory inputs:
|
|
||||||
|
|
||||||
- entity-scoped DSL quotes from `domain/toir.api.dsl`
|
|
||||||
- `prompts/prisma-rules.md`
|
|
||||||
|
|
||||||
Expected outputs:
|
|
||||||
|
|
||||||
- `server/prisma/schema.prisma`
|
- `server/prisma/schema.prisma`
|
||||||
- Prisma initialization or repair steps completed when the workspace was missing required baseline files
|
|
||||||
|
|
||||||
Handoff:
|
|
||||||
|
|
||||||
- backend generation starts only after schema output reflects the DSL and Prisma setup is coherent with runtime rules
|
|
||||||
|
|
||||||
## 3. Backend
|
|
||||||
|
|
||||||
Purpose:
|
|
||||||
|
|
||||||
- generate NestJS modules, controllers, services, DTOs, and module registration from the DSL contract
|
|
||||||
|
|
||||||
Responsible:
|
|
||||||
|
|
||||||
- orchestrator
|
|
||||||
- backend stage worker, ideally one entity at a time when parallelized
|
|
||||||
|
|
||||||
Mandatory inputs:
|
|
||||||
|
|
||||||
- entity-scoped DSL quotes from `domain/toir.api.dsl`
|
|
||||||
- `prompts/backend-rules.md`
|
|
||||||
|
|
||||||
Expected outputs:
|
|
||||||
|
|
||||||
- `server/src/modules/<entity>/...`
|
|
||||||
- `server/src/app.module.ts`
|
|
||||||
|
|
||||||
Handoff:
|
|
||||||
|
|
||||||
- frontend generation starts only after backend contracts, guards, DTOs, and natural-key behavior align with backend rules
|
|
||||||
|
|
||||||
## 4. Frontend
|
|
||||||
|
|
||||||
Purpose:
|
|
||||||
|
|
||||||
- generate React Admin resources and resource registration that match backend and DSL contracts
|
|
||||||
|
|
||||||
Responsible:
|
|
||||||
|
|
||||||
- orchestrator
|
|
||||||
- frontend stage worker, ideally one entity at a time when parallelized
|
|
||||||
|
|
||||||
Mandatory inputs:
|
|
||||||
|
|
||||||
- entity-scoped DSL quotes from `domain/toir.api.dsl`
|
|
||||||
- `prompts/frontend-rules.md`
|
|
||||||
|
|
||||||
Expected outputs:
|
|
||||||
|
|
||||||
- `client/src/resources/<entity>/...`
|
|
||||||
- `client/src/App.tsx`
|
|
||||||
|
|
||||||
Handoff:
|
|
||||||
|
|
||||||
- auth/runtime integration starts only after frontend resource contracts align with DTO-derived field sets and type mappings
|
|
||||||
|
|
||||||
## 5. Auth / Runtime / Realm Artifacts
|
|
||||||
|
|
||||||
Purpose:
|
|
||||||
|
|
||||||
- wire authentication, environment defaults, realm import, and runtime topology around the generated CRUD app
|
|
||||||
|
|
||||||
Responsible:
|
|
||||||
|
|
||||||
- orchestrator
|
|
||||||
- auth/runtime stage worker
|
|
||||||
- `docs_researcher` when Keycloak or framework integration behavior is uncertain
|
|
||||||
|
|
||||||
Mandatory inputs:
|
|
||||||
|
|
||||||
- `prompts/auth-rules.md`
|
|
||||||
- `prompts/runtime-rules.md`
|
|
||||||
|
|
||||||
Expected outputs:
|
|
||||||
|
|
||||||
- `server/src/auth/`
|
|
||||||
- `client/src/auth/`
|
|
||||||
- `client/src/dataProvider.ts`
|
|
||||||
- `server/.env.example`
|
- `server/.env.example`
|
||||||
- `client/.env.example`
|
- `client/.env.example`
|
||||||
|
- `client/src/auth/keycloak.ts`
|
||||||
|
- `client/src/auth/authProvider.ts`
|
||||||
|
- `client/src/dataProvider.ts`
|
||||||
|
- `server/src/auth/*`
|
||||||
- `docker-compose.yml`
|
- `docker-compose.yml`
|
||||||
- `toir-realm.json`
|
- `domain-summary.json`
|
||||||
|
- root-level `*-realm.json`
|
||||||
|
|
||||||
Handoff:
|
The baseline output must also remain a real framework workspace, not a prompt-only file collection:
|
||||||
|
|
||||||
- verification starts only after auth seams, runtime artifacts, and realm output are aligned with backend/frontend expectations
|
- `server/tsconfig.json`
|
||||||
|
- `server/tsconfig.build.json`
|
||||||
|
- `server/nest-cli.json`
|
||||||
|
- `client/index.html`
|
||||||
|
- `client/tsconfig.json`
|
||||||
|
- `client/vite.config.*`
|
||||||
|
|
||||||
## 6. Verification / Success Gate
|
NON-GOALS
|
||||||
|
|
||||||
Purpose:
|
- No new generator engine
|
||||||
|
- No compiler/IR platform
|
||||||
|
- No heavy codegen redesign
|
||||||
|
- No replacement of the old LLM-first architecture
|
||||||
|
|
||||||
- prove that the generation run is complete and not just plausible
|
COMPLETION INVARIANTS
|
||||||
|
|
||||||
Responsible:
|
- Generation is incomplete if `server/` is not a valid NestJS workspace.
|
||||||
|
- Generation is incomplete if `client/` is not a valid Vite React TypeScript workspace.
|
||||||
|
- Generation is incomplete if auth rules, runtime rules, and validation rules describe different truth paths.
|
||||||
|
- Generation is incomplete if buildability is broken.
|
||||||
|
- If buildability cannot be checked because dependencies are missing, report that state explicitly; do not report a green result for buildability.
|
||||||
|
|
||||||
- orchestrator
|
VALIDATION
|
||||||
- `reviewer` before completion
|
|
||||||
|
|
||||||
Mandatory inputs:
|
Before considering the output complete, satisfy `prompts/validation-rules.md`.
|
||||||
|
|
||||||
- `prompts/validation-rules.md`
|
|
||||||
- validation command output
|
|
||||||
- reviewer findings
|
|
||||||
|
|
||||||
Expected outputs:
|
|
||||||
|
|
||||||
- refreshed auxiliary artifacts if the validator/tooling requires them, including `api-summary.json`
|
|
||||||
- passing validation gates
|
|
||||||
- successful backend and frontend builds
|
|
||||||
|
|
||||||
Handoff:
|
|
||||||
|
|
||||||
- there is no next stage; report complete only when every success criterion below is satisfied
|
|
||||||
|
|
||||||
# Success Criteria
|
|
||||||
|
|
||||||
Generation is successful only if all of the following are true:
|
|
||||||
|
|
||||||
- `server/` exists in the project root
|
|
||||||
- `client/` exists in the project root
|
|
||||||
- the backend builds successfully
|
|
||||||
- the frontend builds successfully
|
|
||||||
- `node tools/validate-generation.mjs --artifacts-only` passes
|
|
||||||
- `npm run eval:generation` passes
|
|
||||||
- required auth/runtime/realm artifacts exist and match their companion rules
|
|
||||||
- module/resource registrations are complete
|
|
||||||
- any validator-required auxiliary artifacts, including `api-summary.json`, are refreshed and consistent
|
|
||||||
- the reviewer has not identified unresolved contract violations
|
|
||||||
|
|
||||||
# Non-Goals / Constraints
|
|
||||||
|
|
||||||
- Do not edit `domain/toir.api.dsl` during generation.
|
|
||||||
- Do not treat `api-summary.json` as the source of truth or default starting point.
|
|
||||||
- Do not inline large backend/frontend/prisma/auth/runtime/validation rule sets into this master prompt; load the companion docs instead.
|
|
||||||
- Do not generate domain artifacts on top of a broken scaffold when official CLI repair is required.
|
|
||||||
- Do not claim success from prompt reasoning alone; use builds and repository gates.
|
|
||||||
- Do not load the full DSL blob when entity-scoped context is enough.
|
|
||||||
|
|
||||||
# Companion Rule Documents
|
|
||||||
|
|
||||||
These documents are mandatory when their stage is active:
|
|
||||||
|
|
||||||
- Prisma stage: `prompts/prisma-rules.md`
|
|
||||||
- Backend stage: `prompts/backend-rules.md`
|
|
||||||
- Frontend stage: `prompts/frontend-rules.md`
|
|
||||||
- Auth / realm stage: `prompts/auth-rules.md`
|
|
||||||
- Runtime / bootstrap stage: `prompts/runtime-rules.md`
|
|
||||||
- Verification stage: `prompts/validation-rules.md`
|
|
||||||
|
|
||||||
The master prompt owns orchestration. Companion docs own artifact-specific detail.
|
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
# Prisma Rules
|
|
||||||
|
|
||||||
<!-- prompt-version: 2.0 -->
|
|
||||||
<!-- applies-to: server/prisma/schema.prisma -->
|
|
||||||
<!-- generated-by: LLM following these rules -->
|
|
||||||
<!-- source-of-truth: domain/toir.api.dsl -->
|
|
||||||
<!-- validated-by: tools/validate-generation.mjs §validateBuildChecks -->
|
|
||||||
|
|
||||||
Use this document during the **Prisma** stage defined in `prompts/general-prompt.md`.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
Generate `server/prisma/schema.prisma` as a faithful reflection of `domain/toir.api.dsl`.
|
|
||||||
|
|
||||||
## Mandatory Inputs
|
|
||||||
|
|
||||||
- `prompts/general-prompt.md`
|
|
||||||
- the relevant entity/enum definitions from `domain/toir.api.dsl`
|
|
||||||
- the existing Prisma header if `server/prisma/schema.prisma` already exists
|
|
||||||
|
|
||||||
`api-summary.json` may be used only as an auxiliary validator/inventory artifact. It is not part of the authoritative Prisma source hierarchy.
|
|
||||||
|
|
||||||
## Expected Output
|
|
||||||
|
|
||||||
- `server/prisma/schema.prisma`
|
|
||||||
|
|
||||||
Never edit the schema manually during normal generation. Change the DSL and regenerate instead.
|
|
||||||
|
|
||||||
## Source Of Truth
|
|
||||||
|
|
||||||
Entity definitions, field types, PKs, FKs, enums, optionality, uniqueness, and defaults come from `domain/toir.api.dsl`.
|
|
||||||
|
|
||||||
## Scalar Type Mapping
|
|
||||||
|
|
||||||
| DSL type | Prisma scalar type |
|
|
||||||
| --------- | ------------------ |
|
|
||||||
| `uuid` | `String` |
|
|
||||||
| `string` | `String` |
|
|
||||||
| `text` | `String` |
|
|
||||||
| `integer` | `Int` |
|
|
||||||
| `number` | `Float` |
|
|
||||||
| `decimal` | `Decimal` |
|
|
||||||
| `date` | `DateTime` |
|
|
||||||
| `boolean` | `Boolean` |
|
|
||||||
| enum name | enum name as-is |
|
|
||||||
|
|
||||||
Unknown DSL types pass through as-is for forward compatibility.
|
|
||||||
|
|
||||||
## Primary Key Rules
|
|
||||||
|
|
||||||
- a field marked `key primary` becomes `@id`
|
|
||||||
- if the primary key type is `uuid` or the field name is `id`, use `@id @default(uuid())`
|
|
||||||
- non-uuid natural keys keep plain `@id`
|
|
||||||
- every entity must resolve to exactly one primary key
|
|
||||||
|
|
||||||
## Optionality And Defaults
|
|
||||||
|
|
||||||
- primary keys are required
|
|
||||||
- fields marked `is required` are required
|
|
||||||
- all other fields are optional with Prisma `?`
|
|
||||||
- non-primary unique fields get `@unique`
|
|
||||||
- `default <value>` maps to Prisma `@default(...)`
|
|
||||||
|
|
||||||
## Foreign Key And Relation Rules
|
|
||||||
|
|
||||||
For a DSL field declared `key foreign { relates Entity.field }`:
|
|
||||||
|
|
||||||
1. emit the FK scalar field first
|
|
||||||
2. add a relation field named `lowerFirst(relatedEntity)`
|
|
||||||
3. if that relation name collides, append `Ref`
|
|
||||||
4. annotate with `@relation(fields: [<fkField>], references: [<referencedField>])`
|
|
||||||
|
|
||||||
Inverse array relations:
|
|
||||||
|
|
||||||
- add inverse array fields for referencing entities automatically
|
|
||||||
- pluralization rules:
|
|
||||||
- `equipment` stays `equipment`
|
|
||||||
- names ending in `s` add `es`
|
|
||||||
- all others add `s`
|
|
||||||
- if the inverse name collides, append `List`
|
|
||||||
|
|
||||||
## Enum Rules
|
|
||||||
|
|
||||||
- every DSL enum becomes a Prisma enum
|
|
||||||
- preserve declaration order
|
|
||||||
- preserve the enum name exactly
|
|
||||||
|
|
||||||
## Header Preservation
|
|
||||||
|
|
||||||
If `server/prisma/schema.prisma` already contains a `generator client { ... }` block, preserve everything before the first `enum` or `model` keyword.
|
|
||||||
|
|
||||||
If no valid header exists, emit:
|
|
||||||
|
|
||||||
```prisma
|
|
||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "postgresql"
|
|
||||||
url = env("DATABASE_URL")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Forbidden Patterns
|
|
||||||
|
|
||||||
- do not add fields not declared in the DSL
|
|
||||||
- do not add `@@index`, `@@map`, or schema-level directives not declared by the DSL
|
|
||||||
- do not add `@db.*` modifiers
|
|
||||||
- do not change the datasource provider away from `postgresql`
|
|
||||||
|
|
||||||
## Completion Expectations
|
|
||||||
|
|
||||||
Prisma generation is incomplete if any of the following is true:
|
|
||||||
|
|
||||||
- `server/prisma/schema.prisma` does not exist
|
|
||||||
- the schema no longer reflects the DSL
|
|
||||||
- required relation fields or inverse arrays are missing
|
|
||||||
- header generation or preservation breaks Prisma baseline behavior
|
|
||||||
@@ -1,86 +1,96 @@
|
|||||||
# Runtime Rules
|
# Runtime Rules
|
||||||
|
|
||||||
<!-- prompt-version: 2.0 -->
|
This repository keeps the current LLM-first CRUD generation architecture as the primary working baseline and strengthens the existing pipeline instead of replacing it.
|
||||||
<!-- applies-to: docker-compose.yml, server/.env.example, client/.env.example -->
|
|
||||||
<!-- validated-by: tools/validate-generation.mjs §validateRuntimeContractChecks -->
|
|
||||||
|
|
||||||
Use this document during the **Preparation / Discovery** and **Auth / Runtime / Realm** stages defined in `prompts/general-prompt.md`.
|
## Baseline runtime topology
|
||||||
|
|
||||||
## Purpose
|
- `server/` is the active backend target output path.
|
||||||
|
- `client/` is the active frontend target output path.
|
||||||
|
- Docker scope remains PostgreSQL only.
|
||||||
|
- Keycloak remains external to the repository runtime.
|
||||||
|
- The project remains LLM-first: markdown knowledge blocks in `prompts/` orchestrate generation, while active generated/maintained code lives in `server/` and `client/`.
|
||||||
|
|
||||||
Define the runtime topology, environment defaults, scaffold expectations, and bootstrap sequence for a buildable generated workspace.
|
## Required input and derived artifacts
|
||||||
|
|
||||||
## Mandatory Inputs
|
- Source of truth:
|
||||||
|
- `domain/*.dsl`
|
||||||
|
- Required derived artifacts:
|
||||||
|
- `domain-summary.json`
|
||||||
|
- root-level `*-realm.json`
|
||||||
|
|
||||||
- `prompts/general-prompt.md`
|
`domain-summary.json` exists to stabilize generation and validation; it must be regenerated from the DSL and treated as non-authoritative.
|
||||||
- `prompts/auth-rules.md` when runtime changes affect auth defaults or seams
|
|
||||||
- current repository runtime/auth defaults
|
|
||||||
|
|
||||||
`api-summary.json` is an auxiliary artifact only. Refresh it when validator/tooling requires freshness checks or when a compact inventory helps discovery. Do not treat it as the runtime source of truth.
|
## Output contract
|
||||||
|
|
||||||
## Expected Outputs
|
The strengthened baseline must produce and keep aligned:
|
||||||
|
|
||||||
|
- `server/prisma/schema.prisma`
|
||||||
|
- backend/frontend env examples
|
||||||
|
- backend/frontend auth seams
|
||||||
|
- root `.gitignore`, `server/.gitignore`, `client/.gitignore`
|
||||||
- `docker-compose.yml`
|
- `docker-compose.yml`
|
||||||
- `server/.env.example`
|
- `domain-summary.json`
|
||||||
- `client/.env.example`
|
- root-level realm import artifact
|
||||||
- a buildable NestJS workspace under `server/`
|
|
||||||
- a buildable Vite React TypeScript workspace under `client/`
|
|
||||||
- any validator-required auxiliary artifacts such as `api-summary.json`
|
|
||||||
|
|
||||||
## Baseline Runtime Topology
|
## Concrete runtime examples
|
||||||
|
|
||||||
- `server/` is the backend output path
|
Use these as the baseline examples for this project unless the prompt explicitly overrides them:
|
||||||
- `client/` is the frontend output path
|
|
||||||
- Docker scope stays PostgreSQL-only
|
|
||||||
- Keycloak remains external to repository runtime
|
|
||||||
- the project remains LLM-first and prompt-driven
|
|
||||||
|
|
||||||
## Concrete Runtime Defaults
|
- Backend:
|
||||||
|
- `PORT=3000`
|
||||||
|
- `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/toir"`
|
||||||
|
- `CORS_ALLOWED_ORIGINS="http://localhost:5173,https://toir-frontend.greact.ru"`
|
||||||
|
- `KEYCLOAK_ISSUER_URL="https://sso.greact.ru/realms/toir"`
|
||||||
|
- `KEYCLOAK_AUDIENCE="toir-backend"`
|
||||||
|
- Frontend:
|
||||||
|
- `VITE_API_URL=http://localhost:3000`
|
||||||
|
- `VITE_KEYCLOAK_URL=https://sso.greact.ru`
|
||||||
|
- `VITE_KEYCLOAK_REALM=toir`
|
||||||
|
- `VITE_KEYCLOAK_CLIENT_ID=toir-frontend`
|
||||||
|
|
||||||
Backend:
|
These example values come from the already working runtime shape and are preferred over local-only Keycloak placeholders.
|
||||||
|
|
||||||
- `PORT=3000`
|
## Runtime bootstrap
|
||||||
- `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/toir"`
|
|
||||||
- `CORS_ALLOWED_ORIGINS="http://localhost:5173,https://toir-frontend.greact.ru"`
|
|
||||||
- `KEYCLOAK_ISSUER_URL="https://sso.greact.ru/realms/toir"`
|
|
||||||
- `KEYCLOAK_AUDIENCE="toir-backend"`
|
|
||||||
|
|
||||||
Frontend:
|
1. Import the root-level realm artifact into Keycloak.
|
||||||
|
2. Start PostgreSQL with `docker compose up -d`.
|
||||||
|
3. From `server/` run:
|
||||||
|
- initialize or repair the workspace with official Nest CLI scaffolding if required before generating domain code
|
||||||
|
- `npm install`
|
||||||
|
- `npx prisma generate`
|
||||||
|
- `npx prisma migrate dev`
|
||||||
|
- `npx prisma db seed`
|
||||||
|
- `npm run build`
|
||||||
|
- `npm run start`
|
||||||
|
4. From `client/` run:
|
||||||
|
- initialize or repair the workspace with official Vite React TypeScript scaffolding if required before generating app code
|
||||||
|
- `npm install`
|
||||||
|
- `npm run build`
|
||||||
|
- `npm run dev`
|
||||||
|
|
||||||
- `VITE_API_URL=http://localhost:3000`
|
## Recovery and completion rules
|
||||||
- `VITE_KEYCLOAK_URL=https://sso.greact.ru`
|
|
||||||
- `VITE_KEYCLOAK_REALM=toir`
|
|
||||||
- `VITE_KEYCLOAK_CLIENT_ID=toir-frontend`
|
|
||||||
|
|
||||||
## Scaffold Expectations
|
- Repair degraded framework workspaces before applying any new domain-derived generation changes.
|
||||||
|
- Do not mark generation complete while `server/` or `client/` remains non-buildable.
|
||||||
|
- If dependency installation has not happened yet, buildability may be reported as skipped, but it must never be reported as green without verification.
|
||||||
|
- Runtime/bootstrap instructions are reusable project baseline rules; TOiR names remain examples, not the only supported domain project.
|
||||||
|
|
||||||
- new or repaired backend workspaces start from the official Nest CLI
|
## Scaffold expectations
|
||||||
- new or repaired frontend workspaces start from the official Vite React TypeScript CLI
|
|
||||||
- Prisma initialization uses the official Prisma CLI when relevant
|
|
||||||
- the LLM may customize generated code after scaffold creation, but must not replace official initialization with ad hoc file creation
|
|
||||||
|
|
||||||
## Runtime Bootstrap
|
- NestJS workspace creation should follow the official Nest CLI path for new applications and resource scaffolding.
|
||||||
|
- Vite frontend creation should follow the official Vite `create-vite` path for React TypeScript applications.
|
||||||
|
- The LLM may customize generated code after scaffold creation, but must not replace official workspace initialization with ad hoc file creation.
|
||||||
|
|
||||||
1. import `toir-realm.json` into Keycloak
|
## Common generation failures to avoid
|
||||||
2. start PostgreSQL with `docker compose up -d`
|
|
||||||
3. from `server/`:
|
|
||||||
- repair or create the workspace with official Nest CLI if needed
|
|
||||||
- install dependencies
|
|
||||||
- run Prisma commands required by the schema stage
|
|
||||||
- run `npm run build`
|
|
||||||
- run `npm run start`
|
|
||||||
4. from `client/`:
|
|
||||||
- repair or create the workspace with official Vite CLI if needed
|
|
||||||
- install dependencies
|
|
||||||
- run `npm run build`
|
|
||||||
- run `npm run dev`
|
|
||||||
|
|
||||||
## Completion Expectations
|
- starting feature generation before scaffold repair
|
||||||
|
- leaving deleted framework config files unrepaired because the current diff looks smaller
|
||||||
|
- accepting a form-only validation pass while buildability is unknown
|
||||||
|
- binding runtime rules to one project-specific DSL filename instead of `domain/*.dsl`
|
||||||
|
|
||||||
Runtime preparation is incomplete if any of the following is true:
|
## Baseline intent
|
||||||
|
|
||||||
- `server/` is missing or not buildable as a NestJS workspace
|
- No new generator engine
|
||||||
- `client/` is missing or not buildable as a Vite React TypeScript workspace
|
- No compiler platform
|
||||||
- framework scaffolding was hand-built instead of created or repaired from official CLIs
|
- No planner/emitter/runtime redesign
|
||||||
- shared env defaults drift from the repository auth/runtime contract
|
- Only the current LLM-first pipeline, strengthened by summary, realm, and validation artifacts
|
||||||
- runtime success is claimed without actual build verification
|
|
||||||
|
|||||||
@@ -1,101 +1,98 @@
|
|||||||
# Validation Rules
|
# Validation Rules
|
||||||
|
|
||||||
<!-- prompt-version: 2.0 -->
|
Validation is now a lightweight automated gate instead of a prose-only checklist.
|
||||||
<!-- applies-to: tools/validate-generation.mjs and npm run eval:generation -->
|
|
||||||
<!-- validated-by: self -->
|
|
||||||
|
|
||||||
Use this document during the **Verification / Success Gate** stage defined in `prompts/general-prompt.md`.
|
## Commands
|
||||||
|
|
||||||
## Purpose
|
- `npm run generate:domain-summary`
|
||||||
|
- `npm run validate:generation`
|
||||||
Define the repository gates that convert a plausible generation run into a verified one.
|
- `npm run validate:generation:runtime`
|
||||||
|
|
||||||
## Primary Gates
|
|
||||||
|
|
||||||
- `node tools/validate-generation.mjs --artifacts-only`
|
|
||||||
- `npm run eval:generation`
|
|
||||||
|
|
||||||
## Auxiliary Freshness Prep
|
|
||||||
|
|
||||||
- `npm run generate:api-summary`
|
|
||||||
|
|
||||||
Run the freshness prep when the repository validator or supporting tooling expects `api-summary.json` to exist and match the current DSL. This artifact is auxiliary to validation and inventory, not the generation source of truth.
|
|
||||||
|
|
||||||
## Prompt-Gate Alignment Rule
|
## Prompt-Gate Alignment Rule
|
||||||
|
|
||||||
- every invariant marked required in the active prompt corpus must either be enforced by a gate or called out as manual/runtime-only
|
- Every invariant described as required in the active prompt corpus must either be enforced by this gate or be called out explicitly as a manual/runtime-only check.
|
||||||
- validation must not silently ignore a forbidden pattern
|
- Validation must not stay silent about a violation that the prompts describe as forbidden.
|
||||||
- build verification must not be reported as green when it was skipped
|
- Validation must not report green buildability when build verification was skipped.
|
||||||
|
|
||||||
## Gate Groups
|
## Gate groups
|
||||||
|
|
||||||
### Build Checks
|
### Build checks
|
||||||
|
|
||||||
- at least one `domain/*.api.dsl` file exists
|
- at least one `domain/*.dsl` file exists
|
||||||
- required artifacts exist:
|
- required artifacts exist
|
||||||
- `server/prisma/schema.prisma`
|
- Prisma schema exists
|
||||||
- env examples
|
- frontend/backend env contracts exist
|
||||||
- required scaffold files
|
- frontend/backend framework workspace files exist
|
||||||
- auth/runtime/realm artifacts
|
- `domain-summary.json` matches the current DSL
|
||||||
- if the current validator policy checks `api-summary.json`, it exists and is fresh relative to the DSL
|
- project `.env.example` files keep the working domain-based Keycloak examples unless explicitly overridden
|
||||||
- `server/` remains a valid Nest workspace
|
- `server/` remains a valid Nest workspace
|
||||||
- `client/` remains a valid Vite workspace
|
- `client/` remains a valid Vite workspace
|
||||||
- if dependencies are installed, backend and frontend build verification runs
|
- generation must not pass validation if framework scaffolding files were deleted and replaced by a hand-written minimal skeleton
|
||||||
|
- if dependencies are installed, build verification runs for `server/` and `client/`
|
||||||
- if dependencies are missing, build verification is reported as skipped with reason instead of green
|
- if dependencies are missing, build verification is reported as skipped with reason instead of green
|
||||||
|
|
||||||
### Auth Checks
|
### Auth checks
|
||||||
|
|
||||||
- frontend auth seam files exist
|
- frontend auth seam files exist
|
||||||
- backend auth seam files exist
|
- backend auth seam files exist
|
||||||
- `401` and `403` semantics remain split
|
- `401` and `403` semantics stay split
|
||||||
- auth code keeps the required Keycloak/JWT contracts
|
- auth code keeps the required Keycloak/JWT contracts
|
||||||
- JWKS resolution order remains:
|
- JWKS resolution chain matches the contract:
|
||||||
1. explicit `KEYCLOAK_JWKS_URL`
|
1. explicit `KEYCLOAK_JWKS_URL`
|
||||||
2. OIDC discovery
|
2. OIDC discovery
|
||||||
3. certs fallback
|
3. certs fallback
|
||||||
|
|
||||||
### Filter And UI Checks
|
### Filter checks
|
||||||
|
|
||||||
- list resources expose filter UI including `FilterButton`
|
- list resources expose filter UI (including `FilterButton`)
|
||||||
- reference filters use `ReferenceInput` + `AutocompleteInput` with `filterToQuery`
|
- reference filters use `ReferenceInput` + `AutocompleteInput` with `filterToQuery`
|
||||||
- `dataProvider` preserves repeated query params for array filters
|
- data provider preserves repeated query params for array filters
|
||||||
- backend FK filters remain exact-match
|
- backend FK filters keep exact-match semantics
|
||||||
- repeated enum params map to Prisma `in`
|
- enum repeated params are mapped to Prisma `in`
|
||||||
- Create/Edit forms keep type-correct inputs
|
- typed form mapping is preserved:
|
||||||
- navigable references keep `ReferenceField link="show"`
|
- `integer` / `decimal` -> `NumberInput`
|
||||||
|
- `date` -> `DateInput`
|
||||||
|
- reference fields intended for navigation keep `ReferenceField link="show"`
|
||||||
- resources keep `show={...}` registration in `App.tsx`
|
- resources keep `show={...}` registration in `App.tsx`
|
||||||
|
|
||||||
### Natural-Key Checks
|
### Natural-key checks
|
||||||
|
|
||||||
- response records expose `id`
|
- response records expose `id`
|
||||||
- route/update contracts use the real primary key
|
- route/update contracts use the real primary key
|
||||||
- natural-key sort/update paths do not regress to a fake physical `id`
|
- natural-key sort/update paths do not regress to a fake physical `id`
|
||||||
|
|
||||||
### Realm Checks
|
### Realm checks
|
||||||
|
|
||||||
- a root `*-realm.json` artifact exists
|
- a root `*-realm.json` artifact exists
|
||||||
- required roles, audience delivery, and claims remain explicit
|
- realm roles exist
|
||||||
- SPA and backend client structure remains explicit
|
- audience delivery exists
|
||||||
|
- required claims are explicit
|
||||||
|
- SPA/backend client structure is explicit
|
||||||
|
|
||||||
### Runtime Checks
|
### Runtime checks
|
||||||
|
|
||||||
- Docker topology remains PostgreSQL-only
|
- compose topology stays PostgreSQL-only
|
||||||
- Prisma lifecycle commands remain available where required
|
- Prisma lifecycle scripts remain in place
|
||||||
- `/health` remains public
|
- `/health` stays public
|
||||||
- backend build runs inside `server/`
|
- backend can execute `npm run build` inside `server/`
|
||||||
- frontend build runs inside `client/`
|
- frontend can execute `npm run build` inside `client/` after dependencies are installed
|
||||||
- client/server `.env.example` stay aligned with repository defaults
|
- client/server `.env.example` stay aligned with the working runtime defaults:
|
||||||
|
- `https://sso.greact.ru`
|
||||||
|
- `toir`
|
||||||
|
- `toir-frontend`
|
||||||
|
- `toir-backend`
|
||||||
|
- `https://toir-frontend.greact.ru`
|
||||||
|
- optional runtime execution mode runs:
|
||||||
|
- `npx prisma generate`
|
||||||
|
- `npx prisma migrate dev`
|
||||||
|
- `npx prisma db seed`
|
||||||
|
|
||||||
### Output Contract Checks
|
### Scaffold checks
|
||||||
|
|
||||||
- every generated Create/Update DTO imports from `'class-validator'`
|
- backend initialization starts from official Nest CLI scaffolding
|
||||||
- DTO fields have type-correct decorators
|
- frontend initialization starts from official Vite React TypeScript scaffolding
|
||||||
- optional/nullable fields carry `@IsOptional()` before the type decorator
|
- feature generation happens after scaffold creation, not instead of scaffold creation
|
||||||
- controllers carry the required guards and roles
|
- repair happens before generation when workspace is degraded
|
||||||
- React Admin components use correct input/field types
|
- required framework configs and entry files must survive subsequent LLM edits
|
||||||
|
|
||||||
### Eval Harness
|
The automated gate is intentionally small. It enforces the critical reproducibility contract without turning the repository into a test platform or a generator engine.
|
||||||
|
|
||||||
- `npm run eval:generation` runs fixture-based semantic checks
|
|
||||||
- eval failures block completion
|
|
||||||
- prompt changes that break evals are regressions, not acceptable simplifications
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
.git
|
|
||||||
coverage
|
coverage
|
||||||
*.md
|
.git
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
npm-debug.log*
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/toir"
|
|||||||
CORS_ALLOWED_ORIGINS="http://localhost:5173,https://toir-frontend.greact.ru"
|
CORS_ALLOWED_ORIGINS="http://localhost:5173,https://toir-frontend.greact.ru"
|
||||||
KEYCLOAK_ISSUER_URL="https://sso.greact.ru/realms/toir"
|
KEYCLOAK_ISSUER_URL="https://sso.greact.ru/realms/toir"
|
||||||
KEYCLOAK_AUDIENCE="toir-backend"
|
KEYCLOAK_AUDIENCE="toir-backend"
|
||||||
KEYCLOAK_JWKS_URL=""
|
# Optional: if omitted, backend uses OIDC discovery and then falls back to issuer + /protocol/openid-connect/certs
|
||||||
|
# KEYCLOAK_JWKS_URL="https://sso.greact.ru/realms/toir/protocol/openid-connect/certs"
|
||||||
|
|||||||
25
server/.eslintrc.js
Normal file
25
server/.eslintrc.js
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
||||||
35
server/.gitignore
vendored
35
server/.gitignore
vendored
@@ -1,5 +1,32 @@
|
|||||||
node_modules
|
# Dependencies
|
||||||
# Keep environment variables out of version control
|
node_modules/
|
||||||
.env
|
|
||||||
|
|
||||||
/generated/prisma
|
# 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?
|
||||||
|
|||||||
@@ -2,22 +2,19 @@ FROM node:20-bookworm-slim AS build
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG DATABASE_URL=postgresql://postgres:postgres@localhost:5432/toir
|
|
||||||
ENV DATABASE_URL=$DATABASE_URL
|
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends openssl \
|
&& apt-get install -y --no-install-recommends openssl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
COPY prisma ./prisma
|
COPY prisma ./prisma
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY nest-cli.json tsconfig*.json prisma.config.ts ./
|
COPY nest-cli.json tsconfig*.json ./
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
|
|
||||||
RUN npx prisma generate
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:20-bookworm-slim AS runtime
|
FROM node:20-bookworm-slim AS runtime
|
||||||
@@ -27,18 +24,14 @@ WORKDIR /app
|
|||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends openssl ca-certificates curl \
|
&& apt-get install -y --no-install-recommends openssl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=build /app/package*.json ./
|
COPY --from=build /app/package*.json ./
|
||||||
COPY --from=build /app/node_modules ./node_modules
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
COPY --from=build /app/prisma ./prisma
|
COPY --from=build /app/prisma ./prisma
|
||||||
COPY --from=build /app/prisma.config.ts ./prisma.config.ts
|
|
||||||
COPY --from=build /app/dist ./dist
|
COPY --from=build /app/dist ./dist
|
||||||
COPY docker-entrypoint.sh ./docker-entrypoint.sh
|
|
||||||
|
|
||||||
RUN chmod +x ./docker-entrypoint.sh
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["./docker-entrypoint.sh"]
|
CMD ["sh", "-c", "npx prisma migrate deploy && node dist/src/main.js"]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user