Ядро и типы 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.
Ключевые типы:
StaticValueExpressionValuePropValueComponentNodeComponentLayoutPageSchemaAppStateSchema/PageStateSchemaDataSourceEventHandler
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>.loading—true, если запрос в процессе выполнения;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"
}
Поддерживаемые типы:
stringnumberbooleanenum
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:
dataSourceCallStarteddataSourceChanged
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используется напрямую как URLsourceType === 'upload'→srcсчитается attachment ID, загружается через API
Процесс загрузки:
- Runtime определяет, что
sourceType === 'upload' - Вызывает
fetchAttachment(src)через API хоста - Backend генерирует presigned URL (TTL 1 час)
- Runtime кеширует URL (TTL 5 минут)
- Для Image: отображает
<img src={presignedUrl} /> - Для Video: отображает
<video src={presignedUrl} controls={...} />
Подробнее см. архитектуру API (раздел про Attachments).
9. Где используется DSL
| Модуль | Использование |
|---|---|
| builder-web | редактирование дерева, PropertiesPanel v2, layout-редактор |
| dsl-compiler | typed генерация React TSX, преобразование layout |
| runtime-core | интерпретация DSL, вычисление выражений, DataSource v2 |
| runtime-host | выполнение приложения |
| api | строгая структурная валидация DSL |
10. Ключевые идеи DSL
- DSL — основной формат приложения.
- Typed props + layout формируют структуру UI.
- ExpressionValue связывает UI, state и данные.
- Реестр компонент — единый источник правды.
- Runtime полностью воспроизводит приложение из DSL.
- Любое расширение начинается с расширения DSL.