Skip to main content

Ядро и типы DSL

(обновлено: typed props v2, layout-пропсы, ExpressionValue в layout, AppState/PageState v2, DataSource v2)

🎯 Цель страницы

Эта страница описывает фундаментальные части DSL-подсистемы Lowcode Platform:

  • базовые типы DSL;
  • структуру AppSchema, PageSchema, ComponentNode;
  • свойства компонент (static value, expression, типизированные props, native props);
  • layout-пропсы (layout);
  • встроенный реестр компонент DSL и метаданные пропсов;
  • модель состояния (appState/page.state);
  • источники данных DataSource и их влияние на runtime;
  • как DSL используется редактором, компилятором и runtime.

Страница служит эталонным справочником для всех модулей платформы.


1. Что такое DSL

DSL (Domain-Specific Language) — структурированное JSON-описание приложения, включающее:

  • список страниц;
  • дерево компонент;
  • свойства компонент — typed props + native props;
  • layout-пропсы (layout);
  • выражения (ExpressionValue);
  • глобальное и локальное состояние (appState / page.state);
  • источники данных (DataSource);
  • обработчики событий (EventHandler).

DSL должен быть:

  • простым — безопасно редактируется builder-web,
  • строгим — валидируется типами из реестра и схемами DSL,
  • расширяемым — новые компоненты и типы добавляются декларативно.

Пример минимального DSL-дерева

const root: ComponentNode = {
id: 'container-1',
type: 'Container',
props: {},
layout: { width: 200, height: 80 },
children: [
{ id: 'text-1', type: 'Text', props: { text: 'Hello' } },
{ id: 'btn-1', type: 'Button', props: { label: 'Click' } },
],
};

2. Базовые типы DSL

Основные типы находятся в schema.ts.

Ключевые типы:

  • StaticValue
  • ExpressionValue
  • PropValue
  • ComponentNode
  • ComponentLayout
  • PageSchema
  • AppStateSchema / PageStateSchema
  • DataSource
  • EventHandler

2.1. StaticValue

Простые JSON-значения: строки, числа, boolean, null, объекты, массивы.

2.2. ExpressionValue

{ kind: "expression", expression: "state.user.name" }

Поддерживает доступ к:

  • state.* — переменные приложения и страницы,
  • data.* — данные DataSource.

Контракт data.<source> в выражениях:

  • data.<source>.value — результат последнего успешного запроса (или undefined при отсутствии данных);
  • data.<source>.error — объект ошибки или null;
  • data.<source>.loadingtrue, если запрос в процессе выполнения;
  • data.<source>.lastResultAt — timestamp последнего успешного ответа.

2.3. PropValue

type PropValue = StaticValue | ExpressionValue;

Используется в:

  • props компонент,
  • layout.

3. AppSchema

Корневой объект DSL приложения:

interface AppSchema {
id: string;
name: string;
version: 1;

pages: PageSchema[];
blocks?: BlockSchema[];
shell?: ComponentNode;
appState?: AppStateSchema;
dataSources?: DataSource[];
eventHandlers?: EventHandler[];
theme?: AppTheme;
}

3.1. PageSchema

PageSchema описывает страницу и её поведение при жизненном цикле.

Ключевые поля:

  • id, name, path;
  • rootComponent (дерево UI);
  • layoutComponent (опционально);
  • state (локальное состояние страницы);
  • watchers[] — реакция на изменение выражения (effect).

Пример watcher:

watchers: [
{
when: { kind: "expression", expression: "route.params.id" },
mode: "change", // "change" | "always" (опционально)
debounceMs: 200, // опционально
throttleMs: 0, // опционально
actions: [
{ type: "callDataSource", dataSourceId: "ds-products" }
],
},
]

Watcher срабатывает, когда выражение when меняется (или всегда, если mode = "always"). Контекст выражения: state, data, route. Важно: mode = "always" допустим только при ненулевом debounceMs или throttleMs, иначе возможен бесконечный цикл.

3.2. app.shell

