From c8050daadfe00b937a137879c4420cb7775f1266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 9 Jun 2026 14:28:51 +0300 Subject: [PATCH 1/3] Add low-code model descriptor schemas --- ...command-interceptor-descriptor.schema.json | 37 +++ .../dashboard-aggregation-type.schema.json | 40 +++ .../dashboard-chart-descriptor.schema.json | 91 ++++++ .../dashboard-chart-type.schema.json | 19 ++ .../dashboard-descriptor.schema.json | 53 ++++ .../dashboard-filter-descriptor.schema.json | 47 ++++ ...board-global-filter-descriptor.schema.json | 21 ++ .../dashboard-list-descriptor.schema.json | 75 +++++ ...rd-number-container-descriptor.schema.json | 26 ++ ...shboard-number-item-descriptor.schema.json | 77 ++++++ .../dashboard-row-descriptor.schema.json | 36 +++ ...board-visualization-descriptor.schema.json | 130 +++++++++ .../dashboard-visualization-type.schema.json | 17 ++ .../endpoint-descriptor.schema.json | 60 ++++ .../entity-attachment-descriptor.schema.json | 47 ++++ ...ss-field-validation-descriptor.schema.json | 52 ++++ .../definitions/entity-descriptor.schema.json | 71 +++++ .../entity-property-descriptor.schema.json | 101 +++++++ .../entity-property-type.schema.json | 66 +++++ .../definitions/enum-descriptor.schema.json | 56 ++++ .../foreign-key-descriptor.schema.json | 60 ++++ .../definitions/form-descriptor.schema.json | 79 ++++++ .../form-field-descriptor.schema.json | 68 +++++ .../definitions/form-field-type.schema.json | 68 +++++ .../form-layout-descriptor.schema.json | 128 +++++++++ .../form-rule-descriptor.schema.json | 82 ++++++ .../definitions/interceptor-type.schema.json | 24 ++ .../page-column-descriptor.schema.json | 50 ++++ .../definitions/page-descriptor.schema.json | 260 ++++++++++++++++++ .../page-filter-descriptor.schema.json | 119 ++++++++ .../page-group-descriptor.schema.json | 44 +++ .../page-permission-config.schema.json | 27 ++ .../definitions/page-type.schema.json | 36 +++ .../permission-descriptor.schema.json | 44 +++ ...ript-background-job-descriptor.schema.json | 36 +++ ...t-background-worker-descriptor.schema.json | 58 ++++ ...cript-event-handler-descriptor.schema.json | 42 +++ .../validator-descriptor.schema.json | 96 +++++++ schemas/low-code/manifest.json | 19 ++ 39 files changed, 2462 insertions(+) create mode 100644 schemas/low-code/definitions/command-interceptor-descriptor.schema.json create mode 100644 schemas/low-code/definitions/dashboard-aggregation-type.schema.json create mode 100644 schemas/low-code/definitions/dashboard-chart-descriptor.schema.json create mode 100644 schemas/low-code/definitions/dashboard-chart-type.schema.json create mode 100644 schemas/low-code/definitions/dashboard-descriptor.schema.json create mode 100644 schemas/low-code/definitions/dashboard-filter-descriptor.schema.json create mode 100644 schemas/low-code/definitions/dashboard-global-filter-descriptor.schema.json create mode 100644 schemas/low-code/definitions/dashboard-list-descriptor.schema.json create mode 100644 schemas/low-code/definitions/dashboard-number-container-descriptor.schema.json create mode 100644 schemas/low-code/definitions/dashboard-number-item-descriptor.schema.json create mode 100644 schemas/low-code/definitions/dashboard-row-descriptor.schema.json create mode 100644 schemas/low-code/definitions/dashboard-visualization-descriptor.schema.json create mode 100644 schemas/low-code/definitions/dashboard-visualization-type.schema.json create mode 100644 schemas/low-code/definitions/endpoint-descriptor.schema.json create mode 100644 schemas/low-code/definitions/entity-attachment-descriptor.schema.json create mode 100644 schemas/low-code/definitions/entity-cross-field-validation-descriptor.schema.json create mode 100644 schemas/low-code/definitions/entity-descriptor.schema.json create mode 100644 schemas/low-code/definitions/entity-property-descriptor.schema.json create mode 100644 schemas/low-code/definitions/entity-property-type.schema.json create mode 100644 schemas/low-code/definitions/enum-descriptor.schema.json create mode 100644 schemas/low-code/definitions/foreign-key-descriptor.schema.json create mode 100644 schemas/low-code/definitions/form-descriptor.schema.json create mode 100644 schemas/low-code/definitions/form-field-descriptor.schema.json create mode 100644 schemas/low-code/definitions/form-field-type.schema.json create mode 100644 schemas/low-code/definitions/form-layout-descriptor.schema.json create mode 100644 schemas/low-code/definitions/form-rule-descriptor.schema.json create mode 100644 schemas/low-code/definitions/interceptor-type.schema.json create mode 100644 schemas/low-code/definitions/page-column-descriptor.schema.json create mode 100644 schemas/low-code/definitions/page-descriptor.schema.json create mode 100644 schemas/low-code/definitions/page-filter-descriptor.schema.json create mode 100644 schemas/low-code/definitions/page-group-descriptor.schema.json create mode 100644 schemas/low-code/definitions/page-permission-config.schema.json create mode 100644 schemas/low-code/definitions/page-type.schema.json create mode 100644 schemas/low-code/definitions/permission-descriptor.schema.json create mode 100644 schemas/low-code/definitions/script-background-job-descriptor.schema.json create mode 100644 schemas/low-code/definitions/script-background-worker-descriptor.schema.json create mode 100644 schemas/low-code/definitions/script-event-handler-descriptor.schema.json create mode 100644 schemas/low-code/definitions/validator-descriptor.schema.json create mode 100644 schemas/low-code/manifest.json diff --git a/schemas/low-code/definitions/command-interceptor-descriptor.schema.json b/schemas/low-code/definitions/command-interceptor-descriptor.schema.json new file mode 100644 index 0000000000..ee1fc6d49a --- /dev/null +++ b/schemas/low-code/definitions/command-interceptor-descriptor.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "command-interceptor-descriptor.schema.json", + "title": "CommandInterceptorDescriptor", + "description": "Describes a JavaScript interceptor for an entity Create, Update, or Delete command.", + "markdownDescription": "AI guidance: use `type: \"Pre\"` to validate or block before persistence and `type: \"Post\"` for side effects after persistence. Scripts can inspect `context.commandArgs` and may use services exposed on `context` such as `db`, `currentUser`, `currentTenant`, `emailSender`, `config`, `http`, and logging helpers depending on host configuration. To block the command with a user-facing error, assign `globalError = \"message\"` and return. Keep scripts idempotent where possible.", + "type": "object", + "properties": { + "commandName": { + "type": "string", + "description": "Entity command to intercept: Create, Update, or Delete.", + "enum": ["Create", "Update", "Delete"] + }, + "type": { + "$ref": "interceptor-type.schema.json", + "description": "Whether the script runs before or after the command." + }, + "javascript": { + "type": "string", + "description": "JavaScript code to execute. For Pre interceptors, set globalError to block the command. Example: if (!context.commandArgs.data['Name']) { globalError = 'Name is required.'; }" + } + }, + "required": ["commandName", "type", "javascript"], + "additionalProperties": false, + "examples": [ + { + "commandName": "Create", + "type": "Pre", + "javascript": "if (!context.commandArgs.data['Name']) { globalError = 'Name is required.'; }" + }, + { + "commandName": "Delete", + "type": "Pre", + "javascript": "var record = await db.get('Acme.Events.Event', context.commandArgs.entityId); if (record && record.Status === 2) { globalError = 'Completed events cannot be deleted.'; }" + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-aggregation-type.schema.json b/schemas/low-code/definitions/dashboard-aggregation-type.schema.json new file mode 100644 index 0000000000..e18a2b5b6f --- /dev/null +++ b/schemas/low-code/definitions/dashboard-aggregation-type.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-aggregation-type.schema.json", + "title": "DashboardAggregationType", + "description": "The type of aggregation to perform. Prefer lower-case values in model JSON; PascalCase aliases are accepted for compatibility.", + "markdownDescription": "`count` counts records and does not require a property. `sum`, `average`, `min`, and `max` require a property. `percentFilled` and `percentEmpty` calculate completeness for a nullable/string property.", + "type": "string", + "enum": [ + "count", + "Count", + "sum", + "Sum", + "average", + "Average", + "min", + "Min", + "max", + "Max", + "percentFilled", + "PercentFilled", + "percentEmpty", + "PercentEmpty" + ], + "enumDescriptions": [ + "Count matching records.", + "Count matching records.", + "Sum a numeric property.", + "Sum a numeric property.", + "Average a numeric property.", + "Average a numeric property.", + "Minimum property value.", + "Minimum property value.", + "Maximum property value.", + "Maximum property value.", + "Percentage of records where property has a value.", + "Percentage of records where property has a value.", + "Percentage of records where property is empty/null.", + "Percentage of records where property is empty/null." + ] +} diff --git a/schemas/low-code/definitions/dashboard-chart-descriptor.schema.json b/schemas/low-code/definitions/dashboard-chart-descriptor.schema.json new file mode 100644 index 0000000000..ade648db0f --- /dev/null +++ b/schemas/low-code/definitions/dashboard-chart-descriptor.schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-chart-descriptor.schema.json", + "title": "DashboardChartDescriptor", + "description": "Configuration for a chart visualization.", + "markdownDescription": "AI guidance: use chart visualizations for grouped aggregations. `xAxis.property` is the grouping property. `yAxis` contains one or more aggregations. Use `count` without a property; use `sum`, `average`, `min`, or `max` with a numeric/date property. Use `dateGrouping` when xAxis is date/datetime.", + "type": "object", + "properties": { + "chartType": { + "$ref": "dashboard-chart-type.schema.json", + "description": "Chart renderer type: bar, line, pie, or donut." + }, + "xAxis": { + "type": "object", + "properties": { + "property": { + "type": "string", + "description": "Property name to group by on the X-axis. Must exist on the visualization entity." + }, + "useForeignDisplay": { + "type": "boolean", + "description": "Show the FK display property instead of the raw id when xAxis.property is a foreign key.", + "default": false + }, + "dateGrouping": { + "type": ["string", "null"], + "enum": ["", "day", "week", "month", "quarter", "year", null], + "description": "Grouping interval for date/datetime properties. Omit or use empty string for no date grouping." + } + }, + "required": ["property"], + "additionalProperties": false + }, + "yAxis": { + "type": "array", + "items": { + "type": "object", + "properties": { + "aggregation": { + "$ref": "dashboard-aggregation-type.schema.json" + }, + "property": { + "type": ["string", "null"], + "description": "Property name for sum/average/min/max/percent aggregations. Omit for count." + }, + "label": { + "type": ["string", "null"], + "description": "Display label for this series. Omit to derive from aggregation/property." + }, + "color": { + "type": ["string", "null"] + } + }, + "required": ["aggregation"], + "additionalProperties": false + }, + "minItems": 1 + }, + "barOrientation": { + "type": "string", + "enum": ["vertical", "horizontal"], + "default": "vertical" + }, + "showRecordCount": { + "type": "boolean", + "default": false + }, + "maxItems": { + "type": "integer", + "description": "Maximum number of grouped items to display in the chart.", + "default": 10, + "minimum": 1, + "maximum": 50 + } + }, + "required": ["chartType", "xAxis", "yAxis"], + "additionalProperties": false, + "examples": [ + { + "chartType": "bar", + "xAxis": { "property": "Status" }, + "yAxis": [{ "aggregation": "count", "label": "Events" }], + "maxItems": 10 + }, + { + "chartType": "line", + "xAxis": { "property": "StartDate", "dateGrouping": "month" }, + "yAxis": [{ "aggregation": "sum", "property": "Budget", "label": "Budget" }] + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-chart-type.schema.json b/schemas/low-code/definitions/dashboard-chart-type.schema.json new file mode 100644 index 0000000000..cfada5467e --- /dev/null +++ b/schemas/low-code/definitions/dashboard-chart-type.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-chart-type.schema.json", + "title": "DashboardChartType", + "description": "The type of chart. Prefer lower-case values in model JSON; PascalCase aliases are accepted for compatibility.", + "markdownDescription": "Use `bar` for categorical comparisons, `line` for trends over time, `pie`/`donut` for part-of-whole views with a small number of categories.", + "type": "string", + "enum": ["bar", "Bar", "line", "Line", "pie", "Pie", "donut", "Donut"], + "enumDescriptions": [ + "Bar chart.", + "Bar chart.", + "Line chart.", + "Line chart.", + "Pie chart.", + "Pie chart.", + "Donut chart.", + "Donut chart." + ] +} diff --git a/schemas/low-code/definitions/dashboard-descriptor.schema.json b/schemas/low-code/definitions/dashboard-descriptor.schema.json new file mode 100644 index 0000000000..068031a18b --- /dev/null +++ b/schemas/low-code/definitions/dashboard-descriptor.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-descriptor.schema.json", + "title": "DashboardDescriptor", + "description": "Describes a dashboard page configuration with global filters and rows of visualizations.", + "markdownDescription": "AI guidance: a dashboard belongs to a page with `type: \"dashboard\"`. It contains one or more rows; each row contains chart, list, or numberContainer visualizations. Use visualization `entityName` to select data for chart/list items. Use numberContainer for KPI tiles, charts for grouped aggregations, and lists for recent/top records.", + "type": "object", + "properties": { + "description": { + "type": ["string", "null"], + "description": "Optional description text shown below the dashboard title." + }, + "globalFilters": { + "type": "array", + "description": "Global filters that affect visualizations, for example a date range. Visualizations can map the global date filter through globalDateFilterProperty.", + "items": { + "$ref": "dashboard-global-filter-descriptor.schema.json" + } + }, + "rows": { + "type": "array", + "description": "Dashboard rows. Each row contains one or more visualization items; width 2 items usually take the full row.", + "items": { + "$ref": "dashboard-row-descriptor.schema.json" + }, + "minItems": 1 + } + }, + "required": ["rows"], + "additionalProperties": false, + "examples": [ + { + "description": "Operational overview", + "rows": [ + { + "items": [ + { + "name": "events-by-status", + "type": "chart", + "title": "Events by Status", + "entityName": "Acme.Events.Event", + "chart": { + "chartType": "bar", + "xAxis": { "property": "Status" }, + "yAxis": [{ "aggregation": "count", "label": "Events" }] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-filter-descriptor.schema.json b/schemas/low-code/definitions/dashboard-filter-descriptor.schema.json new file mode 100644 index 0000000000..93cb76e821 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-filter-descriptor.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-filter-descriptor.schema.json", + "title": "DashboardFilterDescriptor", + "description": "Static filter applied to a dashboard visualization or KPI item.", + "markdownDescription": "AI guidance: use dashboard filters to constrain the data behind a visualization, for example only active records or only records above a threshold. Each condition property must exist on the visualization/item entity. Combine conditions with `operator: \"and\"` or `operator: \"or\"`.", + "type": "object", + "properties": { + "operator": { + "type": "string", + "enum": ["and", "or"], + "default": "and" + }, + "conditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "property": { + "type": "string", + "description": "Property name to filter on. Must exist on the visualization/item entityName." + }, + "filterType": { + "type": "string", + "enum": ["equal", "notEqual", "contains", "greaterThan", "lessThan", "isNull", "isNotNull"], + "default": "equal" + }, + "value": { + "description": "Filter value. Shape depends on filterType and target property type." + } + }, + "required": ["property", "filterType"], + "additionalProperties": false + } + } + }, + "required": ["conditions"], + "additionalProperties": false, + "examples": [ + { + "operator": "and", + "conditions": [ + { "property": "Status", "filterType": "equal", "value": 1 } + ] + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-global-filter-descriptor.schema.json b/schemas/low-code/definitions/dashboard-global-filter-descriptor.schema.json new file mode 100644 index 0000000000..ab1d3f79a6 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-global-filter-descriptor.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-global-filter-descriptor.schema.json", + "title": "DashboardGlobalFilterDescriptor", + "description": "Describes a dashboard-level filter control that can affect all visualizations on the dashboard.", + "markdownDescription": "AI guidance: use global filters for dashboard-wide controls, not for per-visualization conditions. The current supported type is `dateRange`; visualization filters can then reference date-like entity properties through their own `filter` or `userFilters` settings.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["dateRange"], + "default": "dateRange", + "description": "Global filter control type. Currently only 'dateRange' is supported." + } + }, + "required": ["type"], + "additionalProperties": false, + "examples": [ + { "type": "dateRange" } + ] +} diff --git a/schemas/low-code/definitions/dashboard-list-descriptor.schema.json b/schemas/low-code/definitions/dashboard-list-descriptor.schema.json new file mode 100644 index 0000000000..b7340c4118 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-list-descriptor.schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-list-descriptor.schema.json", + "title": "DashboardListDescriptor", + "description": "Configuration for a dashboard list/table visualization.", + "markdownDescription": "AI guidance: use list visualizations for recent records, top records, or compact operational queues. `fields` must contain property names from the parent visualization entityName. Use `sortBy` to make results deterministic.", + "type": "object", + "properties": { + "fields": { + "type": "array", + "items": { "type": "string" }, + "description": "Property names to display as columns. Each value must reference a property on the visualization entityName." + }, + "sortBy": { + "type": "object", + "properties": { + "property": { + "type": "string", + "description": "Property name to sort by. Must exist on the visualization entityName." + }, + "direction": { + "type": "string", + "enum": ["asc", "desc"], + "default": "asc" + } + }, + "required": ["property"], + "additionalProperties": false + }, + "maxRows": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 10 + }, + "rowHeight": { + "type": "string", + "enum": ["compact", "normal", "tall"], + "default": "compact" + }, + "colorBy": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["property", "conditions"] + }, + "property": { + "type": "string", + "description": "Enum/status property name for automatic coloring." + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": ["fields"], + "additionalProperties": false, + "examples": [ + { + "fields": ["Title", "StartDate", "Status"], + "sortBy": { "property": "StartDate", "direction": "desc" }, + "maxRows": 10, + "rowHeight": "compact", + "colorBy": { "type": "property", "property": "Status" } + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-number-container-descriptor.schema.json b/schemas/low-code/definitions/dashboard-number-container-descriptor.schema.json new file mode 100644 index 0000000000..5cac8372a9 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-number-container-descriptor.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-number-container-descriptor.schema.json", + "title": "DashboardNumberContainerDescriptor", + "description": "A container that holds multiple number/KPI items, each with its own entity and aggregation.", + "markdownDescription": "AI guidance: use numberContainer for KPI tiles. Each item chooses its own entity, aggregation, optional filter, format, and color.", + "type": "object", + "properties": { + "items": { + "type": "array", + "description": "Number/KPI items within this container. Use 1-4 items for a readable dashboard row.", + "items": { + "$ref": "dashboard-number-item-descriptor.schema.json" + } + } + }, + "required": ["items"], + "additionalProperties": false, + "examples": [ + { + "items": [ + { "name": "total-events", "title": "Total Events", "entityName": "Acme.Events.Event", "aggregation": "count", "format": "number" } + ] + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-number-item-descriptor.schema.json b/schemas/low-code/definitions/dashboard-number-item-descriptor.schema.json new file mode 100644 index 0000000000..eb98b09ac4 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-number-item-descriptor.schema.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-number-item-descriptor.schema.json", + "title": "DashboardNumberItemDescriptor", + "description": "A single number/KPI tile within a number container.", + "markdownDescription": "AI guidance: `count` does not need aggregationProperty. `sum`, `average`, `min`, `max`, `percentFilled`, and `percentEmpty` should specify aggregationProperty. Use filter to scope the KPI, for example count only active records.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Unique identifier for this number item. Prefer kebab-case such as 'total-events'.", + "minLength": 1 + }, + "title": { + "type": "string", + "description": "Display title for this KPI." + }, + "entityName": { + "type": "string", + "description": "Entity this number item sources data from. Must match an entity descriptor or known reference entity.", + "minLength": 1 + }, + "aggregation": { + "$ref": "dashboard-aggregation-type.schema.json", + "description": "Aggregation used to compute the KPI value." + }, + "aggregationProperty": { + "type": "string", + "description": "Property name for sum/average/min/max/percent aggregations. Omit for count." + }, + "format": { + "type": "string", + "enum": ["number", "currency", "percentage"], + "default": "number" + }, + "color": { + "type": "string", + "description": "Display color name (blue, green, red, purple, orange, indigo, amber, teal)" + }, + "globalDateFilterProperty": { + "type": "string", + "description": "Date/DateTime property used to apply dashboard global date filters. Defaults to CreationTime when omitted." + }, + "filter": { + "oneOf": [ + { "$ref": "dashboard-filter-descriptor.schema.json" }, + { "type": "null" } + ] + }, + "clickToSeeRecords": { + "type": "boolean", + "description": "Allow users to click to see underlying records when supported by the runtime.", + "default": false + } + }, + "required": ["name", "title", "entityName", "aggregation"], + "additionalProperties": false, + "examples": [ + { + "name": "total-events", + "title": "Total Events", + "entityName": "Acme.Events.Event", + "aggregation": "count", + "format": "number", + "color": "blue" + }, + { + "name": "total-budget", + "title": "Total Budget", + "entityName": "Acme.Events.Event", + "aggregation": "sum", + "aggregationProperty": "Budget", + "format": "currency", + "color": "green" + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-row-descriptor.schema.json b/schemas/low-code/definitions/dashboard-row-descriptor.schema.json new file mode 100644 index 0000000000..bd1abbc591 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-row-descriptor.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-row-descriptor.schema.json", + "title": "DashboardRowDescriptor", + "description": "Describes one row in the dashboard layout grid. Each row contains one or two visualization items.", + "markdownDescription": "AI guidance: organize dashboard visualizations into rows by visual importance. Use one item for a full-width chart/list/number panel, and two items when related visualizations should be displayed side by side. Do not put more than two visualizations in a row; create another row instead.", + "type": "object", + "properties": { + "items": { + "type": "array", + "description": "Visualization items in this row. Use one item for full-width content or two items for a two-column row.", + "items": { + "$ref": "dashboard-visualization-descriptor.schema.json" + }, + "minItems": 1, + "maxItems": 2 + } + }, + "required": ["items"], + "additionalProperties": false, + "examples": [ + { + "items": [ + { + "type": "numberContainer", + "title": "Overview", + "numberContainer": { + "items": [ + { "title": "Total Records", "entity": "Acme.Events.Event", "aggregation": "count" } + ] + } + } + ] + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-visualization-descriptor.schema.json b/schemas/low-code/definitions/dashboard-visualization-descriptor.schema.json new file mode 100644 index 0000000000..0a958fa9a1 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-visualization-descriptor.schema.json @@ -0,0 +1,130 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-visualization-descriptor.schema.json", + "title": "DashboardVisualizationDescriptor", + "description": "A single visualization element in a dashboard row: chart, list, or number container.", + "markdownDescription": "AI guidance: set `type` and then provide the matching payload: `chart` for type chart, `list` for type list, or `numberContainer` for type numberContainer. Do not populate unrelated payloads. Chart and list visualizations require `entityName`; number containers define entityName per KPI item.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Unique identifier within the dashboard. Prefer kebab-case such as 'events-by-status'.", + "minLength": 1 + }, + "type": { + "$ref": "dashboard-visualization-type.schema.json", + "description": "Visualization renderer type. Determines which payload property must be populated." + }, + "title": { + "type": "string", + "description": "Display title for the visualization." + }, + "description": { + "type": ["string", "null"], + "description": "Optional description text. Can be shown inline or as tooltip depending on showDescriptionAsTooltip." + }, + "width": { + "type": "integer", + "enum": [1, 2], + "default": 1, + "description": "Column width: 1 = half row, 2 = full row." + }, + "entityName": { + "type": "string", + "description": "Entity this visualization sources data from. Required for chart and list visualizations; numberContainer items define their own entityName." + }, + "globalDateFilterProperty": { + "type": "string", + "description": "Date/DateTime property used to apply dashboard global date filters to this visualization. Defaults to CreationTime when omitted." + }, + "filter": { + "oneOf": [ + { "$ref": "dashboard-filter-descriptor.schema.json" }, + { "type": "null" } + ] + }, + "userFilters": { + "type": "array", + "description": "Interactive filters exposed to end users on this visualization.", + "items": { + "type": "object", + "properties": { + "property": { + "type": "string", + "description": "Property name to filter on. Must exist on entityName.", + "minLength": 1 + } + }, + "required": ["property"], + "additionalProperties": false + } + }, + "showDescriptionAsTooltip": { + "type": "boolean", + "default": false + }, + "clickToSeeRecords": { + "type": "boolean", + "description": "Allow users to click to see underlying records when supported by the runtime.", + "default": false + }, + "chart": { + "oneOf": [ + { "$ref": "dashboard-chart-descriptor.schema.json" }, + { "type": "null" } + ] + }, + "list": { + "oneOf": [ + { "$ref": "dashboard-list-descriptor.schema.json" }, + { "type": "null" } + ] + }, + "numberContainer": { + "oneOf": [ + { "$ref": "dashboard-number-container-descriptor.schema.json" }, + { "type": "null" } + ] + } + }, + "required": ["name", "type", "title"], + "allOf": [ + { + "if": { "properties": { "type": { "const": "chart" } } }, + "then": { "required": ["chart", "entityName"] } + }, + { + "if": { "properties": { "type": { "const": "list" } } }, + "then": { "required": ["list", "entityName"] } + }, + { + "if": { "properties": { "type": { "const": "numberContainer" } } }, + "then": { "required": ["numberContainer"] } + } + ], + "additionalProperties": false, + "examples": [ + { + "name": "events-by-status", + "type": "chart", + "title": "Events by Status", + "entityName": "Acme.Events.Event", + "chart": { + "chartType": "bar", + "xAxis": { "property": "Status" }, + "yAxis": [{ "aggregation": "count", "label": "Events" }] + } + }, + { + "name": "recent-events", + "type": "list", + "title": "Recent Events", + "entityName": "Acme.Events.Event", + "list": { + "fields": ["Title", "StartDate", "Status"], + "sortBy": { "property": "StartDate", "direction": "desc" }, + "maxRows": 10 + } + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-visualization-type.schema.json b/schemas/low-code/definitions/dashboard-visualization-type.schema.json new file mode 100644 index 0000000000..721fb2977f --- /dev/null +++ b/schemas/low-code/definitions/dashboard-visualization-type.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-visualization-type.schema.json", + "title": "DashboardVisualizationType", + "description": "The type of dashboard visualization. Prefer lower-case values in model JSON; PascalCase aliases are accepted for compatibility.", + "markdownDescription": "`chart` groups/aggregates entity data into a bar/line/pie/donut chart. `list` displays recent/top records from one entity. `numberContainer` displays one or more KPI number tiles, each with its own entity and aggregation.", + "type": "string", + "enum": ["chart", "Chart", "list", "List", "numberContainer", "NumberContainer"], + "enumDescriptions": [ + "Chart visualization with x/y axis aggregation.", + "Chart visualization with x/y axis aggregation.", + "Record list visualization.", + "Record list visualization.", + "KPI number/tile container.", + "KPI number/tile container." + ] +} diff --git a/schemas/low-code/definitions/endpoint-descriptor.schema.json b/schemas/low-code/definitions/endpoint-descriptor.schema.json new file mode 100644 index 0000000000..4990fb8b89 --- /dev/null +++ b/schemas/low-code/definitions/endpoint-descriptor.schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "endpoint-descriptor.schema.json", + "title": "Custom Endpoint Descriptor", + "description": "Defines a custom HTTP endpoint that executes server-side JavaScript code.", + "markdownDescription": "AI guidance: use custom endpoints for model-owned actions and lightweight APIs. `name` must be unique. `route` should start with `/api/` and must not conflict with another route/method. Use `{id}` style path parameters when needed. Scripts can access request data through the endpoint context and return HTTP results with helpers such as `context.ok(value)`, `context.created(value)`, and `context.noContent()` where available. Require authentication by default and add `requiredPermissions` for protected operations.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Unique endpoint identifier used by designer/model health. Prefer PascalCase or kebab-case, for example 'SearchCustomers'." + }, + "route": { + "type": "string", + "description": "URL route pattern. Must start with '/' and should use an application-specific prefix such as '/api/low-code/events/{id}'. Route parameters use ASP.NET style braces, for example '{id}'." + }, + "method": { + "type": "string", + "description": "HTTP method. GET should be read-only; POST/PUT/PATCH/DELETE may mutate state.", + "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"], + "default": "GET" + }, + "javascript": { + "type": "string", + "description": "JavaScript code to execute. Use context request/response helpers and services exposed by the host, such as db, currentUser/currentTenant, authorization, emailSender, config, http, event bus, background jobs, and logging helpers." + }, + "requireAuthentication": { + "type": "boolean", + "description": "Whether authentication is required. Keep true unless this endpoint is intentionally public.", + "default": true + }, + "requiredPermissions": { + "type": "array", + "description": "Permission names required to access the endpoint. Checked only when authentication is required. Values should reference custom permissions or known static permissions.", + "items": { + "type": "string" + } + }, + "description": { + "type": "string", + "description": "Optional human-readable description for designer documentation and model health context." + } + }, + "required": ["name", "route", "javascript"], + "additionalProperties": false, + "examples": [ + { + "name": "SearchCustomers", + "route": "/api/low-code/customers/search", + "method": "GET", + "requireAuthentication": true, + "requiredPermissions": ["Acme.Customers.View"], + "javascript": "var q = context.request.query.q || ''; var table = await db.query('Acme.Crm.Customer'); var rows = await table.where(c => c.Name.toLowerCase().includes(q.toLowerCase())).take(10).toList(); return context.ok(rows);" + } + ] +} diff --git a/schemas/low-code/definitions/entity-attachment-descriptor.schema.json b/schemas/low-code/definitions/entity-attachment-descriptor.schema.json new file mode 100644 index 0000000000..bf1be2fad4 --- /dev/null +++ b/schemas/low-code/definitions/entity-attachment-descriptor.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "entity-attachment-descriptor.schema.json", + "title": "EntityAttachmentDescriptor", + "description": "Configures record-level attachments for an entity.", + "markdownDescription": "AI guidance: use entity attachments when records need a collection of arbitrary files. Use File/Image entity properties instead when the file is a named business field such as `ContractPdf` or `CoverImage`.", + "type": "object", + "properties": { + "isEnabled": { + "type": "boolean", + "description": "When true, records of this entity can have attachments." + }, + "maxFileCount": { + "type": "integer", + "description": "Maximum number of files allowed per entity record.", + "minimum": 1 + }, + "maxFileSizeBytes": { + "type": "integer", + "description": "Maximum allowed size in bytes for a single attachment.", + "minimum": 1 + }, + "maxTotalSizeBytes": { + "type": "integer", + "description": "Maximum total attachment size in bytes per entity record.", + "minimum": 1 + }, + "allowedContentTypes": { + "type": "array", + "description": "Allowed MIME content types, wildcard MIME patterns, or file extensions. Examples: 'image/*', 'application/pdf', '.docx'.", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["isEnabled"], + "additionalProperties": false, + "examples": [ + { + "isEnabled": true, + "maxFileCount": 5, + "maxFileSizeBytes": 5242880, + "allowedContentTypes": ["image/*", "application/pdf"] + } + ] +} diff --git a/schemas/low-code/definitions/entity-cross-field-validation-descriptor.schema.json b/schemas/low-code/definitions/entity-cross-field-validation-descriptor.schema.json new file mode 100644 index 0000000000..5fff4ea579 --- /dev/null +++ b/schemas/low-code/definitions/entity-cross-field-validation-descriptor.schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "entity-cross-field-validation-descriptor.schema.json", + "title": "EntityCrossFieldValidationDescriptor", + "description": "Describes an entity-level validation rule that compares one property against another property on the same entity.", + "markdownDescription": "AI guidance: use cross-field validations for rules such as EndDate > StartDate, Min <= Max, PasswordRepeat == Password, or PublishedOn >= CreatedOn. `propertyName` receives the validation error; `otherPropertyName` is the comparison target. Both must exist on the entity.", + "type": "object", + "properties": { + "propertyName": { + "type": "string", + "description": "Property that receives the validation error when the rule fails. Must exist on the entity.", + "minLength": 1 + }, + "operator": { + "type": "string", + "description": "Comparison operator", + "enum": [ + "equals", + "notEquals", + "greaterThan", + "greaterThanOrEqual", + "lessThan", + "lessThanOrEqual" + ] + }, + "otherPropertyName": { + "type": "string", + "description": "Property to compare against. Must exist on the same entity.", + "minLength": 1 + }, + "message": { + "type": "string", + "description": "Optional custom validation message" + } + }, + "required": ["propertyName", "operator", "otherPropertyName"], + "additionalProperties": false, + "examples": [ + { + "propertyName": "EndDate", + "operator": "greaterThan", + "otherPropertyName": "StartDate", + "message": "End Date must be greater than Start Date." + }, + { + "propertyName": "PasswordRepeat", + "operator": "equals", + "otherPropertyName": "Password", + "message": "Password repeat must match Password." + } + ] +} diff --git a/schemas/low-code/definitions/entity-descriptor.schema.json b/schemas/low-code/definitions/entity-descriptor.schema.json new file mode 100644 index 0000000000..175966db3a --- /dev/null +++ b/schemas/low-code/definitions/entity-descriptor.schema.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "entity-descriptor.schema.json", + "title": "EntityDescriptor", + "description": "Describes a dynamic entity. An entity is the runtime data model: it defines a table-like aggregate, its properties, validations, parent-child relation, attachment support, and create/update/delete interceptors.", + "markdownDescription": "AI guidance: create one entity per aggregate/root concept. Use a stable namespace-style `name` such as `Acme.Crm.Customer`; this name is referenced by pages, forms, foreign keys, dashboards, scripts, and permissions. Put user-facing labels in `displayName`; choose `displayProperty` as a short string property used by lookups. Use `parent` only for child/detail entities that belong to a parent record.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Stable full name of the entity, usually Namespace.Module.EntityName (for example 'Acme.Crm.Customer'). Must be unique across all model layers. Do not rename after data exists unless a migration/rename flow is intended.", + "minLength": 1 + }, + "displayName": { + "type": "string", + "description": "Default plural/screen label for this entity (for example 'Customers'). Page menu titles are configured separately on page descriptors.", + "minLength": 1 + }, + "displayProperty": { + "type": "string", + "description": "Property name used when another entity references this entity in a lookup/autocomplete. Prefer a required string property such as 'Name', 'Title', or 'Code'." + }, + "parent": { + "type": "string", + "description": "Full name of the parent entity for parent-child/detail entities. When set, records of this entity are scoped under the parent and should include an FK property to the parent.", + "minLength": 1 + }, + "attachments": { + "$ref": "entity-attachment-descriptor.schema.json", + "description": "Record-level attachment settings. Use this for multiple arbitrary files attached to a record; use File/Image properties for first-class file fields." + }, + "properties": { + "type": "array", + "description": "Entity properties. Omit framework audit/id fields such as Id, CreationTime, CreatorId, LastModificationTime, IsDeleted unless intentionally overriding metadata; the runtime supplies standard fields.", + "items": { + "$ref": "entity-property-descriptor.schema.json" + } + }, + "crossFieldValidations": { + "type": "array", + "description": "Entity-level validation rules that compare two properties from the same entity. Use for date ranges, matching password fields, numeric min/max pairs, and other cross-field rules.", + "items": { + "$ref": "entity-cross-field-validation-descriptor.schema.json" + } + }, + "interceptors": { + "type": "array", + "description": "Create/Update/Delete command interceptors. Use Pre interceptors to validate, normalize, or block a command; use Post interceptors for side effects after persistence.", + "items": { + "$ref": "command-interceptor-descriptor.schema.json" + } + } + }, + "required": ["name"], + "additionalProperties": false, + "examples": [ + { + "name": "Acme.Crm.Customer", + "displayName": "Customers", + "displayProperty": "Name", + "properties": [ + { "name": "Name", "type": "string", "isRequired": true }, + { "name": "Email", "type": "string", "validators": [{ "type": "email" }] } + ] + } + ] +} diff --git a/schemas/low-code/definitions/entity-property-descriptor.schema.json b/schemas/low-code/definitions/entity-property-descriptor.schema.json new file mode 100644 index 0000000000..c4e85cf5f6 --- /dev/null +++ b/schemas/low-code/definitions/entity-property-descriptor.schema.json @@ -0,0 +1,101 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "entity-property-descriptor.schema.json", + "title": "EntityPropertyDescriptor", + "description": "Describes one dynamic entity property. Properties define persisted data fields, enum fields, foreign keys, upload fields, server-only fields, defaults, uniqueness, and validators.", + "markdownDescription": "AI guidance: use PascalCase property names. Use `type` for primitive fields, `type: \"enum\"` with `enumType` for enum fields, and a `foreignKey` object for lookup fields. FK property names should normally end with `Id`. Keep sensitive values `serverOnly: true`. Do not add legacy UI configuration here; page columns/filters and form fields own UI behavior.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Stable PascalCase property name, unique within the entity. Examples: 'Name', 'EmailAddress', 'CustomerId', 'StartDate'.", + "minLength": 1 + }, + "type": { + "$ref": "entity-property-type.schema.json", + "description": "Primitive or special property type. If omitted, runtime treats the property as string. Use 'enum' only with enumType; use 'file'/'image' for first-class upload fields." + }, + "displayName": { + "type": "string", + "description": "Default display label for this property. Page columns and form fields can override it. Omit when the label can be derived from the property name.", + "minLength": 1 + }, + "enumType": { + "type": "string", + "description": "Name of a JSON-defined enum from top-level enums or a full type name for code enums. Required when type is 'enum'." + }, + "allowSetByClients": { + "type": "boolean", + "description": "Controls whether create/update clients may set the property. Use false for server-computed or protected fields that can still be returned to clients." + }, + "serverOnly": { + "type": "boolean", + "description": "When true, this property is completely hidden from clients, API responses, and UI definitions. Use for secrets, internal notes, hashes, or backend-only workflow state." + }, + "isMappedToDbField": { + "type": "boolean", + "description": "Whether this property is mapped to a database column. Keep true or omit for normal persisted fields. Use false for computed/transient fields supplied by scripts or backend logic." + }, + "defaultValue": { + "type": ["string", "null"], + "description": "Default value for new records. Stored as a string in model JSON and converted to the declared property type at runtime. Examples: '0' for int/enum, 'true' for boolean, '2026-05-18T09:00:00Z' for datetime." + }, + "isUnique": { + "type": "boolean", + "description": "Whether this property value must be unique across records of this entity. Use for codes, slugs, natural keys, and names only when duplicates are not allowed." + }, + "isRequired": { + "type": "boolean", + "description": "When true, the property is required/not nullable. This affects database schema, backend validation, and generated UI validation. Existing data may need defaults before turning this on." + }, + "foreignKey": { + "$ref": "foreign-key-descriptor.schema.json", + "description": "Foreign key/lookup relation. The current property's value stores the referenced entity id. The property name should usually be 'NameId', for example 'CustomerId'." + }, + "fileMaxSizeBytes": { + "type": "integer", + "description": "Maximum allowed file size in bytes for File and Image properties. Example: 5242880 for 5 MiB.", + "minimum": 1 + }, + "fileAllowedContentTypes": { + "type": "array", + "description": "Allowed MIME content types or wildcard patterns for File and Image properties. Examples: 'image/*', 'application/pdf'.", + "items": { + "type": "string", + "minLength": 1 + } + }, + "imageMaxWidth": { + "type": "integer", + "description": "Optional maximum image width in pixels.", + "minimum": 1 + }, + "imageMaxHeight": { + "type": "integer", + "description": "Optional maximum image height in pixels.", + "minimum": 1 + }, + "imageResizeMode": { + "type": "string", + "description": "How uploaded images should be resized when image dimensions are configured. 'fit' preserves the full image inside the bounds; 'fill' crops/fills the target bounds.", + "enum": ["fit", "fill"] + }, + "validators": { + "type": "array", + "description": "Backend/UI validators for this property. Use required, length, range, pattern, email, phone, url, or creditCard as appropriate. Duplicate required can be omitted when isRequired is true unless a custom message is needed.", + "items": { + "$ref": "validator-descriptor.schema.json" + } + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "examples": [ + { "name": "Name", "type": "string", "isRequired": true, "validators": [{ "type": "maxLength", "length": 128 }] }, + { "name": "Status", "type": "enum", "enumType": "Acme.Events.EventStatus", "defaultValue": "0" }, + { "name": "CustomerId", "foreignKey": { "entityName": "Acme.Crm.Customer", "displayPropertyName": "Name" } }, + { "name": "CoverImage", "type": "image", "fileAllowedContentTypes": ["image/*"], "fileMaxSizeBytes": 5242880, "imageResizeMode": "fit" } + ] +} diff --git a/schemas/low-code/definitions/entity-property-type.schema.json b/schemas/low-code/definitions/entity-property-type.schema.json new file mode 100644 index 0000000000..89d9018446 --- /dev/null +++ b/schemas/low-code/definitions/entity-property-type.schema.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "entity-property-type.schema.json", + "title": "EntityPropertyType", + "description": "Data type of an entity property. Use canonical lowercase values for new descriptors and MCP mutations; compatibility aliases are accepted for older descriptors.", + "markdownDescription": "Use canonical lowercase values in MCP/write payloads: `string`, `int`, `long`, `decimal`, `datetime`, `boolean`, `guid`, `enum`, `date`, `time`, `file`, `image`, or `money`. `string` stores text; `int`/`long` store whole numbers; `decimal` and `money` store numeric decimals; `datetime`, `date`, and `time` model temporal values; `boolean` stores true/false; `guid` stores GUID values; `enum` requires `enumType`; `file` and `image` store upload metadata/content through the low-code file pipeline. Legacy aliases such as `String`, `Int`, `dateTime`, and `DateTime` remain accepted by the schema for compatibility, but new payloads should not use .NET aliases like `Int32`.", + "type": "string", + "enum": [ + "string", + "String", + "int", + "Int", + "long", + "Long", + "decimal", + "Decimal", + "datetime", + "dateTime", + "DateTime", + "boolean", + "Boolean", + "guid", + "Guid", + "enum", + "Enum", + "date", + "Date", + "time", + "Time", + "file", + "File", + "image", + "Image", + "money", + "Money" + ], + "enumDescriptions": [ + "Text/string value.", + "Text/string value.", + "32-bit whole number.", + "32-bit whole number.", + "64-bit whole number.", + "64-bit whole number.", + "Decimal numeric value.", + "Decimal numeric value.", + "Date and time value.", + "Date and time value.", + "Date and time value.", + "Boolean true/false value.", + "Boolean true/false value.", + "GUID/UUID value.", + "GUID/UUID value.", + "Integer-backed enum value; set enumType.", + "Integer-backed enum value; set enumType.", + "Date-only value.", + "Date-only value.", + "Time-only value.", + "Time-only value.", + "Uploaded file field.", + "Uploaded file field.", + "Uploaded image field with optional dimension constraints.", + "Uploaded image field with optional dimension constraints.", + "Money amount rendered with money-aware controls.", + "Money amount rendered with money-aware controls." + ] +} diff --git a/schemas/low-code/definitions/enum-descriptor.schema.json b/schemas/low-code/definitions/enum-descriptor.schema.json new file mode 100644 index 0000000000..129725d5ca --- /dev/null +++ b/schemas/low-code/definitions/enum-descriptor.schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "enum-descriptor.schema.json", + "title": "EnumDescriptor", + "description": "Describes an integer-backed enum definition for use in entity properties and select/kanban UI.", + "markdownDescription": "AI guidance: define enums before entity properties that reference them. Use a stable namespace-style `name` such as `Acme.Events.EventStatus`. Each value name should be PascalCase and unique within the enum. Use explicit integer `value`s for stable persistence; do not reorder or renumber after data exists.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Stable unique enum name. Use a namespace-style name if the enum belongs to a module/domain, for example 'Acme.Events.EventStatus'.", + "minLength": 1 + }, + "values": { + "type": "array", + "description": "Ordered list of integer-backed enum values. Values are stored as integers; names are used for display and model readability.", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "PascalCase enum value name, for example 'Draft', 'Scheduled', or 'Completed'." + }, + "value": { + "type": "integer", + "description": "Stable integer value. Prefer explicit values starting at 0 so generated model JSON is deterministic." + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": [ + "name", + "values" + ], + "additionalProperties": false, + "examples": [ + { + "name": "Acme.Events.EventStatus", + "values": [ + { "name": "Draft", "value": 0 }, + { "name": "Scheduled", "value": 1 }, + { "name": "Completed", "value": 2 } + ] + } + ] +} diff --git a/schemas/low-code/definitions/foreign-key-descriptor.schema.json b/schemas/low-code/definitions/foreign-key-descriptor.schema.json new file mode 100644 index 0000000000..358bdee4ee --- /dev/null +++ b/schemas/low-code/definitions/foreign-key-descriptor.schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "foreign-key-descriptor.schema.json", + "title": "ForeignKeyDescriptor", + "description": "Describes a foreign key/lookup relationship from the owning entity property to another entity or reference entity.", + "markdownDescription": "AI guidance: put `foreignKey` on the property that stores the related record id. The property name should usually end with `Id`, for example `CustomerId`. `entityName` must match a model entity name or a registered code/reference entity such as `Volo.Abp.Identity.IdentityUser`. Use `displayPropertyName` when the target display field is not obvious. Use `dependsOn` for cascading dropdowns such as City filtered by Country. Use `access` only for reverse access from the referenced entity side: `none`, `view`, or `edit`. Do not use `lookup` as `access`; lookup is a form field/control type.", + "type": "object", + "properties": { + "entityName": { + "type": "string", + "description": "Full name of the related entity or registered reference entity. Must match an entity descriptor name or a known code/reference entity.", + "minLength": 1 + }, + "displayPropertyName": { + "type": "string", + "description": "Property name to display from the related entity in lookups/autocomplete. Omit to use the target entity displayProperty.", + "minLength": 1 + }, + "access": { + "type": "string", + "description": "Access level for managing this relation from the referenced entity side. 'none' means no reverse access; 'view' allows the referenced entity page to show related records; 'edit' allows managing related records from the referenced side.", + "enum": ["none", "None", "view", "View", "edit", "Edit"], + "default": "none" + }, + "dependsOn": { + "type": "object", + "description": "Cascading dependency: filter this FK lookup by the value of another FK property on the same owning entity. Example: CityId depends on CountryId and filters City.CountryId.", + "properties": { + "propertyName": { + "type": "string", + "description": "Property name on the owning entity whose value provides the filter (for example 'CountryId' on an Address entity).", + "minLength": 1 + }, + "filterPropertyName": { + "type": "string", + "description": "Property name on the target lookup entity to filter by (for example 'CountryId' on City).", + "minLength": 1 + } + }, + "required": ["propertyName", "filterPropertyName"], + "additionalProperties": false + } + }, + "required": ["entityName"], + "additionalProperties": false, + "examples": [ + { + "entityName": "Acme.Crm.Customer", + "displayPropertyName": "Name" + }, + { + "entityName": "Acme.Geo.City", + "displayPropertyName": "Name", + "dependsOn": { + "propertyName": "CountryId", + "filterPropertyName": "CountryId" + } + } + ] +} diff --git a/schemas/low-code/definitions/form-descriptor.schema.json b/schemas/low-code/definitions/form-descriptor.schema.json new file mode 100644 index 0000000000..f8bec6dfc8 --- /dev/null +++ b/schemas/low-code/definitions/form-descriptor.schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "form-descriptor.schema.json", + "title": "FormDescriptor", + "description": "Describes a named create/edit form definition bound to one entity.", + "markdownDescription": "AI guidance: a form is referenced by page `formName`, `createFormName`, or `editFormName`. Keep `entityName` aligned with the page entityName. Define all fields in `fields`, then place every visible field in `layout.tabs[].groups[].rows[].cells[]` by field id. Use form `rules` for conditional visibility/enabled state; use entity validators for core data validation.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Stable unique form identifier. Prefer kebab-case, for example 'customer-form' or 'event-form'. Pages reference this value.", + "minLength": 1 + }, + "entityName": { + "type": "string", + "description": "Full name of the entity this form is bound to. Must match the page entityName that uses this form.", + "minLength": 1 + }, + "enableSaveAndNew": { + "type": "boolean", + "description": "Whether the form should expose a Save and New action in addition to the standard save action. Useful for rapid data entry.", + "default": false + }, + "fields": { + "type": "array", + "description": "Flat list of all fields in this form. Field ids must be unique within the form; bound fields should point to properties on entityName.", + "items": { + "$ref": "form-field-descriptor.schema.json" + } + }, + "layout": { + "$ref": "form-layout-descriptor.schema.json", + "description": "Visual layout for the fields. Every layout cell fieldId must refer to a field in fields." + }, + "rules": { + "type": "array", + "description": "Conditional rules for field/group visibility, enabled state, and value setting. Use for simple client-side form behavior.", + "items": { + "$ref": "form-rule-descriptor.schema.json" + } + } + }, + "required": ["name", "entityName", "fields", "layout"], + "additionalProperties": false, + "examples": [ + { + "name": "event-form", + "entityName": "Acme.Events.Event", + "fields": [ + { "id": "title", "label": "Title", "type": "text", "binding": "Title" }, + { "id": "status", "label": "Status", "type": "select", "binding": "Status", "enumType": "Acme.Events.EventStatus" } + ], + "layout": { + "tabs": [ + { + "id": "main", + "title": "Main", + "isDefault": true, + "groups": [ + { + "id": "details", + "title": "Details", + "isDefault": true, + "rows": [ + { "cells": [{ "fieldId": "title", "colSpan": 4 }] }, + { "cells": [{ "fieldId": "status", "colSpan": 2 }] } + ] + } + ] + } + ] + } + } + ] +} diff --git a/schemas/low-code/definitions/form-field-descriptor.schema.json b/schemas/low-code/definitions/form-field-descriptor.schema.json new file mode 100644 index 0000000000..fd5f731e19 --- /dev/null +++ b/schemas/low-code/definitions/form-field-descriptor.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "form-field-descriptor.schema.json", + "title": "FormFieldDescriptor", + "description": "Describes a single field in a form. A field may be bound to an entity property or unbound for computed/display-only UI.", + "markdownDescription": "AI guidance: use a stable camelCase `id` for each field. For ordinary data entry, set `binding` to an entity property and choose a field `type` compatible with that property. For enum selects, set `type: \"select\"` and `enumType`. For FK lookups, use `type: \"lookup\"` and bind to the FK property. Put fields into layout cells by id.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this field within the form. Prefer camelCase, for example 'title' or 'customerId'. Layout cells and rules reference this id.", + "minLength": 1 + }, + "label": { + "type": "string", + "description": "Display label for the field.", + "minLength": 1 + }, + "type": { + "$ref": "form-field-type.schema.json", + "description": "Visual/input control type. Choose a type compatible with the bound entity property." + }, + "binding": { + "type": ["string", "null"], + "description": "Entity property name to bind to, or null/omitted for unbound fields. Supports dotted paths like 'Parent.Name' for related entity display where supported." + }, + "enumType": { + "type": "string", + "description": "Enum name for select fields bound to enum properties. Must match the entity property's enumType or a known code enum." + }, + "defaultValue": { + "description": "Default field value used by the UI when creating a new record. Prefer entity property defaultValue for persisted defaults." + }, + "placeholder": { + "type": "string", + "description": "Placeholder text for the input" + }, + "helpText": { + "type": "string", + "description": "Help text displayed below the field" + }, + "readOnly": { + "type": "boolean", + "description": "Whether the field is read-only in the form UI. This does not by itself protect backend writes; use allowSetByClients/serverOnly for security.", + "default": false + }, + "modeVisibility": { + "type": "string", + "enum": ["both", "Both", "createOnly", "CreateOnly", "editOnly", "EditOnly"], + "description": "Controls in which form mode the field is visible: both, createOnly, or editOnly.", + "default": "both" + }, + "validations": { + "type": "array", + "description": "Form-level validation rules that supplement entity-level validators. Use when validation is specific to this form.", + "items": { + "$ref": "validator-descriptor.schema.json" + } + } + }, + "required": ["id", "label", "type"], + "additionalProperties": false, + "examples": [ + { "id": "title", "label": "Title", "type": "text", "binding": "Title", "validations": [{ "type": "required" }] }, + { "id": "status", "label": "Status", "type": "select", "binding": "Status", "enumType": "Acme.Events.EventStatus" }, + { "id": "customerId", "label": "Customer", "type": "lookup", "binding": "CustomerId" } + ] +} diff --git a/schemas/low-code/definitions/form-field-type.schema.json b/schemas/low-code/definitions/form-field-type.schema.json new file mode 100644 index 0000000000..2b0c5630f2 --- /dev/null +++ b/schemas/low-code/definitions/form-field-type.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "form-field-type.schema.json", + "title": "FormFieldType", + "description": "Available form field control types. Use canonical lowercase values for new descriptors and MCP mutations; compatibility aliases are accepted for older descriptors.", + "markdownDescription": "Use canonical lowercase values in MCP/write payloads: `text`, `textarea`, `number`, `checkbox`, `date`, `datetime`, `select`, `lookup`, `guid`, `computed`, `time`, `file`, `image`, or `money`. `text`/`textarea` are for strings, `number` for numeric values, `checkbox` for boolean, `date`/`datetime`/`time` for temporal values, `select` for enum values, `lookup` for foreign keys, `guid` for GUID input/display, `computed` for unbound calculated/display fields, `file` and `image` for upload fields, and `money` for money/decimal amount input. There is no `email` field type; use `text` plus validations where needed. Legacy PascalCase aliases remain accepted by the schema for compatibility.", + "type": "string", + "enum": [ + "text", + "Text", + "textarea", + "Textarea", + "number", + "Number", + "checkbox", + "Checkbox", + "date", + "Date", + "datetime", + "DateTime", + "select", + "Select", + "lookup", + "Lookup", + "guid", + "Guid", + "computed", + "Computed", + "time", + "Time", + "file", + "File", + "image", + "Image", + "money", + "Money" + ], + "enumDescriptions": [ + "Single-line text input.", + "Single-line text input.", + "Multi-line text input.", + "Multi-line text input.", + "Numeric input.", + "Numeric input.", + "Boolean checkbox.", + "Boolean checkbox.", + "Date-only input.", + "Date-only input.", + "Date and time input.", + "Date and time input.", + "Enum/select input.", + "Enum/select input.", + "Foreign key lookup/autocomplete input.", + "Foreign key lookup/autocomplete input.", + "GUID input/display.", + "GUID input/display.", + "Unbound computed/display field.", + "Unbound computed/display field.", + "Time-only input.", + "Time-only input.", + "File upload field.", + "File upload field.", + "Image upload field.", + "Image upload field.", + "Money amount input.", + "Money amount input." + ] +} diff --git a/schemas/low-code/definitions/form-layout-descriptor.schema.json b/schemas/low-code/definitions/form-layout-descriptor.schema.json new file mode 100644 index 0000000000..6d3eb42dc0 --- /dev/null +++ b/schemas/low-code/definitions/form-layout-descriptor.schema.json @@ -0,0 +1,128 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "form-layout-descriptor.schema.json", + "title": "FormLayoutDescriptor", + "description": "Describes the visual layout of a form as tabs, groups, rows, and field cells.", + "markdownDescription": "AI guidance: the layout is a tree: tabs -> groups -> rows -> cells. Each cell `fieldId` must reference a field from the form's `fields` array. Use `colSpan` 4 for full-width fields, 2+2 for two columns, or 1+1+1+1 for four compact controls. The total effective width in a row should not exceed 4.", + "type": "object", + "properties": { + "tabs": { + "type": "array", + "description": "Ordered list of tabs in the form. Use one default tab named 'main' for simple forms.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this tab. Prefer camelCase/kebab-case such as 'main' or 'advanced'.", + "minLength": 1 + }, + "title": { + "type": "string", + "description": "Display title for the tab", + "minLength": 1 + }, + "isDefault": { + "type": "boolean", + "description": "Whether this is the default tab. The designer uses the default tab as the safe target for orphaned fields.", + "default": false + }, + "groups": { + "type": "array", + "description": "Ordered list of groups within this tab. Use one default group for simple forms.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this group. Rules can target group ids.", + "minLength": 1 + }, + "title": { + "type": ["string", "null"], + "description": "Optional display title for the group. Use null or omit for an untitled group." + }, + "isDefault": { + "type": "boolean", + "description": "Whether this is the default group. The designer uses the default group as the safe target for orphaned fields.", + "default": false + }, + "rows": { + "type": "array", + "description": "Ordered list of layout rows; each row contains one or more cells placed side-by-side.", + "items": { + "type": "object", + "properties": { + "cells": { + "type": "array", + "description": "Fields placed side-by-side in this row. The sum of colSpan values should not exceed 4.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "fieldId": { + "type": "string", + "description": "Reference to a field id in the form's fields array. Every field shown in the layout needs a matching field descriptor.", + "minLength": 1 + }, + "colSpan": { + "type": "integer", + "description": "Number of grid columns this field spans from 1 to 4. Use 4 for full-width fields.", + "minimum": 1, + "maximum": 4, + "default": 4 + }, + "colStart": { + "type": "integer", + "description": "Starting grid column from 1 to 4. Omit or null to auto-place after the previous cell.", + "minimum": 1, + "maximum": 4 + } + }, + "required": ["fieldId"], + "additionalProperties": false + } + } + }, + "required": ["cells"], + "additionalProperties": false + } + } + }, + "required": ["id", "rows"], + "additionalProperties": false + } + } + }, + "required": ["id", "title", "groups"], + "additionalProperties": false + } + } + }, + "required": ["tabs"], + "additionalProperties": false, + "examples": [ + { + "tabs": [ + { + "id": "main", + "title": "Main", + "isDefault": true, + "groups": [ + { + "id": "details", + "title": "Details", + "isDefault": true, + "rows": [ + { "cells": [{ "fieldId": "title", "colSpan": 4 }] }, + { "cells": [{ "fieldId": "startDate", "colSpan": 2 }, { "fieldId": "endDate", "colSpan": 2 }] } + ] + } + ] + } + ] + } + ] +} diff --git a/schemas/low-code/definitions/form-rule-descriptor.schema.json b/schemas/low-code/definitions/form-rule-descriptor.schema.json new file mode 100644 index 0000000000..f558b6bb1d --- /dev/null +++ b/schemas/low-code/definitions/form-rule-descriptor.schema.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "form-rule-descriptor.schema.json", + "title": "FormRuleDescriptor", + "description": "Describes a conditional form rule with one or more actions that execute when the condition is met.", + "markdownDescription": "AI guidance: use rules for simple client-side behavior such as hiding a group when a checkbox is false, disabling a field after a status is selected, or setting a default value. `condition.fieldId` and every action `targetId` must reference existing field/group ids. Rules are not security boundaries; enforce sensitive behavior with backend validation/interceptors too.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this rule. Prefer kebab-case or camelCase such as 'show-archive-reason'.", + "minLength": 1 + }, + "name": { + "type": "string", + "description": "Human-readable name for this rule, used by designers/documentation." + }, + "condition": { + "type": "object", + "description": "The condition that triggers this rule. For isEmpty/isNotEmpty, omit value.", + "properties": { + "fieldId": { + "type": "string", + "description": "The field whose value is evaluated. Must match a field id in the same form.", + "minLength": 1 + }, + "operator": { + "type": "string", + "enum": ["equals", "notEquals", "isEmpty", "isNotEmpty"], + "description": "Comparison operator. Use equals/notEquals with value; use isEmpty/isNotEmpty without value." + }, + "value": { + "description": "The value to compare against (not used for isEmpty/isNotEmpty)" + } + }, + "required": ["fieldId", "operator"], + "additionalProperties": false + }, + "actions": { + "type": "array", + "description": "Actions to perform when the condition is met. Actions are executed in order.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["hide", "show", "disable", "enable", "setValue"], + "description": "The action type. hide/show/disable/enable target fields or groups; setValue targets fields." + }, + "targetType": { + "type": "string", + "enum": ["field", "group"], + "description": "Whether the target is a field or a group." + }, + "targetId": { + "type": "string", + "description": "The id of the target field or group. Must exist in this form's fields or layout groups.", + "minLength": 1 + }, + "value": { + "description": "The value to set. Used only for setValue actions." + } + }, + "required": ["type", "targetType", "targetId"], + "additionalProperties": false + } + } + }, + "required": ["id", "condition", "actions"], + "additionalProperties": false, + "examples": [ + { + "id": "show-archive-reason", + "name": "Show archive reason", + "condition": { "fieldId": "status", "operator": "equals", "value": 3 }, + "actions": [ + { "type": "show", "targetType": "field", "targetId": "archiveReason" } + ] + } + ] +} diff --git a/schemas/low-code/definitions/interceptor-type.schema.json b/schemas/low-code/definitions/interceptor-type.schema.json new file mode 100644 index 0000000000..49ce6eca3a --- /dev/null +++ b/schemas/low-code/definitions/interceptor-type.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "interceptor-type.schema.json", + "title": "InterceptorType", + "description": "When an entity command interceptor runs. Prefer PascalCase values shown here.", + "markdownDescription": "`Pre` runs before the command is persisted and can block with `globalError`. `Post` runs after the command succeeds and is best for logging, notifications, publishing events, or enqueueing jobs. `Replace` is reserved for replacing command behavior where supported and should be avoided unless the host explicitly supports it.", + "type": "string", + "enum": [ + "Pre", + "pre", + "Post", + "post", + "Replace", + "replace" + ], + "enumDescriptions": [ + "Before persistence; can block with globalError.", + "Before persistence; can block with globalError.", + "After successful persistence; use for side effects.", + "After successful persistence; use for side effects.", + "Replace command behavior where supported.", + "Replace command behavior where supported." + ] +} diff --git a/schemas/low-code/definitions/page-column-descriptor.schema.json b/schemas/low-code/definitions/page-column-descriptor.schema.json new file mode 100644 index 0000000000..ebf0073e4a --- /dev/null +++ b/schemas/low-code/definitions/page-column-descriptor.schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "page-column-descriptor.schema.json", + "title": "PageColumnDescriptor", + "description": "Describes a property displayed by an entity page list, card, calendar event, or gallery item.", + "markdownDescription": "AI guidance: use page columns/card fields to control runtime page visibility. Each `propertyName` must match a property on the page entity. Keep this separate from entity property definitions so the same entity can have different views on different pages.", + "type": "object", + "properties": { + "propertyName": { + "type": "string", + "description": "Property displayed by this column/card field. Must match a property on the page entityName.", + "minLength": 1 + }, + "label": { + "type": "string", + "description": "Optional page-specific label override. Omit to use the property displayName/name.", + "minLength": 1 + }, + "order": { + "type": "integer", + "description": "Display order. Lower values appear first.", + "default": 0 + }, + "exportOrder": { + "type": "integer", + "description": "Optional export order. Lower values appear first in Excel, CSV, download-link columns, and file bundles. Omit to reuse display order." + }, + "width": { + "type": "string", + "description": "Optional CSS width for tabular columns, for example '160px', '12rem', or '20%'.", + "minLength": 1 + }, + "visible": { + "type": "boolean", + "description": "Whether the field is visible by default on this page.", + "default": true + }, + "exportable": { + "type": "boolean", + "description": "Whether the field can be exported from this page.", + "default": true + } + }, + "required": ["propertyName"], + "additionalProperties": false, + "examples": [ + { "propertyName": "Title", "label": "Title", "order": 0, "exportOrder": 0, "width": "240px", "visible": true }, + { "propertyName": "InternalNotes", "visible": false, "exportable": false } + ] +} diff --git a/schemas/low-code/definitions/page-descriptor.schema.json b/schemas/low-code/definitions/page-descriptor.schema.json new file mode 100644 index 0000000000..3b5c1a2fcd --- /dev/null +++ b/schemas/low-code/definitions/page-descriptor.schema.json @@ -0,0 +1,260 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "page-descriptor.schema.json", + "title": "PageDescriptor", + "description": "Describes a runtime UI page. Pages create menu items and choose how an entity or dashboard is rendered.", + "markdownDescription": "AI guidance: `name` is the stable route/menu key and should be URL-safe kebab-case (for example `customers` or `event-calendar`). `title` is user-facing. For entity pages, set `entityName` to an existing entity. Configure list/gallery/card visibility with `columns`, filtering with `filters`, and create/edit UI with form names. For `dashboard` pages, provide `dashboard` and omit entity-only fields unless intentionally supported by the runtime. Do not put page-only settings on entity properties.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Stable URL-safe page identifier. Prefer kebab-case, for example 'customers', 'event-calendar', or 'sales-dashboard'. This becomes part of the runtime route and is referenced by saveSuccessPageName.", + "minLength": 1 + }, + "title": { + "type": "string", + "description": "Display title for the menu item and page header.", + "minLength": 1 + }, + "icon": { + "type": "string", + "description": "FontAwesome icon class for the menu item, for example 'fa-solid fa-users'." + }, + "type": { + "$ref": "page-type.schema.json" + }, + "entityName": { + "type": "string", + "description": "Full name of the root entity this page displays. Required for dataGrid, kanban, calendar, gallery, and form pages. Usually omitted for dashboard pages.", + "minLength": 1 + }, + "groupByProperty": { + "type": "string", + "description": "Property name to group entities by. Required for kanban pages. Must reference a property on entityName, preferably an enum/status property with a small fixed set of values.", + "minLength": 1 + }, + "calendarStartProperty": { + "type": "string", + "description": "Date or DateTime property used as the start date for calendar pages. Required for calendar pages.", + "minLength": 1 + }, + "calendarEndProperty": { + "type": "string", + "description": "Optional Date or DateTime property used as the end date for calendar pages. Use when events can span a date range.", + "minLength": 1 + }, + "calendarTimeProperty": { + "type": "string", + "description": "Optional Time property used as the start time for calendar pages when the start date is date-only or time is stored separately.", + "minLength": 1 + }, + "calendarDurationProperty": { + "type": "string", + "description": "Optional numeric property used as duration in minutes for calendar pages. Use this instead of calendarEndProperty when records store duration.", + "minLength": 1 + }, + "galleryImageProperty": { + "type": "string", + "description": "Optional Image property used as the cover image for gallery pages. The property should have entity property type image.", + "minLength": 1 + }, + "defaultSortProperty": { + "type": "string", + "description": "Optional property used as the default sorting field when the client does not send explicit sorting. Must reference a property on entityName.", + "minLength": 1 + }, + "defaultSortDescending": { + "type": "boolean", + "description": "Whether the default sort property is sorted descending.", + "default": false + }, + "defaultFileExportMode": { + "type": "integer", + "enum": [0, 1, 2], + "description": "Default file export mode for file/image columns. 0 exports file names, 1 exports metadata columns, and 2 exports download-link columns." + }, + "allowFileBundleExport": { + "type": "boolean", + "description": "Whether file bundle export is available for this page.", + "default": true + }, + "columns": { + "type": "array", + "description": "Page-owned column/card field configuration. Use for dataGrid, kanban card fields, calendar event fields, and gallery card fields. Each propertyName must reference a property on entityName.", + "items": { + "$ref": "page-column-descriptor.schema.json" + } + }, + "filters": { + "type": "array", + "description": "Page-owned filter configuration for filterable page types. Each propertyName must reference a property on entityName. Use this instead of legacy property-level UI filter settings.", + "items": { + "$ref": "page-filter-descriptor.schema.json" + } + }, + "order": { + "type": "integer", + "description": "Menu sort order (lower values appear first)", + "default": 0 + }, + "formName": { + "type": "string", + "description": "Name of the form to render. Required when type is 'form'. Must match a form descriptor whose entityName matches this page entityName.", + "minLength": 1 + }, + "createFormName": { + "type": "string", + "description": "Name of the form to use when creating records from dataGrid, kanban, calendar, or gallery pages. Must match a form descriptor for the same entity." + }, + "editFormName": { + "type": "string", + "description": "Name of the form to use when editing records from dataGrid, kanban, calendar, or gallery pages. Must match a form descriptor for the same entity." + }, + "createFormDisplay": { + "type": "string", + "enum": ["modal", "Modal", "page", "Page"], + "description": "How to display the create form. Use 'modal' for quick CRUD; use 'page' for longer forms or when deep links are desired.", + "default": "modal" + }, + "editFormDisplay": { + "type": "string", + "enum": ["modal", "Modal", "page", "Page"], + "description": "How to display the edit form. Use 'modal' for quick CRUD; use 'page' for longer forms or when deep links are desired.", + "default": "modal" + }, + "saveSuccessNavigation": { + "type": "string", + "enum": ["stay", "page", "url"], + "description": "Navigation behavior after a standalone form page saves successfully. 'stay' keeps the user on the form page; 'page' navigates to saveSuccessPageName; 'url' navigates to saveSuccessUrl.", + "default": "stay" + }, + "saveSuccessPageName": { + "type": "string", + "description": "Existing page name to open after a form page saves successfully when saveSuccessNavigation is 'page'.", + "minLength": 1 + }, + "saveSuccessUrl": { + "type": "string", + "description": "URL or application path to open after a form page saves successfully when saveSuccessNavigation is 'url'.", + "minLength": 1 + }, + "dashboard": { + "$ref": "dashboard-descriptor.schema.json", + "description": "Dashboard layout and visualizations. Required only when type is dashboard." + }, + "group": { + "type": "string", + "description": "Name of the page group this page belongs to. Must match pageGroups[].name. Omit for root-level pages.", + "minLength": 1 + }, + "permissionConfig": { + "$ref": "page-permission-config.schema.json", + "description": "Optional page operation permission overrides. Omit operations to use generated defaults." + } + }, + "required": ["name", "title", "type"], + "allOf": [ + { + "if": { + "properties": { "type": { "enum": ["dataGrid", "DataGrid"] } } + }, + "then": { + "required": ["entityName"] + } + }, + { + "if": { + "properties": { "type": { "enum": ["kanban", "Kanban"] } } + }, + "then": { + "required": ["entityName", "groupByProperty"] + } + }, + { + "if": { + "properties": { "type": { "enum": ["calendar", "Calendar"] } } + }, + "then": { + "required": ["entityName", "calendarStartProperty"] + } + }, + { + "if": { + "properties": { "type": { "enum": ["gallery", "Gallery"] } } + }, + "then": { + "required": ["entityName"] + } + }, + { + "if": { + "properties": { "type": { "enum": ["form", "Form"] } } + }, + "then": { + "required": ["entityName", "formName"] + } + }, + { + "if": { + "properties": { "type": { "enum": ["dashboard", "Dashboard"] } } + }, + "then": { + "required": ["dashboard"] + } + } + ], + "additionalProperties": false, + "examples": [ + { + "name": "events", + "title": "Events", + "type": "dataGrid", + "entityName": "Acme.Events.Event", + "columns": [ + { "propertyName": "Title", "order": 0 }, + { "propertyName": "Status", "order": 1 } + ], + "filters": [ + { "propertyName": "Title", "control": "text", "defaultOperator": "contains" } + ], + "createFormName": "event-form", + "editFormName": "event-form" + }, + { + "name": "event-calendar", + "title": "Event Calendar", + "type": "calendar", + "entityName": "Acme.Events.Event", + "calendarStartProperty": "StartDate", + "calendarEndProperty": "EndDate" + }, + { + "name": "event-dashboard", + "title": "Event Dashboard", + "type": "dashboard", + "dashboard": { + "rows": [ + { + "items": [ + { + "name": "events-by-status", + "type": "chart", + "title": "Events by Status", + "entityName": "Acme.Events.Event", + "chart": { + "chartType": "bar", + "xAxis": { "property": "Status" }, + "yAxis": [{ "aggregation": "count", "label": "Events" }] + } + } + ] + } + ] + } + } + ] +} diff --git a/schemas/low-code/definitions/page-filter-descriptor.schema.json b/schemas/low-code/definitions/page-filter-descriptor.schema.json new file mode 100644 index 0000000000..ed63ba0653 --- /dev/null +++ b/schemas/low-code/definitions/page-filter-descriptor.schema.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "page-filter-descriptor.schema.json", + "title": "PageFilterDescriptor", + "description": "Describes a runtime filter control for an entity page.", + "markdownDescription": "AI guidance: each filter must point to a page entity property. Use `control: \"auto\"` unless a specific control is needed. Use text/contains for string search, range/between for numeric values, dateRange/timeRange for temporal values, select/multiSelect for enums, lookup for foreign keys, and exists for null checks. Do not use the legacy `operator` property; use `defaultOperator`.", + "type": "object", + "properties": { + "propertyName": { + "type": "string", + "description": "Property filtered by this control. Must match a property on the page entityName.", + "minLength": 1 + }, + "label": { + "type": "string", + "description": "Optional page-specific label override. Omit to use the property displayName/name.", + "minLength": 1 + }, + "order": { + "type": "integer", + "description": "Filter display order. Lower values appear first.", + "default": 0 + }, + "visible": { + "type": "boolean", + "description": "Whether the filter is shown at runtime", + "default": true + }, + "control": { + "type": "string", + "description": "Runtime control used by the filter. 'auto' selects based on property type; choose explicit controls for predictable generated UI.", + "enum": ["auto", "text", "range", "dateRange", "timeRange", "select", "multiSelect", "lookup", "exists"], + "default": "auto" + }, + "defaultOperator": { + "type": "string", + "description": "Default operator applied by this filter. Use 'contains' for text search, 'equal' for enum/FK exact matches, 'between' for ranges, and 'default' to let runtime choose.", + "enum": [ + "default", + "equal", + "notEqual", + "contains", + "notContains", + "startsWith", + "endsWith", + "greaterThan", + "greaterThanOrEqual", + "lessThan", + "lessThanOrEqual", + "between", + "hasValue", + "isNull", + "isNotNull", + "in", + "notIn" + ] + }, + "allowedOperators": { + "type": "array", + "description": "Operators the runtime filter UI allows users to choose. Omit to let the runtime choose based on property type/control.", + "items": { + "type": "string", + "enum": [ + "default", + "equal", + "notEqual", + "contains", + "notContains", + "startsWith", + "endsWith", + "greaterThan", + "greaterThanOrEqual", + "lessThan", + "lessThanOrEqual", + "between", + "hasValue", + "isNull", + "isNotNull", + "in", + "notIn" + ] + }, + "uniqueItems": true + }, + "defaultValue": { + "description": "Optional default filter value applied when the page first loads. Shape depends on control/operator." + }, + "placeholder": { + "type": "string", + "description": "Optional runtime input placeholder" + }, + "helpText": { + "type": "string", + "description": "Optional runtime help text" + }, + "allowMultipleValues": { + "type": "boolean", + "description": "Whether the filter can submit multiple values. Useful with multiSelect/in/notIn.", + "default": false + }, + "clearable": { + "type": "boolean", + "description": "Whether runtime users can clear the filter.", + "default": true + }, + "initiallyExpanded": { + "type": "boolean", + "description": "Whether the runtime filter panel opens expanded.", + "default": false + } + }, + "required": ["propertyName"], + "additionalProperties": false, + "examples": [ + { "propertyName": "Title", "control": "text", "defaultOperator": "contains", "placeholder": "Search title" }, + { "propertyName": "Status", "control": "select", "defaultOperator": "equal", "clearable": true }, + { "propertyName": "StartDate", "control": "dateRange", "defaultOperator": "between" } + ] +} diff --git a/schemas/low-code/definitions/page-group-descriptor.schema.json b/schemas/low-code/definitions/page-group-descriptor.schema.json new file mode 100644 index 0000000000..c142f45500 --- /dev/null +++ b/schemas/low-code/definitions/page-group-descriptor.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "page-group-descriptor.schema.json", + "title": "PageGroupDescriptor", + "description": "Describes a menu group/folder that can contain pages and sub-groups.", + "markdownDescription": "AI guidance: create page groups when several pages belong to the same feature area. Pages reference groups by `group`. Nested groups reference parent groups by `parent`. Use stable kebab-case names and concise user-facing titles.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Unique URL-safe identifier for the group. Prefer kebab-case, for example 'crm' or 'content-studio'.", + "minLength": 1 + }, + "title": { + "type": "string", + "description": "Display title for the menu group.", + "minLength": 1 + }, + "icon": { + "type": "string", + "description": "FontAwesome icon class, for example 'fa-solid fa-folder' or 'fa-solid fa-calendar-days'." + }, + "order": { + "type": "integer", + "description": "Sort order within the parent level (lower values appear first)", + "default": 0 + }, + "parent": { + "type": "string", + "description": "Name of the parent group for nesting. Must match another pageGroups[].name. Omit for root-level groups.", + "minLength": 1 + } + }, + "required": ["name", "title"], + "additionalProperties": false, + "examples": [ + { "name": "events", "title": "Events", "icon": "fa-solid fa-calendar-days", "order": 10 }, + { "name": "event-admin", "title": "Admin", "parent": "events", "order": 20 } + ] +} diff --git a/schemas/low-code/definitions/page-permission-config.schema.json b/schemas/low-code/definitions/page-permission-config.schema.json new file mode 100644 index 0000000000..7ae1531402 --- /dev/null +++ b/schemas/low-code/definitions/page-permission-config.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "page-permission-config.schema.json", + "title": "PagePermissionConfig", + "description": "Permission configuration for a page's operations. Only overridden values are stored; omitted values use auto-generated defaults based on the page.", + "markdownDescription": "AI guidance: each operation can be `default`, `public`, `authenticated`, or a permission name. Omit an operation to use runtime-generated defaults. Use `public` only for anonymous pages. Use `authenticated` for pages any logged-in user can view. Use explicit permission names for controlled business operations.", + "type": "object", + "properties": { + "view": { + "type": "string", + "description": "Permission for viewing the page. Can be a specific permission name, 'default', 'public', or 'authenticated'. Omit to use auto-generated default." + }, + "create": { + "type": "string", + "description": "Permission for create operation on this page. Can be a specific permission name, 'default', 'public', or 'authenticated'. Omit to use auto-generated default." + }, + "update": { + "type": "string", + "description": "Permission for update operation on this page. Can be a specific permission name, 'default', 'public', or 'authenticated'. Omit to use auto-generated default." + }, + "delete": { + "type": "string", + "description": "Permission for delete operation on this page. Can be a specific permission name, 'default', 'public', or 'authenticated'. Omit to use auto-generated default." + } + }, + "additionalProperties": false +} diff --git a/schemas/low-code/definitions/page-type.schema.json b/schemas/low-code/definitions/page-type.schema.json new file mode 100644 index 0000000000..679dea16f7 --- /dev/null +++ b/schemas/low-code/definitions/page-type.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "page-type.schema.json", + "title": "PageType", + "description": "The runtime page renderer to use. Prefer lower-case values in model JSON; PascalCase aliases are accepted for compatibility.", + "markdownDescription": "`dataGrid` renders searchable/filterable tabular CRUD for an entity. `kanban` renders grouped cards and requires `groupByProperty`. `calendar` renders entity records on a calendar and requires `calendarStartProperty`. `gallery` renders visual cards and may use `galleryImageProperty`. `form` renders a standalone create/edit form page and requires `formName`. `dashboard` renders dashboard rows/visualizations and requires `dashboard`.", + "type": "string", + "enum": [ + "dataGrid", + "DataGrid", + "kanban", + "Kanban", + "calendar", + "Calendar", + "form", + "Form", + "dashboard", + "Dashboard", + "gallery", + "Gallery" + ], + "enumDescriptions": [ + "Entity data grid/list page.", + "Entity data grid/list page.", + "Entity kanban board grouped by an enum/status property.", + "Entity kanban board grouped by an enum/status property.", + "Entity calendar page using date/time properties.", + "Entity calendar page using date/time properties.", + "Standalone form page bound to a form descriptor.", + "Standalone form page bound to a form descriptor.", + "Dashboard page with chart/list/number visualizations.", + "Dashboard page with chart/list/number visualizations.", + "Entity gallery/card page, optionally image-backed.", + "Entity gallery/card page, optionally image-backed." + ] +} diff --git a/schemas/low-code/definitions/permission-descriptor.schema.json b/schemas/low-code/definitions/permission-descriptor.schema.json new file mode 100644 index 0000000000..f6391be449 --- /dev/null +++ b/schemas/low-code/definitions/permission-descriptor.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "permission-descriptor.schema.json", + "title": "PermissionDescriptor", + "description": "Defines a custom permission created through the designer. Supports parent-child hierarchy.", + "markdownDescription": "AI guidance: use stable dot-separated permission names, for example `Acme.Events.Create`. Define a parent permission for the feature and child permissions for operations. Pages and endpoints can reference these names. Permission definitions only declare permissions; roles/users must still be granted permissions through permission management.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Unique dot-separated permission name, for example 'Acme.Events.Create'.", + "minLength": 1 + }, + "displayName": { + "type": "string", + "description": "Human-readable display name shown in permission management UI.", + "minLength": 1 + }, + "children": { + "type": "array", + "description": "Child permissions forming a hierarchy. Use for feature -> operation grouping.", + "items": { + "$ref": "permission-descriptor.schema.json" + } + } + }, + "required": ["name", "displayName"], + "additionalProperties": false, + "examples": [ + { + "name": "Acme.Events", + "displayName": "Events", + "children": [ + { "name": "Acme.Events.Create", "displayName": "Create events" }, + { "name": "Acme.Events.Update", "displayName": "Update events" }, + { "name": "Acme.Events.Delete", "displayName": "Delete events" } + ] + } + ] +} diff --git a/schemas/low-code/definitions/script-background-job-descriptor.schema.json b/schemas/low-code/definitions/script-background-job-descriptor.schema.json new file mode 100644 index 0000000000..9a186ad083 --- /dev/null +++ b/schemas/low-code/definitions/script-background-job-descriptor.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "script-background-job-descriptor.schema.json", + "title": "Script Background Job Descriptor", + "description": "Defines a named JavaScript background job handler.", + "markdownDescription": "AI guidance: use background jobs for asynchronous work that is explicitly enqueued by code/scripts, such as notifications, imports, or long-running recalculations. `name` is the job identifier used by enqueue calls. Keep scripts retry-safe and idempotent because background jobs may run more than once after failures.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Unique identifier for the background job. Scripts/enqueuers reference this name.", + "minLength": 1 + }, + "javascript": { + "type": "string", + "description": "JavaScript code to execute when the job is enqueued/run. Use context payload/job arguments where available and keep retry-safe.", + "minLength": 1 + }, + "description": { + "type": "string", + "description": "Optional description for designer documentation and model health context." + } + }, + "required": ["name", "javascript"], + "additionalProperties": false, + "examples": [ + { + "name": "SendEventReminder", + "javascript": "context.log('Sending event reminder job.');" + } + ] +} diff --git a/schemas/low-code/definitions/script-background-worker-descriptor.schema.json b/schemas/low-code/definitions/script-background-worker-descriptor.schema.json new file mode 100644 index 0000000000..8e670bd129 --- /dev/null +++ b/schemas/low-code/definitions/script-background-worker-descriptor.schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "script-background-worker-descriptor.schema.json", + "title": "Script Background Worker Descriptor", + "description": "Defines a scheduled JavaScript background worker.", + "markdownDescription": "AI guidance: use background workers for recurring scheduled work. Provide either `period` in milliseconds or `cronExpression`; do not provide both unless the host explicitly chooses one. Keep scripts idempotent and short. Use workers for polling, cleanup, synchronization, or recurring summary generation.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Unique identifier for the background worker.", + "minLength": 1 + }, + "period": { + "type": "integer", + "description": "Execution period in milliseconds. Example: 300000 for every 5 minutes.", + "minimum": 1 + }, + "cronExpression": { + "type": ["string", "null"], + "description": "Cron expression for scheduler-backed providers. Omit it, set it to null, or leave it empty when the worker uses period-based scheduling." + }, + "javascript": { + "type": "string", + "description": "JavaScript code to execute when the worker runs. Keep idempotent; workers may overlap or retry depending on scheduler configuration.", + "minLength": 1 + }, + "description": { + "type": "string", + "description": "Optional description for designer documentation and model health context." + } + }, + "required": ["name", "javascript"], + "anyOf": [ + { "required": ["period"] }, + { + "required": ["cronExpression"], + "properties": { + "cronExpression": { + "type": "string", + "minLength": 1 + } + } + } + ], + "additionalProperties": false, + "examples": [ + { + "name": "CleanupExpiredEvents", + "period": 3600000, + "javascript": "context.log('Running expired event cleanup.');" + } + ] +} diff --git a/schemas/low-code/definitions/script-event-handler-descriptor.schema.json b/schemas/low-code/definitions/script-event-handler-descriptor.schema.json new file mode 100644 index 0000000000..da29d7fd4d --- /dev/null +++ b/schemas/low-code/definitions/script-event-handler-descriptor.schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "script-event-handler-descriptor.schema.json", + "title": "Script Event Handler Descriptor", + "description": "Defines a named JavaScript handler for a distributed event name.", + "markdownDescription": "AI guidance: use event handlers for asynchronous reactions to domain/application events. `eventName` must match the event name published by the application or another script. Keep handlers idempotent because distributed events can be retried. Scripts can use the host-provided context, including event payload/request data where available, db, current tenant/user, email, logging, event publishing, and job enqueue helpers.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Unique identifier for the handler. Prefer a descriptive name such as 'NotifyWhenEventCompleted'.", + "minLength": 1 + }, + "eventName": { + "type": "string", + "description": "Distributed event name to subscribe to. Must exactly match the publisher's event name.", + "minLength": 1 + }, + "javascript": { + "type": "string", + "description": "JavaScript code to execute when the event is received. Keep idempotent; avoid assuming one-time delivery.", + "minLength": 1 + }, + "description": { + "type": "string", + "description": "Optional description for designer documentation and model health context." + } + }, + "required": ["name", "eventName", "javascript"], + "additionalProperties": false, + "examples": [ + { + "name": "NotifyEventCompleted", + "eventName": "Acme.Events.EventCompleted", + "javascript": "context.log('Event completed handler executed.');" + } + ] +} diff --git a/schemas/low-code/definitions/validator-descriptor.schema.json b/schemas/low-code/definitions/validator-descriptor.schema.json new file mode 100644 index 0000000000..f81f51eb9a --- /dev/null +++ b/schemas/low-code/definitions/validator-descriptor.schema.json @@ -0,0 +1,96 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "validator-descriptor.schema.json", + "title": "ValidatorDescriptor", + "description": "A single backend/UI validator applied to an entity property or form field.", + "markdownDescription": "AI guidance: always set `type`. Use `required` for mandatory input, `minLength`/`maxLength`/`stringLength` for strings, `min`/`max`/`range` for numbers and comparable values, `pattern` or `regularExpression` for regex, and `email`, `phone`, `url`, or `creditCard` for common formats. Use lower-case validator names for new JSON. Add `message` only when a custom localized/user-facing message is needed.", + "type": "object", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "description": "Validator type. Prefer lower-case values in model JSON; PascalCase aliases are accepted for compatibility.", + "enum": [ + "required", + "Required", + "minLength", + "MinLength", + "maxLength", + "MaxLength", + "stringLength", + "StringLength", + "min", + "Min", + "minimum", + "Minimum", + "max", + "Max", + "maximum", + "Maximum", + "range", + "Range", + "pattern", + "Pattern", + "regularExpression", + "RegularExpression", + "email", + "Email", + "emailAddress", + "EmailAddress", + "phone", + "Phone", + "url", + "Url", + "creditCard", + "CreditCard" + ] + }, + "message": { + "type": "string", + "description": "Optional custom error message shown when validation fails. Omit to use the default localized message." + }, + "length": { + "type": "integer", + "description": "Length value for minLength or maxLength validators. Example: { \"type\": \"maxLength\", \"length\": 128 }.", + "minimum": 0 + }, + "minimumLength": { + "type": "integer", + "description": "Minimum length for stringLength validator.", + "minimum": 0 + }, + "maximumLength": { + "type": "integer", + "description": "Maximum length for stringLength validator.", + "minimum": 0 + }, + "value": { + "type": "number", + "description": "Generic numeric value alias for single-value validators such as min/minimum, max/maximum, minLength/maxLength, and stringLength maximumLength." + }, + "minimum": { + "type": "number", + "description": "Minimum value for min/minimum or range validators." + }, + "maximum": { + "type": "number", + "description": "Maximum value for max/maximum or range validators." + }, + "pattern": { + "type": "string", + "description": "Regular expression pattern for pattern/regularExpression validators. Store the regex pattern string only; do not include leading/trailing slashes." + }, + "allowEmptyStrings": { + "type": "boolean", + "description": "Whether required validation should allow empty strings. Usually false for user-entered text." + } + }, + "additionalProperties": true, + "examples": [ + { "type": "required" }, + { "type": "maxLength", "length": 128 }, + { "type": "range", "minimum": 0, "maximum": 100 }, + { "type": "pattern", "pattern": "^[A-Z]{3}-[0-9]{4}$", "message": "Code must match ABC-1234." }, + { "type": "email" } + ] +} diff --git a/schemas/low-code/manifest.json b/schemas/low-code/manifest.json new file mode 100644 index 0000000000..de39544885 --- /dev/null +++ b/schemas/low-code/manifest.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "name": "ABP Low-Code Model Descriptor Schemas", + "version": "10.5", + "publicBaseUrl": "https://raw.githubusercontent.com/abpframework/abp/rel-10.5/schemas/low-code", + "definitionsPath": "definitions", + "descriptorSchemas": { + "enums": "definitions/enum-descriptor.schema.json", + "entities": "definitions/entity-descriptor.schema.json", + "endpoints": "definitions/endpoint-descriptor.schema.json", + "eventHandlers": "definitions/script-event-handler-descriptor.schema.json", + "backgroundJobs": "definitions/script-background-job-descriptor.schema.json", + "backgroundWorkers": "definitions/script-background-worker-descriptor.schema.json", + "pageGroups": "definitions/page-group-descriptor.schema.json", + "pages": "definitions/page-descriptor.schema.json", + "forms": "definitions/form-descriptor.schema.json", + "permissions": "definitions/permission-descriptor.schema.json" + } +} From 69cb0c1c9724decc8467cc2bbed09c016abebf25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 9 Jun 2026 14:54:04 +0300 Subject: [PATCH 2/3] Address low-code schema review feedback --- .../dashboard-row-descriptor.schema.json | 3 +- ...board-visualization-descriptor.schema.json | 50 ++++++++++++++++--- .../endpoint-descriptor.schema.json | 10 ++-- .../form-layout-descriptor.schema.json | 2 +- .../permission-descriptor.schema.json | 2 +- ...t-background-worker-descriptor.schema.json | 14 +++++- 6 files changed, 68 insertions(+), 13 deletions(-) diff --git a/schemas/low-code/definitions/dashboard-row-descriptor.schema.json b/schemas/low-code/definitions/dashboard-row-descriptor.schema.json index bd1abbc591..909048ec29 100644 --- a/schemas/low-code/definitions/dashboard-row-descriptor.schema.json +++ b/schemas/low-code/definitions/dashboard-row-descriptor.schema.json @@ -22,11 +22,12 @@ { "items": [ { + "name": "overview", "type": "numberContainer", "title": "Overview", "numberContainer": { "items": [ - { "title": "Total Records", "entity": "Acme.Events.Event", "aggregation": "count" } + { "name": "total-records", "title": "Total Records", "entityName": "Acme.Events.Event", "aggregation": "count" } ] } } diff --git a/schemas/low-code/definitions/dashboard-visualization-descriptor.schema.json b/schemas/low-code/definitions/dashboard-visualization-descriptor.schema.json index 0a958fa9a1..6fd6e91f31 100644 --- a/schemas/low-code/definitions/dashboard-visualization-descriptor.schema.json +++ b/schemas/low-code/definitions/dashboard-visualization-descriptor.schema.json @@ -90,16 +90,54 @@ "required": ["name", "type", "title"], "allOf": [ { - "if": { "properties": { "type": { "const": "chart" } } }, - "then": { "required": ["chart", "entityName"] } + "if": { + "required": ["type"], + "properties": { + "type": { "enum": ["chart", "Chart"] } + } + }, + "then": { + "required": ["chart", "entityName"], + "properties": { + "chart": { "$ref": "dashboard-chart-descriptor.schema.json" }, + "entityName": { + "type": "string", + "minLength": 1 + } + } + } }, { - "if": { "properties": { "type": { "const": "list" } } }, - "then": { "required": ["list", "entityName"] } + "if": { + "required": ["type"], + "properties": { + "type": { "enum": ["list", "List"] } + } + }, + "then": { + "required": ["list", "entityName"], + "properties": { + "list": { "$ref": "dashboard-list-descriptor.schema.json" }, + "entityName": { + "type": "string", + "minLength": 1 + } + } + } }, { - "if": { "properties": { "type": { "const": "numberContainer" } } }, - "then": { "required": ["numberContainer"] } + "if": { + "required": ["type"], + "properties": { + "type": { "enum": ["numberContainer", "NumberContainer"] } + } + }, + "then": { + "required": ["numberContainer"], + "properties": { + "numberContainer": { "$ref": "dashboard-number-container-descriptor.schema.json" } + } + } } ], "additionalProperties": false, diff --git a/schemas/low-code/definitions/endpoint-descriptor.schema.json b/schemas/low-code/definitions/endpoint-descriptor.schema.json index 4990fb8b89..cdbfaa18fb 100644 --- a/schemas/low-code/definitions/endpoint-descriptor.schema.json +++ b/schemas/low-code/definitions/endpoint-descriptor.schema.json @@ -12,11 +12,14 @@ }, "name": { "type": "string", - "description": "Unique endpoint identifier used by designer/model health. Prefer PascalCase or kebab-case, for example 'SearchCustomers'." + "description": "Unique endpoint identifier used by designer/model health. Prefer PascalCase or kebab-case, for example 'SearchCustomers'.", + "minLength": 1 }, "route": { "type": "string", - "description": "URL route pattern. Must start with '/' and should use an application-specific prefix such as '/api/low-code/events/{id}'. Route parameters use ASP.NET style braces, for example '{id}'." + "description": "URL route pattern. Must start with '/' and should use an application-specific prefix such as '/api/low-code/events/{id}'. Route parameters use ASP.NET style braces, for example '{id}'.", + "minLength": 1, + "pattern": "^/" }, "method": { "type": "string", @@ -26,7 +29,8 @@ }, "javascript": { "type": "string", - "description": "JavaScript code to execute. Use context request/response helpers and services exposed by the host, such as db, currentUser/currentTenant, authorization, emailSender, config, http, event bus, background jobs, and logging helpers." + "description": "JavaScript code to execute. Use context request/response helpers and services exposed by the host, such as db, currentUser/currentTenant, authorization, emailSender, config, http, event bus, background jobs, and logging helpers.", + "minLength": 1 }, "requireAuthentication": { "type": "boolean", diff --git a/schemas/low-code/definitions/form-layout-descriptor.schema.json b/schemas/low-code/definitions/form-layout-descriptor.schema.json index 6d3eb42dc0..abadb86e21 100644 --- a/schemas/low-code/definitions/form-layout-descriptor.schema.json +++ b/schemas/low-code/definitions/form-layout-descriptor.schema.json @@ -75,7 +75,7 @@ "default": 4 }, "colStart": { - "type": "integer", + "type": ["integer", "null"], "description": "Starting grid column from 1 to 4. Omit or null to auto-place after the previous cell.", "minimum": 1, "maximum": 4 diff --git a/schemas/low-code/definitions/permission-descriptor.schema.json b/schemas/low-code/definitions/permission-descriptor.schema.json index f6391be449..1abeb49841 100644 --- a/schemas/low-code/definitions/permission-descriptor.schema.json +++ b/schemas/low-code/definitions/permission-descriptor.schema.json @@ -24,7 +24,7 @@ "type": "array", "description": "Child permissions forming a hierarchy. Use for feature -> operation grouping.", "items": { - "$ref": "permission-descriptor.schema.json" + "$ref": "#" } } }, diff --git a/schemas/low-code/definitions/script-background-worker-descriptor.schema.json b/schemas/low-code/definitions/script-background-worker-descriptor.schema.json index 8e670bd129..2c0ba07eb8 100644 --- a/schemas/low-code/definitions/script-background-worker-descriptor.schema.json +++ b/schemas/low-code/definitions/script-background-worker-descriptor.schema.json @@ -36,9 +36,21 @@ }, "required": ["name", "javascript"], "anyOf": [ - { "required": ["period"] }, + { + "required": ["period"], + "not": { + "required": ["cronExpression"], + "properties": { + "cronExpression": { + "type": "string", + "minLength": 1 + } + } + } + }, { "required": ["cronExpression"], + "not": { "required": ["period"] }, "properties": { "cronExpression": { "type": "string", From 26cc9978d1d8300d1f43b926a580d84cfae88725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Wed, 10 Jun 2026 09:23:43 +0300 Subject: [PATCH 3/3] Use descriptor JSON wording in low-code schemas --- .../low-code/definitions/dashboard-aggregation-type.schema.json | 2 +- schemas/low-code/definitions/dashboard-chart-type.schema.json | 2 +- .../definitions/dashboard-visualization-type.schema.json | 2 +- .../low-code/definitions/entity-property-descriptor.schema.json | 2 +- schemas/low-code/definitions/enum-descriptor.schema.json | 2 +- schemas/low-code/definitions/page-type.schema.json | 2 +- schemas/low-code/definitions/validator-descriptor.schema.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/schemas/low-code/definitions/dashboard-aggregation-type.schema.json b/schemas/low-code/definitions/dashboard-aggregation-type.schema.json index e18a2b5b6f..0de9d6da48 100644 --- a/schemas/low-code/definitions/dashboard-aggregation-type.schema.json +++ b/schemas/low-code/definitions/dashboard-aggregation-type.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "dashboard-aggregation-type.schema.json", "title": "DashboardAggregationType", - "description": "The type of aggregation to perform. Prefer lower-case values in model JSON; PascalCase aliases are accepted for compatibility.", + "description": "The type of aggregation to perform. Prefer lower-case values in descriptor JSON; PascalCase aliases are accepted for compatibility.", "markdownDescription": "`count` counts records and does not require a property. `sum`, `average`, `min`, and `max` require a property. `percentFilled` and `percentEmpty` calculate completeness for a nullable/string property.", "type": "string", "enum": [ diff --git a/schemas/low-code/definitions/dashboard-chart-type.schema.json b/schemas/low-code/definitions/dashboard-chart-type.schema.json index cfada5467e..fe3b2124e5 100644 --- a/schemas/low-code/definitions/dashboard-chart-type.schema.json +++ b/schemas/low-code/definitions/dashboard-chart-type.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "dashboard-chart-type.schema.json", "title": "DashboardChartType", - "description": "The type of chart. Prefer lower-case values in model JSON; PascalCase aliases are accepted for compatibility.", + "description": "The type of chart. Prefer lower-case values in descriptor JSON; PascalCase aliases are accepted for compatibility.", "markdownDescription": "Use `bar` for categorical comparisons, `line` for trends over time, `pie`/`donut` for part-of-whole views with a small number of categories.", "type": "string", "enum": ["bar", "Bar", "line", "Line", "pie", "Pie", "donut", "Donut"], diff --git a/schemas/low-code/definitions/dashboard-visualization-type.schema.json b/schemas/low-code/definitions/dashboard-visualization-type.schema.json index 721fb2977f..0f490d6cdf 100644 --- a/schemas/low-code/definitions/dashboard-visualization-type.schema.json +++ b/schemas/low-code/definitions/dashboard-visualization-type.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "dashboard-visualization-type.schema.json", "title": "DashboardVisualizationType", - "description": "The type of dashboard visualization. Prefer lower-case values in model JSON; PascalCase aliases are accepted for compatibility.", + "description": "The type of dashboard visualization. Prefer lower-case values in descriptor JSON; PascalCase aliases are accepted for compatibility.", "markdownDescription": "`chart` groups/aggregates entity data into a bar/line/pie/donut chart. `list` displays recent/top records from one entity. `numberContainer` displays one or more KPI number tiles, each with its own entity and aggregation.", "type": "string", "enum": ["chart", "Chart", "list", "List", "numberContainer", "NumberContainer"], diff --git a/schemas/low-code/definitions/entity-property-descriptor.schema.json b/schemas/low-code/definitions/entity-property-descriptor.schema.json index c4e85cf5f6..ed4cc8da06 100644 --- a/schemas/low-code/definitions/entity-property-descriptor.schema.json +++ b/schemas/low-code/definitions/entity-property-descriptor.schema.json @@ -38,7 +38,7 @@ }, "defaultValue": { "type": ["string", "null"], - "description": "Default value for new records. Stored as a string in model JSON and converted to the declared property type at runtime. Examples: '0' for int/enum, 'true' for boolean, '2026-05-18T09:00:00Z' for datetime." + "description": "Default value for new records. Stored as a string in descriptor JSON and converted to the declared property type at runtime. Examples: '0' for int/enum, 'true' for boolean, '2026-05-18T09:00:00Z' for datetime." }, "isUnique": { "type": "boolean", diff --git a/schemas/low-code/definitions/enum-descriptor.schema.json b/schemas/low-code/definitions/enum-descriptor.schema.json index 129725d5ca..4a70d5c8f0 100644 --- a/schemas/low-code/definitions/enum-descriptor.schema.json +++ b/schemas/low-code/definitions/enum-descriptor.schema.json @@ -27,7 +27,7 @@ }, "value": { "type": "integer", - "description": "Stable integer value. Prefer explicit values starting at 0 so generated model JSON is deterministic." + "description": "Stable integer value. Prefer explicit values starting at 0 so generated descriptor JSON is deterministic." } }, "required": [ diff --git a/schemas/low-code/definitions/page-type.schema.json b/schemas/low-code/definitions/page-type.schema.json index 679dea16f7..5ddeec0eb5 100644 --- a/schemas/low-code/definitions/page-type.schema.json +++ b/schemas/low-code/definitions/page-type.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "page-type.schema.json", "title": "PageType", - "description": "The runtime page renderer to use. Prefer lower-case values in model JSON; PascalCase aliases are accepted for compatibility.", + "description": "The runtime page renderer to use. Prefer lower-case values in descriptor JSON; PascalCase aliases are accepted for compatibility.", "markdownDescription": "`dataGrid` renders searchable/filterable tabular CRUD for an entity. `kanban` renders grouped cards and requires `groupByProperty`. `calendar` renders entity records on a calendar and requires `calendarStartProperty`. `gallery` renders visual cards and may use `galleryImageProperty`. `form` renders a standalone create/edit form page and requires `formName`. `dashboard` renders dashboard rows/visualizations and requires `dashboard`.", "type": "string", "enum": [ diff --git a/schemas/low-code/definitions/validator-descriptor.schema.json b/schemas/low-code/definitions/validator-descriptor.schema.json index f81f51eb9a..83f18a39a0 100644 --- a/schemas/low-code/definitions/validator-descriptor.schema.json +++ b/schemas/low-code/definitions/validator-descriptor.schema.json @@ -9,7 +9,7 @@ "properties": { "type": { "type": "string", - "description": "Validator type. Prefer lower-case values in model JSON; PascalCase aliases are accepted for compatibility.", + "description": "Validator type. Prefer lower-case values in descriptor JSON; PascalCase aliases are accepted for compatibility.", "enum": [ "required", "Required",