diff --git a/docs/en/low-code/custom-endpoints.md b/docs/en/low-code/custom-endpoints.md index ad0057df703..c75f92461f3 100644 --- a/docs/en/low-code/custom-endpoints.md +++ b/docs/en/low-code/custom-endpoints.md @@ -1,112 +1,138 @@ ```json //[doc-seo] { - "Description": "Define custom REST API endpoints with JavaScript handlers in the ABP Low-Code System. Create dynamic APIs without writing C# controllers." + "Description": "Define JavaScript-backed custom REST endpoints in the ABP Low-Code System without writing custom .NET controllers." } ``` # Custom Endpoints -Custom Endpoints allow you to define REST API routes with server-side JavaScript handlers directly in `model.json`. Each endpoint is registered as an ASP.NET Core endpoint at startup and supports hot-reload when the model changes. +> **Preview:** Custom endpoint descriptors and scripting helpers are part of the preview Low-Code System. Names, validation rules, and runtime behavior may change before general availability. -## Defining Endpoints +Use generated page CRUD APIs for normal dynamic entity pages. Custom endpoints are an advanced option for exposing small model-owned REST APIs that do not map to standard list, get, create, update, delete, export, file, or attachment operations. + +Custom endpoints are defined in JSON descriptor files or through the Low-Code Designer. Each endpoint executes server-side JavaScript and is registered as an ASP.NET Core route. -Add endpoints to the `endpoints` array in `model.json`: +## Defining Endpoints ```json { "endpoints": [ { - "name": "GetProductStats", - "route": "/api/custom/products/stats", + "name": "GetCampaignStats", + "route": "/api/custom/campaigns/stats", "method": "GET", - "description": "Get product statistics", - "requireAuthentication": false, - "javascript": "var count = await db.count('LowCodeDemo.Products.Product');\nreturn ok({ totalProducts: count });" + "description": "Get campaign statistics", + "requireAuthentication": true, + "requiredPermissions": ["Acme.Campaigns"], + "javascript": "var count = await db.count('Acme.Campaigns.Campaign');\nreturn ok({ totalCampaigns: count });" } ] } ``` -### Endpoint Descriptor +## Endpoint Descriptor | Field | Type | Default | Description | |-------|------|---------|-------------| -| `name` | string | **Required** | Unique endpoint name | -| `route` | string | **Required** | URL route pattern (supports `{parameters}`) | -| `method` | string | `"GET"` | HTTP method: `GET`, `POST`, `PUT`, `DELETE` | -| `javascript` | string | **Required** | JavaScript handler code | -| `description` | string | null | Description for documentation | -| `requireAuthentication` | bool | `true` | Require authenticated user | -| `requiredPermissions` | string[] | null | Required permission names | +| `name` | string | Required | Unique endpoint identifier | +| `route` | string | Required | URL route pattern; must start with `/` and can contain `{parameters}` | +| `method` | string | `GET` | `GET`, `POST`, `PUT`, `DELETE`, or `PATCH` | +| `javascript` | string | Required | JavaScript handler code | +| `description` | string | null | Optional designer/documentation text | +| `requireAuthentication` | bool | `true` | Whether the caller must be authenticated | +| `requiredPermissions` | string[] | null | Permission names required to call the endpoint | -## Route Parameters +Permission checks require an authorized user even when `requireAuthentication` is set to `false`. Keep endpoints authenticated by default and use `requireAuthentication: false` only for intentionally public APIs without `requiredPermissions`. -Use `{paramName}` syntax in the route. Access values via the `route` object: +## Route and Request Data + +Use `{paramName}` syntax for route parameters. Endpoint scripts can read request data through globals: + +| Variable | Description | +|----------|-------------| +| `request` | Full request object | +| `route` | Route values, for example `route.id` | +| `params` | Alias for `route` | +| `query` | Query string values, for example `query.q` | +| `body` | Parsed request body | +| `headers` | Selected safe request headers | ```json { - "name": "GetProductById", - "route": "/api/custom/products/{id}", + "name": "GetCampaignById", + "route": "/api/custom/campaigns/{id}", "method": "GET", - "javascript": "var product = await db.get('LowCodeDemo.Products.Product', route.id);\nif (!product) { return notFound('Product not found'); }\nreturn ok({ id: product.Id, name: product.Name, price: product.Price });" + "javascript": "var campaign = await db.get('Acme.Campaigns.Campaign', route.id);\nif (!campaign) { return notFound('Campaign not found'); }\nreturn ok({ id: campaign.Id, name: campaign.Name });" } ``` -## JavaScript Context +For non-GET requests, `body` is parsed when a body is present and remains subject to the configured request size limit. The `headers` object intentionally contains only selected request headers such as `Content-Type`, `Accept`, `Accept-Language`, and `X-Requested-With`. + +## Response Helpers + +Endpoint scripts can return plain data, an endpoint response object, or one of the response helpers. + +| Function | HTTP status | Response kind | +|----------|-------------|---------------| +| `ok(data, headers?)` | 200 | JSON | +| `okText(text, contentType?)` | 200 | Text | +| `okBinary(base64Data, contentType?)` | 200 | Binary from base64 | +| `created(data, headers?)` | 201 | JSON | +| `noContent()` | 204 | Empty | +| `badRequest(message)` | 400 | JSON error | +| `unauthorized(message)` | 401 | JSON error | +| `forbidden(message)` | 403 | JSON error | +| `notFound(message)` | 404 | JSON error | +| `error(message)` | 500 | JSON error | + +For custom status codes or response metadata, return an object with response fields: + +```javascript +return { + statusCode: 202, + kind: 'json', + data: { accepted: true }, + headers: { 'x-trace-id': guid() } +}; +``` -Inside custom endpoint scripts, you have access to: +Response kinds are: -### Request Context +| Kind | Description | +|------|-------------| +| `json` | JSON serialization, default | +| `text` | Plain text | +| `binaryBase64` | Base64-encoded binary payload | -| Variable | Description | -|----------|-------------| -| `request` | Full request object | -| `route` | Route parameter values (e.g., `route.id`) | -| `params` | Alias for route parameters | -| `query` | Query string parameters (e.g., `query.q`, `query.page`) | -| `body` | Request body (for POST/PUT) | -| `headers` | Request headers | -| `user` | Current user (same as `context.currentUser` in [Interceptors](interceptors.md)) | -| `email` | Email sender (same as `context.emailSender` in [Interceptors](interceptors.md)) | - -### Response Helpers - -| Function | HTTP Status | Description | -|----------|-------------|-------------| -| `ok(data)` | 200 | Success response with data | -| `created(data)` | 201 | Created response with data | -| `noContent()` | 204 | No content response | -| `badRequest(message)` | 400 | Bad request response | -| `unauthorized(message)` | 401 | Unauthorized response | -| `forbidden(message)` | 403 | Forbidden response | -| `notFound(message)` | 404 | Not found response | -| `error(message)` | 500 | Internal server error response | -| `response(statusCode, data, error)` | Custom | Custom status code response | - -### Logging - -| Function | Description | -|----------|-------------| -| `log(message)` | Log an informational message | -| `logWarning(message)` | Log a warning message | -| `logError(message)` | Log an error message | +## Script Services -### Database API +Custom endpoint scripts use the same common [Scripting API](scripting-api.md) services as other low-code scripts: -The full [Scripting API](scripting-api.md) (`db` object) is available for querying and mutating data. +| Service | Example | +|---------|---------| +| `db` | Query or mutate dynamic entities | +| `user` / `currentUser` | Read current user information | +| `tenant` / `currentTenant` | Read tenant information | +| `auth` / `authorization` | Check permissions | +| `settings`, `features`, `config` | Read allowed settings, features, and app configuration | +| `http` | Call allowed outbound HTTP services | +| `email` | Send or queue email | +| `events`, `jobs` | Publish distributed events or enqueue background jobs | +| `files`, `images`, `attachments` | Work with low-code files and record attachments | +| `log`, `logWarning`, `logError` | Write logs | ## Examples -### Get Statistics +### Statistics ```json { - "name": "GetProductStats", - "route": "/api/custom/products/stats", + "name": "GetCampaignStats", + "route": "/api/custom/campaigns/stats", "method": "GET", - "requireAuthentication": false, - "javascript": "var totalCount = await db.count('LowCodeDemo.Products.Product');\nvar avgPrice = totalCount > 0 ? await db.query('LowCodeDemo.Products.Product').average(p => p.Price) : 0;\nreturn ok({ totalProducts: totalCount, averagePrice: avgPrice });" + "requireAuthentication": true, + "javascript": "var campaignQuery = await db.query('Acme.Campaigns.Campaign');\nvar total = await campaignQuery.count();\nvar active = await campaignQuery.where(c => c.Status === 1).count();\nreturn ok({ total: total, active: active });" } ``` @@ -114,36 +140,98 @@ The full [Scripting API](scripting-api.md) (`db` object) is available for queryi ```json { - "name": "SearchCustomers", - "route": "/api/custom/customers/search", + "name": "SearchCampaigns", + "route": "/api/custom/campaigns/search", "method": "GET", "requireAuthentication": true, - "javascript": "var searchTerm = query.q || '';\nvar customers = await db.query('LowCodeDemo.Customers.Customer')\n .where(c => c.Name.toLowerCase().includes(searchTerm.toLowerCase()))\n .take(10)\n .toList();\nreturn ok(customers.map(c => ({ id: c.Id, name: c.Name, email: c.EmailAddress })));" + "javascript": "var q = query.q || '';\nvar campaignQuery = await db.query('Acme.Campaigns.Campaign');\nvar rows = await campaignQuery\n .where(c => c.Name.toLowerCase().includes(q.toLowerCase()))\n .take(10)\n .toList();\nreturn ok(rows.map(c => ({ id: c.Id, name: c.Name })));" } ``` -### Dashboard Summary +### Create with Validation ```json { - "name": "GetDashboardSummary", - "route": "/api/custom/dashboard", - "method": "GET", + "name": "CreateCampaignDraft", + "route": "/api/custom/campaigns/draft", + "method": "POST", "requireAuthentication": true, - "javascript": "var productCount = await db.count('LowCodeDemo.Products.Product');\nvar customerCount = await db.count('LowCodeDemo.Customers.Customer');\nvar orderCount = await db.count('LowCodeDemo.Orders.Order');\nreturn ok({ products: productCount, customers: customerCount, orders: orderCount, user: user.isAuthenticated ? user.userName : 'Anonymous' });" + "requiredPermissions": ["Acme.Campaigns.Create"], + "javascript": "if (!body.name) { return badRequest('Name is required.'); }\nvar record = await db.insert('Acme.Campaigns.Campaign', { Name: body.name, Status: 0 });\nreturn created({ id: record.Id, name: record.Name });" } ``` -## Authentication and Authorization +## Testing Endpoint Scripts + +The Low-Code Designer endpoint editor includes **Test JavaScript**. Use it to run the current editor content without saving it. + +The dry-run request editor lets you provide: + +* HTTP method +* Request path +* Route values +* Query values +* Headers +* Body JSON +* Outbound HTTP mocks + +Dry-run execution evaluates the endpoint descriptor, request context, script, authentication metadata, and required permissions against the current user. It returns the same response shape that a real endpoint execution would return. + +Side effects are captured instead of being sent to external systems: + +| Operation | Dry-run behavior | +|-----------|------------------| +| Database writes | Rolled back | +| Email send or queue | Captured under **Captured Side Effects** | +| Event publish | Captured under **Captured Side Effects** | +| Background job enqueue | Captured under **Captured Side Effects** | +| Outbound HTTP | Matched against HTTP mocks | +| File, image, and attachment operations | Captured without persisting files | + +If a script calls the `http` helper and no mock matches the method and URL, the result contains a mock miss instead of sending a real HTTP request. + +## Response Policy + +Dynamic endpoint responses are validated by `LowCode:Scripting:EndpointResponse`. + +```json +{ + "LowCode": { + "Scripting": { + "EndpointResponse": { + "MaxBodyBytes": 1048576, + "AllowedContentTypes": [ + "application/json", + "text/plain", + "application/octet-stream" + ], + "BlockedHeaders": [ + "Set-Cookie", + "Content-Length", + "Content-Type" + ] + } + } + } +} +``` + +Default blocked headers also include hop-by-hop headers such as `Connection`, `Transfer-Encoding`, and `Upgrade`. + +`Content-Type` is blocked as a custom response header. Choose the response kind or `contentType` field instead of setting a raw `Content-Type` header from script. + +## Security Notes -| Setting | Behavior | -|---------|----------| -| `requireAuthentication: false` | Endpoint is publicly accessible | -| `requireAuthentication: true` | User must be authenticated | -| `requiredPermissions: ["MyApp.Products"]` | User must have the specified permissions | +* Prefer authenticated endpoints with explicit `requiredPermissions`. +* Treat endpoints with `requireAuthentication: false` and no `requiredPermissions` as public API surface. +* Keep endpoint scripts small and focused. +* Validate route, query, and body input before using it. +* Use `take()` for list queries. +* Use the configured HTTP, email, file, and response limits for untrusted integrations. ## See Also * [Scripting API](scripting-api.md) +* [Script Actions](script-actions.md) * [Interceptors](interceptors.md) -* [model.json Structure](model-json.md) +* [Model Descriptor Files](model-json.md) diff --git a/docs/en/low-code/designer.md b/docs/en/low-code/designer.md new file mode 100644 index 00000000000..5f6380cc812 --- /dev/null +++ b/docs/en/low-code/designer.md @@ -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. + +![Low-Code Designer overview](images/designer-overview.png) + +## 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. + +![Entity summary in the designer](images/designer-entity.png) + +![Entity property list](images/designer-properties.png) + +### 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. + +![Relation overview](images/designer-relations.png) + +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. + +![Page setup](images/designer-page-filters.png) + +Pages are exposed in React under `/dynamic/` 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" + +![Form setup](images/designer-forms.png) + +## 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. diff --git a/docs/en/low-code/fluent-api.md b/docs/en/low-code/fluent-api.md index 5aa38fe66a1..74c0caf118e 100644 --- a/docs/en/low-code/fluent-api.md +++ b/docs/en/low-code/fluent-api.md @@ -1,13 +1,15 @@ ```json //[doc-seo] { - "Description": "Define dynamic entities using C# attributes and configure them with the Fluent API in the ABP Low-Code System. The primary way to build auto-generated admin panels." + "Description": "Define dynamic entities using .NET attributes and configure them with the Fluent API in the ABP Low-Code System for advanced source-controlled model configuration." } ``` # Attributes & Fluent API -C# Attributes and the Fluent API are the **recommended way** to define dynamic entities. They provide compile-time checking, IntelliSense, refactoring support, and keep your entity definitions close to your domain code. +> **Preview:** Attributes and Fluent API configuration for the Low-Code System are preview APIs. Prefer the designer for normal modeling work, and review release notes before relying on these APIs in long-lived integrations. + +Use the [Low-Code Designer](designer.md) for day-to-day entity, page, form, and filter work. C# attributes and the Fluent API are advanced configuration options for teams that need source-controlled model definitions, compile-time checking, or programmatic overrides. ## Quick Start @@ -37,7 +39,7 @@ dotnet ef migrations add Added_Product dotnet ef database update ``` -You now have a complete Product management page with data grid, create/edit modals, search, sorting, and pagination. +After migrations and runtime startup, the React low-code runtime can render a Product management page with data grid, create/edit forms, search, sorting, filters, and pagination. ### Step 3: Add Relationships @@ -71,7 +73,7 @@ The `Order` page now has a foreign key dropdown for Customer, and `OrderLine` is The Low-Code System uses a layered configuration model. From lowest to highest priority: 1. **Code Layer** — C# classes with `[DynamicEntity]` and other attributes -2. **JSON Layer** — `model.json` file (see [model.json Structure](model-json.md)) +2. **JSON Descriptor Layer** — source-controlled descriptor files under `_Dynamic` (see [Model Descriptor Files](model-json.md)) 3. **Fluent Layer** — `AbpDynamicEntityConfig.EntityConfigurations` A `DefaultLayer` runs last to fill in any missing values with conventions. @@ -257,7 +259,7 @@ Enum values can be localized using ABP's localization system. Add localization k } ``` -The Blazor UI automatically uses these localization keys for enum dropdowns and display values. If no localization key is found, the enum member name is used as-is. +The React runtime automatically uses these localization keys for enum dropdowns and display values. If no localization key is found, the enum member name is used as-is. ## Fluent API @@ -265,7 +267,7 @@ The Fluent API has the **highest priority** in the configuration system. Use `Ab ### Basic Usage -Configure in your Low-Code Initializer (e.g. `MyAppLowCodeInitializer`): +Configure in startup initialization code (for example `MyAppLowCodeInitializer`): ````csharp public static class MyAppLowCodeInitializer @@ -378,23 +380,23 @@ entity.Interceptors.Add(new CommandInterceptorDescriptor("Create") ## Assembly Registration -Register assemblies containing `[DynamicEntity]` classes in your [Low-Code Initializer](index.md#1-create-a-low-code-initializer): +Register assemblies containing `[DynamicEntity]` classes in startup initialization code: ````csharp AbpDynamicEntityConfig.SourceAssemblies.Add( new DynamicEntityAssemblyInfo( typeof(MyDomainModule).Assembly, rootNamespace: "MyApp", - projectRootPath: sourcePath // For model.json hot-reload + projectRootPath: sourcePath // For descriptor hot-reload ) ); ```` | Parameter | Description | |-----------|-------------| -| `assembly` | The assembly containing `[DynamicEntity]` classes and/or `model.json` | +| `assembly` | The assembly containing `[DynamicEntity]` classes and/or descriptor metadata | | `rootNamespace` | Root namespace for the assembly (used for embedded resource lookup) | -| `projectRootPath` | Path to the Domain project source folder (enables `model.json` hot-reload in development) | +| `projectRootPath` | Path to the Domain project source folder (enables descriptor hot-reload in development) | You can also register entity types directly: @@ -403,21 +405,21 @@ AbpDynamicEntityConfig.DynamicEntityTypes.Add(typeof(Product)); AbpDynamicEntityConfig.DynamicEnumTypes.Add(typeof(OrganizationType)); ```` -## Combining with model.json +## Combining with JSON Descriptors -Attributes and model.json work together seamlessly. A common pattern: +Attributes and JSON descriptors work together seamlessly. A common pattern: 1. **Define core entities** with C# attributes (compile-time safety) -2. **Add additional entities** via model.json (no recompilation needed) +2. **Add additional entities** via descriptor files (no recompilation needed) 3. **Fine-tune configuration** with Fluent API (overrides everything) The three-layer system merges all definitions: ``` -Fluent API (highest) > JSON (model.json) > Code (Attributes) > Defaults (lowest) +Fluent API (highest) > JSON descriptors > Code (Attributes) > Defaults (lowest) ``` -For example, if an attribute sets `[DynamicPropertyUnique]` and model.json sets `"isUnique": false`, the JSON value wins because JSON layer has higher priority than Code layer. +For example, if an attribute sets `[DynamicPropertyUnique]` and a descriptor sets `"isUnique": false`, the JSON value wins because the JSON descriptor layer has higher priority than the Code layer. ## End-to-End Example @@ -497,7 +499,7 @@ public class OrderLine : DynamicEntityBase } ```` -Register everything in your [Low-Code Initializer](index.md#1-create-a-low-code-initializer): +Register everything in startup initialization code: ````csharp public static class MyAppLowCodeInitializer @@ -562,11 +564,12 @@ public class MyAppDbContextFactory : IDesignTimeDbContextFactory // ... BuildConfiguration method ... } +```` This gives you four auto-generated pages (Customers, Products, Orders with nested OrderLines), complete with permissions, menu items, foreign key lookups, and interceptor-based business rules. ## See Also -* [model.json Structure](model-json.md) +* [Model Descriptor Files](model-json.md) * [Reference Entities](reference-entities.md) * [Interceptors](interceptors.md) diff --git a/docs/en/low-code/foreign-access.md b/docs/en/low-code/foreign-access.md index f7545dad059..4c6f87e9562 100644 --- a/docs/en/low-code/foreign-access.md +++ b/docs/en/low-code/foreign-access.md @@ -7,6 +7,10 @@ # Foreign Access +> **Preview:** Foreign access metadata is part of the preview Low-Code System. Relation behavior and designer options may change before general availability. + +Use the [Low-Code Designer](designer.md) to review relation metadata visually. This page explains the advanced configuration values that control how related dynamic entities can be reached from runtime pages. + Foreign Access controls how related **dynamic entities** can be accessed through foreign key relationships. It determines whether users can view or manage related data directly from the **target entity's** UI. > **Important:** Foreign Access only works between **dynamic entities**. It does not apply to [reference entities](reference-entities.md) because they are read-only and don't have UI pages. @@ -52,7 +56,7 @@ AbpDynamicEntityConfig.EntityConfigurations.Configure( ); ```` -## Configuring in model.json +## Configuring in JSON Descriptors Set the `access` field on a foreign key property: @@ -108,7 +112,7 @@ Set the `access` field on a foreign key property: When foreign access is configured between two **dynamic entities**: -![Actions menu showing foreign access items (Order, Visited Country, etc.)](images/actions-menu.png) +![Relation overview in the Low-Code Designer](images/designer-relations.png) ### `ForeignAccess.View` @@ -118,8 +122,6 @@ An **action menu item** appears on the target entity's data grid row (e.g., a "V An **action menu item** appears on the target entity's data grid row (e.g., an "Orders" item on the Customer row). Clicking it opens a fully functional CRUD modal where users can create, edit, and delete related records. -![Foreign access modal with full CRUD capabilities](images/foreign-access-modal.png) - ### `ForeignAccess.None` No action menu item is added. The foreign key exists only for data integrity and lookup display. @@ -143,6 +145,6 @@ The `DynamicEntityAppService` checks these relations when building entity action ## See Also -* [model.json Structure](model-json.md) +* [Model Descriptor Files](model-json.md) * [Reference Entities](reference-entities.md) * [Attributes & Fluent API](fluent-api.md) diff --git a/docs/en/low-code/images/actions-menu.png b/docs/en/low-code/images/actions-menu.png deleted file mode 100644 index b9533942157..00000000000 Binary files a/docs/en/low-code/images/actions-menu.png and /dev/null differ diff --git a/docs/en/low-code/images/create-modal.png b/docs/en/low-code/images/create-modal.png deleted file mode 100644 index ebc77debb51..00000000000 Binary files a/docs/en/low-code/images/create-modal.png and /dev/null differ diff --git a/docs/en/low-code/images/data-grid.png b/docs/en/low-code/images/data-grid.png deleted file mode 100644 index 2a97d9ca6fe..00000000000 Binary files a/docs/en/low-code/images/data-grid.png and /dev/null differ diff --git a/docs/en/low-code/images/designer-entity.png b/docs/en/low-code/images/designer-entity.png new file mode 100644 index 00000000000..f321f0f4a96 Binary files /dev/null and b/docs/en/low-code/images/designer-entity.png differ diff --git a/docs/en/low-code/images/designer-forms.png b/docs/en/low-code/images/designer-forms.png new file mode 100644 index 00000000000..230c7c69826 Binary files /dev/null and b/docs/en/low-code/images/designer-forms.png differ diff --git a/docs/en/low-code/images/designer-overview.png b/docs/en/low-code/images/designer-overview.png new file mode 100644 index 00000000000..f75ced14790 Binary files /dev/null and b/docs/en/low-code/images/designer-overview.png differ diff --git a/docs/en/low-code/images/designer-page-filters.png b/docs/en/low-code/images/designer-page-filters.png new file mode 100644 index 00000000000..647df1e0648 Binary files /dev/null and b/docs/en/low-code/images/designer-page-filters.png differ diff --git a/docs/en/low-code/images/designer-properties.png b/docs/en/low-code/images/designer-properties.png new file mode 100644 index 00000000000..8d9cdf8569e Binary files /dev/null and b/docs/en/low-code/images/designer-properties.png differ diff --git a/docs/en/low-code/images/designer-relations.png b/docs/en/low-code/images/designer-relations.png new file mode 100644 index 00000000000..07e8aadfdb2 Binary files /dev/null and b/docs/en/low-code/images/designer-relations.png differ diff --git a/docs/en/low-code/images/foreign-access-modal.png b/docs/en/low-code/images/foreign-access-modal.png deleted file mode 100644 index b7a9c3189e7..00000000000 Binary files a/docs/en/low-code/images/foreign-access-modal.png and /dev/null differ diff --git a/docs/en/low-code/images/menu-items.png b/docs/en/low-code/images/menu-items.png deleted file mode 100644 index d2fd84b0098..00000000000 Binary files a/docs/en/low-code/images/menu-items.png and /dev/null differ diff --git a/docs/en/low-code/images/runtime-create-form.png b/docs/en/low-code/images/runtime-create-form.png new file mode 100644 index 00000000000..258bab6370c Binary files /dev/null and b/docs/en/low-code/images/runtime-create-form.png differ diff --git a/docs/en/low-code/images/runtime-data-grid.png b/docs/en/low-code/images/runtime-data-grid.png new file mode 100644 index 00000000000..c47cc03014a Binary files /dev/null and b/docs/en/low-code/images/runtime-data-grid.png differ diff --git a/docs/en/low-code/images/runtime-filters-has-value.png b/docs/en/low-code/images/runtime-filters-has-value.png new file mode 100644 index 00000000000..dfc7b307881 Binary files /dev/null and b/docs/en/low-code/images/runtime-filters-has-value.png differ diff --git a/docs/en/low-code/images/runtime-filters.png b/docs/en/low-code/images/runtime-filters.png new file mode 100644 index 00000000000..e63b8dcbeab Binary files /dev/null and b/docs/en/low-code/images/runtime-filters.png differ diff --git a/docs/en/low-code/index.md b/docs/en/low-code/index.md index 7b5acfebe1f..0960303ddba 100644 --- a/docs/en/low-code/index.md +++ b/docs/en/low-code/index.md @@ -3,363 +3,171 @@ ```json //[doc-seo] { - "Description": "ABP Low-Code System: Build admin panels with auto-generated CRUD UI, APIs, and permissions using C# attributes and Fluent API. No boilerplate code needed." + "Description": "ABP Low-Code System: design dynamic entities, forms, pages, permissions, menus, filters, and React runtime pages with the Admin Console Low-Code Designer." } ``` > You must have an ABP Team or a higher license to use this module. -The ABP Low-Code System allows you to define entities using C# attributes or Fluent API and automatically generates: +> **Preview:** The Low-Code System is currently in preview. APIs, designer behavior, generated metadata, and React runtime details may change before general availability. Use it for evaluation and controlled projects, and review release notes before upgrading. -* **Database tables** (via EF Core migrations) -* **CRUD REST APIs** (Get, GetList, Create, Update, Delete) -* **Permissions** (View, Create, Update, Delete per entity) -* **Menu items** (auto-added to the admin sidebar) -* **Full Blazor UI** (data grid, create/edit modals, filters, foreign key lookups) +The ABP Low-Code System lets you build data-driven admin screens from metadata. The primary workflow is the **Low-Code Designer** in ABP Admin Console, backed by the **React runtime** in your application. -No need to write DTOs, application services, repositories, or UI pages manually. +Use the designer to model entities, enums, properties, relations, pages, forms, filters, permissions, actions, and health checks. The runtime uses the same metadata to provide: -![Auto-generated menu items in the sidebar](images/menu-items.png) +* CRUD REST APIs +* EF Core dynamic entity tables +* Permission definitions +* Dynamic menu items +* React data grid, kanban, calendar, gallery, form, and dashboard pages +* Create and edit forms +* Advanced filters +* Excel, CSV, and file bundle export -## Why Low-Code? +No DTO, repository, application service, controller, or React CRUD page is required for the standard flow. -Traditionally, adding a new entity with full CRUD functionality to an ABP application requires: +![Low-Code Designer overview](images/designer-overview.png) -* Entity class in Domain -* DbContext configuration in EF Core -* DTOs in Application.Contracts -* AppService in Application -* Controller in HttpApi -* Razor/Blazor pages in UI -* Permissions, menu items, localization +## Supported UI -**With Low-Code, a single C# class replaces all of the above:** +Low-Code runtime UI is currently documented for **React**. The backend model, APIs, permissions, scripting, and custom endpoint infrastructure are shared by the module, but the UI walkthroughs in this section focus on Admin Console plus React. -````csharp -[DynamicEntity(DefaultDisplayPropertyName = "Name")] -[DynamicEntityUI(PageTitle = "Products")] -public class Product : DynamicEntityBase -{ - [DynamicPropertyUnique] - public string Name { get; set; } - - [DynamicPropertyUI(DisplayName = "Unit Price")] - public decimal Price { get; set; } - - public int StockCount { get; set; } - - [DynamicForeignKey("MyApp.Categories.Category", "Name")] - public Guid? CategoryId { get; set; } -} -```` - -Run `dotnet ef migrations add Added_Product` and start your application. You get a complete Product management page with search, filtering, sorting, pagination, create/edit forms, and foreign key dropdown — all auto-generated. +## How to Enable -![Auto-generated data grid with search, filters, and actions](images/data-grid.png) +The Low-Code System is an optional startup template feature. When creating a new application with [ABP Studio](../studio/index.md), choose a modern React application template and enable **Low-Code System** in the project creation wizard. -![Auto-generated create/edit modal with form fields and foreign key lookups](images/create-modal.png) +ABP Studio creates the required backend module references, dynamic model initializer, EF Core configuration, Admin Console integration, and React runtime wiring. -## Getting Started +The generated React project includes: -### 1. Create a Low-Code Initializer +* `@volo/abp-react-lowcode` +* `configureLowCode` +* `LowCodeLocalizationProvider` +* `createDynamicRoutes` +* `useMenuItems` +* Page, form, dashboard, file, and attachment hooks -Create a static initializer class in your Domain project's `_Dynamic` folder that registers your assembly and calls `DynamicModelManager.Instance.InitializeAsync()`: +The host application wires the low-code modules, calls the generated `_Dynamic` initializer, configures EF Core dynamic entities, and seeds the required OpenIddict clients. -````csharp -using Volo.Abp.Identity; -using Volo.Abp.LowCode.Configuration; -using Volo.Abp.LowCode.Modeling; -using Volo.Abp.Threading; +## Run the Application -namespace MyApp._Dynamic; - -public static class MyAppLowCodeInitializer -{ - private static readonly AsyncOneTimeRunner Runner = new(); - - public static async Task InitializeAsync() - { - await Runner.RunAsync(async () => - { - // Register reference entities (optional — for linking to existing C# entities) - AbpDynamicEntityConfig.ReferencedEntityList.Add( - nameof(IdentityUser.UserName), - nameof(IdentityUser.Email) - ); - - // Register assemblies containing [DynamicEntity] classes and model.json - var sourcePath = ResolveDomainSourcePath(); - AbpDynamicEntityConfig.SourceAssemblies.Add( - new DynamicEntityAssemblyInfo( - typeof(MyAppDomainModule).Assembly, - rootNamespace: "MyApp", - projectRootPath: sourcePath // Required for model.json hot-reload in development - ) - ); - - // Fluent API configurations (optional — highest priority) - AbpDynamicEntityConfig.EntityConfigurations.Configure("MyApp.Products.Product", entity => - { - entity.AddOrGetProperty("InternalNotes").AsServerOnly(); - }); - - // Initialize the dynamic model manager - await DynamicModelManager.Instance.InitializeAsync(); - }); - } - - private static string ResolveDomainSourcePath() - { - // Traverse up from bin folder to find the Domain project source - var baseDir = AppContext.BaseDirectory; - var current = new DirectoryInfo(baseDir); - - for (int i = 0; i < 10 && current != null; i++) - { - var candidate = Path.Combine(current.FullName, "src", "MyApp.Domain"); - if (Directory.Exists(Path.Combine(candidate, "_Dynamic"))) - { - return candidate; - } - current = current.Parent; - } - - // Fallback for production (embedded resource will be used instead) - return string.Empty; - } -} -```` - -> The `projectRootPath` parameter enables hot-reload of `model.json` during development. When the path is empty or the file doesn't exist, the module falls back to reading `model.json` as an embedded resource. - -### 2. Call the Initializer in Program.cs - -The initializer must be called **before** the application starts. Add it to `Program.cs`: - -````csharp -public static async Task Main(string[] args) -{ - // Initialize Low-Code before building the application - await MyAppLowCodeInitializer.InitializeAsync(); - - var builder = WebApplication.CreateBuilder(args); - // ... rest of your startup code -} -```` +After ABP Studio creates the solution, use **Solution Runner** to run the backend host and the React application. Run the database migration task before opening the runtime pages. -> **Important:** The initializer must also be called in your `DbMigrator` project and any other entry points (AuthServer, HttpApi.Host, etc.) that use dynamic entities. This ensures EF Core migrations can discover the entity schema. +The generated solution README contains the exact command-line equivalents if you prefer to run the projects outside ABP Studio. -### 3. Configure DbContext +If you generate a solution inside another repository, make sure parent build files such as `Directory.Packages.props` are not inherited accidentally. Use an empty output folder outside another solution, or isolate the generated solution's MSBuild configuration before running `dotnet build`. -Call `ConfigureDynamicEntities()` in your `DbContext`: +Open Admin Console and navigate to **Low-Code Designer** after the backend is running: -````csharp -protected override void OnModelCreating(ModelBuilder builder) -{ - builder.ConfigureDynamicEntities(); - base.OnModelCreating(builder); -} -```` - -### 3. Define Your First Entity - -````csharp -[DynamicEntity] -[DynamicEntityUI(PageTitle = "Customers")] -public class Customer : DynamicEntityBase -{ - public string Name { get; set; } - - [DynamicPropertyUI(DisplayName = "Phone Number")] - public string Telephone { get; set; } - - [DynamicForeignKey("Volo.Abp.Identity.IdentityUser", "UserName")] - public Guid? UserId { get; set; } -} -```` +```text +https://localhost:/admin-console/lowcode-designer +``` -### 4. Add Migration and Run +Open generated runtime pages after the React application is running: -```bash -dotnet ef migrations add Added_Customer -dotnet ef database update +```text +http://localhost:/dynamic/ ``` -Start your application — the Customer page is ready. +## Designer Workflow -## Two Ways to Define Entities +The designer is the day-to-day entry point. -### C# Attributes (Recommended) +1. Use **Data** to create entities, enums, properties, and relations. +2. Use **Pages** to choose a page type, menu placement, fields, default sorting, filters, dashboards, and linked forms. +3. Use **Forms** to arrange create and edit forms with tabs, groups, controls, validations, and actions. +4. Use **Permissions** to review generated permissions and control access. +5. Use **Actions** and **Interceptors** when the standard CRUD flow needs custom logic, endpoints, event handlers, jobs, or workers. +6. Use **Health** to review model issues before publishing changes. -Define entities as C# classes with attributes. You get compile-time checking, IntelliSense, and refactoring support: +![Entity properties in the designer](images/designer-properties.png) -````csharp -[DynamicEntity] -[DynamicEntityUI(PageTitle = "Orders")] -public class Order : DynamicEntityBase -{ - [DynamicForeignKey("MyApp.Customers.Customer", "Name", ForeignAccess.Edit)] - public Guid CustomerId { get; set; } +![Form setup in the designer](images/designer-forms.png) - public decimal TotalAmount { get; set; } - public bool IsDelivered { get; set; } -} +## React Runtime -[DynamicEntity(Parent = "MyApp.Orders.Order")] -public class OrderLine : DynamicEntityBase -{ - [DynamicForeignKey("MyApp.Products.Product", "Name")] - public Guid ProductId { get; set; } +React runtime pages are generated from the same metadata. The page below was produced from a low-code page definition and includes the grid, menu item, permissions, display values, export, create form, and filters. The same runtime can render kanban, calendar, gallery, standalone form, and dashboard page definitions. - public int Quantity { get; set; } - public decimal Amount { get; set; } -} -```` +![Generated React data grid](images/runtime-data-grid.png) -See [Attributes & Fluent API](fluent-api.md) for the full attribute reference. +![Generated React advanced filters](images/runtime-filters.png) -### model.json (Declarative) +![Generated React create form](images/runtime-create-form.png) -Alternatively, define entities in a JSON file without writing C# classes: +## Filters -```json -{ - "entities": [ - { - "name": "MyApp.Customers.Customer", - "displayProperty": "Name", - "properties": [ - { "name": "Name", "isRequired": true }, - { "name": "Telephone", "ui": { "displayName": "Phone Number" } } - ], - "ui": { "pageTitle": "Customers" } - } - ] -} -``` +React low-code filters are type-aware. The runtime shows only operators that make sense for the field type. For example: -See [model.json Structure](model-json.md) for the full specification. +* Text fields support contains, equals, starts with, ends with, and has value. +* Numeric fields support equals, comparison, between, and has value. +* Date fields use date-friendly labels such as on, after, before, and between. +* Boolean fields use an `All / Yes / No` value selector. +* File and image fields use `Has value` with an `All / Yes / No` value selector. -> Both approaches can be combined. The [three-layer configuration system](fluent-api.md#three-layer-configuration-system) merges Attributes, JSON, and Fluent API with clear priority rules. +`All` means no filter is applied. `Yes` maps to non-empty values. `No` maps to empty values. -## Key Features +![Has value filter options](images/runtime-filters-has-value.png) -| Feature | Description | Documentation | -|---------|-------------|---------------| -| **Attributes & Fluent API** | Define dynamic entities with C# attributes and configure programmatically | [Attributes & Fluent API](fluent-api.md) | -| **model.json** | Declarative dynamic entity definitions in JSON | [model.json Structure](model-json.md) | -| **Reference Entities** | Read-only access to existing C# entities (e.g., `IdentityUser`) for foreign key lookups | [Reference Entities](reference-entities.md) | -| **Interceptors** | Pre/Post hooks for Create, Update, Delete with JavaScript | [Interceptors](interceptors.md) | -| **Scripting API** | Server-side JavaScript for database queries and CRUD | [Scripting API](scripting-api.md) | -| **Custom Endpoints** | REST APIs with JavaScript handlers | [Custom Endpoints](custom-endpoints.md) | -| **Foreign Access** | View/Edit related dynamic entities from the target entity's UI | [Foreign Access](foreign-access.md) | -| **Export** | Export dynamic entity data to Excel (XLSX) or CSV | See below | +## Export -## Export (Excel / CSV) +Every dynamic entity page can export data to Excel or CSV. Pages with file or image fields can also export a file bundle as a ZIP. Export requests use the current search, sorting, and filters from the runtime view, so a filtered page exports the matching subset instead of the whole entity. -The Low-Code System provides built-in export functionality for all dynamic entities. Users can export filtered data to **Excel (XLSX)** or **CSV** directly from the Blazor UI. +The React runtime exports visible exportable columns by default. These columns and their default order come from the page-level **Export Fields** settings in the Low-Code Designer. A field can be visible but not exportable, or hidden but still available in the **All exportable fields** option. Use this when a page should display operational data that should not leave the system through Excel or CSV. Server-only fields are always excluded, and foreign key values are displayed through their configured display property. -### How It Works +File and image fields are exported as file names by default. Export options can expand those fields into metadata columns or temporary download-link columns. Download-link columns include file name, URL, expiry, content type, size, dimensions, and status. The links are short-lived and should be treated like signed download links, not permanent public URLs. -1. The client calls `GET /api/low-code/entities/{entityName}/download-token` to obtain a single-use download token (valid for 30 seconds). -2. The client calls `GET /api/low-code/entities/{entityName}/export-as-excel` or `GET /api/low-code/entities/{entityName}/export-as-csv` with the token and optional filters. +Use **Files (.zip)** when users need the actual uploaded files. The action appears only when the Designer allows file bundle export and at least one selected file/image field is exportable. The ZIP contains `manifest.csv` and files under `files/{recordId}/{fieldName}/{safeFileName}`. The manifest records missing, malformed, unlinked, and limit-skipped files instead of failing the whole export. -### API Endpoints +Spreadsheet and ZIP exports require a short-lived, single-use download token. The token is bound to the tenant, page, entity, child page, and foreign-access context. File download links use separate short-lived tokens bound to the exported file value. Spreadsheet formula-like text values are escaped before writing CSV or Excel headers and cells. | Endpoint | Description | |----------|-------------| -| `GET /api/low-code/entities/{entityName}/download-token` | Get a single-use download token | -| `GET /api/low-code/entities/{entityName}/export-as-excel` | Export as Excel (.xlsx) | -| `GET /api/low-code/entities/{entityName}/export-as-csv` | Export as CSV (.csv) | - -Export requests accept the same filtering, sorting, and search parameters as the list endpoint. Server-only properties are automatically excluded, and foreign key columns display the referenced entity's display value instead of the raw ID. - -## Custom Commands and Queries - -The Low-Code System allows you to replace or extend the default CRUD operations by implementing custom command and query handlers in C#. - -### Custom Commands - -Create a class that implements `ILcCommand` and decorate it with `[CustomCommand]`: - -````csharp -[CustomCommand("Create", "MyApp.Products.Product")] -public class CustomProductCreateCommand : CreateCommand -{ - public override async Task ExecuteWithResultAsync(DynamicCommandArgs commandArgs) - { - // Your custom create logic here - // ... - } -} -```` - -| Parameter | Description | -|-----------|-------------| -| `commandName` | The command to replace: `"Create"`, `"Update"`, or `"Delete"` | -| `entityName` | Full entity name (e.g., `"MyApp.Products.Product"`) | - -### Custom Queries - -Create a class that implements `ILcQuery` and decorate it with `[CustomQuery]`: - -````csharp -[CustomQuery("List", "MyApp.Products.Product")] -public class CustomProductListQuery : ILcQuery -{ - public async Task ExecuteAsync(DynamicQueryArgs queryArgs) - { - // Your custom list query logic here - // ... - } -} -```` - -````csharp -[CustomQuery("Single", "MyApp.Products.Product")] -public class CustomProductListQuery : ILcQuery -{ - public async Task ExecuteAsync(DynamicQueryArgs queryArgs) - { - // Your custom single query logic here - // ... - } -} -```` - -| Parameter | Description | -|-----------|-------------| -| `queryName` | The query to replace: `"List"` or `"Single"` | -| `entityName` | Full entity name (e.g., `"MyApp.Products.Product"`) | - -Custom commands and queries are automatically discovered and registered at startup. They completely replace the default handler for the specified entity and operation. - -## Internals - -### Domain Layer - -* `DynamicModelManager`: Singleton managing all entity metadata with a layered configuration architecture (Code > JSON > Fluent > Defaults). -* `EntityDescriptor`: Entity definition with properties, foreign keys, interceptors, and UI configuration. -* `EntityPropertyDescriptor`: Property definition with type, validation, UI settings, and foreign key info. -* `IDynamicEntityRepository`: Repository for dynamic entity CRUD operations. - -### Application Layer - -* `DynamicEntityAppService`: CRUD operations for all dynamic entities (Get, GetList, Create, Update, Delete, Export). -* `DynamicEntityUIAppService`: UI definitions, menu items, and page configurations. Provides: - * `GetUiDefinitionAsync(entityName)` — Full UI definition (filters, columns, forms, children, foreign access actions, permissions) - * `GetUiCreationFormDefinitionAsync(entityName)` — Creation form fields with validation rules - * `GetUiEditFormDefinitionAsync(entityName)` — Edit form fields with validation rules - * `GetMenuItemsAsync()` — Menu items for all entities that have a `pageTitle` configured (filtered by permissions) -* `DynamicPermissionDefinitionProvider`: Auto-generates permissions per entity. -* `CustomEndpointExecutor`: Executes JavaScript-based custom endpoints. - -### Database Providers - -**Entity Framework Core**: Dynamic entities are configured as EF Core [shared-type entities](https://learn.microsoft.com/en-us/ef/core/modeling/entity-types?tabs=fluent-api#shared-type-entity-types) via the `ConfigureDynamicEntities()` extension method. +| `GET /api/low-code/pages/{pageName}/download-token` | Gets a short-lived download token | +| `GET /api/low-code/pages/{pageName}/export/excel` | Exports filtered data as Excel | +| `GET /api/low-code/pages/{pageName}/export/csv` | Exports filtered data as CSV | +| `GET /api/low-code/pages/{pageName}/export/files` | Exports selected file/image fields as a ZIP bundle | +| `GET /api/low-code/pages/export/files/{token}` | Downloads one temporary file link created by spreadsheet export | + +Useful export settings: + +| Setting | Default | Purpose | +|---------|---------|---------| +| `LowCode:Export:MaxRows` | `100000` | Maximum rows in one all-filtered export | +| `LowCode:Export:DownloadTokenLifetimeSeconds` | `30` | Download token lifetime | +| `LowCode:Export:FileLinkTokenLifetimeSeconds` | `900` | Temporary file link lifetime | +| `LowCode:Export:MaxFileBundleFiles` | `1000` | Maximum files in one ZIP export | +| `LowCode:Export:MaxFileBundleBytes` | `268435456` | Maximum total file bytes in one ZIP export | + +## Advanced Configuration + +The designer stores and reads the same descriptor metadata described in the reference pages below. Use these pages when you need source-controlled descriptor files, custom startup wiring, script handlers, or low-level integration details. + +| Topic | Use it for | +|-------|------------| +| [Designer](designer.md) | Admin Console tabs, entity/page/form setup, permissions, and health | +| [React Runtime](react-runtime.md) | React package wiring, routes, menu items, filters, forms, and export | +| [Attributes & Fluent API](fluent-api.md) | Source-controlled C# metadata and runtime overrides | +| [Model Descriptor Files](model-json.md) | JSON descriptor files and public descriptor schemas used by the designer and runtime | +| [Reference Entities](reference-entities.md) | Lookups to existing entities such as Identity users | +| [Foreign Access](foreign-access.md) | Access to related dynamic entities through relations | +| [Interceptors](interceptors.md) | JavaScript lifecycle logic for CRUD operations | +| [Custom Endpoints](custom-endpoints.md) | JavaScript-backed REST endpoints | +| [Script Actions](script-actions.md) | Event handlers, background jobs, background workers, script editor, and dry-run testing | +| [Scripting API](scripting-api.md) | Server-side script context and helpers | + +## Runtime Internals + +The generated pages are powered by these services: + +* `DynamicEntityAppService` handles CRUD, list queries, filtering, sorting, and export. +* `DynamicPageAppService` exposes page-based CRUD, file, attachment, lookup, child, foreign-access, and export endpoints. +* `DynamicEntityUIAppService` returns page, form, dashboard, field, filter, and menu metadata. +* `DynamicPermissionDefinitionProvider` creates permissions for dynamic entities. +* `CustomEndpointExecutor` runs JavaScript-backed custom endpoints. +* EF Core maps dynamic entities as shared-type entities. ## See Also -* [Attributes & Fluent API](fluent-api.md) -* [model.json Structure](model-json.md) -* [Scripting API](scripting-api.md) +* [Low-Code Designer](designer.md) +* [React Runtime](react-runtime.md) +* [Model Descriptor Files](model-json.md) diff --git a/docs/en/low-code/interceptors.md b/docs/en/low-code/interceptors.md index 7eaf97eb778..a8f1b069428 100644 --- a/docs/en/low-code/interceptors.md +++ b/docs/en/low-code/interceptors.md @@ -7,6 +7,10 @@ # Interceptors +> **Preview:** Interceptors and their JavaScript context are preview extension points. Script context members, validation behavior, and lifecycle hooks may change before general availability. + +Use designer actions and descriptor metadata for standard low-code behavior first. Interceptors are an advanced extension point for adding JavaScript lifecycle logic when the generated CRUD flow needs validation, transformation, or replacement behavior. + Interceptors allow you to run custom JavaScript code before, after, or instead of Create, Update, and Delete operations on dynamic entities. ## Interceptor Types @@ -23,6 +27,31 @@ Interceptors allow you to run custom JavaScript code before, after, or instead o | `Delete` | `Post` | After entity deletion — cleanup | | `Delete` | `Replace` | Instead of entity deletion — no return value needed | +## Defining Interceptors in JSON Descriptors + +The designer stores entity interceptors in the entity `interceptors` array: + +```json +{ + "name": "LowCodeDemo.Customers.Customer", + "interceptors": [ + { + "commandName": "Create", + "type": "Pre", + "javascript": "if (!args.getValue('Name')) {\n globalError = 'Name is required.';\n}" + } + ] +} +``` + +### Interceptor Descriptor + +| Field | Type | Description | +|-------|------|-------------| +| `commandName` | string | `"Create"`, `"Update"`, or `"Delete"` | +| `type` | string | `"Pre"`, `"Post"`, or `"Replace"` | +| `javascript` | string | JavaScript code to execute | + ## Defining Interceptors with Attributes Use the `[DynamicEntityCommandInterceptor]` attribute on a C# class: @@ -49,7 +78,7 @@ The `Name` parameter must be one of: `"Create"`, `"Update"`, or `"Delete"`. The ## Defining Interceptors with Fluent API -Use the `Interceptors` list on an `EntityDescriptor` to add interceptors programmatically in your [Low-Code Initializer](index.md#1-create-a-low-code-initializer): +Use the `Interceptors` list on an `EntityDescriptor` to add interceptors programmatically in startup configuration: ````csharp AbpDynamicEntityConfig.EntityConfigurations.Configure( @@ -71,32 +100,7 @@ AbpDynamicEntityConfig.EntityConfigurations.Configure( ); ```` -See [Attributes & Fluent API](fluent-api.md#adding-interceptors) for more details on Fluent API configuration. - -## Defining Interceptors in model.json - -Add interceptors to the `interceptors` array of an entity: - -```json -{ - "name": "LowCodeDemo.Customers.Customer", - "interceptors": [ - { - "commandName": "Create", - "type": "Pre", - "javascript": "if(context.commandArgs.data['Name'] == 'Invalid') {\n globalError = 'Invalid Customer Name!';\n}" - } - ] -} -``` - -### Interceptor Descriptor - -| Field | Type | Description | -|-------|------|-------------| -| `commandName` | string | `"Create"`, `"Update"`, or `"Delete"` | -| `type` | string | `"Pre"`, `"Post"`, or `"Replace"` | -| `javascript` | string | JavaScript code to execute | +See [Attributes & Fluent API](fluent-api.md) for more details on Fluent API configuration. ## JavaScript Context @@ -138,7 +142,13 @@ Inside interceptor scripts, you have access to: |--------|-------------| | `isAvailable` | Whether the email sender is configured and available | | `sendAsync(to, subject, body)` | Send a plain-text email | +| `sendAsync(from, to, subject, body)` | Send a plain-text email with an explicit sender | | `sendHtmlAsync(to, subject, htmlBody)` | Send an HTML email | +| `sendHtmlAsync(from, to, subject, htmlBody)` | Send an HTML email with an explicit sender | +| `queueAsync(to, subject, body)` | Queue a plain-text email | +| `queueAsync(from, to, subject, body)` | Queue a plain-text email with an explicit sender | +| `queueHtmlAsync(to, subject, htmlBody)` | Queue an HTML email | +| `queueHtmlAsync(from, to, subject, htmlBody)` | Queue an HTML email with an explicit sender | ### Logging @@ -154,6 +164,8 @@ Inside interceptor scripts, you have access to: Full access to the [Scripting API](scripting-api.md) for querying and mutating data. +Interceptors can also use common scripting services such as `user`, `tenant`, `auth`, `settings`, `features`, `config`, `http`, `events`, `jobs`, `blob`, `encryption`, `textTemplating`, `files`, `images`, and `attachments` when they are enabled by the scripting capability profile. + ### `globalError` Set this variable to a string to **abort** the operation and return an error: @@ -245,5 +257,5 @@ When you need to completely replace the default create operation with custom log ## See Also * [Scripting API](scripting-api.md) -* [model.json Structure](model-json.md) +* [Model Descriptor Files](model-json.md) * [Custom Endpoints](custom-endpoints.md) diff --git a/docs/en/low-code/model-json.md b/docs/en/low-code/model-json.md index 2f58876a29a..ce59b0b3ec4 100644 --- a/docs/en/low-code/model-json.md +++ b/docs/en/low-code/model-json.md @@ -1,309 +1,416 @@ ```json //[doc-seo] { - "Description": "Define dynamic entities using model.json in the ABP Low-Code System. Learn about entity properties, enums, foreign keys, validators, UI configuration, and migration requirements." + "Description": "Define ABP Low-Code descriptor metadata and descriptor schemas for dynamic entities, pages, forms, filters, permissions, script endpoints, event handlers, background jobs, and workers." } ``` -# model.json Structure +# Model Descriptor Files -The `model.json` file defines all your dynamic entities, their properties, enums, relationships, interceptors, custom endpoints, and UI configurations. It is an alternative configuration source to [C# Attributes and Fluent API](fluent-api.md), ideal when you prefer a declarative JSON approach. +> **Preview:** The Low-Code System is currently in preview. The descriptor format is stable enough for evaluation and source control, but fields may change before general availability. + +Low-code metadata is source-controlled as JSON descriptor files used by the Low-Code Designer and React runtime. Current generated projects keep descriptors as split files under `_Dynamic`; older projects may still contain an aggregate `_Dynamic/model.json` document. Use the [Low-Code Designer](designer.md) for normal editing. Use this page when you need to review, generate, merge, or source-control the JSON metadata directly. ## File Location -Place your `model.json` in a `_Dynamic` folder inside your **Domain** project: +Generated low-code applications keep descriptor files in a `_Dynamic` folder under the application domain project. A typical project stores one JSON file per descriptor: -``` +```text YourApp.Domain/ -└── _Dynamic/ - └── model.json +`-- _Dynamic/ + |-- entities/ + | `-- Acme.Campaigns.Campaign.json + |-- pages/ + | `-- campaigns.json + |-- forms/ + | `-- campaign-form.json + `-- permissions/ + `-- Acme.Campaigns.json ``` -The module automatically discovers and loads this file at application startup. +Exact folders and file names are generated by the tooling for the descriptor type. Keep the whole `_Dynamic` folder and the generated initializer in source control. The low-code module discovers the descriptor metadata during application startup. + +## JSON Schemas + +ABP publishes JSON Schema definitions for the descriptor objects that make up low-code metadata. These schemas are useful when you generate descriptors, review changes in source control, or want IDE validation for split descriptor files. + +The schema files live in the ABP repository under `schemas/low-code`. Use the branch or tag that matches your ABP version. + +The schema manifest is published at: + +```text +https://raw.githubusercontent.com/abpframework/abp/rel-10.5/schemas/low-code/manifest.json +``` -> A JSON Schema file (`model.schema.json`) is available in the module source for IDE IntelliSense. Reference it using the `$schema` property: +For another version, replace `rel-10.5` with the matching ABP branch or tag. + +The manifest maps each descriptor collection to its descriptor schema: + +| Descriptor collection | Descriptor schema | +|-----------------------|-------------------| +| `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` | + +### Split Descriptor Files + +Use the descriptor schema directly when a descriptor is stored as its own JSON file. The descriptor object is the same shape used for one item inside the related descriptor collection. ```json { - "$schema": "path/to/model.schema.json", - "entities": [] + "$schema": "https://raw.githubusercontent.com/abpframework/abp/rel-10.5/schemas/low-code/definitions/entity-descriptor.schema.json", + "name": "Acme.Campaigns.Campaign", + "displayName": "Campaigns", + "properties": [] } ``` -## Top-Level Structure +For example, a file that stores one page descriptor can reference `page-descriptor.schema.json`, and a file that stores one form descriptor can reference `form-descriptor.schema.json`. -The `model.json` file has three root sections: +The published schemas validate individual descriptor objects, not an aggregate descriptor document. If you still maintain an aggregate `_Dynamic/model.json`, do not point it at `entity-descriptor.schema.json`, `page-descriptor.schema.json`, or another descriptor schema; those schemas expect one descriptor object, not the top-level arrays. For aggregate metadata, use the manifest table above as the section-level reference and validate each array item with its matching descriptor schema. + +## Top-Level Sections + +When descriptor metadata is viewed as an aggregate document, the logical sections are page/form centered. Entities define data shape; pages and forms define the React runtime UI. ```json { - "$schema": "...", "enums": [], "entities": [], - "endpoints": [] + "endpoints": [], + "eventHandlers": [], + "backgroundJobs": [], + "backgroundWorkers": [], + "pageGroups": [], + "pages": [], + "forms": [], + "permissions": [] } ``` | Section | Description | |---------|-------------| -| `enums` | Enum type definitions | -| `entities` | Entity definitions with properties, foreign keys, interceptors, and UI | -| `endpoints` | Custom REST API endpoints with JavaScript handlers | +| `enums` | Reusable enum definitions | +| `entities` | Dynamic entities, properties, relations, attachments, validations, and interceptors | +| `endpoints` | JavaScript-backed custom HTTP endpoints | +| `eventHandlers` | JavaScript handlers for distributed events | +| `backgroundJobs` | Named JavaScript background job handlers | +| `backgroundWorkers` | Scheduled JavaScript workers | +| `pageGroups` | Menu folders used by runtime pages | +| `pages` | React runtime page definitions, including data grids, kanban, calendar, gallery, form pages, and dashboards | +| `forms` | Named form definitions referenced by pages | +| `permissions` | Custom permission definitions referenced by pages and endpoints | -## Enum Definitions +## Enums -Define enums that can be used as property types: +Define enums before properties that reference them: ```json { "enums": [ { - "name": "LowCodeDemo.Organizations.OrganizationType", + "name": "Acme.Campaigns.CampaignStatus", "values": [ - { "name": "Corporate", "value": 0 }, - { "name": "Enterprise", "value": 1 }, - { "name": "Startup", "value": 2 }, - { "name": "Consulting", "value": 3 } + { "name": "Draft", "value": 0 }, + { "name": "Active", "value": 1 }, + { "name": "Paused", "value": 2 }, + { "name": "Completed", "value": 3 } ] } ] } ``` -Reference enums in entity properties using the `enumType` field: +Use the enum from a property with `type: "enum"` and `enumType`: ```json { - "name": "OrganizationType", - "enumType": "LowCodeDemo.Organizations.OrganizationType" + "name": "Status", + "type": "enum", + "enumType": "Acme.Campaigns.CampaignStatus", + "defaultValue": "0" } ``` -## Entity Definition +## Entities -Each entity has the following structure: +Entities describe the persisted data model. UI is not configured with legacy property `ui` objects. Use page `columns` and `filters`, and named `forms`, for runtime UI behavior. ```json { - "name": "LowCodeDemo.Products.Product", + "name": "Acme.Campaigns.Campaign", + "displayName": "Campaigns", "displayProperty": "Name", - "parent": null, "properties": [], - "interceptors": [], - "ui": {} + "crossFieldValidations": [], + "interceptors": [] } ``` -### Entity Attributes - -| Attribute | Type | Description | -|-----------|------|-------------| -| `name` | string | **Required.** Full entity name with namespace (e.g., `"MyApp.Products.Product"`) | -| `displayProperty` | string | Property to display in lookups and foreign key dropdowns | -| `parent` | string | Parent entity name for parent-child (master-detail) relationships | -| `properties` | array | Property definitions | -| `interceptors` | array | CRUD lifecycle interceptors | -| `ui` | object | UI configuration | - -### Parent-Child Relationships - -Use the `parent` field to create nested entities. Children are managed through the parent entity's UI: - -```json -{ - "name": "LowCodeDemo.Orders.OrderLine", - "parent": "LowCodeDemo.Orders.Order", - "properties": [ - { - "name": "ProductId", - "foreignKey": { - "entityName": "LowCodeDemo.Products.Product" - } - }, - { "name": "Quantity", "type": "int" }, - { "name": "Amount", "type": "decimal" } - ] -} -``` - -Multi-level nesting is supported (e.g., `Order > OrderLine > ShipmentItem > ShipmentTracking`). +| Field | Description | +|-------|-------------| +| `name` | Required stable full entity name, for example `Acme.Campaigns.Campaign` | +| `displayName` | Default plural/screen label | +| `displayProperty` | Property shown in lookups and foreign key display values | +| `parent` | Parent entity name for child/detail entities | +| `attachments` | Record-level attachment settings | +| `properties` | Entity property definitions | +| `crossFieldValidations` | Validation rules comparing two properties | +| `interceptors` | Create, update, and delete lifecycle scripts | -## Property Definition +### Properties ```json { - "name": "Price", - "type": "decimal", + "name": "Budget", + "type": "money", "isRequired": true, "isUnique": false, - "isMappedToDbField": true, - "serverOnly": false, "allowSetByClients": true, - "enumType": null, - "foreignKey": null, - "validators": [], - "ui": {} + "serverOnly": false, + "isMappedToDbField": true, + "validators": [ + { "type": "range", "minimum": 0, "maximum": 1000000 } + ] } ``` +| Field | Description | +|-------|-------------| +| `name` | Required PascalCase property name | +| `type` | Property type; omitted means `string` | +| `displayName` | Default field label; pages/forms can override it | +| `enumType` | Enum name when `type` is `enum` | +| `defaultValue` | Default value for new records, stored as a string and converted at runtime | +| `isRequired` | Required/not nullable backend and UI validation | +| `isUnique` | Unique value validation | +| `serverOnly` | Hidden from clients, API responses, and UI metadata | +| `allowSetByClients` | Whether create/update clients may set this value | +| `isMappedToDbField` | Whether the property is stored in the database | +| `foreignKey` | Lookup relation metadata | +| `validators` | Backend/UI validation rules | + ### Property Types | Type | Description | |------|-------------| -| `string` | Text (default if type is omitted) | -| `int` | 32-bit integer | -| `long` | 64-bit integer | -| `decimal` | Decimal number | -| `DateTime` | Date and time | +| `string` | Text | +| `int`, `long` | Whole numbers | +| `decimal`, `money` | Decimal numbers and money values | +| `dateTime`, `date`, `time` | Date/time values | | `boolean` | True/false | -| `Guid` | GUID/UUID | -| `Enum` | Enum type (requires `enumType` field) | +| `guid` | GUID value | +| `enum` | Integer-backed enum; requires `enumType` | +| `file`, `image` | Upload metadata handled by the low-code file pipeline | -### Property Flags +### File, Image, and Attachments -| Flag | Type | Default | Description | -|------|------|---------|-------------| -| `isRequired` | bool | `false` | Property must have a value | -| `isUnique` | bool | `false` | Value must be unique across all records | -| `isMappedToDbField` | bool | `true` | Property is stored in the database | -| `serverOnly` | bool | `false` | Property is hidden from API clients | -| `allowSetByClients` | bool | `true` | Whether clients can set this value | +Use `file` or `image` properties for first-class upload fields: -### Foreign Key Properties +```json +{ + "name": "CoverImage", + "type": "image", + "fileAllowedContentTypes": ["image/*"], + "fileMaxSizeBytes": 5242880, + "imageMaxWidth": 1600, + "imageMaxHeight": 900, + "imageResizeMode": "fit" +} +``` -Define a foreign key relationship inline on a property: +Use entity `attachments` when each record can have multiple arbitrary files: ```json { - "name": "CustomerId", - "foreignKey": { - "entityName": "LowCodeDemo.Customers.Customer", - "displayPropertyName": "Name", - "access": "edit" + "name": "Acme.Campaigns.Campaign", + "attachments": { + "isEnabled": true, + "maxFileCount": 10, + "maxFileSizeBytes": 5242880, + "allowedContentTypes": ["application/pdf", "image/*"] } } ``` -| Attribute | Description | -|-----------|-------------| -| `entityName` | **Required.** Full name of the target entity — can be a **dynamic entity** (e.g., `"LowCodeDemo.Customers.Customer"`) or a **[reference entity](reference-entities.md)** (e.g., `"Volo.Abp.Identity.IdentityUser"`) | -| `displayPropertyName` | Property to display in lookups (defaults to entity's `displayProperty`) | -| `access` | [Foreign access](foreign-access.md) level: `"none"`, `"view"`, or `"edit"` | +### Foreign Keys + +```json +{ + "name": "OwnerId", + "type": "guid", + "foreignKey": { + "entityName": "Volo.Abp.Identity.IdentityUser", + "displayPropertyName": "UserName", + "access": "none" + } +} +``` -> **Note:** [Reference entities](reference-entities.md) are existing C# entities (like ABP's `IdentityUser`) that are registered for read-only access. Unlike dynamic entities, they don't get CRUD pages — they're used only for foreign key lookups and display values. +`entityName` can point to another dynamic entity or a registered [reference entity](reference-entities.md). `access` controls [Foreign Access](foreign-access.md) behavior for dynamic entity relations. ### Validators -Add validation rules to properties: - ```json { "name": "EmailAddress", + "type": "string", "validators": [ { "type": "required" }, - { "type": "emailAddress" }, - { "type": "minLength", "length": 5 }, + { "type": "email" }, { "type": "maxLength", "length": 255 } ] } ``` -Additional validator examples: +Common validators include `required`, `minLength`, `maxLength`, `stringLength`, `email`, `emailAddress`, `phone`, `url`, `creditCard`, `regularExpression`, `range`, `min`, and `max`. Validators can include a custom `message`. + +## Pages + +Pages create runtime routes and menu entries. They also choose how entity data is rendered in React. ```json { - "name": "Website", - "validators": [ - { "type": "url", "message": "Please enter a valid URL" } - ] -}, -{ - "name": "PhoneNumber", - "validators": [ - { "type": "phone" } - ] -}, -{ - "name": "ProductCode", - "validators": [ - { "type": "regularExpression", "pattern": "^[A-Z]{3}-\\d{4}$", "message": "Code must be in format ABC-1234" } - ] -}, -{ - "name": "Price", - "type": "decimal", - "validators": [ - { "type": "range", "minimum": 0.01, "maximum": 99999.99 } - ] + "name": "campaigns", + "title": "Campaigns", + "icon": "fa-solid fa-bullhorn", + "type": "dataGrid", + "entityName": "Acme.Campaigns.Campaign", + "group": "marketing", + "defaultFileExportMode": 0, + "allowFileBundleExport": true, + "columns": [ + { "propertyName": "Name", "order": 0, "exportOrder": 0 }, + { "propertyName": "Status", "order": 1, "exportOrder": 1 }, + { "propertyName": "Budget", "order": 2, "exportOrder": 2, "exportable": false } + ], + "filters": [ + { "propertyName": "Name", "control": "text", "defaultOperator": "contains" }, + { "propertyName": "Status", "control": "select", "defaultOperator": "equal" } + ], + "createFormName": "campaign-form", + "editFormName": "campaign-form" } ``` -| Validator | Parameters | Applies To | Description | -|-----------|------------|------------|-------------| -| `required` | `allowEmptyStrings` (optional) | All types | Value is required | -| `minLength` | `length` | String | Minimum string length | -| `maxLength` | `length` | String | Maximum string length | -| `stringLength` | `minimumLength`, `maximumLength` | String | String length range (min and max together) | -| `emailAddress` | — | String | Must be a valid email | -| `phone` | — | String | Must be a valid phone number | -| `url` | — | String | Must be a valid URL | -| `creditCard` | — | String | Must be a valid credit card number | -| `regularExpression` | `pattern` | String | Must match the regex pattern | -| `range` | `minimum`, `maximum` | Numeric | Numeric range | -| `min` | `minimum` | Numeric | Minimum numeric value | -| `max` | `maximum` | Numeric | Maximum numeric value | +Page columns support two independent flags: + +| Field | Default | Purpose | +|-------|---------|---------| +| `visible` | `true` | Renders the field in the React page view | +| `exportOrder` | `order` | Optional page-level export order. Lower values are exported first | +| `exportable` | `true` | Page-level export flag managed by **Export Fields**. Allows the field to be included in Excel, CSV, download-link columns, and file bundle export | + +If `columns` is present, export uses this list as the page-level export policy. `exportable: false` prevents the field from being exported even if a caller sends the field name manually. `exportOrder` controls default export order without changing display order. Server-only entity properties are never exportable. -> All validators accept an optional `message` parameter for a custom error message. The `regularExpression` validator also accepts the alias `pattern`, and `emailAddress` also accepts `email`. +Page export settings: -## UI Configuration +| Field | Default | Purpose | +|-------|---------|---------| +| `defaultFileExportMode` | `0` | Default spreadsheet output for file/image fields. `0` = file name, `1` = metadata columns, `2` = temporary download-link columns | +| `allowFileBundleExport` | `true` | Allows **Files (.zip)** export for exportable file/image columns on the page | -### Entity-Level UI +ZIP file bundle export only includes selected page columns that are file or image fields and are exportable. The ZIP contains `manifest.csv` plus files under `files/{recordId}/{fieldName}/{safeFileName}`. + +| Page type | Required fields | Purpose | +|-----------|-----------------|---------| +| `dataGrid` | `entityName` | Searchable, sortable CRUD grid | +| `kanban` | `entityName`, `groupByProperty` | Cards grouped by an enum/status-like property | +| `calendar` | `entityName`, `calendarStartProperty` | Records shown on a calendar | +| `gallery` | `entityName` | Visual/card list, optionally using `galleryImageProperty` | +| `form` | `entityName`, `formName` | Standalone form page | +| `dashboard` | `dashboard` | Dashboard visualizations | + +Runtime routes use the page name: + +```text +/dynamic/ +/dynamic//create +/dynamic//edit/ +/dynamic// +``` + +## Forms + +Forms are named definitions referenced by pages through `formName`, `createFormName`, or `editFormName`. ```json { - "ui": { - "pageTitle": "Products" + "name": "campaign-form", + "entityName": "Acme.Campaigns.Campaign", + "enableSaveAndNew": true, + "fields": [ + { "id": "name", "label": "Name", "type": "text", "binding": "Name" }, + { "id": "status", "label": "Status", "type": "select", "binding": "Status", "enumType": "Acme.Campaigns.CampaignStatus" }, + { "id": "ownerId", "label": "Owner", "type": "lookup", "binding": "OwnerId" } + ], + "layout": { + "tabs": [ + { + "id": "main", + "title": "Main", + "isDefault": true, + "groups": [ + { + "id": "details", + "title": "Details", + "isDefault": true, + "rows": [ + { "cells": [{ "fieldId": "name", "colSpan": 4 }] }, + { "cells": [{ "fieldId": "status", "colSpan": 2 }, { "fieldId": "ownerId", "colSpan": 2 }] } + ] + } + ] + } + ] } } ``` -> Only entities with `ui.pageTitle` get a menu item and a dedicated page in the UI. +Form fields can be `text`, `textarea`, `number`, `checkbox`, `date`, `datetime`, `time`, `file`, `image`, `money`, `select`, `lookup`, `guid`, or `computed`. Form rules can hide, show, disable, enable, or set values for fields/groups. + +## Filters + +Filters are page-owned. Use `control: "auto"` unless you need a specific control. + +| Property type | Typical operators | +|---------------|-------------------| +| `string` | `contains`, `equal`, `notEqual`, `startsWith`, `endsWith`, `notContains`, `hasValue` | +| `int`, `long`, `decimal`, `money` | `between`, `equal`, `notEqual`, `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual`, `hasValue` | +| `date`, `dateTime`, `time` | `between`, `equal`, `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual`, `hasValue` | +| `boolean` | `All / Yes / No` value selector | +| `enum`, lookup, `guid` | `equal`, `notEqual`, `in`, `notIn`, `hasValue` | +| `file`, `image` | `hasValue` with `All / Yes / No` | + +`hasValue` is a UI alias. At runtime, `Yes` maps to `IsNotNull`, `No` maps to `IsNull`, and `All` does not add a filter. + +## Permissions -### Property-Level UI +Pages can use generated defaults or explicit permission configuration: ```json { - "name": "RegistrationNumber", - "ui": { - "displayName": "Registration Number", - "isAvailableOnDataTable": true, - "isAvailableOnDataTableFiltering": true, - "creationFormAvailability": "Hidden", - "editingFormAvailability": "NotAvailable", - "quickLookOrder": 100 + "permissionConfig": { + "view": "authenticated", + "create": "Acme.Campaigns.Create", + "update": "Acme.Campaigns.Update", + "delete": "Acme.Campaigns.Delete" } } ``` -| Attribute | Type | Default | Description | -|-----------|------|---------|-------------| -| `displayName` | string | Property name | Display label in UI | -| `isAvailableOnDataTable` | bool | `true` | Show in data grid | -| `isAvailableOnDataTableFiltering` | bool | `true` | Show in filter panel | -| `creationFormAvailability` | string | `"Available"` | Visibility in create form | -| `editingFormAvailability` | string | `"Available"` | Visibility in edit form | -| `quickLookOrder` | int | -2 | Order in quick-look panel (-2 = not shown) | +Custom permission definitions live in the top-level `permissions` section and can be granted through the normal ABP permission management UI. -#### Form Availability Values +## Scripts -| Value | Description | -|-------|-------------| -| `Available` | Visible and editable | -| `Hidden` | Not visible in the form | -| `NotAvailable` | Visible but disabled/read-only | - -## Interceptors - -Define JavaScript interceptors for CRUD lifecycle hooks: +### Interceptors ```json { @@ -311,94 +418,148 @@ Define JavaScript interceptors for CRUD lifecycle hooks: { "commandName": "Create", "type": "Pre", - "javascript": "if(!context.commandArgs.data['Name']) { globalError = 'Name is required!'; }" + "javascript": "if (!args.getValue('Name')) { globalError = 'Name is required.'; }" } ] } ``` -See [Interceptors](interceptors.md) for details. - -## Endpoints +See [Interceptors](interceptors.md) and [Scripting API](scripting-api.md). -Define custom REST endpoints with JavaScript handlers: +### Custom Endpoints ```json { "endpoints": [ { - "name": "GetProductStats", - "route": "/api/custom/products/stats", + "name": "GetCampaignStats", + "route": "/api/custom/campaigns/stats", "method": "GET", - "requireAuthentication": false, - "javascript": "var count = await db.count('Products.Product'); return ok({ total: count });" + "requireAuthentication": true, + "requiredPermissions": ["Acme.Campaigns"], + "javascript": "var count = await db.count('Acme.Campaigns.Campaign'); return ok({ total: count });" } ] } ``` -See [Custom Endpoints](custom-endpoints.md) for details. +See [Custom Endpoints](custom-endpoints.md). + +### Event Handlers, Jobs, and Workers + +```json +{ + "eventHandlers": [ + { + "name": "NotifyCampaignCompleted", + "eventName": "Acme.Campaigns.CampaignCompleted", + "javascript": "log('Campaign completed: ' + eventData.id);" + } + ], + "backgroundJobs": [ + { + "name": "SendCampaignSummary", + "javascript": "log('Sending summary for ' + jobData.campaignId);" + } + ], + "backgroundWorkers": [ + { + "name": "CampaignCleanup", + "period": 3600000, + "javascript": "log('Cleaning campaign data.');" + } + ] +} +``` + +Background workers require either `period` in milliseconds or `cronExpression`. See [Script Actions](script-actions.md) for event handler, background job, background worker, code editor, and dry-run testing details. ## Complete Example +The complete example below shows the logical aggregate shape. Split projects store each descriptor as its own file, but field shapes are the same. + ```json { "enums": [ { - "name": "ShipmentStatus", + "name": "Acme.Campaigns.CampaignStatus", "values": [ - { "name": "Pending", "value": 0 }, - { "name": "Shipped", "value": 2 }, - { "name": "Delivered", "value": 4 } + { "name": "Draft", "value": 0 }, + { "name": "Active", "value": 1 }, + { "name": "Completed", "value": 2 } ] } ], "entities": [ { - "name": "LowCodeDemo.Products.Product", + "name": "Acme.Campaigns.Campaign", + "displayName": "Campaigns", "displayProperty": "Name", "properties": [ - { "name": "Name", "isUnique": true, "isRequired": true }, - { "name": "Price", "type": "decimal" }, - { "name": "StockCount", "type": "int" }, - { "name": "ReleaseDate", "type": "DateTime" } - ], - "ui": { "pageTitle": "Products" } - }, + { "name": "Name", "type": "string", "isRequired": true, "validators": [{ "type": "maxLength", "length": 128 }] }, + { "name": "Status", "type": "enum", "enumType": "Acme.Campaigns.CampaignStatus", "defaultValue": "0" }, + { "name": "Budget", "type": "money" }, + { "name": "StartDate", "type": "date" }, + { "name": "CoverImage", "type": "image", "fileAllowedContentTypes": ["image/*"] } + ] + } + ], + "forms": [ { - "name": "LowCodeDemo.Orders.Order", - "displayProperty": "Id", - "properties": [ - { - "name": "CustomerId", - "foreignKey": { - "entityName": "LowCodeDemo.Customers.Customer", - "access": "edit" + "name": "campaign-form", + "entityName": "Acme.Campaigns.Campaign", + "fields": [ + { "id": "name", "label": "Name", "type": "text", "binding": "Name" }, + { "id": "status", "label": "Status", "type": "select", "binding": "Status", "enumType": "Acme.Campaigns.CampaignStatus" }, + { "id": "budget", "label": "Budget", "type": "money", "binding": "Budget" }, + { "id": "startDate", "label": "Start Date", "type": "date", "binding": "StartDate" }, + { "id": "coverImage", "label": "Cover Image", "type": "image", "binding": "CoverImage" } + ], + "layout": { + "tabs": [ + { + "id": "main", + "title": "Main", + "isDefault": true, + "groups": [ + { + "id": "details", + "title": "Details", + "isDefault": true, + "rows": [ + { "cells": [{ "fieldId": "name", "colSpan": 4 }] }, + { "cells": [{ "fieldId": "status", "colSpan": 2 }, { "fieldId": "budget", "colSpan": 2 }] }, + { "cells": [{ "fieldId": "startDate", "colSpan": 2 }, { "fieldId": "coverImage", "colSpan": 2 }] } + ] + } + ] } - }, - { "name": "TotalAmount", "type": "decimal" }, - { "name": "IsDelivered", "type": "boolean" } + ] + } + } + ], + "pageGroups": [ + { "name": "marketing", "title": "Marketing", "icon": "fa-solid fa-bullhorn", "order": 10 } + ], + "pages": [ + { + "name": "campaigns", + "title": "Campaigns", + "type": "dataGrid", + "entityName": "Acme.Campaigns.Campaign", + "group": "marketing", + "columns": [ + { "propertyName": "Name", "order": 0, "exportOrder": 0 }, + { "propertyName": "Status", "order": 1, "exportOrder": 1 }, + { "propertyName": "Budget", "order": 2, "exportOrder": 2, "exportable": false } ], - "interceptors": [ - { - "commandName": "Create", - "type": "Post", - "javascript": "context.log('Order created: ' + context.commandArgs.entityId);" - } + "filters": [ + { "propertyName": "Name", "control": "text", "defaultOperator": "contains" }, + { "propertyName": "Status", "control": "select", "defaultOperator": "equal" }, + { "propertyName": "CoverImage", "control": "exists", "defaultOperator": "hasValue" } ], - "ui": { "pageTitle": "Orders" } - }, - { - "name": "LowCodeDemo.Orders.OrderLine", - "parent": "LowCodeDemo.Orders.Order", - "properties": [ - { - "name": "ProductId", - "foreignKey": { "entityName": "LowCodeDemo.Products.Product" } - }, - { "name": "Quantity", "type": "int" }, - { "name": "Amount", "type": "decimal" } - ] + "createFormName": "campaign-form", + "editFormName": "campaign-form" } ] } @@ -406,17 +567,22 @@ See [Custom Endpoints](custom-endpoints.md) for details. ## Migration Requirements -When you modify `model.json`, you need database migrations for the changes to take effect: +Entity shape changes require database migrations before they can be used safely: -* **New entity**: `dotnet ef migrations add Added_{EntityName}` -* **New property**: `dotnet ef migrations add Added_{PropertyName}_To_{EntityName}` -* **Type change**: `dotnet ef migrations add Changed_{PropertyName}_In_{EntityName}` +* New entity +* New persisted property +* Property type change +* Required/nullability change +* Unique index change -> The same migration requirement applies when using [C# Attributes](fluent-api.md). Any change to entity structure requires an EF Core migration. +In ABP Studio, run the generated migration task for the solution. If you run the application from the command line, use the migration workflow generated by the startup template. ## See Also +* [Low-Code Designer](designer.md) +* [React Runtime](react-runtime.md) * [Attributes & Fluent API](fluent-api.md) * [Interceptors](interceptors.md) * [Custom Endpoints](custom-endpoints.md) +* [Script Actions](script-actions.md) * [Scripting API](scripting-api.md) diff --git a/docs/en/low-code/react-runtime.md b/docs/en/low-code/react-runtime.md new file mode 100644 index 00000000000..01e0e37ad9d --- /dev/null +++ b/docs/en/low-code/react-runtime.md @@ -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": "" + } +} +``` + +## 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 + + + +``` + +## 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/ +/dynamic//create +/dynamic//edit/ +/dynamic// +``` + +`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 + +![Generated React data grid](images/runtime-data-grid.png) + +## 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. + +![Generated create form](images/runtime-create-form.png) + +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. + +![Generated advanced filters](images/runtime-filters.png) + +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. + +![Has value options](images/runtime-filters-has-value.png) + +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`. diff --git a/docs/en/low-code/reference-entities.md b/docs/en/low-code/reference-entities.md index b937175c7a4..3e6475f258b 100644 --- a/docs/en/low-code/reference-entities.md +++ b/docs/en/low-code/reference-entities.md @@ -1,19 +1,23 @@ ```json //[doc-seo] { - "Description": "Link dynamic entities to existing C# entities like IdentityUser using Reference Entities in the ABP Low-Code System." + "Description": "Link dynamic entities to existing .NET entities like IdentityUser using Reference Entities in the ABP Low-Code System." } ``` # Reference Entities +> **Preview:** Reference entity metadata is part of the preview Low-Code System. Registration APIs, relation options, and designer behavior may change before general availability. + +Use the [Low-Code Designer](designer.md) to select reference entities after they are registered in application startup. This page explains the registration and metadata details behind that designer experience. + Reference Entities allow you to create foreign key relationships from **dynamic entities** to **existing C# entities** that live outside the Low-Code System. ## Dynamic Entities vs Reference Entities | | Dynamic Entities | Reference Entities | |---|-----------------|-------------------| -| **Definition** | Defined via `[DynamicEntity]` attribute or `model.json` | Existing C# classes (e.g., `IdentityUser`, `Tenant`) | +| **Definition** | Defined via `[DynamicEntity]` attribute or JSON descriptor files | Existing C# classes (e.g., `IdentityUser`, `Tenant`) | | **CRUD Operations** | Full CRUD (Create, Read, Update, Delete) | **Read-only** — no create/update/delete | | **UI Pages** | Auto-generated pages with data grids and forms | No UI pages | | **Permissions** | Auto-generated permissions | No permissions | @@ -22,7 +26,7 @@ Reference Entities allow you to create foreign key relationships from **dynamic ## Overview -Dynamic entities defined via [Attributes](fluent-api.md) or [model.json](model-json.md) can reference **other dynamic entities** using foreign keys. However, you may also need to link to entities that exist **outside** the Low-Code System — such as ABP's `IdentityUser`, `Tenant`, or your own C# entity classes. +Dynamic entities defined via [Attributes](fluent-api.md) or [JSON descriptor files](model-json.md) can reference **other dynamic entities** using foreign keys. However, you may also need to link to entities that exist **outside** the Low-Code System — such as ABP's `IdentityUser`, `Tenant`, or your own C# entity classes. **Reference entities** make this possible by exposing existing entities for: @@ -34,7 +38,7 @@ Dynamic entities defined via [Attributes](fluent-api.md) or [model.json](model-j ## Registering Reference Entities -Register reference entities in your [Low-Code Initializer](index.md#1-create-a-low-code-initializer) using `AbpDynamicEntityConfig.ReferencedEntityList`: +Register reference entities in startup configuration using `AbpDynamicEntityConfig.ReferencedEntityList`: ````csharp public static async Task InitializeAsync() @@ -76,7 +80,7 @@ public void Add( > The entity type must implement `IEntity`. -## Using Reference Entities in model.json +## Using Reference Entities in JSON Descriptors Reference a registered entity in a foreign key definition: @@ -142,6 +146,6 @@ if (user) { ## See Also -* [model.json Structure](model-json.md) +* [Model Descriptor Files](model-json.md) * [Foreign Access](foreign-access.md) * [Attributes & Fluent API](fluent-api.md) diff --git a/docs/en/low-code/script-actions.md b/docs/en/low-code/script-actions.md new file mode 100644 index 00000000000..197d59ed37f --- /dev/null +++ b/docs/en/low-code/script-actions.md @@ -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) diff --git a/docs/en/low-code/scripting-api.md b/docs/en/low-code/scripting-api.md index a08b1107eca..dd5da7c0670 100644 --- a/docs/en/low-code/scripting-api.md +++ b/docs/en/low-code/scripting-api.md @@ -7,7 +7,38 @@ # Scripting API -The Low-Code System provides a server-side JavaScript scripting engine for executing custom business logic within [interceptors](interceptors.md) and [custom endpoints](custom-endpoints.md). Scripts run in a sandboxed environment with access to a database API backed by EF Core. +> **Preview:** The Low-Code scripting API is a preview server-side JavaScript surface. Available globals, helper methods, limits, and sandbox behavior may change before general availability. + +The designer and React runtime cover the standard CRUD, form, filter, and export workflows. Use the scripting API when an interceptor, action, or custom endpoint needs server-side JavaScript. + +The Low-Code System provides a server-side JavaScript scripting engine for executing custom business logic within [interceptors](interceptors.md), [custom endpoints](custom-endpoints.md), event handlers, background jobs, and background workers. Scripts run in a sandboxed environment with access to a database API backed by EF Core. + +Scripts are wrapped in an async function, so `await` and top-level `return` are supported. + +## Designer Code Editor + +JavaScript fields in the Low-Code Designer use a code editor with syntax highlighting and low-code-aware autocomplete. The editor is available for interceptors, custom endpoints, event handlers, background jobs, and background workers. + +The **Available context** list shows the globals enabled for the current script type. The list is based on the scripting capability profile, so an application can disable services such as HTTP, email, files, or background jobs for a specific script type. + +Autocomplete covers: + +* Common globals such as `db`, `currentUser`, `emailSender`, `http`, `events`, and `jobs` +* Endpoint, event handler, background job, background worker, and interceptor context variables +* Dynamic entity names in `db.query(...)`, `db.get(...)`, file, image, and attachment helpers +* Dynamic entity properties inside query lambda parameters and query results +* Enum names and values through `enums` and `enumValues` +* File and image field selectors through `fileFields` and `imageFields` + +`fileFields` is intentionally limited to `File` properties and `imageFields` is limited to `Image` properties. They are safe selector trees for file and image helpers, not lists of every entity property. + +```javascript +await files.save(fileFields.Acme.Campaigns.Campaign.Document, { + fileName: 'brief.pdf', + contentType: 'application/pdf', + base64: base64Content +}); +``` ## Unified Database API (`db`) @@ -19,9 +50,11 @@ The `db` object is the main entry point for all data operations. * **Database-Level Execution** — all operations (filters, aggregations, joins, set operations) translate to SQL via EF Core and Dynamic LINQ. * **No in-memory processing** of large datasets. +> `db.query(entityName)` is asynchronous. Always `await` it before chaining query methods, and `await` the terminal operation such as `toList()`, `count()`, `first()`, or `sum()`. + ```javascript // Immutable pattern — each call creates a new builder -var baseQuery = db.query('Entity').where(x => x.Active); +var baseQuery = (await db.query('Entity')).where(x => x.Active); var cheap = baseQuery.where(x => x.Price < 100); // baseQuery unchanged var expensive = baseQuery.where(x => x.Price > 500); // baseQuery unchanged ``` @@ -31,13 +64,15 @@ var expensive = baseQuery.where(x => x.Price > 500); // baseQuery unchanged ### Basic Queries ```javascript -var products = await db.query('LowCodeDemo.Products.Product') +var productQuery = await db.query('LowCodeDemo.Products.Product'); +var products = await productQuery .where(x => x.Price > 100) .orderBy(x => x.Price) .take(10) .toList(); -var result = await db.query('LowCodeDemo.Products.Product') +var filteredProductQuery = await db.query('LowCodeDemo.Products.Product'); +var result = await filteredProductQuery .where(x => x.Price > 100 && x.Price < 500) .where(x => x.StockCount > 0) .orderByDescending(x => x.Price) @@ -92,16 +127,19 @@ var minPrice = 100; var config = { minStock: 10 }; var nested = { range: { min: 50, max: 200 } }; -var result = await db.query('Entity').where(x => x.Price > minPrice).toList(); -var result2 = await db.query('Entity').where(x => x.StockCount > config.minStock).toList(); -var result3 = await db.query('Entity').where(x => x.Price >= nested.range.min).toList(); +var query = await db.query('Entity'); + +var result = await query.where(x => x.Price > minPrice).toList(); +var result2 = await query.where(x => x.StockCount > config.minStock).toList(); +var result3 = await query.where(x => x.Price >= nested.range.min).toList(); ``` ### Contains / IN Operator ```javascript var targetPrices = [50, 100, 200]; -var products = await db.query('Entity') +var query = await db.query('Entity'); +var products = await query .where(x => targetPrices.includes(x.Price)) .toList(); ``` @@ -109,7 +147,8 @@ var products = await db.query('Entity') ### Select Projection ```javascript -var projected = await db.query('LowCodeDemo.Products.Product') +var productQuery = await db.query('LowCodeDemo.Products.Product'); +var projected = await productQuery .where(x => x.Price > 0) .select(x => ({ ProductName: x.Name, ProductPrice: x.Price })) .toList(); @@ -120,7 +159,8 @@ var projected = await db.query('LowCodeDemo.Products.Product') ### Explicit Joins ```javascript -var orderLines = await db.query('LowCodeDemo.Orders.OrderLine') +var orderLineQuery = await db.query('LowCodeDemo.Orders.OrderLine'); +var orderLines = await orderLineQuery .join('LowCodeDemo.Products.Product', 'p', (ol, p) => ol.ProductId === p.Id) .take(10) .toList(); @@ -135,7 +175,8 @@ orderLines.forEach(line => { ### Left Join ```javascript -var orders = await db.query('LowCodeDemo.Orders.Order') +var orderQuery = await db.query('LowCodeDemo.Orders.Order'); +var orders = await orderQuery .leftJoin('LowCodeDemo.Products.Product', 'p', (o, p) => o.CustomerId === p.Id) .toList(); @@ -149,7 +190,8 @@ orders.forEach(order => { ### LINQ-Style Join ```javascript -db.query('Order') +var orderQuery = await db.query('Order'); +orderQuery .join('LowCodeDemo.Products.Product', o => o.ProductId, p => p.Id) @@ -158,9 +200,11 @@ db.query('Order') ### Join with Filtered Query ```javascript -var expensiveProducts = db.query('Product').where(p => p.Price > 100); +var productQuery = await db.query('Product'); +var expensiveProducts = productQuery.where(p => p.Price > 100); -var orders = await db.query('OrderLine') +var orderLineQuery = await db.query('OrderLine'); +var orders = await orderLineQuery .join(expensiveProducts, ol => ol.ProductId, p => p.Id) @@ -179,8 +223,9 @@ Set operations execute at the database level using SQL: | `except(query)` | `EXCEPT` | Elements in first, not second | ```javascript -var cheap = db.query('Product').where(x => x.Price <= 100); -var popular = db.query('Product').where(x => x.Rating > 4); +var productQuery = await db.query('Product'); +var cheap = productQuery.where(x => x.Price <= 100); +var popular = productQuery.where(x => x.Rating > 4); var bestDeals = await cheap.intersect(popular).toList(); var underrated = await cheap.except(popular).toList(); @@ -197,18 +242,21 @@ All aggregations execute as SQL statements: | `min(x => x.Property)` | `SELECT MIN(...)` | `Promise` | | `max(x => x.Property)` | `SELECT MAX(...)` | `Promise` | | `distinct(x => x.Property)` | `SELECT DISTINCT ...` | `Promise` | -| `groupBy(x => x.Property)` | `GROUP BY ...` | `Promise` | +| `groupBy(x => x.Property)` | `GROUP BY ...` | `QueryBuilder` | ```javascript -var totalValue = await db.query('Product').sum(x => x.Price); -var avgPrice = await db.query('Product').where(x => x.InStock).average(x => x.Price); -var cheapest = await db.query('Product').min(x => x.Price); +var productQuery = await db.query('Product'); + +var totalValue = await productQuery.sum(x => x.Price); +var avgPrice = await productQuery.where(x => x.InStock).average(x => x.Price); +var cheapest = await productQuery.min(x => x.Price); ``` ### GroupBy with Select ```javascript -var grouped = await db.query('Product') +var productQuery = await db.query('Product'); +var grouped = await productQuery .groupBy(x => x.Category) .select(g => ({ Category: g.Key, @@ -237,7 +285,8 @@ var grouped = await db.query('Product') ### GroupBy with Items ```javascript -var grouped = await db.query('Product') +var productQuery = await db.query('Product'); +var grouped = await productQuery .groupBy(x => x.Category) .select(g => ({ Category: g.Key, @@ -251,18 +300,19 @@ var grouped = await db.query('Product') | Limit | Default | Description | |-------|---------|-------------| -| `MaxGroupCount` | No limit | Maximum groups | +| `MaxGroupCount` | 500 | Maximum groups; set to `null` to disable this limit | ## Math Functions Math functions translate to SQL functions (ROUND, FLOOR, CEILING, ABS, etc.): ```javascript -var products = await db.query('Product') +var productQuery = await db.query('Product'); +var products = await productQuery .where(x => Math.round(x.Price) > 100) .toList(); -var result = await db.query('Product') +var result = await productQuery .where(x => Math.abs(x.Balance) < 10 && Math.floor(x.Rating) >= 4) .toList(); ``` @@ -274,7 +324,9 @@ Direct CRUD methods on the `db` object: | Method | Description | Returns | |--------|-------------|---------| | `db.get(entityName, id)` | Get by ID | `Promise` | +| `db.getList(entityName, take?)` | Get a list with an optional limit | `Promise` | | `db.getCount(entityName)` | Get count | `Promise` | +| `db.count(entityName)` | Alias for `db.getCount(entityName)` | `Promise` | | `db.exists(entityName)` | Check if any records exist | `Promise` | | `db.insert(entityName, entity)` | Insert new | `Promise` | | `db.update(entityName, entity)` | Update existing | `Promise` | @@ -304,47 +356,300 @@ var updated = await db.update('LowCodeDemo.Products.Product', { await db.delete('LowCodeDemo.Products.Product', id); ``` -## Context Object +## Script Context + +Scripts receive a `context` object and common global shortcuts. Available services can be enabled or disabled per script type with capability profiles. + +### Common Services + +| Global | Context property | Description | +|--------|------------------|-------------| +| `db` | `context.db` | Query and CRUD API | +| `user`, `currentUser` | `context.currentUser` | Current user information and claims | +| `tenant`, `currentTenant` | `context.currentTenant` | Current tenant information | +| `email`, `emailSender` | `context.emailSender` | Email send and queue helpers | +| `config` | `context.config` | Filtered application configuration reader | +| `http` | `context.http` | Hardened outbound HTTP client | +| `auth`, `authorization` | `context.authorization` | Permission checks | +| `settings` | `context.settings` | Filtered setting provider | +| `features` | `context.features` | Feature checks | +| `events` | `context.events` | Distributed event publishing | +| `jobs` | `context.jobs` | Dynamic background job enqueueing | +| `encryption` | `context.encryption` | String encryption and decryption | +| `textTemplating` | `context.textTemplating` | ABP text template rendering | +| `blob` | `context.blob` | Base64 blob storage wrapper | +| `files` | `context.files` | Low-code file field helper | +| `images` | `context.images` | Low-code image field helper | +| `attachments` | `context.attachments` | Record attachment helper | +| `fileFields` | `context.fileFields` | File field selector tree | +| `imageFields` | `context.imageFields` | Image field selector tree | +| `enums`, `enumValues` | enum registry | Low-code enum value registry | +| `log`, `logWarning`, `logError` | logging methods | Script logging | + +Global helpers are also available: + +| Helper | Description | +|--------|-------------| +| `guid()` | Generates a GUID string | +| `userFriendlyError(message)` | Throws a `UserFriendlyException` | +| `businessError(message, code?)` | Throws a `BusinessException` | + +### Interceptor Context + +Interceptors add `args` and `commandArgs`: + +| Property / Method | Description | +|-------------------|-------------| +| `commandArgs.data` | Entity data dictionary for create/update | +| `commandArgs.entityId` | Entity ID for update/delete | +| `commandArgs.commandName` | `Create`, `Update`, or `Delete` | +| `commandArgs.entityName` | Full entity name | +| `commandArgs.getValue(name)` | Get a property value | +| `commandArgs.setValue(name, value)` | Set a property value | +| `commandArgs.hasValue(name)` | Check whether the input contains a property | +| `commandArgs.removeValue(name)` | Remove a property from the input | + +Set `globalError` to abort an operation with a user-facing error: + +```javascript +if (!args.getValue('Name')) { + globalError = 'Name is required.'; +} +``` + +### Custom Endpoint Context + +Custom endpoints add request globals and response helpers. See [Custom Endpoints](custom-endpoints.md) for details. + +| Variable | Description | +|----------|-------------| +| `request` | Full request object | +| `route`, `params` | Route values | +| `query` | Query string values | +| `body` | Request body | +| `headers` | Selected safe request headers | + +### Event, Job, and Worker Context + +| Script type | Additional globals | +|-------------|--------------------| +| Event handler | `handler`, `event`, `eventName`, `eventData` | +| Background job | `job`, `jobName`, `jobData`, `jobJsonData` | +| Background worker | `worker`, `workerName` | + +Event handlers, background jobs, and background workers are configured in the Designer **Actions** section or in JSON descriptor files. See [Script Actions](script-actions.md) for descriptors, examples, and dry-run testing. + +Event handler example: + +```javascript +log('Received event ' + eventName); + +if (eventData && eventData.campaignId) { + await jobs.enqueueAsync('SendCampaignSummary', { + campaignId: eventData.campaignId + }); +} +``` + +Background job example: + +```javascript +var campaign = await db.get('Acme.Campaigns.Campaign', jobData.campaignId); +if (!campaign) { + userFriendlyError('Campaign not found.'); +} + +await email.queueAsync(jobData.to, 'Campaign summary', campaign.Name); +``` + +Background worker example: + +```javascript +var campaignQuery = await db.query('Acme.Campaigns.Campaign'); +var staleCount = await campaignQuery + .where(campaign => campaign.Status === 0) + .count(); + +log('Stale draft campaigns: ' + staleCount); +``` + +## Service Helpers + +### HTTP + +The `http` helper supports outbound requests with timeout, response-size, host, and HTTPS policy checks. + +| Method | Description | +|--------|-------------| +| `http.getAsync(url, options?)` | GET | +| `http.postAsync(url, body?, options?)` | POST | +| `http.putAsync(url, body?, options?)` | PUT | +| `http.patchAsync(url, body?, options?)` | PATCH | +| `http.deleteAsync(url, options?)` | DELETE | +| `http.requestAsync(method, url, bodyOrOptions?, options?)` | Custom method | + +Options include `headers`, `query`, `timeoutMs`, `contentType`, and `responseType` (`json`, `text`, or `base64`). + +### Authorization, Settings, Features, and Config + +```javascript +if (await auth.isGrantedAsync('Acme.Campaigns.Create')) { + var enabled = await features.isEnabledAsync('Acme.Campaigns'); + var threshold = await settings.getIntAsync('Acme.Campaigns.Threshold', 10); + var baseUrl = config.get('ExternalApi:BaseUrl'); +} +``` + +### Events and Background Jobs + +```javascript +await events.publishAsync('Acme.Campaigns.CampaignCompleted', { id: campaignId }); + +await jobs.enqueueAsync('SendCampaignSummary', { campaignId: campaignId }, { + priority: 'Normal', + delayMs: 60000 +}); +``` + +### Files, Images, and Attachments + +The file helpers use low-code page services so permissions, file validation, linked-blob checks, and foreign access stay consistent with the runtime. + +| Helper | Purpose | +|--------|---------| +| `files.parse(value)` | Parse a stored file value | +| `files.format(value, includeSize?)` | Format a file display value | +| `files.save(...)` / `images.save(...)` | Save file or image content | +| `files.get(...)` / `images.get(...)` | Read file or image content | +| `files.upload(entityName, fieldName, fileInput, options?)` | Upload field content | +| `attachments.list(...)` | List record attachments | +| `attachments.upload(...)` / `attachments.save(...)` | Upload a record attachment | +| `attachments.get(...)` / `attachments.download(...)` | Download a record attachment | +| `attachments.delete(...)` | Delete a record attachment | + +File content is passed as base64 data. File operations are subject to configured read/write size limits. + +Use `fileFields` and `imageFields` when you want typed selectors for file or image properties: + +```javascript +await files.save(fileFields.Acme.Campaigns.Campaign.Document, { + fileName: 'brief.pdf', + contentType: 'application/pdf', + base64: base64Content +}); + +var content = await images.get( + imageFields.Acme.Campaigns.Campaign.BannerImage, + campaignId +); +``` + +The selector path is based on the full entity name and the `File` or `Image` property name. Record-level attachments are entity-level, not property-level, so they use the `attachments` helper instead of field selectors. + +### Email -Available in [interceptors](interceptors.md): +The `email` and `emailSender` globals use the configured ABP `IEmailSender`. -| Property | Type | Description | -|----------|------|-------------| -| `context.commandArgs` | object | Command arguments (data, entityId, commandName, entityName) | -| `context.commandArgs.getValue(name)` | function | Get property value | -| `context.commandArgs.setValue(name, value)` | function | Set property value | -| `context.commandArgs.hasValue(name)` | function | Check if a property exists | -| `context.commandArgs.removeValue(name)` | function | Remove a property value | -| `context.currentUser` | object | Current user info (see [Interceptors](interceptors.md) for full list) | -| `context.emailSender` | object | Email sending (`sendAsync`, `sendHtmlAsync`) | -| `context.log(msg)` | function | Log an informational message | -| `context.logWarning(msg)` | function | Log a warning message | -| `context.logError(msg)` | function | Log an error message | +| Method | Description | +|--------|-------------| +| `email.sendAsync(to, subject, body)` | Send plain text email | +| `email.sendAsync(from, to, subject, body)` | Send plain text email with explicit sender | +| `email.sendHtmlAsync(to, subject, htmlBody)` | Send HTML email | +| `email.sendHtmlAsync(from, to, subject, htmlBody)` | Send HTML email with explicit sender | +| `email.queueAsync(to, subject, body)` | Queue plain text email | +| `email.queueAsync(from, to, subject, body)` | Queue plain text email with explicit sender | +| `email.queueHtmlAsync(to, subject, htmlBody)` | Queue HTML email | +| `email.queueHtmlAsync(from, to, subject, htmlBody)` | Queue HTML email with explicit sender | + +Email operations validate the recipient address, apply allowed or blocked domain rules when configured, and enforce the per-execution email limit. + +```javascript +if (email.isAvailable) { + await email.queueAsync( + 'ops@example.com', + 'Campaign completed', + 'Campaign ' + campaignId + ' completed.' + ); +} +``` + +### Test JavaScript Dry Run + +The Designer can run JavaScript without saving it where the **Test JavaScript** panel is available. The built-in dry-run panel supports custom endpoints, interceptors, event handlers, background jobs, and background workers. + +Dry-run execution returns the endpoint response or script status, logs, captured side effects, duration, and error diagnostics. + +| Operation | Dry-run behavior | +|-----------|------------------| +| Database writes | Executed in a transaction and rolled back | +| File, image, and 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 | Resolved from configured HTTP mocks; no real HTTP request is sent | +| Logs | Returned in the result | +| Errors | Returned with type, message, and diagnostics when available | + +For endpoint dry runs, the request method, path, route values, query values, headers, and body are supplied by the test panel. Endpoint authentication and permission metadata are checked against the current user. For interceptor dry runs, the test panel supplies command metadata and command data. For event handler dry runs, it supplies `eventData`. For background job and worker dry runs, it supplies the job or worker input JSON. ## Configuration -You can configure scripting limits using `AbpLowCodeScriptingOptions` in your module's `ConfigureServices` method: +Configure scripting limits with the `LowCode:Scripting` configuration section or `AbpLowCodeScriptingOptions`. ```csharp Configure(options => { - // Script execution limits (null = no limit) options.Script.Timeout = TimeSpan.FromMinutes(1); options.Script.MaxStatements = 100_000; - options.Script.MaxMemoryBytes = 512 * 1024 * 1024; // 512 MB - options.Script.MaxRecursionDepth = 500; + options.Script.MaxMemoryBytes = 128 * 1024 * 1024; + options.Script.MaxRecursionDepth = 64; - // Query API limits (null = no limit) options.Query.MaxLimit = 10_000; options.Query.DefaultLimit = 1000; options.Query.MaxExpressionNodes = 200; - options.Query.MaxExpressionDepth = 20; + options.Query.MaxExpressionDepth = 10; options.Query.MaxArraySize = 500; options.Query.MaxGroupCount = 500; + + options.Capabilities.Endpoint.EnableHttp = false; }); ``` -All limits default to `null` (no limit). Configure them based on your security requirements and expected workload. +Most numeric limits can be set to `null` to explicitly disable that limit. Keep the defaults for untrusted or tenant-authored scripts. + +### Capability Profiles + +The same services are not required in every script type. Capability profiles let you disable services per execution type: + +```json +{ + "LowCode": { + "Scripting": { + "Capabilities": { + "Interception": { + "EnableDb": true, + "EnableHttp": false + }, + "Endpoint": { + "EnableDb": true, + "EnableHttp": true + }, + "EventHandler": { + "EnableDb": true + }, + "BackgroundJob": { + "EnableDb": true + }, + "BackgroundWorker": { + "EnableDb": true + } + } + } + } +} +``` + +Each profile supports flags such as `EnableDb`, `EnableCurrentUser`, `EnableCurrentTenant`, `EnableEmail`, `EnableConfig`, `EnableHttp`, `EnableAuthorization`, `EnableSettings`, `EnableFeatures`, `EnableEvents`, `EnableBackgroundJobs`, `EnableEncryption`, `EnableTextTemplating`, `EnableBlob`, and `EnableFiles`. ## Security @@ -352,22 +657,38 @@ All limits default to `null` (no limit). Configure them based on your security r | Constraint | Default | Configurable | |------------|---------|--------------| -| Script Timeout | No limit | Yes | -| Max Statements | No limit | Yes | -| Memory Limit | No limit | Yes | -| Recursion Depth | No limit | Yes | +| Script Timeout | 30 seconds | Yes | +| Max Statements | 100,000 | Yes | +| Memory Limit | 128 MB | Yes | +| Recursion Depth | 64 | Yes | +| Max Script Length | 500,000 characters | Yes | | CLR Access | Disabled | No | ### Query Security Limits | Limit | Default | Description | |-------|---------|-------------| -| MaxExpressionNodes | No limit | Max AST nodes per expression | -| MaxExpressionDepth | No limit | Max nesting depth | -| MaxLimit (take) | No limit | Max records per query | -| DefaultLimit | No limit | Default if `take()` not specified | -| MaxArraySize (includes) | No limit | Max array size for IN operations | -| MaxGroupCount | No limit | Max groups in GroupBy | +| MaxExpressionNodes | 200 | Max AST nodes per expression | +| MaxExpressionDepth | 10 | Max nesting depth | +| MaxLimit (take) | 10,000 | Max records per query | +| DefaultLimit | 1,000 | Default if `take()` is not specified | +| MaxArraySize (includes) | 500 | Max array size for IN operations | +| MaxGroupCount | 500 | Max groups in GroupBy | + +### Integration Limits + +| Area | Default | +|------|---------| +| HTTP timeout | 30 seconds | +| HTTP response size | 5 MB | +| HTTP requests per execution | 50 | +| HTTP blocked hosts | `localhost` and private IP ranges | +| Email sends per execution | 5 | +| Blob read/write size | 10 MB read, 5 MB write | +| Low-code file read/write size | 10 MB read, 5 MB write | +| Event publishes per execution | 10 | +| Background jobs per execution | 10 | +| Endpoint response body | 1 MB | ### Property Whitelist @@ -380,7 +701,8 @@ All values are parameterized: ```javascript var malicious = "'; DROP TABLE Products;--"; // Safely treated as a literal string — no injection -var result = await db.query('Entity').where(x => x.Name.includes(malicious)).count(); +var query = await db.query('Entity'); +var result = await query.where(x => x.Name.includes(malicious)).count(); ``` ### Blocked Features @@ -395,9 +717,16 @@ if (!context.commandArgs.getValue('Email').includes('@')) { throw new Error('Valid email is required'); } +// User-friendly ABP exception +userFriendlyError('The campaign is not ready to publish.'); + +// Business exception with a code +businessError('Budget is exceeded.', 'Acme.Campaigns:BudgetExceeded'); + // Try-catch for safe execution try { - var products = await db.query('Entity').where(x => x.Price > 0).toList(); + var query = await db.query('Entity'); + var products = await query.where(x => x.Price > 0).toList(); } catch (error) { context.log('Query failed: ' + error.message); } @@ -422,7 +751,8 @@ try { var productId = context.commandArgs.getValue('ProductId'); var quantity = context.commandArgs.getValue('Quantity'); -var product = await db.query('LowCodeDemo.Products.Product') +var productQuery = await db.query('LowCodeDemo.Products.Product'); +var product = await productQuery .where(x => x.Id === productId) .first(); @@ -435,10 +765,12 @@ context.commandArgs.setValue('TotalAmount', product.Price * quantity); ### Sales Dashboard (Custom Endpoint) ```javascript -var totalOrders = await db.query('LowCodeDemo.Orders.Order').count(); -var delivered = await db.query('LowCodeDemo.Orders.Order') +var orderQuery = await db.query('LowCodeDemo.Orders.Order'); + +var totalOrders = await orderQuery.count(); +var delivered = await orderQuery .where(x => x.IsDelivered === true).count(); -var revenue = await db.query('LowCodeDemo.Orders.Order') +var revenue = await orderQuery .where(x => x.IsDelivered === true).sum(x => x.TotalAmount); return ok({ @@ -452,4 +784,5 @@ return ok({ * [Interceptors](interceptors.md) * [Custom Endpoints](custom-endpoints.md) -* [model.json Structure](model-json.md) +* [Script Actions](script-actions.md) +* [Model Descriptor Files](model-json.md) diff --git a/modules/cms-kit/angular/package.json b/modules/cms-kit/angular/package.json index fd9963dc47c..4a198eb5097 100644 --- a/modules/cms-kit/angular/package.json +++ b/modules/cms-kit/angular/package.json @@ -18,8 +18,8 @@ "@abp/ng.account": "~10.5.0-rc.2", "@abp/ng.identity": "~10.5.0-rc.2", "@abp/ng.setting-management": "~10.5.0-rc.2", - "@abp/ng.tenant-management": "~10.5.0-rc.1", - "@abp/ng.theme.basic": "~10.5.0-rc.1", + "@abp/ng.tenant-management": "~10.5.0-rc.2", + "@abp/ng.theme.basic": "~10.5.0-rc.2", "@angular/animations": "~10.0.0", "@angular/common": "~10.0.0", "@angular/compiler": "~10.0.0", diff --git a/schemas/low-code/definitions/command-interceptor-descriptor.schema.json b/schemas/low-code/definitions/command-interceptor-descriptor.schema.json new file mode 100644 index 00000000000..ee1fc6d49ae --- /dev/null +++ b/schemas/low-code/definitions/command-interceptor-descriptor.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "command-interceptor-descriptor.schema.json", + "title": "CommandInterceptorDescriptor", + "description": "Describes a JavaScript interceptor for an entity Create, Update, or Delete command.", + "markdownDescription": "AI guidance: use `type: \"Pre\"` to validate or block before persistence and `type: \"Post\"` for side effects after persistence. Scripts can inspect `context.commandArgs` and may use services exposed on `context` such as `db`, `currentUser`, `currentTenant`, `emailSender`, `config`, `http`, and logging helpers depending on host configuration. To block the command with a user-facing error, assign `globalError = \"message\"` and return. Keep scripts idempotent where possible.", + "type": "object", + "properties": { + "commandName": { + "type": "string", + "description": "Entity command to intercept: Create, Update, or Delete.", + "enum": ["Create", "Update", "Delete"] + }, + "type": { + "$ref": "interceptor-type.schema.json", + "description": "Whether the script runs before or after the command." + }, + "javascript": { + "type": "string", + "description": "JavaScript code to execute. For Pre interceptors, set globalError to block the command. Example: if (!context.commandArgs.data['Name']) { globalError = 'Name is required.'; }" + } + }, + "required": ["commandName", "type", "javascript"], + "additionalProperties": false, + "examples": [ + { + "commandName": "Create", + "type": "Pre", + "javascript": "if (!context.commandArgs.data['Name']) { globalError = 'Name is required.'; }" + }, + { + "commandName": "Delete", + "type": "Pre", + "javascript": "var record = await db.get('Acme.Events.Event', context.commandArgs.entityId); if (record && record.Status === 2) { globalError = 'Completed events cannot be deleted.'; }" + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-aggregation-type.schema.json b/schemas/low-code/definitions/dashboard-aggregation-type.schema.json new file mode 100644 index 00000000000..0de9d6da48c --- /dev/null +++ b/schemas/low-code/definitions/dashboard-aggregation-type.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-aggregation-type.schema.json", + "title": "DashboardAggregationType", + "description": "The type of aggregation to perform. Prefer lower-case values in 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." + ] +} diff --git a/schemas/low-code/definitions/dashboard-chart-descriptor.schema.json b/schemas/low-code/definitions/dashboard-chart-descriptor.schema.json new file mode 100644 index 00000000000..ade648db0fa --- /dev/null +++ b/schemas/low-code/definitions/dashboard-chart-descriptor.schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-chart-descriptor.schema.json", + "title": "DashboardChartDescriptor", + "description": "Configuration for a chart visualization.", + "markdownDescription": "AI guidance: use chart visualizations for grouped aggregations. `xAxis.property` is the grouping property. `yAxis` contains one or more aggregations. Use `count` without a property; use `sum`, `average`, `min`, or `max` with a numeric/date property. Use `dateGrouping` when xAxis is date/datetime.", + "type": "object", + "properties": { + "chartType": { + "$ref": "dashboard-chart-type.schema.json", + "description": "Chart renderer type: bar, line, pie, or donut." + }, + "xAxis": { + "type": "object", + "properties": { + "property": { + "type": "string", + "description": "Property name to group by on the X-axis. Must exist on the visualization entity." + }, + "useForeignDisplay": { + "type": "boolean", + "description": "Show the FK display property instead of the raw id when xAxis.property is a foreign key.", + "default": false + }, + "dateGrouping": { + "type": ["string", "null"], + "enum": ["", "day", "week", "month", "quarter", "year", null], + "description": "Grouping interval for date/datetime properties. Omit or use empty string for no date grouping." + } + }, + "required": ["property"], + "additionalProperties": false + }, + "yAxis": { + "type": "array", + "items": { + "type": "object", + "properties": { + "aggregation": { + "$ref": "dashboard-aggregation-type.schema.json" + }, + "property": { + "type": ["string", "null"], + "description": "Property name for sum/average/min/max/percent aggregations. Omit for count." + }, + "label": { + "type": ["string", "null"], + "description": "Display label for this series. Omit to derive from aggregation/property." + }, + "color": { + "type": ["string", "null"] + } + }, + "required": ["aggregation"], + "additionalProperties": false + }, + "minItems": 1 + }, + "barOrientation": { + "type": "string", + "enum": ["vertical", "horizontal"], + "default": "vertical" + }, + "showRecordCount": { + "type": "boolean", + "default": false + }, + "maxItems": { + "type": "integer", + "description": "Maximum number of grouped items to display in the chart.", + "default": 10, + "minimum": 1, + "maximum": 50 + } + }, + "required": ["chartType", "xAxis", "yAxis"], + "additionalProperties": false, + "examples": [ + { + "chartType": "bar", + "xAxis": { "property": "Status" }, + "yAxis": [{ "aggregation": "count", "label": "Events" }], + "maxItems": 10 + }, + { + "chartType": "line", + "xAxis": { "property": "StartDate", "dateGrouping": "month" }, + "yAxis": [{ "aggregation": "sum", "property": "Budget", "label": "Budget" }] + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-chart-type.schema.json b/schemas/low-code/definitions/dashboard-chart-type.schema.json new file mode 100644 index 00000000000..fe3b2124e5a --- /dev/null +++ b/schemas/low-code/definitions/dashboard-chart-type.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-chart-type.schema.json", + "title": "DashboardChartType", + "description": "The type of chart. Prefer lower-case values in 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." + ] +} diff --git a/schemas/low-code/definitions/dashboard-descriptor.schema.json b/schemas/low-code/definitions/dashboard-descriptor.schema.json new file mode 100644 index 00000000000..068031a18b6 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-descriptor.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-descriptor.schema.json", + "title": "DashboardDescriptor", + "description": "Describes a dashboard page configuration with global filters and rows of visualizations.", + "markdownDescription": "AI guidance: a dashboard belongs to a page with `type: \"dashboard\"`. It contains one or more rows; each row contains chart, list, or numberContainer visualizations. Use visualization `entityName` to select data for chart/list items. Use numberContainer for KPI tiles, charts for grouped aggregations, and lists for recent/top records.", + "type": "object", + "properties": { + "description": { + "type": ["string", "null"], + "description": "Optional description text shown below the dashboard title." + }, + "globalFilters": { + "type": "array", + "description": "Global filters that affect visualizations, for example a date range. Visualizations can map the global date filter through globalDateFilterProperty.", + "items": { + "$ref": "dashboard-global-filter-descriptor.schema.json" + } + }, + "rows": { + "type": "array", + "description": "Dashboard rows. Each row contains one or more visualization items; width 2 items usually take the full row.", + "items": { + "$ref": "dashboard-row-descriptor.schema.json" + }, + "minItems": 1 + } + }, + "required": ["rows"], + "additionalProperties": false, + "examples": [ + { + "description": "Operational overview", + "rows": [ + { + "items": [ + { + "name": "events-by-status", + "type": "chart", + "title": "Events by Status", + "entityName": "Acme.Events.Event", + "chart": { + "chartType": "bar", + "xAxis": { "property": "Status" }, + "yAxis": [{ "aggregation": "count", "label": "Events" }] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-filter-descriptor.schema.json b/schemas/low-code/definitions/dashboard-filter-descriptor.schema.json new file mode 100644 index 00000000000..93cb76e821a --- /dev/null +++ b/schemas/low-code/definitions/dashboard-filter-descriptor.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-filter-descriptor.schema.json", + "title": "DashboardFilterDescriptor", + "description": "Static filter applied to a dashboard visualization or KPI item.", + "markdownDescription": "AI guidance: use dashboard filters to constrain the data behind a visualization, for example only active records or only records above a threshold. Each condition property must exist on the visualization/item entity. Combine conditions with `operator: \"and\"` or `operator: \"or\"`.", + "type": "object", + "properties": { + "operator": { + "type": "string", + "enum": ["and", "or"], + "default": "and" + }, + "conditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "property": { + "type": "string", + "description": "Property name to filter on. Must exist on the visualization/item entityName." + }, + "filterType": { + "type": "string", + "enum": ["equal", "notEqual", "contains", "greaterThan", "lessThan", "isNull", "isNotNull"], + "default": "equal" + }, + "value": { + "description": "Filter value. Shape depends on filterType and target property type." + } + }, + "required": ["property", "filterType"], + "additionalProperties": false + } + } + }, + "required": ["conditions"], + "additionalProperties": false, + "examples": [ + { + "operator": "and", + "conditions": [ + { "property": "Status", "filterType": "equal", "value": 1 } + ] + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-global-filter-descriptor.schema.json b/schemas/low-code/definitions/dashboard-global-filter-descriptor.schema.json new file mode 100644 index 00000000000..ab1d3f79a68 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-global-filter-descriptor.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-global-filter-descriptor.schema.json", + "title": "DashboardGlobalFilterDescriptor", + "description": "Describes a dashboard-level filter control that can affect all visualizations on the dashboard.", + "markdownDescription": "AI guidance: use global filters for dashboard-wide controls, not for per-visualization conditions. The current supported type is `dateRange`; visualization filters can then reference date-like entity properties through their own `filter` or `userFilters` settings.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["dateRange"], + "default": "dateRange", + "description": "Global filter control type. Currently only 'dateRange' is supported." + } + }, + "required": ["type"], + "additionalProperties": false, + "examples": [ + { "type": "dateRange" } + ] +} diff --git a/schemas/low-code/definitions/dashboard-list-descriptor.schema.json b/schemas/low-code/definitions/dashboard-list-descriptor.schema.json new file mode 100644 index 00000000000..b7340c41183 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-list-descriptor.schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-list-descriptor.schema.json", + "title": "DashboardListDescriptor", + "description": "Configuration for a dashboard list/table visualization.", + "markdownDescription": "AI guidance: use list visualizations for recent records, top records, or compact operational queues. `fields` must contain property names from the parent visualization entityName. Use `sortBy` to make results deterministic.", + "type": "object", + "properties": { + "fields": { + "type": "array", + "items": { "type": "string" }, + "description": "Property names to display as columns. Each value must reference a property on the visualization entityName." + }, + "sortBy": { + "type": "object", + "properties": { + "property": { + "type": "string", + "description": "Property name to sort by. Must exist on the visualization entityName." + }, + "direction": { + "type": "string", + "enum": ["asc", "desc"], + "default": "asc" + } + }, + "required": ["property"], + "additionalProperties": false + }, + "maxRows": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 10 + }, + "rowHeight": { + "type": "string", + "enum": ["compact", "normal", "tall"], + "default": "compact" + }, + "colorBy": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["property", "conditions"] + }, + "property": { + "type": "string", + "description": "Enum/status property name for automatic coloring." + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": ["fields"], + "additionalProperties": false, + "examples": [ + { + "fields": ["Title", "StartDate", "Status"], + "sortBy": { "property": "StartDate", "direction": "desc" }, + "maxRows": 10, + "rowHeight": "compact", + "colorBy": { "type": "property", "property": "Status" } + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-number-container-descriptor.schema.json b/schemas/low-code/definitions/dashboard-number-container-descriptor.schema.json new file mode 100644 index 00000000000..5cac8372a9a --- /dev/null +++ b/schemas/low-code/definitions/dashboard-number-container-descriptor.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-number-container-descriptor.schema.json", + "title": "DashboardNumberContainerDescriptor", + "description": "A container that holds multiple number/KPI items, each with its own entity and aggregation.", + "markdownDescription": "AI guidance: use numberContainer for KPI tiles. Each item chooses its own entity, aggregation, optional filter, format, and color.", + "type": "object", + "properties": { + "items": { + "type": "array", + "description": "Number/KPI items within this container. Use 1-4 items for a readable dashboard row.", + "items": { + "$ref": "dashboard-number-item-descriptor.schema.json" + } + } + }, + "required": ["items"], + "additionalProperties": false, + "examples": [ + { + "items": [ + { "name": "total-events", "title": "Total Events", "entityName": "Acme.Events.Event", "aggregation": "count", "format": "number" } + ] + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-number-item-descriptor.schema.json b/schemas/low-code/definitions/dashboard-number-item-descriptor.schema.json new file mode 100644 index 00000000000..eb98b09ac41 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-number-item-descriptor.schema.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-number-item-descriptor.schema.json", + "title": "DashboardNumberItemDescriptor", + "description": "A single number/KPI tile within a number container.", + "markdownDescription": "AI guidance: `count` does not need aggregationProperty. `sum`, `average`, `min`, `max`, `percentFilled`, and `percentEmpty` should specify aggregationProperty. Use filter to scope the KPI, for example count only active records.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Unique identifier for this number item. Prefer kebab-case such as 'total-events'.", + "minLength": 1 + }, + "title": { + "type": "string", + "description": "Display title for this KPI." + }, + "entityName": { + "type": "string", + "description": "Entity this number item sources data from. Must match an entity descriptor or known reference entity.", + "minLength": 1 + }, + "aggregation": { + "$ref": "dashboard-aggregation-type.schema.json", + "description": "Aggregation used to compute the KPI value." + }, + "aggregationProperty": { + "type": "string", + "description": "Property name for sum/average/min/max/percent aggregations. Omit for count." + }, + "format": { + "type": "string", + "enum": ["number", "currency", "percentage"], + "default": "number" + }, + "color": { + "type": "string", + "description": "Display color name (blue, green, red, purple, orange, indigo, amber, teal)" + }, + "globalDateFilterProperty": { + "type": "string", + "description": "Date/DateTime property used to apply dashboard global date filters. Defaults to CreationTime when omitted." + }, + "filter": { + "oneOf": [ + { "$ref": "dashboard-filter-descriptor.schema.json" }, + { "type": "null" } + ] + }, + "clickToSeeRecords": { + "type": "boolean", + "description": "Allow users to click to see underlying records when supported by the runtime.", + "default": false + } + }, + "required": ["name", "title", "entityName", "aggregation"], + "additionalProperties": false, + "examples": [ + { + "name": "total-events", + "title": "Total Events", + "entityName": "Acme.Events.Event", + "aggregation": "count", + "format": "number", + "color": "blue" + }, + { + "name": "total-budget", + "title": "Total Budget", + "entityName": "Acme.Events.Event", + "aggregation": "sum", + "aggregationProperty": "Budget", + "format": "currency", + "color": "green" + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-row-descriptor.schema.json b/schemas/low-code/definitions/dashboard-row-descriptor.schema.json new file mode 100644 index 00000000000..909048ec295 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-row-descriptor.schema.json @@ -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" } + ] + } + } + ] + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-visualization-descriptor.schema.json b/schemas/low-code/definitions/dashboard-visualization-descriptor.schema.json new file mode 100644 index 00000000000..6fd6e91f31d --- /dev/null +++ b/schemas/low-code/definitions/dashboard-visualization-descriptor.schema.json @@ -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 + } + } + ] +} diff --git a/schemas/low-code/definitions/dashboard-visualization-type.schema.json b/schemas/low-code/definitions/dashboard-visualization-type.schema.json new file mode 100644 index 00000000000..0f490d6cdf9 --- /dev/null +++ b/schemas/low-code/definitions/dashboard-visualization-type.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "dashboard-visualization-type.schema.json", + "title": "DashboardVisualizationType", + "description": "The type of dashboard visualization. Prefer lower-case values in 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." + ] +} diff --git a/schemas/low-code/definitions/endpoint-descriptor.schema.json b/schemas/low-code/definitions/endpoint-descriptor.schema.json new file mode 100644 index 00000000000..cdbfaa18fb5 --- /dev/null +++ b/schemas/low-code/definitions/endpoint-descriptor.schema.json @@ -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);" + } + ] +} diff --git a/schemas/low-code/definitions/entity-attachment-descriptor.schema.json b/schemas/low-code/definitions/entity-attachment-descriptor.schema.json new file mode 100644 index 00000000000..bf1be2fad43 --- /dev/null +++ b/schemas/low-code/definitions/entity-attachment-descriptor.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "entity-attachment-descriptor.schema.json", + "title": "EntityAttachmentDescriptor", + "description": "Configures record-level attachments for an entity.", + "markdownDescription": "AI guidance: use entity attachments when records need a collection of arbitrary files. Use File/Image entity properties instead when the file is a named business field such as `ContractPdf` or `CoverImage`.", + "type": "object", + "properties": { + "isEnabled": { + "type": "boolean", + "description": "When true, records of this entity can have attachments." + }, + "maxFileCount": { + "type": "integer", + "description": "Maximum number of files allowed per entity record.", + "minimum": 1 + }, + "maxFileSizeBytes": { + "type": "integer", + "description": "Maximum allowed size in bytes for a single attachment.", + "minimum": 1 + }, + "maxTotalSizeBytes": { + "type": "integer", + "description": "Maximum total attachment size in bytes per entity record.", + "minimum": 1 + }, + "allowedContentTypes": { + "type": "array", + "description": "Allowed MIME content types, wildcard MIME patterns, or file extensions. Examples: 'image/*', 'application/pdf', '.docx'.", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["isEnabled"], + "additionalProperties": false, + "examples": [ + { + "isEnabled": true, + "maxFileCount": 5, + "maxFileSizeBytes": 5242880, + "allowedContentTypes": ["image/*", "application/pdf"] + } + ] +} diff --git a/schemas/low-code/definitions/entity-cross-field-validation-descriptor.schema.json b/schemas/low-code/definitions/entity-cross-field-validation-descriptor.schema.json new file mode 100644 index 00000000000..5fff4ea5792 --- /dev/null +++ b/schemas/low-code/definitions/entity-cross-field-validation-descriptor.schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "entity-cross-field-validation-descriptor.schema.json", + "title": "EntityCrossFieldValidationDescriptor", + "description": "Describes an entity-level validation rule that compares one property against another property on the same entity.", + "markdownDescription": "AI guidance: use cross-field validations for rules such as EndDate > StartDate, Min <= Max, PasswordRepeat == Password, or PublishedOn >= CreatedOn. `propertyName` receives the validation error; `otherPropertyName` is the comparison target. Both must exist on the entity.", + "type": "object", + "properties": { + "propertyName": { + "type": "string", + "description": "Property that receives the validation error when the rule fails. Must exist on the entity.", + "minLength": 1 + }, + "operator": { + "type": "string", + "description": "Comparison operator", + "enum": [ + "equals", + "notEquals", + "greaterThan", + "greaterThanOrEqual", + "lessThan", + "lessThanOrEqual" + ] + }, + "otherPropertyName": { + "type": "string", + "description": "Property to compare against. Must exist on the same entity.", + "minLength": 1 + }, + "message": { + "type": "string", + "description": "Optional custom validation message" + } + }, + "required": ["propertyName", "operator", "otherPropertyName"], + "additionalProperties": false, + "examples": [ + { + "propertyName": "EndDate", + "operator": "greaterThan", + "otherPropertyName": "StartDate", + "message": "End Date must be greater than Start Date." + }, + { + "propertyName": "PasswordRepeat", + "operator": "equals", + "otherPropertyName": "Password", + "message": "Password repeat must match Password." + } + ] +} diff --git a/schemas/low-code/definitions/entity-descriptor.schema.json b/schemas/low-code/definitions/entity-descriptor.schema.json new file mode 100644 index 00000000000..175966db3a2 --- /dev/null +++ b/schemas/low-code/definitions/entity-descriptor.schema.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "entity-descriptor.schema.json", + "title": "EntityDescriptor", + "description": "Describes a dynamic entity. An entity is the runtime data model: it defines a table-like aggregate, its properties, validations, parent-child relation, attachment support, and create/update/delete interceptors.", + "markdownDescription": "AI guidance: create one entity per aggregate/root concept. Use a stable namespace-style `name` such as `Acme.Crm.Customer`; this name is referenced by pages, forms, foreign keys, dashboards, scripts, and permissions. Put user-facing labels in `displayName`; choose `displayProperty` as a short string property used by lookups. Use `parent` only for child/detail entities that belong to a parent record.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Stable full name of the entity, usually Namespace.Module.EntityName (for example 'Acme.Crm.Customer'). Must be unique across all model layers. Do not rename after data exists unless a migration/rename flow is intended.", + "minLength": 1 + }, + "displayName": { + "type": "string", + "description": "Default plural/screen label for this entity (for example 'Customers'). Page menu titles are configured separately on page descriptors.", + "minLength": 1 + }, + "displayProperty": { + "type": "string", + "description": "Property name used when another entity references this entity in a lookup/autocomplete. Prefer a required string property such as 'Name', 'Title', or 'Code'." + }, + "parent": { + "type": "string", + "description": "Full name of the parent entity for parent-child/detail entities. When set, records of this entity are scoped under the parent and should include an FK property to the parent.", + "minLength": 1 + }, + "attachments": { + "$ref": "entity-attachment-descriptor.schema.json", + "description": "Record-level attachment settings. Use this for multiple arbitrary files attached to a record; use File/Image properties for first-class file fields." + }, + "properties": { + "type": "array", + "description": "Entity properties. Omit framework audit/id fields such as Id, CreationTime, CreatorId, LastModificationTime, IsDeleted unless intentionally overriding metadata; the runtime supplies standard fields.", + "items": { + "$ref": "entity-property-descriptor.schema.json" + } + }, + "crossFieldValidations": { + "type": "array", + "description": "Entity-level validation rules that compare two properties from the same entity. Use for date ranges, matching password fields, numeric min/max pairs, and other cross-field rules.", + "items": { + "$ref": "entity-cross-field-validation-descriptor.schema.json" + } + }, + "interceptors": { + "type": "array", + "description": "Create/Update/Delete command interceptors. Use Pre interceptors to validate, normalize, or block a command; use Post interceptors for side effects after persistence.", + "items": { + "$ref": "command-interceptor-descriptor.schema.json" + } + } + }, + "required": ["name"], + "additionalProperties": false, + "examples": [ + { + "name": "Acme.Crm.Customer", + "displayName": "Customers", + "displayProperty": "Name", + "properties": [ + { "name": "Name", "type": "string", "isRequired": true }, + { "name": "Email", "type": "string", "validators": [{ "type": "email" }] } + ] + } + ] +} diff --git a/schemas/low-code/definitions/entity-property-descriptor.schema.json b/schemas/low-code/definitions/entity-property-descriptor.schema.json new file mode 100644 index 00000000000..ed4cc8da061 --- /dev/null +++ b/schemas/low-code/definitions/entity-property-descriptor.schema.json @@ -0,0 +1,101 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "entity-property-descriptor.schema.json", + "title": "EntityPropertyDescriptor", + "description": "Describes one dynamic entity property. Properties define persisted data fields, enum fields, foreign keys, upload fields, server-only fields, defaults, uniqueness, and validators.", + "markdownDescription": "AI guidance: use PascalCase property names. Use `type` for primitive fields, `type: \"enum\"` with `enumType` for enum fields, and a `foreignKey` object for lookup fields. FK property names should normally end with `Id`. Keep sensitive values `serverOnly: true`. Do not add legacy UI configuration here; page columns/filters and form fields own UI behavior.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Stable PascalCase property name, unique within the entity. Examples: 'Name', 'EmailAddress', 'CustomerId', 'StartDate'.", + "minLength": 1 + }, + "type": { + "$ref": "entity-property-type.schema.json", + "description": "Primitive or special property type. If omitted, runtime treats the property as string. Use 'enum' only with enumType; use 'file'/'image' for first-class upload fields." + }, + "displayName": { + "type": "string", + "description": "Default display label for this property. Page columns and form fields can override it. Omit when the label can be derived from the property name.", + "minLength": 1 + }, + "enumType": { + "type": "string", + "description": "Name of a JSON-defined enum from top-level enums or a full type name for code enums. Required when type is 'enum'." + }, + "allowSetByClients": { + "type": "boolean", + "description": "Controls whether create/update clients may set the property. Use false for server-computed or protected fields that can still be returned to clients." + }, + "serverOnly": { + "type": "boolean", + "description": "When true, this property is completely hidden from clients, API responses, and UI definitions. Use for secrets, internal notes, hashes, or backend-only workflow state." + }, + "isMappedToDbField": { + "type": "boolean", + "description": "Whether this property is mapped to a database column. Keep true or omit for normal persisted fields. Use false for computed/transient fields supplied by scripts or backend logic." + }, + "defaultValue": { + "type": ["string", "null"], + "description": "Default value for new records. Stored as a string in 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 'NameId', for example 'CustomerId'." + }, + "fileMaxSizeBytes": { + "type": "integer", + "description": "Maximum allowed file size in bytes for File and Image properties. Example: 5242880 for 5 MiB.", + "minimum": 1 + }, + "fileAllowedContentTypes": { + "type": "array", + "description": "Allowed MIME content types or wildcard patterns for File and Image properties. Examples: 'image/*', 'application/pdf'.", + "items": { + "type": "string", + "minLength": 1 + } + }, + "imageMaxWidth": { + "type": "integer", + "description": "Optional maximum image width in pixels.", + "minimum": 1 + }, + "imageMaxHeight": { + "type": "integer", + "description": "Optional maximum image height in pixels.", + "minimum": 1 + }, + "imageResizeMode": { + "type": "string", + "description": "How uploaded images should be resized when image dimensions are configured. 'fit' preserves the full image inside the bounds; 'fill' crops/fills the target bounds.", + "enum": ["fit", "fill"] + }, + "validators": { + "type": "array", + "description": "Backend/UI validators for this property. Use required, length, range, pattern, email, phone, url, or creditCard as appropriate. Duplicate required can be omitted when isRequired is true unless a custom message is needed.", + "items": { + "$ref": "validator-descriptor.schema.json" + } + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "examples": [ + { "name": "Name", "type": "string", "isRequired": true, "validators": [{ "type": "maxLength", "length": 128 }] }, + { "name": "Status", "type": "enum", "enumType": "Acme.Events.EventStatus", "defaultValue": "0" }, + { "name": "CustomerId", "foreignKey": { "entityName": "Acme.Crm.Customer", "displayPropertyName": "Name" } }, + { "name": "CoverImage", "type": "image", "fileAllowedContentTypes": ["image/*"], "fileMaxSizeBytes": 5242880, "imageResizeMode": "fit" } + ] +} diff --git a/schemas/low-code/definitions/entity-property-type.schema.json b/schemas/low-code/definitions/entity-property-type.schema.json new file mode 100644 index 00000000000..89d9018446a --- /dev/null +++ b/schemas/low-code/definitions/entity-property-type.schema.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "entity-property-type.schema.json", + "title": "EntityPropertyType", + "description": "Data type of an entity property. Use canonical lowercase values for new descriptors and MCP mutations; compatibility aliases are accepted for older descriptors.", + "markdownDescription": "Use canonical lowercase values in MCP/write payloads: `string`, `int`, `long`, `decimal`, `datetime`, `boolean`, `guid`, `enum`, `date`, `time`, `file`, `image`, or `money`. `string` stores text; `int`/`long` store whole numbers; `decimal` and `money` store numeric decimals; `datetime`, `date`, and `time` model temporal values; `boolean` stores true/false; `guid` stores GUID values; `enum` requires `enumType`; `file` and `image` store upload metadata/content through the low-code file pipeline. Legacy aliases such as `String`, `Int`, `dateTime`, and `DateTime` remain accepted by the schema for compatibility, but new payloads should not use .NET aliases like `Int32`.", + "type": "string", + "enum": [ + "string", + "String", + "int", + "Int", + "long", + "Long", + "decimal", + "Decimal", + "datetime", + "dateTime", + "DateTime", + "boolean", + "Boolean", + "guid", + "Guid", + "enum", + "Enum", + "date", + "Date", + "time", + "Time", + "file", + "File", + "image", + "Image", + "money", + "Money" + ], + "enumDescriptions": [ + "Text/string value.", + "Text/string value.", + "32-bit whole number.", + "32-bit whole number.", + "64-bit whole number.", + "64-bit whole number.", + "Decimal numeric value.", + "Decimal numeric value.", + "Date and time value.", + "Date and time value.", + "Date and time value.", + "Boolean true/false value.", + "Boolean true/false value.", + "GUID/UUID value.", + "GUID/UUID value.", + "Integer-backed enum value; set enumType.", + "Integer-backed enum value; set enumType.", + "Date-only value.", + "Date-only value.", + "Time-only value.", + "Time-only value.", + "Uploaded file field.", + "Uploaded file field.", + "Uploaded image field with optional dimension constraints.", + "Uploaded image field with optional dimension constraints.", + "Money amount rendered with money-aware controls.", + "Money amount rendered with money-aware controls." + ] +} diff --git a/schemas/low-code/definitions/enum-descriptor.schema.json b/schemas/low-code/definitions/enum-descriptor.schema.json new file mode 100644 index 00000000000..4a70d5c8f01 --- /dev/null +++ b/schemas/low-code/definitions/enum-descriptor.schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "enum-descriptor.schema.json", + "title": "EnumDescriptor", + "description": "Describes an integer-backed enum definition for use in entity properties and select/kanban UI.", + "markdownDescription": "AI guidance: define enums before entity properties that reference them. Use a stable namespace-style `name` such as `Acme.Events.EventStatus`. Each value name should be PascalCase and unique within the enum. Use explicit integer `value`s for stable persistence; do not reorder or renumber after data exists.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Stable unique enum name. Use a namespace-style name if the enum belongs to a module/domain, for example 'Acme.Events.EventStatus'.", + "minLength": 1 + }, + "values": { + "type": "array", + "description": "Ordered list of integer-backed enum values. Values are stored as integers; names are used for display and model readability.", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "PascalCase enum value name, for example 'Draft', 'Scheduled', or 'Completed'." + }, + "value": { + "type": "integer", + "description": "Stable integer value. Prefer explicit values starting at 0 so generated 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 } + ] + } + ] +} diff --git a/schemas/low-code/definitions/foreign-key-descriptor.schema.json b/schemas/low-code/definitions/foreign-key-descriptor.schema.json new file mode 100644 index 00000000000..358bdee4ee4 --- /dev/null +++ b/schemas/low-code/definitions/foreign-key-descriptor.schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "foreign-key-descriptor.schema.json", + "title": "ForeignKeyDescriptor", + "description": "Describes a foreign key/lookup relationship from the owning entity property to another entity or reference entity.", + "markdownDescription": "AI guidance: put `foreignKey` on the property that stores the related record id. The property name should usually end with `Id`, for example `CustomerId`. `entityName` must match a model entity name or a registered code/reference entity such as `Volo.Abp.Identity.IdentityUser`. Use `displayPropertyName` when the target display field is not obvious. Use `dependsOn` for cascading dropdowns such as City filtered by Country. Use `access` only for reverse access from the referenced entity side: `none`, `view`, or `edit`. Do not use `lookup` as `access`; lookup is a form field/control type.", + "type": "object", + "properties": { + "entityName": { + "type": "string", + "description": "Full name of the related entity or registered reference entity. Must match an entity descriptor name or a known code/reference entity.", + "minLength": 1 + }, + "displayPropertyName": { + "type": "string", + "description": "Property name to display from the related entity in lookups/autocomplete. Omit to use the target entity displayProperty.", + "minLength": 1 + }, + "access": { + "type": "string", + "description": "Access level for managing this relation from the referenced entity side. 'none' means no reverse access; 'view' allows the referenced entity page to show related records; 'edit' allows managing related records from the referenced side.", + "enum": ["none", "None", "view", "View", "edit", "Edit"], + "default": "none" + }, + "dependsOn": { + "type": "object", + "description": "Cascading dependency: filter this FK lookup by the value of another FK property on the same owning entity. Example: CityId depends on CountryId and filters City.CountryId.", + "properties": { + "propertyName": { + "type": "string", + "description": "Property name on the owning entity whose value provides the filter (for example 'CountryId' on an Address entity).", + "minLength": 1 + }, + "filterPropertyName": { + "type": "string", + "description": "Property name on the target lookup entity to filter by (for example 'CountryId' on City).", + "minLength": 1 + } + }, + "required": ["propertyName", "filterPropertyName"], + "additionalProperties": false + } + }, + "required": ["entityName"], + "additionalProperties": false, + "examples": [ + { + "entityName": "Acme.Crm.Customer", + "displayPropertyName": "Name" + }, + { + "entityName": "Acme.Geo.City", + "displayPropertyName": "Name", + "dependsOn": { + "propertyName": "CountryId", + "filterPropertyName": "CountryId" + } + } + ] +} diff --git a/schemas/low-code/definitions/form-descriptor.schema.json b/schemas/low-code/definitions/form-descriptor.schema.json new file mode 100644 index 00000000000..f8bec6dfc8e --- /dev/null +++ b/schemas/low-code/definitions/form-descriptor.schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "form-descriptor.schema.json", + "title": "FormDescriptor", + "description": "Describes a named create/edit form definition bound to one entity.", + "markdownDescription": "AI guidance: a form is referenced by page `formName`, `createFormName`, or `editFormName`. Keep `entityName` aligned with the page entityName. Define all fields in `fields`, then place every visible field in `layout.tabs[].groups[].rows[].cells[]` by field id. Use form `rules` for conditional visibility/enabled state; use entity validators for core data validation.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Stable unique form identifier. Prefer kebab-case, for example 'customer-form' or 'event-form'. Pages reference this value.", + "minLength": 1 + }, + "entityName": { + "type": "string", + "description": "Full name of the entity this form is bound to. Must match the page entityName that uses this form.", + "minLength": 1 + }, + "enableSaveAndNew": { + "type": "boolean", + "description": "Whether the form should expose a Save and New action in addition to the standard save action. Useful for rapid data entry.", + "default": false + }, + "fields": { + "type": "array", + "description": "Flat list of all fields in this form. Field ids must be unique within the form; bound fields should point to properties on entityName.", + "items": { + "$ref": "form-field-descriptor.schema.json" + } + }, + "layout": { + "$ref": "form-layout-descriptor.schema.json", + "description": "Visual layout for the fields. Every layout cell fieldId must refer to a field in fields." + }, + "rules": { + "type": "array", + "description": "Conditional rules for field/group visibility, enabled state, and value setting. Use for simple client-side form behavior.", + "items": { + "$ref": "form-rule-descriptor.schema.json" + } + } + }, + "required": ["name", "entityName", "fields", "layout"], + "additionalProperties": false, + "examples": [ + { + "name": "event-form", + "entityName": "Acme.Events.Event", + "fields": [ + { "id": "title", "label": "Title", "type": "text", "binding": "Title" }, + { "id": "status", "label": "Status", "type": "select", "binding": "Status", "enumType": "Acme.Events.EventStatus" } + ], + "layout": { + "tabs": [ + { + "id": "main", + "title": "Main", + "isDefault": true, + "groups": [ + { + "id": "details", + "title": "Details", + "isDefault": true, + "rows": [ + { "cells": [{ "fieldId": "title", "colSpan": 4 }] }, + { "cells": [{ "fieldId": "status", "colSpan": 2 }] } + ] + } + ] + } + ] + } + } + ] +} diff --git a/schemas/low-code/definitions/form-field-descriptor.schema.json b/schemas/low-code/definitions/form-field-descriptor.schema.json new file mode 100644 index 00000000000..fd5f731e198 --- /dev/null +++ b/schemas/low-code/definitions/form-field-descriptor.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "form-field-descriptor.schema.json", + "title": "FormFieldDescriptor", + "description": "Describes a single field in a form. A field may be bound to an entity property or unbound for computed/display-only UI.", + "markdownDescription": "AI guidance: use a stable camelCase `id` for each field. For ordinary data entry, set `binding` to an entity property and choose a field `type` compatible with that property. For enum selects, set `type: \"select\"` and `enumType`. For FK lookups, use `type: \"lookup\"` and bind to the FK property. Put fields into layout cells by id.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this field within the form. Prefer camelCase, for example 'title' or 'customerId'. Layout cells and rules reference this id.", + "minLength": 1 + }, + "label": { + "type": "string", + "description": "Display label for the field.", + "minLength": 1 + }, + "type": { + "$ref": "form-field-type.schema.json", + "description": "Visual/input control type. Choose a type compatible with the bound entity property." + }, + "binding": { + "type": ["string", "null"], + "description": "Entity property name to bind to, or null/omitted for unbound fields. Supports dotted paths like 'Parent.Name' for related entity display where supported." + }, + "enumType": { + "type": "string", + "description": "Enum name for select fields bound to enum properties. Must match the entity property's enumType or a known code enum." + }, + "defaultValue": { + "description": "Default field value used by the UI when creating a new record. Prefer entity property defaultValue for persisted defaults." + }, + "placeholder": { + "type": "string", + "description": "Placeholder text for the input" + }, + "helpText": { + "type": "string", + "description": "Help text displayed below the field" + }, + "readOnly": { + "type": "boolean", + "description": "Whether the field is read-only in the form UI. This does not by itself protect backend writes; use allowSetByClients/serverOnly for security.", + "default": false + }, + "modeVisibility": { + "type": "string", + "enum": ["both", "Both", "createOnly", "CreateOnly", "editOnly", "EditOnly"], + "description": "Controls in which form mode the field is visible: both, createOnly, or editOnly.", + "default": "both" + }, + "validations": { + "type": "array", + "description": "Form-level validation rules that supplement entity-level validators. Use when validation is specific to this form.", + "items": { + "$ref": "validator-descriptor.schema.json" + } + } + }, + "required": ["id", "label", "type"], + "additionalProperties": false, + "examples": [ + { "id": "title", "label": "Title", "type": "text", "binding": "Title", "validations": [{ "type": "required" }] }, + { "id": "status", "label": "Status", "type": "select", "binding": "Status", "enumType": "Acme.Events.EventStatus" }, + { "id": "customerId", "label": "Customer", "type": "lookup", "binding": "CustomerId" } + ] +} diff --git a/schemas/low-code/definitions/form-field-type.schema.json b/schemas/low-code/definitions/form-field-type.schema.json new file mode 100644 index 00000000000..2b0c5630f2f --- /dev/null +++ b/schemas/low-code/definitions/form-field-type.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "form-field-type.schema.json", + "title": "FormFieldType", + "description": "Available form field control types. Use canonical lowercase values for new descriptors and MCP mutations; compatibility aliases are accepted for older descriptors.", + "markdownDescription": "Use canonical lowercase values in MCP/write payloads: `text`, `textarea`, `number`, `checkbox`, `date`, `datetime`, `select`, `lookup`, `guid`, `computed`, `time`, `file`, `image`, or `money`. `text`/`textarea` are for strings, `number` for numeric values, `checkbox` for boolean, `date`/`datetime`/`time` for temporal values, `select` for enum values, `lookup` for foreign keys, `guid` for GUID input/display, `computed` for unbound calculated/display fields, `file` and `image` for upload fields, and `money` for money/decimal amount input. There is no `email` field type; use `text` plus validations where needed. Legacy PascalCase aliases remain accepted by the schema for compatibility.", + "type": "string", + "enum": [ + "text", + "Text", + "textarea", + "Textarea", + "number", + "Number", + "checkbox", + "Checkbox", + "date", + "Date", + "datetime", + "DateTime", + "select", + "Select", + "lookup", + "Lookup", + "guid", + "Guid", + "computed", + "Computed", + "time", + "Time", + "file", + "File", + "image", + "Image", + "money", + "Money" + ], + "enumDescriptions": [ + "Single-line text input.", + "Single-line text input.", + "Multi-line text input.", + "Multi-line text input.", + "Numeric input.", + "Numeric input.", + "Boolean checkbox.", + "Boolean checkbox.", + "Date-only input.", + "Date-only input.", + "Date and time input.", + "Date and time input.", + "Enum/select input.", + "Enum/select input.", + "Foreign key lookup/autocomplete input.", + "Foreign key lookup/autocomplete input.", + "GUID input/display.", + "GUID input/display.", + "Unbound computed/display field.", + "Unbound computed/display field.", + "Time-only input.", + "Time-only input.", + "File upload field.", + "File upload field.", + "Image upload field.", + "Image upload field.", + "Money amount input.", + "Money amount input." + ] +} diff --git a/schemas/low-code/definitions/form-layout-descriptor.schema.json b/schemas/low-code/definitions/form-layout-descriptor.schema.json new file mode 100644 index 00000000000..abadb86e21c --- /dev/null +++ b/schemas/low-code/definitions/form-layout-descriptor.schema.json @@ -0,0 +1,128 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "form-layout-descriptor.schema.json", + "title": "FormLayoutDescriptor", + "description": "Describes the visual layout of a form as tabs, groups, rows, and field cells.", + "markdownDescription": "AI guidance: the layout is a tree: tabs -> groups -> rows -> cells. Each cell `fieldId` must reference a field from the form's `fields` array. Use `colSpan` 4 for full-width fields, 2+2 for two columns, or 1+1+1+1 for four compact controls. The total effective width in a row should not exceed 4.", + "type": "object", + "properties": { + "tabs": { + "type": "array", + "description": "Ordered list of tabs in the form. Use one default tab named 'main' for simple forms.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this tab. Prefer camelCase/kebab-case such as 'main' or 'advanced'.", + "minLength": 1 + }, + "title": { + "type": "string", + "description": "Display title for the tab", + "minLength": 1 + }, + "isDefault": { + "type": "boolean", + "description": "Whether this is the default tab. The designer uses the default tab as the safe target for orphaned fields.", + "default": false + }, + "groups": { + "type": "array", + "description": "Ordered list of groups within this tab. Use one default group for simple forms.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this group. Rules can target group ids.", + "minLength": 1 + }, + "title": { + "type": ["string", "null"], + "description": "Optional display title for the group. Use null or omit for an untitled group." + }, + "isDefault": { + "type": "boolean", + "description": "Whether this is the default group. The designer uses the default group as the safe target for orphaned fields.", + "default": false + }, + "rows": { + "type": "array", + "description": "Ordered list of layout rows; each row contains one or more cells placed side-by-side.", + "items": { + "type": "object", + "properties": { + "cells": { + "type": "array", + "description": "Fields placed side-by-side in this row. The sum of colSpan values should not exceed 4.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "fieldId": { + "type": "string", + "description": "Reference to a field id in the form's fields array. Every field shown in the layout needs a matching field descriptor.", + "minLength": 1 + }, + "colSpan": { + "type": "integer", + "description": "Number of grid columns this field spans from 1 to 4. Use 4 for full-width fields.", + "minimum": 1, + "maximum": 4, + "default": 4 + }, + "colStart": { + "type": ["integer", "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 }] } + ] + } + ] + } + ] + } + ] +} diff --git a/schemas/low-code/definitions/form-rule-descriptor.schema.json b/schemas/low-code/definitions/form-rule-descriptor.schema.json new file mode 100644 index 00000000000..f558b6bb1d0 --- /dev/null +++ b/schemas/low-code/definitions/form-rule-descriptor.schema.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "form-rule-descriptor.schema.json", + "title": "FormRuleDescriptor", + "description": "Describes a conditional form rule with one or more actions that execute when the condition is met.", + "markdownDescription": "AI guidance: use rules for simple client-side behavior such as hiding a group when a checkbox is false, disabling a field after a status is selected, or setting a default value. `condition.fieldId` and every action `targetId` must reference existing field/group ids. Rules are not security boundaries; enforce sensitive behavior with backend validation/interceptors too.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this rule. Prefer kebab-case or camelCase such as 'show-archive-reason'.", + "minLength": 1 + }, + "name": { + "type": "string", + "description": "Human-readable name for this rule, used by designers/documentation." + }, + "condition": { + "type": "object", + "description": "The condition that triggers this rule. For isEmpty/isNotEmpty, omit value.", + "properties": { + "fieldId": { + "type": "string", + "description": "The field whose value is evaluated. Must match a field id in the same form.", + "minLength": 1 + }, + "operator": { + "type": "string", + "enum": ["equals", "notEquals", "isEmpty", "isNotEmpty"], + "description": "Comparison operator. Use equals/notEquals with value; use isEmpty/isNotEmpty without value." + }, + "value": { + "description": "The value to compare against (not used for isEmpty/isNotEmpty)" + } + }, + "required": ["fieldId", "operator"], + "additionalProperties": false + }, + "actions": { + "type": "array", + "description": "Actions to perform when the condition is met. Actions are executed in order.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["hide", "show", "disable", "enable", "setValue"], + "description": "The action type. hide/show/disable/enable target fields or groups; setValue targets fields." + }, + "targetType": { + "type": "string", + "enum": ["field", "group"], + "description": "Whether the target is a field or a group." + }, + "targetId": { + "type": "string", + "description": "The id of the target field or group. Must exist in this form's fields or layout groups.", + "minLength": 1 + }, + "value": { + "description": "The value to set. Used only for setValue actions." + } + }, + "required": ["type", "targetType", "targetId"], + "additionalProperties": false + } + } + }, + "required": ["id", "condition", "actions"], + "additionalProperties": false, + "examples": [ + { + "id": "show-archive-reason", + "name": "Show archive reason", + "condition": { "fieldId": "status", "operator": "equals", "value": 3 }, + "actions": [ + { "type": "show", "targetType": "field", "targetId": "archiveReason" } + ] + } + ] +} diff --git a/schemas/low-code/definitions/interceptor-type.schema.json b/schemas/low-code/definitions/interceptor-type.schema.json new file mode 100644 index 00000000000..49ce6eca3a0 --- /dev/null +++ b/schemas/low-code/definitions/interceptor-type.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "interceptor-type.schema.json", + "title": "InterceptorType", + "description": "When an entity command interceptor runs. Prefer PascalCase values shown here.", + "markdownDescription": "`Pre` runs before the command is persisted and can block with `globalError`. `Post` runs after the command succeeds and is best for logging, notifications, publishing events, or enqueueing jobs. `Replace` is reserved for replacing command behavior where supported and should be avoided unless the host explicitly supports it.", + "type": "string", + "enum": [ + "Pre", + "pre", + "Post", + "post", + "Replace", + "replace" + ], + "enumDescriptions": [ + "Before persistence; can block with globalError.", + "Before persistence; can block with globalError.", + "After successful persistence; use for side effects.", + "After successful persistence; use for side effects.", + "Replace command behavior where supported.", + "Replace command behavior where supported." + ] +} diff --git a/schemas/low-code/definitions/page-column-descriptor.schema.json b/schemas/low-code/definitions/page-column-descriptor.schema.json new file mode 100644 index 00000000000..ebf0073e4af --- /dev/null +++ b/schemas/low-code/definitions/page-column-descriptor.schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "page-column-descriptor.schema.json", + "title": "PageColumnDescriptor", + "description": "Describes a property displayed by an entity page list, card, calendar event, or gallery item.", + "markdownDescription": "AI guidance: use page columns/card fields to control runtime page visibility. Each `propertyName` must match a property on the page entity. Keep this separate from entity property definitions so the same entity can have different views on different pages.", + "type": "object", + "properties": { + "propertyName": { + "type": "string", + "description": "Property displayed by this column/card field. Must match a property on the page entityName.", + "minLength": 1 + }, + "label": { + "type": "string", + "description": "Optional page-specific label override. Omit to use the property displayName/name.", + "minLength": 1 + }, + "order": { + "type": "integer", + "description": "Display order. Lower values appear first.", + "default": 0 + }, + "exportOrder": { + "type": "integer", + "description": "Optional export order. Lower values appear first in Excel, CSV, download-link columns, and file bundles. Omit to reuse display order." + }, + "width": { + "type": "string", + "description": "Optional CSS width for tabular columns, for example '160px', '12rem', or '20%'.", + "minLength": 1 + }, + "visible": { + "type": "boolean", + "description": "Whether the field is visible by default on this page.", + "default": true + }, + "exportable": { + "type": "boolean", + "description": "Whether the field can be exported from this page.", + "default": true + } + }, + "required": ["propertyName"], + "additionalProperties": false, + "examples": [ + { "propertyName": "Title", "label": "Title", "order": 0, "exportOrder": 0, "width": "240px", "visible": true }, + { "propertyName": "InternalNotes", "visible": false, "exportable": false } + ] +} diff --git a/schemas/low-code/definitions/page-descriptor.schema.json b/schemas/low-code/definitions/page-descriptor.schema.json new file mode 100644 index 00000000000..3b5c1a2fcd1 --- /dev/null +++ b/schemas/low-code/definitions/page-descriptor.schema.json @@ -0,0 +1,260 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "page-descriptor.schema.json", + "title": "PageDescriptor", + "description": "Describes a runtime UI page. Pages create menu items and choose how an entity or dashboard is rendered.", + "markdownDescription": "AI guidance: `name` is the stable route/menu key and should be URL-safe kebab-case (for example `customers` or `event-calendar`). `title` is user-facing. For entity pages, set `entityName` to an existing entity. Configure list/gallery/card visibility with `columns`, filtering with `filters`, and create/edit UI with form names. For `dashboard` pages, provide `dashboard` and omit entity-only fields unless intentionally supported by the runtime. Do not put page-only settings on entity properties.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Stable URL-safe page identifier. Prefer kebab-case, for example 'customers', 'event-calendar', or 'sales-dashboard'. This becomes part of the runtime route and is referenced by saveSuccessPageName.", + "minLength": 1 + }, + "title": { + "type": "string", + "description": "Display title for the menu item and page header.", + "minLength": 1 + }, + "icon": { + "type": "string", + "description": "FontAwesome icon class for the menu item, for example 'fa-solid fa-users'." + }, + "type": { + "$ref": "page-type.schema.json" + }, + "entityName": { + "type": "string", + "description": "Full name of the root entity this page displays. Required for dataGrid, kanban, calendar, gallery, and form pages. Usually omitted for dashboard pages.", + "minLength": 1 + }, + "groupByProperty": { + "type": "string", + "description": "Property name to group entities by. Required for kanban pages. Must reference a property on entityName, preferably an enum/status property with a small fixed set of values.", + "minLength": 1 + }, + "calendarStartProperty": { + "type": "string", + "description": "Date or DateTime property used as the start date for calendar pages. Required for calendar pages.", + "minLength": 1 + }, + "calendarEndProperty": { + "type": "string", + "description": "Optional Date or DateTime property used as the end date for calendar pages. Use when events can span a date range.", + "minLength": 1 + }, + "calendarTimeProperty": { + "type": "string", + "description": "Optional Time property used as the start time for calendar pages when the start date is date-only or time is stored separately.", + "minLength": 1 + }, + "calendarDurationProperty": { + "type": "string", + "description": "Optional numeric property used as duration in minutes for calendar pages. Use this instead of calendarEndProperty when records store duration.", + "minLength": 1 + }, + "galleryImageProperty": { + "type": "string", + "description": "Optional Image property used as the cover image for gallery pages. The property should have entity property type image.", + "minLength": 1 + }, + "defaultSortProperty": { + "type": "string", + "description": "Optional property used as the default sorting field when the client does not send explicit sorting. Must reference a property on entityName.", + "minLength": 1 + }, + "defaultSortDescending": { + "type": "boolean", + "description": "Whether the default sort property is sorted descending.", + "default": false + }, + "defaultFileExportMode": { + "type": "integer", + "enum": [0, 1, 2], + "description": "Default file export mode for file/image columns. 0 exports file names, 1 exports metadata columns, and 2 exports download-link columns." + }, + "allowFileBundleExport": { + "type": "boolean", + "description": "Whether file bundle export is available for this page.", + "default": true + }, + "columns": { + "type": "array", + "description": "Page-owned column/card field configuration. Use for dataGrid, kanban card fields, calendar event fields, and gallery card fields. Each propertyName must reference a property on entityName.", + "items": { + "$ref": "page-column-descriptor.schema.json" + } + }, + "filters": { + "type": "array", + "description": "Page-owned filter configuration for filterable page types. Each propertyName must reference a property on entityName. Use this instead of legacy property-level UI filter settings.", + "items": { + "$ref": "page-filter-descriptor.schema.json" + } + }, + "order": { + "type": "integer", + "description": "Menu sort order (lower values appear first)", + "default": 0 + }, + "formName": { + "type": "string", + "description": "Name of the form to render. Required when type is 'form'. Must match a form descriptor whose entityName matches this page entityName.", + "minLength": 1 + }, + "createFormName": { + "type": "string", + "description": "Name of the form to use when creating records from dataGrid, kanban, calendar, or gallery pages. Must match a form descriptor for the same entity." + }, + "editFormName": { + "type": "string", + "description": "Name of the form to use when editing records from dataGrid, kanban, calendar, or gallery pages. Must match a form descriptor for the same entity." + }, + "createFormDisplay": { + "type": "string", + "enum": ["modal", "Modal", "page", "Page"], + "description": "How to display the create form. Use 'modal' for quick CRUD; use 'page' for longer forms or when deep links are desired.", + "default": "modal" + }, + "editFormDisplay": { + "type": "string", + "enum": ["modal", "Modal", "page", "Page"], + "description": "How to display the edit form. Use 'modal' for quick CRUD; use 'page' for longer forms or when deep links are desired.", + "default": "modal" + }, + "saveSuccessNavigation": { + "type": "string", + "enum": ["stay", "page", "url"], + "description": "Navigation behavior after a standalone form page saves successfully. 'stay' keeps the user on the form page; 'page' navigates to saveSuccessPageName; 'url' navigates to saveSuccessUrl.", + "default": "stay" + }, + "saveSuccessPageName": { + "type": "string", + "description": "Existing page name to open after a form page saves successfully when saveSuccessNavigation is 'page'.", + "minLength": 1 + }, + "saveSuccessUrl": { + "type": "string", + "description": "URL or application path to open after a form page saves successfully when saveSuccessNavigation is 'url'.", + "minLength": 1 + }, + "dashboard": { + "$ref": "dashboard-descriptor.schema.json", + "description": "Dashboard layout and visualizations. Required only when type is dashboard." + }, + "group": { + "type": "string", + "description": "Name of the page group this page belongs to. Must match pageGroups[].name. Omit for root-level pages.", + "minLength": 1 + }, + "permissionConfig": { + "$ref": "page-permission-config.schema.json", + "description": "Optional page operation permission overrides. Omit operations to use generated defaults." + } + }, + "required": ["name", "title", "type"], + "allOf": [ + { + "if": { + "properties": { "type": { "enum": ["dataGrid", "DataGrid"] } } + }, + "then": { + "required": ["entityName"] + } + }, + { + "if": { + "properties": { "type": { "enum": ["kanban", "Kanban"] } } + }, + "then": { + "required": ["entityName", "groupByProperty"] + } + }, + { + "if": { + "properties": { "type": { "enum": ["calendar", "Calendar"] } } + }, + "then": { + "required": ["entityName", "calendarStartProperty"] + } + }, + { + "if": { + "properties": { "type": { "enum": ["gallery", "Gallery"] } } + }, + "then": { + "required": ["entityName"] + } + }, + { + "if": { + "properties": { "type": { "enum": ["form", "Form"] } } + }, + "then": { + "required": ["entityName", "formName"] + } + }, + { + "if": { + "properties": { "type": { "enum": ["dashboard", "Dashboard"] } } + }, + "then": { + "required": ["dashboard"] + } + } + ], + "additionalProperties": false, + "examples": [ + { + "name": "events", + "title": "Events", + "type": "dataGrid", + "entityName": "Acme.Events.Event", + "columns": [ + { "propertyName": "Title", "order": 0 }, + { "propertyName": "Status", "order": 1 } + ], + "filters": [ + { "propertyName": "Title", "control": "text", "defaultOperator": "contains" } + ], + "createFormName": "event-form", + "editFormName": "event-form" + }, + { + "name": "event-calendar", + "title": "Event Calendar", + "type": "calendar", + "entityName": "Acme.Events.Event", + "calendarStartProperty": "StartDate", + "calendarEndProperty": "EndDate" + }, + { + "name": "event-dashboard", + "title": "Event Dashboard", + "type": "dashboard", + "dashboard": { + "rows": [ + { + "items": [ + { + "name": "events-by-status", + "type": "chart", + "title": "Events by Status", + "entityName": "Acme.Events.Event", + "chart": { + "chartType": "bar", + "xAxis": { "property": "Status" }, + "yAxis": [{ "aggregation": "count", "label": "Events" }] + } + } + ] + } + ] + } + } + ] +} diff --git a/schemas/low-code/definitions/page-filter-descriptor.schema.json b/schemas/low-code/definitions/page-filter-descriptor.schema.json new file mode 100644 index 00000000000..ed63ba0653d --- /dev/null +++ b/schemas/low-code/definitions/page-filter-descriptor.schema.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "page-filter-descriptor.schema.json", + "title": "PageFilterDescriptor", + "description": "Describes a runtime filter control for an entity page.", + "markdownDescription": "AI guidance: each filter must point to a page entity property. Use `control: \"auto\"` unless a specific control is needed. Use text/contains for string search, range/between for numeric values, dateRange/timeRange for temporal values, select/multiSelect for enums, lookup for foreign keys, and exists for null checks. Do not use the legacy `operator` property; use `defaultOperator`.", + "type": "object", + "properties": { + "propertyName": { + "type": "string", + "description": "Property filtered by this control. Must match a property on the page entityName.", + "minLength": 1 + }, + "label": { + "type": "string", + "description": "Optional page-specific label override. Omit to use the property displayName/name.", + "minLength": 1 + }, + "order": { + "type": "integer", + "description": "Filter display order. Lower values appear first.", + "default": 0 + }, + "visible": { + "type": "boolean", + "description": "Whether the filter is shown at runtime", + "default": true + }, + "control": { + "type": "string", + "description": "Runtime control used by the filter. 'auto' selects based on property type; choose explicit controls for predictable generated UI.", + "enum": ["auto", "text", "range", "dateRange", "timeRange", "select", "multiSelect", "lookup", "exists"], + "default": "auto" + }, + "defaultOperator": { + "type": "string", + "description": "Default operator applied by this filter. Use 'contains' for text search, 'equal' for enum/FK exact matches, 'between' for ranges, and 'default' to let runtime choose.", + "enum": [ + "default", + "equal", + "notEqual", + "contains", + "notContains", + "startsWith", + "endsWith", + "greaterThan", + "greaterThanOrEqual", + "lessThan", + "lessThanOrEqual", + "between", + "hasValue", + "isNull", + "isNotNull", + "in", + "notIn" + ] + }, + "allowedOperators": { + "type": "array", + "description": "Operators the runtime filter UI allows users to choose. Omit to let the runtime choose based on property type/control.", + "items": { + "type": "string", + "enum": [ + "default", + "equal", + "notEqual", + "contains", + "notContains", + "startsWith", + "endsWith", + "greaterThan", + "greaterThanOrEqual", + "lessThan", + "lessThanOrEqual", + "between", + "hasValue", + "isNull", + "isNotNull", + "in", + "notIn" + ] + }, + "uniqueItems": true + }, + "defaultValue": { + "description": "Optional default filter value applied when the page first loads. Shape depends on control/operator." + }, + "placeholder": { + "type": "string", + "description": "Optional runtime input placeholder" + }, + "helpText": { + "type": "string", + "description": "Optional runtime help text" + }, + "allowMultipleValues": { + "type": "boolean", + "description": "Whether the filter can submit multiple values. Useful with multiSelect/in/notIn.", + "default": false + }, + "clearable": { + "type": "boolean", + "description": "Whether runtime users can clear the filter.", + "default": true + }, + "initiallyExpanded": { + "type": "boolean", + "description": "Whether the runtime filter panel opens expanded.", + "default": false + } + }, + "required": ["propertyName"], + "additionalProperties": false, + "examples": [ + { "propertyName": "Title", "control": "text", "defaultOperator": "contains", "placeholder": "Search title" }, + { "propertyName": "Status", "control": "select", "defaultOperator": "equal", "clearable": true }, + { "propertyName": "StartDate", "control": "dateRange", "defaultOperator": "between" } + ] +} diff --git a/schemas/low-code/definitions/page-group-descriptor.schema.json b/schemas/low-code/definitions/page-group-descriptor.schema.json new file mode 100644 index 00000000000..c142f45500a --- /dev/null +++ b/schemas/low-code/definitions/page-group-descriptor.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "page-group-descriptor.schema.json", + "title": "PageGroupDescriptor", + "description": "Describes a menu group/folder that can contain pages and sub-groups.", + "markdownDescription": "AI guidance: create page groups when several pages belong to the same feature area. Pages reference groups by `group`. Nested groups reference parent groups by `parent`. Use stable kebab-case names and concise user-facing titles.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Unique URL-safe identifier for the group. Prefer kebab-case, for example 'crm' or 'content-studio'.", + "minLength": 1 + }, + "title": { + "type": "string", + "description": "Display title for the menu group.", + "minLength": 1 + }, + "icon": { + "type": "string", + "description": "FontAwesome icon class, for example 'fa-solid fa-folder' or 'fa-solid fa-calendar-days'." + }, + "order": { + "type": "integer", + "description": "Sort order within the parent level (lower values appear first)", + "default": 0 + }, + "parent": { + "type": "string", + "description": "Name of the parent group for nesting. Must match another pageGroups[].name. Omit for root-level groups.", + "minLength": 1 + } + }, + "required": ["name", "title"], + "additionalProperties": false, + "examples": [ + { "name": "events", "title": "Events", "icon": "fa-solid fa-calendar-days", "order": 10 }, + { "name": "event-admin", "title": "Admin", "parent": "events", "order": 20 } + ] +} diff --git a/schemas/low-code/definitions/page-permission-config.schema.json b/schemas/low-code/definitions/page-permission-config.schema.json new file mode 100644 index 00000000000..7ae15314029 --- /dev/null +++ b/schemas/low-code/definitions/page-permission-config.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "page-permission-config.schema.json", + "title": "PagePermissionConfig", + "description": "Permission configuration for a page's operations. Only overridden values are stored; omitted values use auto-generated defaults based on the page.", + "markdownDescription": "AI guidance: each operation can be `default`, `public`, `authenticated`, or a permission name. Omit an operation to use runtime-generated defaults. Use `public` only for anonymous pages. Use `authenticated` for pages any logged-in user can view. Use explicit permission names for controlled business operations.", + "type": "object", + "properties": { + "view": { + "type": "string", + "description": "Permission for viewing the page. Can be a specific permission name, 'default', 'public', or 'authenticated'. Omit to use auto-generated default." + }, + "create": { + "type": "string", + "description": "Permission for create operation on this page. Can be a specific permission name, 'default', 'public', or 'authenticated'. Omit to use auto-generated default." + }, + "update": { + "type": "string", + "description": "Permission for update operation on this page. Can be a specific permission name, 'default', 'public', or 'authenticated'. Omit to use auto-generated default." + }, + "delete": { + "type": "string", + "description": "Permission for delete operation on this page. Can be a specific permission name, 'default', 'public', or 'authenticated'. Omit to use auto-generated default." + } + }, + "additionalProperties": false +} diff --git a/schemas/low-code/definitions/page-type.schema.json b/schemas/low-code/definitions/page-type.schema.json new file mode 100644 index 00000000000..5ddeec0eb59 --- /dev/null +++ b/schemas/low-code/definitions/page-type.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "page-type.schema.json", + "title": "PageType", + "description": "The runtime page renderer to use. Prefer lower-case values in 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." + ] +} diff --git a/schemas/low-code/definitions/permission-descriptor.schema.json b/schemas/low-code/definitions/permission-descriptor.schema.json new file mode 100644 index 00000000000..1abeb498413 --- /dev/null +++ b/schemas/low-code/definitions/permission-descriptor.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "permission-descriptor.schema.json", + "title": "PermissionDescriptor", + "description": "Defines a custom permission created through the designer. Supports parent-child hierarchy.", + "markdownDescription": "AI guidance: use stable dot-separated permission names, for example `Acme.Events.Create`. Define a parent permission for the feature and child permissions for operations. Pages and endpoints can reference these names. Permission definitions only declare permissions; roles/users must still be granted permissions through permission management.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Unique dot-separated permission name, for example 'Acme.Events.Create'.", + "minLength": 1 + }, + "displayName": { + "type": "string", + "description": "Human-readable display name shown in permission management UI.", + "minLength": 1 + }, + "children": { + "type": "array", + "description": "Child permissions forming a hierarchy. Use for feature -> operation grouping.", + "items": { + "$ref": "#" + } + } + }, + "required": ["name", "displayName"], + "additionalProperties": false, + "examples": [ + { + "name": "Acme.Events", + "displayName": "Events", + "children": [ + { "name": "Acme.Events.Create", "displayName": "Create events" }, + { "name": "Acme.Events.Update", "displayName": "Update events" }, + { "name": "Acme.Events.Delete", "displayName": "Delete events" } + ] + } + ] +} diff --git a/schemas/low-code/definitions/script-background-job-descriptor.schema.json b/schemas/low-code/definitions/script-background-job-descriptor.schema.json new file mode 100644 index 00000000000..9a186ad0839 --- /dev/null +++ b/schemas/low-code/definitions/script-background-job-descriptor.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "script-background-job-descriptor.schema.json", + "title": "Script Background Job Descriptor", + "description": "Defines a named JavaScript background job handler.", + "markdownDescription": "AI guidance: use background jobs for asynchronous work that is explicitly enqueued by code/scripts, such as notifications, imports, or long-running recalculations. `name` is the job identifier used by enqueue calls. Keep scripts retry-safe and idempotent because background jobs may run more than once after failures.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Unique identifier for the background job. Scripts/enqueuers reference this name.", + "minLength": 1 + }, + "javascript": { + "type": "string", + "description": "JavaScript code to execute when the job is enqueued/run. Use context payload/job arguments where available and keep retry-safe.", + "minLength": 1 + }, + "description": { + "type": "string", + "description": "Optional description for designer documentation and model health context." + } + }, + "required": ["name", "javascript"], + "additionalProperties": false, + "examples": [ + { + "name": "SendEventReminder", + "javascript": "context.log('Sending event reminder job.');" + } + ] +} diff --git a/schemas/low-code/definitions/script-background-worker-descriptor.schema.json b/schemas/low-code/definitions/script-background-worker-descriptor.schema.json new file mode 100644 index 00000000000..2c0ba07eb86 --- /dev/null +++ b/schemas/low-code/definitions/script-background-worker-descriptor.schema.json @@ -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.');" + } + ] +} diff --git a/schemas/low-code/definitions/script-event-handler-descriptor.schema.json b/schemas/low-code/definitions/script-event-handler-descriptor.schema.json new file mode 100644 index 00000000000..da29d7fd4d2 --- /dev/null +++ b/schemas/low-code/definitions/script-event-handler-descriptor.schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "script-event-handler-descriptor.schema.json", + "title": "Script Event Handler Descriptor", + "description": "Defines a named JavaScript handler for a distributed event name.", + "markdownDescription": "AI guidance: use event handlers for asynchronous reactions to domain/application events. `eventName` must match the event name published by the application or another script. Keep handlers idempotent because distributed events can be retried. Scripts can use the host-provided context, including event payload/request data where available, db, current tenant/user, email, logging, event publishing, and job enqueue helpers.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Optional schema reference used when this descriptor is stored as a model descriptor file." + }, + "name": { + "type": "string", + "description": "Unique identifier for the handler. Prefer a descriptive name such as 'NotifyWhenEventCompleted'.", + "minLength": 1 + }, + "eventName": { + "type": "string", + "description": "Distributed event name to subscribe to. Must exactly match the publisher's event name.", + "minLength": 1 + }, + "javascript": { + "type": "string", + "description": "JavaScript code to execute when the event is received. Keep idempotent; avoid assuming one-time delivery.", + "minLength": 1 + }, + "description": { + "type": "string", + "description": "Optional description for designer documentation and model health context." + } + }, + "required": ["name", "eventName", "javascript"], + "additionalProperties": false, + "examples": [ + { + "name": "NotifyEventCompleted", + "eventName": "Acme.Events.EventCompleted", + "javascript": "context.log('Event completed handler executed.');" + } + ] +} diff --git a/schemas/low-code/definitions/validator-descriptor.schema.json b/schemas/low-code/definitions/validator-descriptor.schema.json new file mode 100644 index 00000000000..83f18a39a05 --- /dev/null +++ b/schemas/low-code/definitions/validator-descriptor.schema.json @@ -0,0 +1,96 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "validator-descriptor.schema.json", + "title": "ValidatorDescriptor", + "description": "A single backend/UI validator applied to an entity property or form field.", + "markdownDescription": "AI guidance: always set `type`. Use `required` for mandatory input, `minLength`/`maxLength`/`stringLength` for strings, `min`/`max`/`range` for numbers and comparable values, `pattern` or `regularExpression` for regex, and `email`, `phone`, `url`, or `creditCard` for common formats. Use lower-case validator names for new JSON. Add `message` only when a custom localized/user-facing message is needed.", + "type": "object", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "description": "Validator type. Prefer lower-case values in 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" } + ] +} diff --git a/schemas/low-code/manifest.json b/schemas/low-code/manifest.json new file mode 100644 index 00000000000..de39544885e --- /dev/null +++ b/schemas/low-code/manifest.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "name": "ABP Low-Code Model Descriptor Schemas", + "version": "10.5", + "publicBaseUrl": "https://raw.githubusercontent.com/abpframework/abp/rel-10.5/schemas/low-code", + "definitionsPath": "definitions", + "descriptorSchemas": { + "enums": "definitions/enum-descriptor.schema.json", + "entities": "definitions/entity-descriptor.schema.json", + "endpoints": "definitions/endpoint-descriptor.schema.json", + "eventHandlers": "definitions/script-event-handler-descriptor.schema.json", + "backgroundJobs": "definitions/script-background-job-descriptor.schema.json", + "backgroundWorkers": "definitions/script-background-worker-descriptor.schema.json", + "pageGroups": "definitions/page-group-descriptor.schema.json", + "pages": "definitions/page-descriptor.schema.json", + "forms": "definitions/form-descriptor.schema.json", + "permissions": "definitions/permission-descriptor.schema.json" + } +}