app.shell — общий layout приложения (левое меню/хедер/футер), рендерится на всех страницах.

Внутри app.shell должен быть ровно один компонент PageOutlet — это слот, куда подставляется активная страница.

Ограничения:

  • PageOutlet разрешён только внутри app.shell;
  • в app.shell ровно один PageOutlet;
  • если PageOutlet отсутствует, страницы не будут отображаться.

3.3. blocks (custom blocks)

blocks[] — набор переиспользуемых UI‑блоков (например, ProductCard, Footer).

interface BlockSchema {
id: string;
name: string;
rootComponent: ComponentNode;
propsSchema?: Array<{
key: string;
type?: "string" | "number" | "boolean";
}>;
}

Использование блока на странице:

  • добавить компонент BlockInstance;
  • задать props.blockId равным BlockSchema.id;
  • остальные props становятся входными параметрами блока (доступны в выражениях как props.<key>).

4. ComponentNode — UI-компонент DSL

interface ComponentNode {
id: ID;
type: string;

props?: Record<string, PropValue>;
children?: ComponentNode[];
order?: number;

layout?: Record<string, PropValue>; // width, height, margin, padding, flex и т.п.
style?: Record<string, PropValue>; // colors, typography, borders, className
styleStates?: {
hover?: {
transformScale?: PropValue;
transitionMs?: PropValue;
transitionEasing?: PropValue;
transitionProperty?: PropValue;
};
};
}

4.1. props — typed + native

Typed props берутся из ComponentDefinition.props.

Native props (например, className, style) передаются напрямую в runtime.

4.2. layout

Layout — отдельный канал для визуальных параметров компонента.

Поддерживает:

  • StaticValue;
  • ExpressionValue;
  • валидируется и компилируется отдельно от props.

Пример:

layout: {
width: 200,
height: { kind: "expression", expression: "state.cardHeight" },
margin: 8
}

4.3. style

style — визуальные параметры компонента: цвета, типографика, границы, className.

4.4. styleStates

styleStates — интерактивные состояния. Сейчас поддерживается hover:

  • transformScale — масштаб при наведении;
  • opacity — прозрачность при наведении;
  • rotateDeg — поворот при наведении (в градусах);
  • transitionMs — длительность transition (мс).
  • transitionEasing — easing функции (например, "ease-in-out");
  • transitionProperty — свойство transition (например, "transform" или "all").

4.5. Специальные компоненты

  • PageOutlet — слот для рендера активной страницы внутри app.shell. Не принимает детей и используется только в shell‑дереве.

4.6. order

order — опциональный порядковый номер ребёнка среди братьев.

  • индекс 0-based;
  • целое число >= 0;
  • используется рантаймом/компилятором для сортировки children.

5. Typed props v2 и реестр компонент DSL

Реестр компонент определён в @lowcode/dsl/components.

Содержит:

  • типы описаний пропсов (ComponentPropDefinition),
  • описание компонента (ComponentDefinition),
  • встроенный реестр (builtinComponentRegistry).

Эти данные используются:

  • builder-web — PropertiesPanel v2;
  • dsl-compiler — typed генерация React TSX;
  • runtime-core — интерпретация дерева.

5.1. ComponentPropDefinition

{
name: "label",
kind: "string",
label: "Label",
required: true,
defaultValue: "Click me",
allowExpression: true,
format: "singleline"
}

Поддерживаемые типы:

  • string
  • number
  • boolean
  • enum

5.2. ComponentDefinition

interface ComponentDefinition {
type: string;
displayName?: string;
category?: string;
canHaveChildren?: boolean;
props: Record<string, ComponentPropDefinition>;
}

5.3. Реестр компонент

builtinComponentRegistry: Record<string, ComponentDefinition>;

Реестр — единый источник истины для всех модулей.

Встроенные компоненты сейчас включают, среди прочих, Repeat:

  • items — expression, возвращающий массив (например data.products.items);
  • keyExpr — опциональный expression для ключа элемента (fallback — index).
  • Внутри дочерних компонентов доступны item и index (контекст текущего элемента).

