@ -0,0 +1,135 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Use the ABP Admin Console Low-Code Designer to create dynamic entities, pages, forms, relations, filters, permissions, and model health checks." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# Low-Code Designer |
||||
|
|
||||
|
> **Preview:** The Low-Code Designer is part of the preview Low-Code System. Designer screens, metadata fields, and validation rules may change before general availability. |
||||
|
|
||||
|
The Low-Code Designer is available in ABP Admin Console. It is the main UI for building and maintaining low-code descriptor metadata. |
||||
|
|
||||
|
```text |
||||
|
/admin-console/lowcode-designer |
||||
|
``` |
||||
|
|
||||
|
The designer works with layered descriptor metadata. In development, generated projects include source-controlled descriptor files under `_Dynamic` and a generated initializer. The designer can also persist changes to the database JSON layer, depending on the selected layer and permissions. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Permissions and Layers |
||||
|
|
||||
|
Designer APIs require the low-code designer permission. Users who can open runtime pages do not automatically get designer access. |
||||
|
|
||||
|
The selected layer controls whether the designer can save changes. Read-only layers can be inspected but not mutated, and the designer blocks edits to layers that are not currently selected. Check the active layer before changing entities, pages, forms, scripts, or permissions. |
||||
|
|
||||
|
## Sections |
||||
|
|
||||
|
| Section | Purpose | |
||||
|
|---------|---------| |
||||
|
| Data | Entities, enums, properties, relations, inherited fields, and reference entities | |
||||
|
| Actions | Script-backed custom endpoints, event handlers, background jobs, workers, and model actions | |
||||
|
| Pages | Runtime pages, menu placement, page type, grid/card fields, filters, sorting, dashboards, and linked forms | |
||||
|
| Forms | Create/edit forms, tabs, groups, fields, controls, defaults, and actions | |
||||
|
| Permissions | Generated permission names and access control | |
||||
|
| Health | Model validation and runtime readiness checks | |
||||
|
|
||||
|
## Data |
||||
|
|
||||
|
Use **Data** to define the domain model. |
||||
|
|
||||
|
Entities contain properties, display names, display property configuration, inherited audit fields, relations, and optional interceptors. Enums are created once and then used by enum properties. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Relations |
||||
|
|
||||
|
Relations are driven by foreign key properties. The designer shows direct N to 1 relations and many-to-many relations that are modeled through junction entities. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
For reference entities such as `IdentityUser`, register the entity in the generated `_Dynamic` initializer. The designer and runtime can then show friendly display values instead of raw IDs. |
||||
|
|
||||
|
## Pages |
||||
|
|
||||
|
Use **Pages** to expose an entity in the React runtime. |
||||
|
|
||||
|
Pages can define data grid, kanban, calendar, gallery, standalone form, and dashboard experiences. A data grid page can define: |
||||
|
|
||||
|
* Title and icon |
||||
|
* Menu group and order |
||||
|
* Entity |
||||
|
* Create and edit forms |
||||
|
* Visible grid/card fields |
||||
|
* Export fields for Excel, CSV, download-link columns, and file bundles |
||||
|
* Field labels and column widths |
||||
|
* Default sorting |
||||
|
* Filter fields and defaults |
||||
|
* File/image export defaults when the page has exportable file fields |
||||
|
* Whether file bundle ZIP export is allowed |
||||
|
|
||||
|
Kanban pages add `groupByProperty`, calendar pages add date/time properties, gallery pages can use an image property, form pages reference a named form, and dashboard pages define rows and visualizations. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Pages are exposed in React under `/dynamic/<page-name>` and can also appear as dynamic menu items. |
||||
|
|
||||
|
The **View Fields** section controls what users see in the runtime view. The separate **Export Fields** section controls what may leave the page through Excel, CSV, download-link columns, or file bundles. A visible field can be excluded from export, and an exportable field can be hidden from the page but still available through **Export options > All exportable fields**. Use the bulk actions to copy visible fields into the export policy, enable all fields, or disable all fields. |
||||
|
|
||||
|
Open **Export Fields > More settings** only when you need custom export details. If **Use default export settings** is enabled, export uses display labels, the view field order, file name output, and file bundle export enabled. Turn it off to customize export labels, assign a separate export order, choose **Default File/Image Output**, or disable **Allow file bundle export**. File/image output controls appear only when the page has file or image fields, and they stay inactive until at least one file/image field is exportable. |
||||
|
|
||||
|
## Forms |
||||
|
|
||||
|
Use **Forms** to define create and edit experiences. |
||||
|
|
||||
|
Forms can contain: |
||||
|
|
||||
|
* Tabs |
||||
|
* Groups |
||||
|
* Ordered fields |
||||
|
* Control types |
||||
|
* Placeholder and help text |
||||
|
* Default values |
||||
|
* Required and validation behavior |
||||
|
* Conditional rules for hide/show, enable/disable, and set value behavior |
||||
|
* Save actions such as "save and new" |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Filters |
||||
|
|
||||
|
Filters are configured per page and rendered by the React runtime. The runtime uses type-specific operators to keep the UI simple: |
||||
|
|
||||
|
* String fields use text operators. |
||||
|
* Number and money fields use range and comparison operators. |
||||
|
* Date fields use date labels such as on, after, and before. |
||||
|
* Boolean fields use `All / Yes / No`. |
||||
|
* File and image fields use `Has value` with `All / Yes / No`. |
||||
|
|
||||
|
The URL query parameter keeps the existing `lcFilters` format, so bookmarked filtered pages continue to work. |
||||
|
|
||||
|
## Permissions |
||||
|
|
||||
|
Dynamic permissions are generated for entities and pages. Use the **Permissions** section to review names and grant access through the normal ABP permission management UI. |
||||
|
|
||||
|
Generated pages and menus are permission-aware. If a user cannot access a page, the runtime does not show the menu item and API calls remain protected by backend authorization. |
||||
|
|
||||
|
## Actions and Scripts |
||||
|
|
||||
|
Use **Actions** only when descriptor metadata and standard CRUD behavior are not enough. The scripting surface can define custom HTTP endpoints, distributed event handlers, background jobs, and scheduled background workers. Scripts run server-side and use the [Scripting API](scripting-api.md). |
||||
|
|
||||
|
JavaScript editors in the Designer provide syntax highlighting, service autocomplete, entity name autocomplete, entity property autocomplete, enum autocomplete, and an **Available context** list. The context list reflects the services enabled for the selected script type. The `fileFields` and `imageFields` context items are selector trees for `File` and `Image` properties used by the file and image helpers; they are not lists of every entity property. |
||||
|
|
||||
|
Endpoint and event handler editors include **Test JavaScript**. Dry-run execution runs the current editor content without saving it, rolls back database writes, captures email/event/job/file side effects, and resolves outbound HTTP calls through test mocks. See [Script Actions](script-actions.md) for action descriptors and dry-run behavior. |
||||
|
|
||||
|
## Health |
||||
|
|
||||
|
Use **Health** before shipping changes. It helps catch missing display properties, invalid relation targets, form/page references, script problems, and other model issues that would otherwise surface at runtime. |
||||
|
|
||||
|
## Source Control |
||||
|
|
||||
|
For source-controlled models, keep the `_Dynamic` descriptor files and the generated initializer in your application repository. Use [Model Descriptor Files](model-json.md), [Attributes & Fluent API](fluent-api.md), and [Reference Entities](reference-entities.md) for advanced editing and integration details. |
||||
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 70 KiB |
@ -0,0 +1,276 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Configure the ABP React Low-Code runtime with configureLowCode, dynamic routes, dynamic menu items, generated pages, filters, forms, and export." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# React Runtime |
||||
|
|
||||
|
> **Preview:** The React low-code runtime is part of the preview Low-Code System. Runtime APIs, generated routes, metadata contracts, and UI behavior may change before general availability. |
||||
|
|
||||
|
The React runtime renders low-code pages from backend metadata. Generated low-code React applications include the required package and wiring. |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"dependencies": { |
||||
|
"@volo/abp-react-lowcode": "<version>" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Configure the Runtime |
||||
|
|
||||
|
Call `configureLowCode` once during React startup. Pass the application's Axios instance, notifications, localization, navigation integration, and optional extension points. |
||||
|
|
||||
|
```tsx |
||||
|
import { |
||||
|
configureLowCode, |
||||
|
LowCodeLocalizationProvider, |
||||
|
} from '@volo/abp-react-lowcode'; |
||||
|
|
||||
|
function configureLowCodeRuntime(translate?: (key: string, defaultValue: string) => string) { |
||||
|
configureLowCode({ |
||||
|
axios: api, |
||||
|
onError: (err) => toast.error(err.message), |
||||
|
onSuccess: (message) => toast.success(message), |
||||
|
translate, |
||||
|
navigate: (path) => router.navigate({ to: path }), |
||||
|
validators: { |
||||
|
customRule: (value) => value ? null : 'Value is required.', |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Wrap the router with `LowCodeLocalizationProvider` when you want dynamic labels and validation messages to use the application localization pipeline. |
||||
|
|
||||
|
```tsx |
||||
|
<LowCodeLocalizationProvider translate={translate}> |
||||
|
<RouterProvider router={router} /> |
||||
|
</LowCodeLocalizationProvider> |
||||
|
``` |
||||
|
|
||||
|
## Add Dynamic Routes |
||||
|
|
||||
|
Use `createDynamicRoutes` in the TanStack Router tree. |
||||
|
|
||||
|
```tsx |
||||
|
import { createDynamicRoutes } from '@volo/abp-react-lowcode'; |
||||
|
|
||||
|
const dynamicEntityRoute = createDynamicRoutes(rootRoute, { |
||||
|
beforeLoad: authGuard, |
||||
|
}); |
||||
|
|
||||
|
const routeTree = rootRoute.addChildren([ |
||||
|
indexRoute, |
||||
|
accountRoute, |
||||
|
identityRoute, |
||||
|
dynamicEntityRoute, |
||||
|
]); |
||||
|
``` |
||||
|
|
||||
|
Runtime pages are then available under: |
||||
|
|
||||
|
```text |
||||
|
/dynamic/<page-name> |
||||
|
/dynamic/<page-name>/create |
||||
|
/dynamic/<page-name>/edit/<record-id> |
||||
|
/dynamic/<page-name>/<record-id> |
||||
|
``` |
||||
|
|
||||
|
`createDynamicRoutes` also accepts `basePath` when your application should mount dynamic pages somewhere other than `/dynamic`. |
||||
|
|
||||
|
Generated low-code React templates do not register every possible dynamic page path in TanStack Router's module augmentation. Page names come from backend metadata, so keep dynamic low-code routes out of the static route augmentation or cast the dynamic `path` inside your router navigation adapter if your application uses strict typed navigation. |
||||
|
|
||||
|
## Add Dynamic Menu Items |
||||
|
|
||||
|
Use `useMenuItems` to load menu items defined by low-code pages. Merge them with your static route configuration and apply the same permission checks used by the rest of the application. |
||||
|
|
||||
|
```tsx |
||||
|
import { useMenuItems } from '@volo/abp-react-lowcode'; |
||||
|
|
||||
|
const { data: dynamicMenuItems } = useMenuItems({ |
||||
|
enabled: isAuthenticated, |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
Each menu item includes its page name, display name, icon, order, grouping information, and children. |
||||
|
|
||||
|
## Page Types |
||||
|
|
||||
|
The runtime includes built-in renderers for these page types: |
||||
|
|
||||
|
| Page type | Runtime behavior | |
||||
|
|-----------|------------------| |
||||
|
| `dataGrid` | Searchable, sortable CRUD grid | |
||||
|
| `kanban` | Card board grouped by a configured property | |
||||
|
| `calendar` | Calendar view using date/time properties | |
||||
|
| `gallery` | Card/gallery view, optionally image-backed | |
||||
|
| `form` | Standalone form page | |
||||
|
| `dashboard` | Dashboard rows with chart, list, and number visualizations | |
||||
|
|
||||
|
The generated data grid page includes: |
||||
|
|
||||
|
* Search |
||||
|
* Sorting |
||||
|
* Paging |
||||
|
* Action menu |
||||
|
* Create and edit forms |
||||
|
* Permission-aware commands |
||||
|
* Display values for lookups |
||||
|
* File and image fields |
||||
|
* Export |
||||
|
* Type-aware filters |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Forms |
||||
|
|
||||
|
Create and edit forms are rendered from form metadata. Tabs, groups, labels, placeholders, controls, default values, validation rules, conditional form rules, and save actions come from the designer. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
The runtime can render forms in a modal or on full pages. Full-page forms use the dynamic create/edit routes and the `navigate` callback configured in `configureLowCode`. |
||||
|
|
||||
|
## Filters |
||||
|
|
||||
|
Filters are rendered as an ABP-style advanced filter area. The runtime shows all configured filters and only exposes operator UI where it is useful. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
File and image filters use a single `Has value` concept. The value selector controls whether the filter is applied: |
||||
|
|
||||
|
* `All` does not add a filter. |
||||
|
* `Yes` returns records with a value. |
||||
|
* `No` returns records without a value. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
The URL keeps the existing `lcFilters` query parameter shape. The runtime maps user-friendly filter choices to the existing backend `FilterType` values. |
||||
|
|
||||
|
## Export |
||||
|
|
||||
|
The runtime export button opens a small menu with direct Excel and CSV actions. If the selected page has exportable file or image fields and bundle export is allowed in the Designer, the menu also shows **Files (.zip)**. Direct export uses the current search, sorting, filters, and visible exportable columns from the page definition maintained in the Low-Code Designer. Use **Export options** when users need a different row, column, or file output scope. |
||||
|
|
||||
|
Available options: |
||||
|
|
||||
|
| Option | Behavior | |
||||
|
|--------|----------| |
||||
|
| Rows: all matching records | Exports all records matching the current search, filters, and sorting | |
||||
|
| Rows: current page | Exports only the current page using the runtime `skipCount` and `maxResultCount` | |
||||
|
| Columns: visible exportable columns | Exports columns that are both visible and exportable in the current page definition, ordered by Designer export order | |
||||
|
| Columns: all exportable fields | Exports page fields marked exportable in the Low-Code Designer, including fields that are hidden from the grid, ordered by Designer export order | |
||||
|
| File/image: file name | Writes the uploaded file name, or an empty value | |
||||
|
| File/image: metadata columns | Writes file name, content type, size, width, and height columns | |
||||
|
| File/image: download links | Writes file name, temporary download URL, link expiry, content type, size, width, height, and status columns | |
||||
|
|
||||
|
Spreadsheet export stays tabular. It does not embed file bytes in cells. Use download-link columns when spreadsheet readers need a controlled way to fetch individual files, or use **Files (.zip)** when they need the actual file set. ZIP export contains `manifest.csv` and files under `files/{recordId}/{fieldName}/{safeFileName}`. The manifest reports missing, malformed, unlinked, skipped, and exported files. |
||||
|
|
||||
|
The columns available in the runtime export dialog and their default order are controlled by the page-level **Export Fields** section in the Low-Code Designer. The runtime cannot use the dialog to bypass non-exportable fields. |
||||
|
|
||||
|
The runtime first requests a short-lived token and then calls the Excel, CSV, or ZIP export endpoint. The export token is single-use and is bound to the current page, entity, tenant, child page, and foreign-access context. Temporary file links use separate short-lived tokens bound to the exported record field and blob. Text that looks like a spreadsheet formula is escaped in exported headers and cells. If a caller manually sends a non-exportable field name, the backend rejects the request. |
||||
|
|
||||
|
| Endpoint | Description | |
||||
|
|----------|-------------| |
||||
|
| `GET /api/low-code/pages/{pageName}/download-token` | Gets a short-lived token | |
||||
|
| `GET /api/low-code/pages/{pageName}/export/excel` | Downloads Excel | |
||||
|
| `GET /api/low-code/pages/{pageName}/export/csv` | Downloads CSV | |
||||
|
| `GET /api/low-code/pages/{pageName}/export/files` | Downloads a ZIP with selected file/image fields | |
||||
|
| `GET /api/low-code/pages/export/files/{token}` | Downloads one temporary file link from spreadsheet link mode | |
||||
|
|
||||
|
Child and foreign-access pages use the matching `/children/{childEntityName}` and `/foreign-access/{sourceEntityName}` page endpoints. |
||||
|
|
||||
|
Troubleshooting: |
||||
|
|
||||
|
| Symptom | Likely cause | |
||||
|
|---------|--------------| |
||||
|
| Invalid or expired download token | The token is single-use, expired, or was requested for a different page/context | |
||||
|
| Export row limit exceeded | Narrow the filters, export the current page, or increase `LowCode:Export:MaxRows` | |
||||
|
| File link expired | Re-run export; temporary file links are intentionally short-lived | |
||||
|
| File not exported marker | The file was missing, malformed, no longer linked to the exported record, or skipped by ZIP file count/size limits | |
||||
|
| ZIP bundle export disabled | Enable file bundle export for the page in the Low-Code Designer | |
||||
|
|
||||
|
## Files and Attachments |
||||
|
|
||||
|
File and image fields use the page file endpoints. Record-level attachments use attachment endpoints when attachments are enabled for the entity. |
||||
|
|
||||
|
| API | Purpose | |
||||
|
|-----|---------| |
||||
|
| `uploadPageFile` | Upload a file/image field value for a page field | |
||||
|
| `downloadPageFile` | Download a file/image field value | |
||||
|
| `buildPageFileDownloadUrl` | Build a URL for image previews or download links | |
||||
|
| `usePageFileObjectUrl` | Create and revoke an object URL for file previews | |
||||
|
| `useEntityAttachments` | Load record attachments | |
||||
|
| `useUploadEntityAttachments` | Upload one or more attachments | |
||||
|
| `useDeleteEntityAttachment` | Delete an attachment | |
||||
|
| `downloadEntityAttachment` | Download an attachment | |
||||
|
|
||||
|
Uploads are still validated on the backend by the configured file size and content type rules. Downloads are checked against the owning record before returning the blob, and user-provided file names should be treated as display text only. Use `usePageFileObjectUrl` for previews so object URLs are revoked when the component unmounts. |
||||
|
|
||||
|
## Hooks and Components |
||||
|
|
||||
|
The package exposes hooks for composing custom pages around the same backend API: |
||||
|
|
||||
|
| API | Purpose | |
||||
|
|-----|---------| |
||||
|
| `usePageDefinitions`, `usePageDefinition` | Load page metadata | |
||||
|
| `useFormDefinition` | Load a named runtime form | |
||||
|
| `useDashboardDefinition`, `useDashboardData` | Load dashboard metadata and data | |
||||
|
| `usePageData`, `usePageRecord` | Load list data and a single record | |
||||
|
| `usePageCreate`, `usePageUpdate`, `usePageDelete` | Mutate records | |
||||
|
| `usePageLookup` | Load lookup/autocomplete options | |
||||
|
| `usePageExport` | Export current list state | |
||||
|
|
||||
|
The package also exports renderer components such as `DynamicPage`, `DynamicEntityPage`, `DynamicKanbanRenderer`, `DynamicCalendarRenderer`, `DynamicGalleryRenderer`, `DynamicDashboardRenderer`, `DynamicFormPageRenderer`, `DynamicFilters`, `DynamicEntityForm`, and `ForeignKeyAutocomplete`. |
||||
|
|
||||
|
## Extension Points |
||||
|
|
||||
|
`configureLowCode` supports these extension points: |
||||
|
|
||||
|
| Option | Purpose | |
||||
|
|--------|---------| |
||||
|
| `validators` | Custom validation functions keyed by rule type | |
||||
|
| `pageRenderers` | Override built-in page renderers or add new page types | |
||||
|
| `fieldRenderers` | Override field rendering by field type or `customRenderer` | |
||||
|
| `translate` | Resolve low-code localization keys through the application | |
||||
|
| `navigate` | Connect full-page form navigation to the application router | |
||||
|
|
||||
|
Custom page renderers receive `PageRendererProps`. Custom field renderers receive `FieldRendererProps`. |
||||
|
|
||||
|
## Runtime API Surface |
||||
|
|
||||
|
The React runtime talks to these backend endpoints: |
||||
|
|
||||
|
| Endpoint | Purpose | |
||||
|
|----------|---------| |
||||
|
| `GET /api/low-code/ui/menu-items` | Dynamic menu tree | |
||||
|
| `GET /api/low-code/ui/pages` | Page list | |
||||
|
| `GET /api/low-code/ui/pages/{pageName}` | Page summary metadata | |
||||
|
| `GET /api/low-code/ui/pages/{pageName}/ui-definition` | Runtime page UI definition | |
||||
|
| `GET /api/low-code/ui/forms/{formName}` | Runtime form definition | |
||||
|
| `GET /api/low-code/ui/dashboards/{pageName}` | Dashboard definition | |
||||
|
| `GET /api/low-code/pages/{pageName}/data` | Page data with search, filters, sorting, and paging | |
||||
|
| `GET /api/low-code/pages/{pageName}/data/{id}` | Single page record | |
||||
|
| `POST /api/low-code/pages/{pageName}/data` | Create record | |
||||
|
| `PUT /api/low-code/pages/{pageName}/data/{id}` | Update record | |
||||
|
| `DELETE /api/low-code/pages/{pageName}/data/{id}` | Delete record | |
||||
|
| `GET /api/low-code/pages/{pageName}/lookup/{fieldName}` | Lookup options | |
||||
|
| `POST /api/low-code/pages/{pageName}/files/{fieldName}` | Upload file/image field | |
||||
|
| `GET /api/low-code/pages/{pageName}/data/{id}/files/{fieldName}/{blobName}` | Download file/image field | |
||||
|
| `GET /api/low-code/pages/{pageName}/data/{id}/attachments` | List attachments | |
||||
|
| `POST /api/low-code/pages/{pageName}/data/{id}/attachments` | Upload attachment | |
||||
|
| `DELETE /api/low-code/pages/{pageName}/data/{id}/attachments/{attachmentId}` | Delete attachment | |
||||
|
| `POST /api/low-code/dashboards/{pageName}/data` | Dashboard visualization data | |
||||
|
|
||||
|
## Troubleshooting |
||||
|
|
||||
|
If a generated page does not appear: |
||||
|
|
||||
|
* Confirm the page exists in the designer and has a page name. |
||||
|
* Confirm the user has the generated page/entity permissions. |
||||
|
* Confirm `createDynamicRoutes` is part of the router tree. |
||||
|
* Confirm `useMenuItems` is enabled for authenticated users. |
||||
|
* Confirm the backend host has run migrations and seed data. |
||||
|
|
||||
|
If authentication loops back to the login page, check the generated OpenIddict clients and React root URL in `appsettings.json`. |
||||
@ -0,0 +1,259 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Define event handlers, background jobs, background workers, script code editing, autocomplete, and dry-run testing in the ABP Low-Code Designer." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# Script Actions |
||||
|
|
||||
|
> **Preview:** Script actions are part of the preview Low-Code System. Descriptor fields, designer screens, and script context members may change before general availability. |
||||
|
|
||||
|
Use **Actions** in the Low-Code Designer when descriptor metadata and generated CRUD behavior are not enough. Actions can add JavaScript-backed HTTP endpoints, distributed event handlers, background jobs, and scheduled background workers. |
||||
|
|
||||
|
Custom HTTP endpoints are documented separately in [Custom Endpoints](custom-endpoints.md). This page focuses on the shared action designer experience and the event handler, background job, and background worker action types. |
||||
|
|
||||
|
## Action Types |
||||
|
|
||||
|
| Type | Use it for | Runtime trigger | |
||||
|
|------|------------|-----------------| |
||||
|
| Custom endpoint | Expose a small model-owned REST API | HTTP request to the configured route | |
||||
|
| Event handler | React to a named distributed event | `events.publishAsync(...)` or any compatible distributed event publisher | |
||||
|
| Background job | Run named JavaScript work asynchronously | `jobs.enqueueAsync(...)` | |
||||
|
| Background worker | Run recurring JavaScript work | `period` or `cronExpression` | |
||||
|
|
||||
|
All action scripts run server-side and use the shared [Scripting API](scripting-api.md). Available services can be enabled or disabled per action type with scripting capability profiles. |
||||
|
|
||||
|
## Script Code Editor |
||||
|
|
||||
|
The Designer uses a code editor for JavaScript fields in custom endpoints, event handlers, background jobs, background workers, and interceptors. |
||||
|
|
||||
|
The editor provides: |
||||
|
|
||||
|
* Syntax highlighting for JavaScript |
||||
|
* Type-aware completions for low-code services |
||||
|
* Entity name completions for `db`, file, image, and attachment helpers |
||||
|
* Entity property completions for query lambda parameters and query results |
||||
|
* Enum completions through `enums` and `enumValues` |
||||
|
* File and image field selector completions through `fileFields` and `imageFields` |
||||
|
* An **Available context** list for the services enabled for the selected script type |
||||
|
|
||||
|
The `fileFields` and `imageFields` globals are not lists of every entity property. They are selector trees for `File` and `Image` properties used by `files.save(...)`, `files.get(...)`, `images.save(...)`, and `images.get(...)`. |
||||
|
|
||||
|
```javascript |
||||
|
await files.save(fileFields.Acme.Campaigns.Campaign.Document, { |
||||
|
fileName: 'brief.pdf', |
||||
|
contentType: 'application/pdf', |
||||
|
base64: base64Content |
||||
|
}); |
||||
|
|
||||
|
await images.save(imageFields.Acme.Campaigns.Campaign.BannerImage, { |
||||
|
fileName: 'banner.png', |
||||
|
contentType: 'image/png', |
||||
|
base64: base64Image |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
Regular entity fields autocomplete from entity records and query lambda parameters: |
||||
|
|
||||
|
```javascript |
||||
|
var campaignQuery = await db.query('Acme.Campaigns.Campaign'); |
||||
|
var rows = await campaignQuery |
||||
|
.where(campaign => campaign.Name.includes('Spring')) |
||||
|
.select(campaign => ({ |
||||
|
id: campaign.Id, |
||||
|
name: campaign.Name |
||||
|
})) |
||||
|
.toList(); |
||||
|
``` |
||||
|
|
||||
|
If the selected model layer is read-only, the Designer shows the JavaScript in a read-only editor. Switch to a writable layer before editing. |
||||
|
|
||||
|
## Event Handlers |
||||
|
|
||||
|
Event handlers run when a distributed event with the configured name is published. |
||||
|
|
||||
|
### Descriptor |
||||
|
|
||||
|
| Field | Type | Description | |
||||
|
|-------|------|-------------| |
||||
|
| `name` | string | Unique event handler name | |
||||
|
| `eventName` | string | Distributed event name to handle | |
||||
|
| `javascript` | string | JavaScript handler body | |
||||
|
| `description` | string | Optional documentation text | |
||||
|
|
||||
|
### Context |
||||
|
|
||||
|
| Global | Description | |
||||
|
|--------|-------------| |
||||
|
| `handler` | Handler runtime metadata | |
||||
|
| `event` | Runtime event metadata with `name` and `data` | |
||||
|
| `eventName` | Event name string | |
||||
|
| `eventData` | Event payload | |
||||
|
|
||||
|
### Example |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"eventHandlers": [ |
||||
|
{ |
||||
|
"name": "NotifyCampaignCompleted", |
||||
|
"eventName": "Acme.Campaigns.CampaignCompleted", |
||||
|
"description": "Logs and notifies when a campaign is completed", |
||||
|
"javascript": "log('Campaign completed: ' + eventData.id);\nawait email.queueAsync('ops@example.com', 'Campaign completed', eventData.id);" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Publish the event from another script: |
||||
|
|
||||
|
```javascript |
||||
|
await events.publishAsync('Acme.Campaigns.CampaignCompleted', { |
||||
|
id: campaignId, |
||||
|
completedAt: new Date().toISOString() |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
## Background Jobs |
||||
|
|
||||
|
Background jobs define named JavaScript handlers that can be enqueued from scripts. |
||||
|
|
||||
|
### Descriptor |
||||
|
|
||||
|
| Field | Type | Description | |
||||
|
|-------|------|-------------| |
||||
|
| `name` | string | Unique job name used by `jobs.enqueueAsync(...)` | |
||||
|
| `javascript` | string | JavaScript job body | |
||||
|
| `description` | string | Optional documentation text | |
||||
|
|
||||
|
### Context |
||||
|
|
||||
|
| Global | Description | |
||||
|
|--------|-------------| |
||||
|
| `job` | Job runtime metadata | |
||||
|
| `jobName` | Job name string | |
||||
|
| `jobData` | Parsed job payload | |
||||
|
| `jobJsonData` | Raw JSON payload | |
||||
|
|
||||
|
### Example |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"backgroundJobs": [ |
||||
|
{ |
||||
|
"name": "SendCampaignSummary", |
||||
|
"description": "Sends a summary for one campaign", |
||||
|
"javascript": "var campaign = await db.get('Acme.Campaigns.Campaign', jobData.campaignId);\nif (!campaign) { userFriendlyError('Campaign not found.'); }\nawait email.queueAsync(jobData.to, 'Campaign summary', campaign.Name);" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Enqueue the job from another script: |
||||
|
|
||||
|
```javascript |
||||
|
var jobId = await jobs.enqueueAsync('SendCampaignSummary', { |
||||
|
campaignId: campaignId, |
||||
|
to: 'ops@example.com' |
||||
|
}, { |
||||
|
priority: 'Normal', |
||||
|
delayMs: 60000 |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
## Background Workers |
||||
|
|
||||
|
Background workers run recurring JavaScript work on a schedule. |
||||
|
|
||||
|
### Descriptor |
||||
|
|
||||
|
| Field | Type | Description | |
||||
|
|-------|------|-------------| |
||||
|
| `name` | string | Unique worker name | |
||||
|
| `period` | number | Period in milliseconds | |
||||
|
| `cronExpression` | string | Cron expression for scheduled execution | |
||||
|
| `javascript` | string | JavaScript worker body | |
||||
|
| `description` | string | Optional documentation text | |
||||
|
|
||||
|
Configure either `period` or `cronExpression`. |
||||
|
|
||||
|
### Context |
||||
|
|
||||
|
| Global | Description | |
||||
|
|--------|-------------| |
||||
|
| `worker` | Worker runtime metadata | |
||||
|
| `workerName` | Worker name string | |
||||
|
|
||||
|
### Example |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"backgroundWorkers": [ |
||||
|
{ |
||||
|
"name": "CampaignCleanup", |
||||
|
"period": 3600000, |
||||
|
"description": "Runs every hour", |
||||
|
"javascript": "var query = await db.query('Acme.Campaigns.Campaign');\nvar stale = await query.where(c => c.Status === 0).take(100).toList();\nlog('Stale draft count: ' + stale.length);" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Test JavaScript |
||||
|
|
||||
|
Where the Designer shows **Test JavaScript**, you can run the current editor content without saving it. The built-in dry-run panel is available for custom endpoints, interceptors, event handlers, background jobs, and background workers. |
||||
|
|
||||
|
For custom endpoints, provide request data: |
||||
|
|
||||
|
* Method |
||||
|
* Path |
||||
|
* Route values |
||||
|
* Query values |
||||
|
* Headers |
||||
|
* Body JSON |
||||
|
|
||||
|
For interceptors, provide the command name, entity name, command data, and an optional record id. For event handlers, provide event data JSON. For background jobs and background workers, provide the job or worker input JSON that the script expects. |
||||
|
|
||||
|
When the script uses the HTTP API, you can also define outbound HTTP mocks. If a script calls `http.getAsync(...)`, `http.postAsync(...)`, or another HTTP helper, the dry-run engine returns the matching mock response instead of calling the real URL. If no mock matches, the result includes an HTTP mock miss. |
||||
|
|
||||
|
Dry-run behavior: |
||||
|
|
||||
|
| Operation | Dry-run behavior | |
||||
|
|-----------|------------------| |
||||
|
| Database writes | Executed in a transaction and rolled back | |
||||
|
| Low-code file/image/attachment operations | Captured as side effects without persisting files | |
||||
|
| Email send or queue | Captured as an `email` side effect; no email is sent | |
||||
|
| Event publish | Captured as an `event` side effect; no event is published | |
||||
|
| Background job enqueue | Captured as a `job` side effect; no job is enqueued | |
||||
|
| Outbound HTTP | Matched against HTTP mocks; no real HTTP call is made | |
||||
|
| Logs | Returned in the test result | |
||||
|
| Errors | Returned with type, message, and diagnostics when available | |
||||
|
|
||||
|
Dry-run results can include: |
||||
|
|
||||
|
* Endpoint response data |
||||
|
* Execution status and duration |
||||
|
* Logs |
||||
|
* Captured side effects |
||||
|
* Error details |
||||
|
|
||||
|
The endpoint dry-run still evaluates the endpoint authentication and permission metadata against the current user. If the test user is not authenticated or does not have the required permission, the dry-run returns the corresponding `401` or `403` endpoint response. |
||||
|
|
||||
|
## Operational Guidance |
||||
|
|
||||
|
* Prefer metadata and generated CRUD behavior before adding scripts. |
||||
|
* Keep scripts small and focused. |
||||
|
* Use explicit permissions for custom endpoints. |
||||
|
* Use `take()` and specific filters for database queries. |
||||
|
* Treat public unauthenticated endpoints as public API surface. |
||||
|
* Keep outbound HTTP, email, event, job, file, and blob limits enabled for tenant-authored scripts. |
||||
|
* Use capability profiles to disable services that a script type does not need. |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [Low-Code Designer](designer.md) |
||||
|
* [Custom Endpoints](custom-endpoints.md) |
||||
|
* [Interceptors](interceptors.md) |
||||
|
* [Scripting API](scripting-api.md) |
||||
|
* [Model Descriptor Files](model-json.md) |
||||
@ -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.'; }" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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 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": [ |
||||
|
"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." |
||||
|
] |
||||
|
} |
||||
@ -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" }] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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 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"], |
||||
|
"enumDescriptions": [ |
||||
|
"Bar chart.", |
||||
|
"Bar chart.", |
||||
|
"Line chart.", |
||||
|
"Line chart.", |
||||
|
"Pie chart.", |
||||
|
"Pie chart.", |
||||
|
"Donut chart.", |
||||
|
"Donut chart." |
||||
|
] |
||||
|
} |
||||
@ -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" }] |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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 } |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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" } |
||||
|
] |
||||
|
} |
||||
@ -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" } |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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" } |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
{ |
||||
|
"$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": [ |
||||
|
{ |
||||
|
"name": "overview", |
||||
|
"type": "numberContainer", |
||||
|
"title": "Overview", |
||||
|
"numberContainer": { |
||||
|
"items": [ |
||||
|
{ "name": "total-records", "title": "Total Records", "entityName": "Acme.Events.Event", "aggregation": "count" } |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,168 @@ |
|||||
|
{ |
||||
|
"$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": { |
||||
|
"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": { |
||||
|
"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": { |
||||
|
"required": ["type"], |
||||
|
"properties": { |
||||
|
"type": { "enum": ["numberContainer", "NumberContainer"] } |
||||
|
} |
||||
|
}, |
||||
|
"then": { |
||||
|
"required": ["numberContainer"], |
||||
|
"properties": { |
||||
|
"numberContainer": { "$ref": "dashboard-number-container-descriptor.schema.json" } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
"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 |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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 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"], |
||||
|
"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." |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
{ |
||||
|
"$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'.", |
||||
|
"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}'.", |
||||
|
"minLength": 1, |
||||
|
"pattern": "^/" |
||||
|
}, |
||||
|
"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.", |
||||
|
"minLength": 1 |
||||
|
}, |
||||
|
"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);" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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"] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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." |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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" }] } |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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 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", |
||||
|
"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 '<ReferencedEntity>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" } |
||||
|
] |
||||
|
} |
||||
@ -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." |
||||
|
] |
||||
|
} |
||||
@ -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 descriptor 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 } |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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" |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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 }] } |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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" } |
||||
|
] |
||||
|
} |
||||
@ -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." |
||||
|
] |
||||
|
} |
||||
@ -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", "null"], |
||||
|
"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 }] } |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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" } |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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." |
||||
|
] |
||||
|
} |
||||
@ -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 } |
||||
|
] |
||||
|
} |
||||
@ -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" }] |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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" } |
||||
|
] |
||||
|
} |
||||
@ -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 } |
||||
|
] |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -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 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": [ |
||||
|
"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." |
||||
|
] |
||||
|
} |
||||
@ -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": "#" |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
"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" } |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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.');" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,70 @@ |
|||||
|
{ |
||||
|
"$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"], |
||||
|
"not": { |
||||
|
"required": ["cronExpression"], |
||||
|
"properties": { |
||||
|
"cronExpression": { |
||||
|
"type": "string", |
||||
|
"minLength": 1 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"required": ["cronExpression"], |
||||
|
"not": { "required": ["period"] }, |
||||
|
"properties": { |
||||
|
"cronExpression": { |
||||
|
"type": "string", |
||||
|
"minLength": 1 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
"additionalProperties": false, |
||||
|
"examples": [ |
||||
|
{ |
||||
|
"name": "CleanupExpiredEvents", |
||||
|
"period": 3600000, |
||||
|
"javascript": "context.log('Running expired event cleanup.');" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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.');" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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 descriptor 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" } |
||||
|
] |
||||
|
} |
||||
@ -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" |
||||
|
} |
||||
|
} |
||||