Files
crank/docs/data-model.md
T
2026-03-28 00:58:56 +03:00

727 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Модель данных
## 1. Назначение документа
Этот документ фиксирует формальную модель данных платформы. Его цель - определить такие структуры, которые можно без существенных изменений перенести в:
- Rust domain types,
- HTTP DTO,
- структуру таблиц БД,
- runtime-представление operation,
- UI-формы и конфигурационные экраны.
Документ не привязан к конкретной СУБД, но задает каноническую JSON-модель сущностей.
## 2. Общие принципы модели
### 2.1. Одна операция - один tool
Каждая `Operation` соответствует одному MCP tool. Это особенно важно для:
- GraphQL, где одна operation соответствует одному конкретному `query` или `mutation`;
- gRPC, где одна operation соответствует одному unary-методу;
- REST, где одна operation соответствует одному endpoint-сценарию.
### 2.2. Внутренний транспортный формат - JSON
Независимо от внешнего протокола внутри системы данные должны быть представлены в JSON-ориентированном виде. Даже если внешний вызов работает с protobuf, runtime, mapping и UI опираются на нормализованный JSON.
### 2.3. Mapping всегда явный
Даже если система умеет строить черновой mapping по примерам данных, итоговая конфигурация mapping должна быть явно сохранена в operation. Нельзя полагаться на неявную "магию" сопоставления во время выполнения.
### 2.4. JSONPath как единый язык адресации
Для input и output mapping используется `JSONPath`. Это позволяет единообразно ссылаться на вложенные поля во входе, промежуточном представлении запроса и нормализованном ответе.
### 2.5. YAML как формат обмена конфигурацией
Помимо канонической JSON-модели система должна поддерживать импорт и экспорт конфигураций в `YAML`. Это внешний формат обмена, а не отдельная доменная модель.
## 3. Корневая сущность `Operation`
`Operation` - основная конфигурационная сущность платформы.
### Поля
- `id` - уникальный идентификатор операции.
- `name` - стабильное внутреннее имя.
- `display_name` - отображаемое имя в UI.
- `protocol` - `rest`, `graphql`, `grpc`.
- `status` - `draft`, `testing`, `published`, `archived`.
- `version` - версия конфигурации операции.
- `target` - описание внешней операции.
- `input_schema` - схема MCP-входа.
- `output_schema` - схема MCP-выхода.
- `input_mapping` - правила подготовки внешнего запроса.
- `output_mapping` - правила формирования MCP-ответа.
- `execution_config` - auth, headers, timeout, retries и protocol-specific execution settings.
- `tool_description` - описание tool для MCP и LLM.
- `samples` - загруженные образцы JSON и schema artifacts.
- `generated_draft` - автоматически построенный черновик схем и mappings.
- `config_export` - опциональные метаданные экспортируемой конфигурации.
- `created_at`
- `updated_at`
- `published_at`
### Пример
```json
{
"id": "op_01hr7w0m6p8x9z4n7s2k3q5t6u",
"name": "crm_create_lead",
"display_name": "Create Lead",
"protocol": "rest",
"status": "draft",
"version": 3,
"target": {
"kind": "rest",
"base_url": "https://api.example.com",
"method": "POST",
"path_template": "/v1/leads"
},
"input_schema": {
"type": "object",
"fields": {
"name": {
"type": "string",
"required": true
},
"email": {
"type": "string",
"required": true
}
}
},
"output_schema": {
"type": "object",
"fields": {
"id": {
"type": "string",
"required": true
},
"status": {
"type": "string",
"required": true
}
}
},
"input_mapping": {
"rules": [
{
"source": "$.mcp.name",
"target": "$.request.body.name"
},
{
"source": "$.mcp.email",
"target": "$.request.body.email"
}
]
},
"output_mapping": {
"rules": [
{
"source": "$.response.body.id",
"target": "$.output.id"
},
{
"source": "$.response.body.status",
"target": "$.output.status"
}
]
},
"execution_config": {
"timeout_ms": 10000,
"auth_profile_ref": "auth_01hr7x8rj2d8nq8v0c4m4t1r9e"
},
"tool_description": {
"title": "Create CRM lead",
"description": "Creates a new lead in CRM by name and email."
},
"samples": {
"input_json_sample_ref": "file_01hr7xj7z4k1t0qm0cxsw6f1wx",
"output_json_sample_ref": "file_01hr7xkvt3m1p6ms3m7r2dr8wz"
},
"generated_draft": {
"status": "available",
"source_types": ["input_json_sample", "output_json_sample"]
},
"config_export": {
"format_version": "1",
"export_mode": "portable"
},
"created_at": "2026-03-25T08:00:00Z",
"updated_at": "2026-03-25T08:10:00Z",
"published_at": null
}
```
## 4. `Target`
`Target` описывает конкретный внешний вызов. Это discriminated union по протоколу.
### 4.1. `RestTarget`
```json
{
"kind": "rest",
"base_url": "https://api.example.com",
"method": "PATCH",
"path_template": "/v1/users/{userId}",
"static_headers": {
"X-App-Source": "crank"
}
}
```
Поля:
- `kind`
- `base_url`
- `method`
- `path_template`
- `static_headers`
### 4.2. `GraphqlTarget`
```json
{
"kind": "graphql",
"endpoint": "https://api.example.com/graphql",
"operation_type": "mutation",
"operation_name": "CreateLead",
"query_template": "mutation CreateLead($input: LeadInput!) { createLead(input: $input) { id status } }",
"response_path": "$.response.body.data.createLead"
}
```
Поля:
- `kind`
- `endpoint`
- `operation_type`
- `operation_name`
- `query_template`
- `response_path`
### 4.3. `GrpcTarget`
```json
{
"kind": "grpc",
"server_addr": "https://grpc.example.com:443",
"package": "crm.v1",
"service": "LeadService",
"method": "CreateLead",
"descriptor_ref": "desc_01hr7yn4d6g1x6vwt7h9n0e7ab",
"descriptor_set_b64": "<base64-encoded-descriptor-set>"
}
```
Поля:
- `kind`
- `server_addr`
- `package`
- `service`
- `method`
- `descriptor_ref`
- `descriptor_set_b64`
`descriptor_ref` остается ссылкой на загруженный descriptor artifact в storage и registry.
`descriptor_set_b64` - runtime-ready snapshot descriptor set, который используется unary gRPC adapter для динамического вызова метода без генерации Rust-кода.
## 5. `Schema`
`Schema` - нормализованное описание входа или выхода. Это не JSON Schema в полном объеме, а внутренняя структурная модель, удобная для UI и runtime.
### Базовая форма
```json
{
"type": "object",
"description": "Lead input",
"fields": {
"name": {
"type": "string",
"required": true,
"description": "Lead full name"
},
"tags": {
"type": "array",
"required": false,
"items": {
"type": "string"
}
}
}
}
```
### Поддерживаемые типы
- `object`
- `array`
- `string`
- `integer`
- `number`
- `boolean`
- `enum`
- `null`
- `oneof`
### Модель поля
```json
{
"type": "string",
"required": true,
"nullable": false,
"description": "User email",
"default": null
}
```
Тип объекта:
```json
{
"type": "object",
"required": true,
"fields": {
"email": {
"type": "string",
"required": true
}
}
}
```
Тип массива:
```json
{
"type": "array",
"required": false,
"items": {
"type": "object",
"fields": {
"id": {
"type": "string",
"required": true
}
}
}
}
```
### Нормализация protobuf-структур
Для protobuf -> schema bridge дополнительно фиксируются такие правила:
- `repeated` поле преобразуется в `array`;
- `enum` преобразуется в `type: enum` со списком `enum_values`;
- `map<K, V>` преобразуется в `array` объектов `{ key, value }`;
- `oneof` преобразуется в `type: oneof`, где каждый вариант представлен объектом с одним допустимым полем.
## 6. `MappingSet` и `MappingRule`
`MappingSet` - набор правил преобразования между внутренним MCP input/output и protocol-specific request/response model.
### `MappingSet`
```json
{
"rules": [
{
"source": "$.mcp.user_id",
"target": "$.request.path.userId"
}
]
}
```
### `MappingRule`
Поля:
- `source` - `JSONPath` в исходном контексте.
- `target` - `JSONPath` в целевом контексте.
- `required` - обязательно ли правило для корректного вызова.
- `default_value` - значение по умолчанию.
- `transform` - встроенное преобразование.
- `condition` - условие применения правила.
- `notes` - служебное описание для UI.
Пример:
```json
{
"source": "$.mcp.profile.email",
"target": "$.request.body.contact.email",
"required": true,
"default_value": null,
"transform": {
"kind": "identity"
},
"condition": null,
"notes": "Map email to CRM contact payload"
}
```
### Контексты `source` и `target`
Для input mapping:
- `$.mcp.*`
- `$.request.path.*`
- `$.request.query.*`
- `$.request.headers.*`
- `$.request.body.*`
- `$.request.variables.*`
- `$.request.grpc.*`
Для output mapping:
- `$.response.body.*`
- `$.response.data.*`
- `$.response.grpc.*`
- `$.output.*`
Для MVP достаточно управляемого подмножества `JSONPath`:
- путь всегда начинается с фиксированного root context;
- дальше используются dot-separated поля;
- для массивов допускаются numeric indexes вида `[0]`;
- quoted selectors, filter expressions и произвольные функции не поддерживаются.
### `Transform`
Для MVP transformations должны быть ограничены:
- `identity`
- `to_string`
- `to_number`
- `to_boolean`
- `join`
- `split`
- `wrap_array`
- `unwrap_singleton`
Пример:
```json
{
"kind": "to_string"
}
```
### Черновая генерация mapping
Для MVP generation draft mapping может опираться на простое правило:
- система сопоставляет уникальные leaf-поля с одинаковыми именами в source sample и target sample;
- неоднозначные совпадения автоматически не связываются;
- результат всегда остается черновиком и требует ручной проверки оператором.
## 7. `ExecutionConfig`
`ExecutionConfig` задает параметры выполнения operation.
```json
{
"timeout_ms": 10000,
"retry_policy": {
"max_attempts": 1
},
"auth_profile_ref": "auth_01hr7x8rj2d8nq8v0c4m4t1r9e",
"headers": {
"X-Client": "crank"
},
"protocol_options": {
"rest": null,
"graphql": null,
"grpc": {
"use_tls": true
}
}
}
```
Поля:
- `timeout_ms`
- `retry_policy`
- `auth_profile_ref`
- `headers`
- `protocol_options`
Важно:
- здесь хранятся execution settings, а не описание бизнес-схемы;
- `protocol_options` должны оставаться узкими и протокол-специфичными;
- секреты не хранятся внутри operation, только ссылки на secret store или auth profile.
## 8. `AuthProfile`
`AuthProfile` - переиспользуемая конфигурация аутентификации для внешних вызовов.
Operation ссылается на auth profile через `auth_profile_ref`, а не хранит секреты внутри себя.
### Базовая форма
```json
{
"id": "auth_01hr7x8rj2d8nq8v0c4m4t1r9e",
"name": "crm-prod-bearer",
"kind": "bearer",
"config": {
"header_name": "Authorization",
"secret_ref": "secret://auth/crm-prod-token"
},
"created_at": "2026-03-25T08:00:00Z",
"updated_at": "2026-03-25T08:10:00Z"
}
```
### Поддерживаемые виды
- `bearer`
- `basic`
- `api_key_header`
- `api_key_query`
### MVP-решение по секретам
Для MVP секреты должны храниться не в open text внутри operation version, а в отдельном secret storage слое.
Рекомендуемое решение:
- логическая ссылка в формате `secret://...`;
- реальное значение хранится в приложении либо в зашифрованном хранилище, либо в env-backed secret store;
- в документации и YAML export секреты всегда представляются только через `secret_ref`.
## 9. `ToolDescription`
`ToolDescription` задает MCP-представление operation.
```json
{
"title": "Get customer profile",
"description": "Returns a customer profile by external customer identifier.",
"tags": ["crm", "customer"],
"examples": [
{
"input": {
"customer_id": "123"
}
}
]
}
```
Поля:
- `title`
- `description`
- `tags`
- `examples`
## 10. `Samples`
`Samples` связывает operation с загруженными артефактами, на основе которых может быть построен черновик.
```json
{
"input_json_sample_ref": "file_01hr7xj7z4k1t0qm0cxsw6f1wx",
"output_json_sample_ref": "file_01hr7xkvt3m1p6ms3m7r2dr8wz",
"proto_file_ref": null,
"descriptor_ref": null
}
```
Поля:
- `input_json_sample_ref`
- `output_json_sample_ref`
- `proto_file_ref`
- `descriptor_ref`
Примечания:
- для REST чаще всего используются входной и выходной JSON samples;
- для GraphQL чаще всего полезен sample ответа;
- для gRPC основным artifact остается `.proto` или descriptor set, но input/output JSON samples тоже могут использоваться для MCP-facing модели.
## 11. `GeneratedDraft`
`GeneratedDraft` хранит результат автоматической генерации схем и mappings.
```json
{
"status": "available",
"source_types": ["input_json_sample", "output_json_sample"],
"generated_at": "2026-03-25T08:05:00Z",
"input_schema_generated": true,
"output_schema_generated": true,
"input_mapping_generated": true,
"output_mapping_generated": true,
"warnings": [
"Field $.response.body.meta was not mapped automatically"
]
}
```
Поля:
- `status` - `none`, `available`, `stale`, `failed`
- `source_types`
- `generated_at`
- `input_schema_generated`
- `output_schema_generated`
- `input_mapping_generated`
- `output_mapping_generated`
- `warnings`
Важно:
- generated draft - это не runtime-источник истины;
- runtime использует только сохраненные `input_schema`, `output_schema`, `input_mapping`, `output_mapping`;
- generated draft нужен как вспомогательный слой для UI и ускорения конфигурирования.
## 12. Runtime view
Для исполнения operation должно существовать упрощенное runtime-представление без UI-специфики.
```json
{
"id": "op_01hr7w0m6p8x9z4n7s2k3q5t6u",
"protocol": "rest",
"target": {
"kind": "rest",
"base_url": "https://api.example.com",
"method": "POST",
"path_template": "/v1/leads"
},
"input_schema": { "type": "object", "fields": {} },
"output_schema": { "type": "object", "fields": {} },
"input_mapping": { "rules": [] },
"output_mapping": { "rules": [] },
"execution_config": {
"timeout_ms": 10000
}
}
```
Runtime view должно:
- не содержать UI draft metadata;
- не зависеть от raw uploaded files;
- быть готовым к немедленному исполнению адаптером.
## 13. YAML-конфигурация
Для импорта и экспорта система должна поддерживать `YAML`-представление operation.
Принцип:
- внутренняя доменная модель одна;
- `JSON` и `YAML` - это два способа сериализации одной и той же конфигурации;
- runtime не зависит от конкретного формата файла;
- `YAML` нужен для переносимости и ручного редактирования.
### Базовая структура YAML
```yaml
format_version: "1"
kind: operation
operation:
id: op_01hr7w0m6p8x9z4n7s2k3q5t6u
name: crm_create_lead
display_name: Create Lead
protocol: rest
status: draft
version: 3
target:
kind: rest
base_url: https://api.example.com
method: POST
path_template: /v1/leads
input_schema:
type: object
fields:
name:
type: string
required: true
email:
type: string
required: true
output_schema:
type: object
fields:
id:
type: string
required: true
status:
type: string
required: true
input_mapping:
rules:
- source: $.mcp.name
target: $.request.body.name
- source: $.mcp.email
target: $.request.body.email
output_mapping:
rules:
- source: $.response.body.id
target: $.output.id
- source: $.response.body.status
target: $.output.status
execution_config:
timeout_ms: 10000
auth_profile_ref: auth_01hr7x8rj2d8nq8v0c4m4t1r9e
tool_description:
title: Create CRM lead
description: Creates a new lead in CRM by name and email.
```
### Требования к YAML import/export
- формат должен быть детерминированным;
- структура должна быть человекочитаемой;
- импорт должен валидировать схему, mapping и protocol-specific target;
- экспорт не должен включать секреты в открытом виде;
- ссылки на внешние артефакты допустимы, но режим экспорта должен быть явным.
### Режимы экспорта
Минимально стоит предусмотреть два режима:
- `portable` - экспорт только конфигурации operation и ссылок на внешние артефакты;
- `bundle` - экспорт конфигурации operation вместе с вложенными sample metadata и descriptor metadata, если это допустимо.
Для MVP можно начать только с `portable`.
## 14. Что важно не допустить
- одну гигантскую `Operation`, в которой protocol-specific поля лежат вперемешку;
- неявный mapping, который не сохраняется после генерации черновика;
- смешивание uploaded artifacts и runtime-ready configuration;
- хранение секретов внутри operation;
- хранение реальных auth credentials внутри YAML export;
- произвольные пользовательские скрипты в mapping;
- YAML-экспорт, который становится отдельной несовместимой моделью по отношению к доменной структуре.
## 15. Практический итог
Эта модель задает основу для:
- Rust structs в `crank-core`, `crank-schema`, `crank-mapping`;
- DTO для `admin-api`;
- таблиц `operations`, `operation_versions`, `operation_samples`, `operation_descriptors`;
- import/export layer для `YAML` конфигураций;
- runtime view, который будет передаваться в `crank-runtime`.
Следующим логическим шагом после этого документа должна стать схема БД, в которой эти сущности будут разложены по таблицам и связям.