Также доступен If:

  • condition — expression, возвращающий boolean;
  • дочерние компоненты трактуются как [then, else], где else опционален.

И DataBoundary:

  • dataSourceName — строковое имя источника данных (name);
  • дочерние компоненты трактуются как [loading, empty, error, success].

6. Expression-пропсы (в props и layout)

ExpressionValue теперь поддерживается:

  • в обычных props,
  • в layout.

Пример:

layout: {
width: { kind: "expression", expression: "state.cardWidth" }
}

В runtime выражения вычисляются в контексте:

{
(state, data);
}

7. AppState и PageState v2

7.1. Формат

interface StateVariable {
key: string;
type: 'string' | 'number' | 'boolean' | 'any';
initialValue?: StaticValue;
}

7.2. Использование

  • Переменные доступны в выражениях через state.*.
  • runtime-core создаёт snapshot на основе схемы DSL.

Пример:

appState: {
variables: [
{ key: 'username', type: 'string' },
{ key: 'counter', type: 'number', initialValue: 10 },
];
}

8. DataSource v2

DSL описывает источники данных:

interface DataSource {
id: string;
name: string;
kind: "static" | "rest" | "db";
config: object;
}

8.1. static

{
id: "users",
kind: "static",
config: { data: ["A", "B"] }
}

Runtime инициализирует snapshot сразу.

8.2. rest и другие источники

Во время выполнения runtime добавляет служебные поля:

  • __lastCallAt
  • __lastResultAt
  • __lastError
  • __lastPayload
  • __value

REST-конфиг (DSL):

type RestDataSourceConfig = {
baseUrl?: string;
path?: string;
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
queryParams?: Record<string, DataSourceParamValue>;
headers?: Record<string, DataSourceParamValue>;
body?: DataSourceParamValue;
timeoutMs?: number;
};

SecretValue:

type SecretValue = { kind: 'secret'; key: string };
  • queryParams/headers/body поддерживают ExpressionValue;
  • queryParams/headers/body также поддерживают SecretValue, который хранит только ссылку на секрет (без значения);
  • timeoutMs — клиентский таймаут в миллисекундах для реального HTTP в runtime-host.

8.3. db-источники (CRUD)

db-источники описывают схему таблиц и CRUD-запросы:

type DbDataSource = {
kind: 'db';
schema: {
tables: Array<{
name: string;
fields: Array<{
name: string;
type: 'string' | 'text' | 'number' | 'boolean' | 'date' | 'datetime' | 'json';
nullable?: boolean;
default?: StaticValue;
unique?: boolean;
primaryKey?: boolean;
references?: {
table: string;
field: string;
onDelete?: 'restrict' | 'cascade' | 'setNull' | 'noAction';
onUpdate?: 'restrict' | 'cascade' | 'setNull' | 'noAction';
};
}>;
indexes?: Array<{ fields: string[]; unique?: boolean; name?: string }>;
}>;
};
queries: Array<
| { kind: 'list'; table: string; filters?: unknown[]; sort?: unknown[]; pagination?: { limit?: number; offset?: number } }
| { kind: 'get'; table: string; by: string }
| { kind: 'create'; table: string; fields: string[] }
| { kind: 'update'; table: string; by: string; fields?: string[] }
| { kind: 'delete'; table: string; by: string }
>;
};
  • references задаёт внешний ключ на поле другой таблицы (FK);
  • допускаются только ссылки на primaryKey/unique поля;
  • setNull возможен только если поле nullable.

Валидация API-входа для DB-queries

Правила валидации для backend-API генератора:

  • dataSourceId/queryId должны существовать и совпадать с DSL.
  • params валидируются по DbQuery.params (тип/required), лишние ключи запрещены.
  • list: filters/sort только по полям таблицы; pagination.limit/offset — числа >= 0, limit с верхним cap.
  • get/update/delete: поле by обязательно, значение совместимо с типом поля.
  • create/update: fields ограничивают допустимые ключи body; отсутствие required поля при отсутствии default — ошибка.

8.4. Моки источников данных

Каждый DataSource может содержать конфигурацию mock, которая используется в превью и локальном runtime:

interface DataSourceMockConfig {
enabled?: boolean;
activeScenario?: 'success' | 'error';
successMock?: {
mode: 'static' | 'sequence';
value?: StaticValue;
sequence?: StaticValue[];
sequenceValueType?: 'json' | 'text';
delayMs?: number;
};
errorScenarios?: {
api?: {
kind: 'api';
statusCode?: number;
payload?: StaticValue;
delayMs?: number;
};
network?: {
kind: 'network';
message?: string;
statusCode?: number;
statusText?: string;
delayMs?: number;
};
activeKind?: 'api' | 'network';
};
}
  • successMock описывает «хороший» ответ (static или sequence);
  • errorScenarios содержит два независимых негативных сценария:
    • api — имитация ответа сервера с произвольным JSON-пейлоадом и HTTP-кодом;
    • network — сетевой сбой с текстовым сообщением и опциональным статусом/описанием;
    • activeKind управляет тем, какой сценарий будет выбран, когда activeScenario: 'error';
  • activeScenario подсказывает runtime, какой блок (success/error) использовать по умолчанию.

Флаг sequenceValueType помогает явно указать ожидания пользователя:

  • json — элементы sequence интерпретируются как JSON (StaticValue). Это значение по умолчанию для обратной совместимости;
  • text — элементы трактуются как сырые строки и не парсятся, что удобно для имитации ответов вида "{\"a\":\"b\"}".

Runtime записывает дополнительную информацию об ошибках в snapshot.dataSources[dsId]:

  • __errorPayload, __errorStatusCode, __errorStatusText, __errorScenario;
  • эти данные можно сохранить в состояние через CallDataSourceAction.assignErrorToStateKey.

8.4. События Runtime

При работе с DataSource генерируются причины изменения snapshot:

  • dataSourceCallStarted
  • dataSourceChanged

8.5. Override сценария при вызове

DSL-экшн callDataSource получил вспомогательное поле mockScenarioOverride?: 'success' | 'error'. Оно используется только в инструментах разработки (builder preview, devtools) и позволяет на один вызов принудительно выбрать success- или error-сценарий, даже если mock.activeScenario установлен иначе. Runtime фиксирует выбранное значение в snapshot.dataSources[dsId].__mockScenario, чтобы инспекторы и логи понимали контекст последнего вызова.


8.6. Attachments (медиа/файлы)

DSL поддерживает ссылки на вложения, которые затем могут использоваться в компонентах (Image/Video и т.п.).

Типы Attachment

type Attachment = {
id: string;
projectId: string;
kind: 'link' | 's3';

// Для kind = 'link'
url?: string | null;

// Для kind = 's3'
storageKey?: string | null;
downloadUrl?: string | null; // presigned URL для скачивания
sourceUrl?: string | null; // presigned URL для загрузки

name?: string | null;
mimeType?: string | null;
size?: number | null;

createdAt: string;
updatedAt: string;
};
  • link — внешние URL (CDN, сторонние сервисы)
  • s3 — файлы в AWS S3 bucket (presigned URLs с TTL 1 час)

Image Component в DSL

Компонент Image добавлен в реестр компонент с тремя основными пропсами:

{
type: 'Image',
displayName: 'Image',
category: 'content',
canHaveChildren: false,
props: [
{
kind: 'string',
name: 'src',
label: 'Image source',
description: 'Image URL or attachment ID.',
required: true,
allowExpression: true,
},
{
kind: 'string',
name: 'alt',
label: 'Alt text',
description: 'Alternative text for accessibility.',
allowExpression: true,
},
{
kind: 'enum',
name: 'sourceType',
label: 'Source type',
description: 'Type of image source: URL or uploaded file.',
defaultValue: 'url',
options: [
{ value: 'url', label: 'URL' },
{ value: 'upload', label: 'Upload file' },
],
},
],
}

Использование в DSL:

// URL режим
{
id: 'img-1',
type: 'Image',
props: {
sourceType: { kind: 'static', value: 'url' },
src: { kind: 'static', value: 'https://example.com/image.jpg' },
alt: { kind: 'static', value: 'Product photo' },
},
}

// Upload режим (attachment)
{
id: 'img-2',
type: 'Image',
props: {
sourceType: { kind: 'static', value: 'upload' },
src: { kind: 'static', value: 'att-uuid-12345' }, // attachment ID
alt: { kind: 'static', value: 'User avatar' },
},
}

Video Component в DSL

Компонент Video работает аналогично Image, но с дополнительными пропсами для управления воспроизведением:

{
type: 'Video',
displayName: 'Video',
category: 'content',
canHaveChildren: false,
props: [
{
kind: 'enum',
name: 'sourceType',
label: 'Source type',
description: 'Type of video source: URL or uploaded file.',
defaultValue: 'url',
options: [
{ value: 'url', label: 'URL' },
{ value: 'upload', label: 'Upload file' },
],
},
{
kind: 'string',
name: 'src',
label: 'Video source',
description: 'Video URL or attachment ID.',
required: true,
allowExpression: true,
},
{
kind: 'boolean',
name: 'controls',
label: 'Show controls',
description: 'Show video controls (play, pause, volume, etc.).',
defaultValue: true,
allowExpression: true,
},
{
kind: 'boolean',
name: 'autoplay',
label: 'Autoplay',
description: 'Start playing automatically.',
defaultValue: false,
allowExpression: true,
},
{
kind: 'boolean',
name: 'loop',
label: 'Loop',
description: 'Loop playback.',
defaultValue: false,
allowExpression: true,
},
{
kind: 'boolean',
name: 'muted',
label: 'Muted',
description: 'Start muted.',
defaultValue: false,
allowExpression: true,
},
{
kind: 'string',
name: 'poster',
label: 'Poster image',
description: 'Image shown before video starts playing.',
allowExpression: true,
},
],
}

Использование в DSL:

// URL режим
{
id: 'video-1',
type: 'Video',
props: {
sourceType: { kind: 'static', value: 'url' },
src: { kind: 'static', value: 'https://example.com/video.mp4' },
controls: { kind: 'static', value: true },
poster: { kind: 'static', value: 'https://example.com/poster.jpg' },
},
}

// Upload режим (attachment)
{
id: 'video-2',
type: 'Video',
props: {
sourceType: { kind: 'static', value: 'upload' },
src: { kind: 'static', value: 'video-uuid-12345' }, // attachment ID
controls: { kind: 'static', value: true },
autoplay: { kind: 'static', value: false },
loop: { kind: 'static', value: true },
muted: { kind: 'static', value: true },
},
}

Runtime обработка

Runtime использует sourceType для определения способа загрузки:

  • sourceType === 'url'src используется напрямую как URL
  • sourceType === 'upload'src считается attachment ID, загружается через API

Процесс загрузки:

  1. Runtime определяет, что sourceType === 'upload'
  2. Вызывает fetchAttachment(src) через API хоста
  3. Backend генерирует presigned URL (TTL 1 час)
  4. Runtime кеширует URL (TTL 5 минут)
  5. Для Image: отображает <img src={presignedUrl} />
  6. Для Video: отображает <video src={presignedUrl} controls={...} />

Подробнее см. архитектуру API (раздел про Attachments).


9. Где используется DSL

МодульИспользование
builder-webредактирование дерева, PropertiesPanel v2, layout-редактор
dsl-compilertyped генерация React TSX, преобразование layout
runtime-coreинтерпретация DSL, вычисление выражений, DataSource v2
runtime-hostвыполнение приложения
apiстрогая структурная валидация DSL

10. Ключевые идеи DSL

  1. DSL — основной формат приложения.
  2. Typed props + layout формируют структуру UI.
  3. ExpressionValue связывает UI, state и данные.
  4. Реестр компонент — единый источник правды.
  5. Runtime полностью воспроизводит приложение из DSL.
  6. Любое расширение начинается с расширения DSL.