diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index f82f29d551..d6406a4db0 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -446,6 +446,52 @@ } ] }, + { + "text": "Low-Code", + "items": [ + { + "text": "Overview", + "path": "low-code", + "isIndex": true + }, + { + "text": "Low-Code Designer", + "path": "low-code/designer.md" + }, + { + "text": "React Runtime", + "path": "low-code/react-runtime.md" + }, + { + "text": "Attributes & Fluent API", + "path": "low-code/fluent-api.md" + }, + { + "text": "model.json Structure", + "path": "low-code/model-json.md" + }, + { + "text": "Reference Entities", + "path": "low-code/reference-entities.md" + }, + { + "text": "Foreign Access", + "path": "low-code/foreign-access.md" + }, + { + "text": "Interceptors", + "path": "low-code/interceptors.md" + }, + { + "text": "Custom Endpoints", + "path": "low-code/custom-endpoints.md" + }, + { + "text": "Scripting API", + "path": "low-code/scripting-api.md" + } + ] + }, { "text": "Framework", "items": [ @@ -1040,7 +1086,7 @@ ] } ] - }, + } ] }, { diff --git a/docs/en/low-code/custom-endpoints.md b/docs/en/low-code/custom-endpoints.md index ad0057df70..31bc72ff30 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 `model.json` 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 | + +`requiredPermissions` is checked only when `requireAuthentication` is true. Keep endpoints authenticated by default and use `requireAuthentication: false` only for intentionally public APIs. -## Route Parameters +## Route and Request Data -Use `{paramName}` syntax in the route. Access values via the `route` object: +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,33 +140,65 @@ 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 });" +} +``` + +## 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" + ] + } + } + } } ``` -## Authentication and Authorization +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 public endpoints 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 diff --git a/docs/en/low-code/designer.md b/docs/en/low-code/designer.md new file mode 100644 index 0000000000..0ac48b6a2f --- /dev/null +++ b/docs/en/low-code/designer.md @@ -0,0 +1,124 @@ +```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 models. + +```text +/admin-console/lowcode-designer +``` + +The designer works with layered model metadata. In development, generated projects include a `_Dynamic/model.json` file 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 +* Field labels and column widths +* Default sorting +* Filter fields and defaults + +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. + +## 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 model 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). + +## 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 `_Dynamic/model.json` and the generated initializer in your application repository. Use [model.json Structure](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 5aa38fe66a..5a57c36ff3 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 @@ -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,7 +380,7 @@ 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( @@ -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,6 +564,7 @@ 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. diff --git a/docs/en/low-code/foreign-access.md b/docs/en/low-code/foreign-access.md index f7545dad05..219e182742 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. @@ -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. 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 b953394215..0000000000 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 ebc77debb5..0000000000 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 2a97d9ca6f..0000000000 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 0000000000..f321f0f4a9 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 0000000000..230c7c6982 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 0000000000..f75ced1479 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 0000000000..647df1e064 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 0000000000..8d9cdf8569 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 0000000000..07e8aadfdb 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 b7a9c3189e..0000000000 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 d2fd84b009..0000000000 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 0000000000..258bab6370 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 0000000000..c47cc03014 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 0000000000..dfc7b30788 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 0000000000..e63b8dcbea 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 dd8b7298ec..d9a208b894 100644 --- a/docs/en/low-code/index.md +++ b/docs/en/low-code/index.md @@ -1,7 +1,7 @@ ```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." } ``` @@ -9,357 +9,144 @@ > 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 and CSV 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. - -![Auto-generated data grid with search, filters, and actions](images/data-grid.png) - -![Auto-generated create/edit modal with form fields and foreign key lookups](images/create-modal.png) - -## Getting Started - -### 1. Create a Low-Code Initializer - -Create a static initializer class in your Domain project's `_Dynamic` folder that registers your assembly and calls `DynamicModelManager.Instance.InitializeAsync()`: - -````csharp -using Volo.Abp.Identity; -using Volo.Abp.LowCode.Configuration; -using Volo.Abp.LowCode.Modeling; -using Volo.Abp.Threading; - -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 +## How to Enable -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 -} -```` +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. -> **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. +ABP Studio creates the required backend module references, dynamic model initializer, EF Core configuration, Admin Console integration, and React runtime wiring. -### 3. Configure DbContext +The generated React project includes: -Call `ConfigureDynamicEntities()` in your `DbContext`: +* `@volo/abp-react-lowcode` +* `configureLowCode` +* `LowCodeLocalizationProvider` +* `createDynamicRoutes` +* `useMenuItems` +* Page, form, dashboard, file, and attachment hooks -````csharp -protected override void OnModelCreating(ModelBuilder builder) -{ - builder.ConfigureDynamicEntities(); - base.OnModelCreating(builder); -} -```` +The host application wires the low-code modules, calls the generated `_Dynamic` initializer, configures EF Core dynamic entities, and seeds the required OpenIddict clients. -### 3. Define Your First Entity +## Run the Application -````csharp -[DynamicEntity] -[DynamicEntityUI(PageTitle = "Customers")] -public class Customer : DynamicEntityBase -{ - public string Name { get; set; } +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. - [DynamicPropertyUI(DisplayName = "Phone Number")] - public string Telephone { get; set; } +The generated solution README contains the exact command-line equivalents if you prefer to run the projects outside ABP Studio. - [DynamicForeignKey("Volo.Abp.Identity.IdentityUser", "UserName")] - public Guid? UserId { get; set; } -} -```` +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`. -### 4. Add Migration and Run +Open Admin Console and navigate to **Low-Code Designer** after the backend is running: -```bash -dotnet ef migrations add Added_Customer -dotnet ef database update +```text +https://localhost:/admin-console/lowcode-designer ``` -Start your application — the Customer page is ready. +Open generated runtime pages after the React application is running: -## Two Ways to Define Entities - -### C# Attributes (Recommended) - -Define entities as C# classes with attributes. You get compile-time checking, IntelliSense, and refactoring support: +```text +http://localhost:/dynamic/ +``` -````csharp -[DynamicEntity] -[DynamicEntityUI(PageTitle = "Orders")] -public class Order : DynamicEntityBase -{ - [DynamicForeignKey("MyApp.Customers.Customer", "Name", ForeignAccess.Edit)] - public Guid CustomerId { get; set; } +## Designer Workflow - public decimal TotalAmount { get; set; } - public bool IsDelivered { get; set; } -} +The designer is the day-to-day entry point. -[DynamicEntity(Parent = "MyApp.Orders.Order")] -public class OrderLine : DynamicEntityBase -{ - [DynamicForeignKey("MyApp.Products.Product", "Name")] - public Guid ProductId { get; set; } +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. +6. Use **Health** to review model issues before publishing changes. - public int Quantity { get; set; } - public decimal Amount { get; set; } -} -```` +![Entity properties in the designer](images/designer-properties.png) -See [Attributes & Fluent API](fluent-api.md) for the full attribute reference. +![Form setup in the designer](images/designer-forms.png) -### model.json (Declarative) +## React Runtime -Alternatively, define entities in a JSON file without writing C# classes: +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. -```json -{ - "entities": [ - { - "name": "MyApp.Customers.Customer", - "displayProperty": "Name", - "properties": [ - { "name": "Name", "isRequired": true }, - { "name": "Telephone", "ui": { "displayName": "Phone Number" } } - ], - "ui": { "pageTitle": "Customers" } - } - ] -} -``` +![Generated React data grid](images/runtime-data-grid.png) -See [model.json Structure](model-json.md) for the full specification. +![Generated React advanced filters](images/runtime-filters.png) -> 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. +![Generated React create form](images/runtime-create-form.png) -## Key Features +## Filters -| 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 | +React low-code filters are type-aware. The runtime shows only operators that make sense for the field type. For example: -## Export (Excel / CSV) +* 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. -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. +`All` means no filter is applied. `Yes` maps to non-empty values. `No` maps to empty values. -### How It Works +![Has value filter options](images/runtime-filters-has-value.png) -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. +## Export -### API Endpoints +Every dynamic entity page can export filtered data to Excel or CSV. Export requests use the same search, sorting, and filter input as the list endpoint. Server-only fields are excluded and foreign key values are displayed through their configured display property. | 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 +| `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 | -### Domain Layer +## Advanced Configuration -* `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. +The designer stores and reads the same model metadata described in the reference pages below. Use these pages when you need source-controlled model files, custom startup wiring, script handlers, or low-level integration details. -### Application Layer +| 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.json Structure](model-json.md) | JSON descriptor format 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 | +| [Scripting API](scripting-api.md) | Server-side script context and helpers | -* `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. +## Runtime Internals -### Database Providers +The generated pages are powered by these services: -**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. +* `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) +* [Low-Code Designer](designer.md) +* [React Runtime](react-runtime.md) * [model.json Structure](model-json.md) -* [Scripting API](scripting-api.md) diff --git a/docs/en/low-code/interceptors.md b/docs/en/low-code/interceptors.md index 7eaf97eb77..048e8c6168 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 model 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 model.json + +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 @@ -154,6 +158,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`, `events`, `jobs`, `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: diff --git a/docs/en/low-code/model-json.md b/docs/en/low-code/model-json.md index 2f58876a29..28662cde7e 100644 --- a/docs/en/low-code/model-json.md +++ b/docs/en/low-code/model-json.md @@ -1,27 +1,27 @@ ```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 model.json metadata for dynamic entities, pages, forms, filters, permissions, script endpoints, event handlers, background jobs, and workers." } ``` # model.json Structure -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. + +`model.json` is the source-controlled descriptor format used by the Low-Code Designer and React runtime. 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 the model file in a `_Dynamic` folder under the application domain project: -``` +```text YourApp.Domain/ -└── _Dynamic/ - └── model.json +`-- _Dynamic/ + `-- model.json ``` -The module automatically discovers and loads this file at application startup. - -> A JSON Schema file (`model.schema.json`) is available in the module source for IDE IntelliSense. Reference it using the `$schema` property: +The low-code module discovers this file during application startup. A JSON Schema (`model.schema.json`) is available in the module source and can be referenced with `$schema` for IDE IntelliSense. ```json { @@ -30,280 +30,321 @@ The module automatically discovers and loads this file at application startup. } ``` -## Top-Level Structure +## Top-Level Sections -The `model.json` file has three root sections: +The current model format is 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": {} -} -``` - -### 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" } - ] + "crossFieldValidations": [], + "interceptors": [] } ``` -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 -> **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. +```json +{ + "name": "OwnerId", + "type": "guid", + "foreignKey": { + "entityName": "Volo.Abp.Identity.IdentityUser", + "displayPropertyName": "UserName", + "access": "none" + } +} +``` -### Validators +`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. -Add validation rules to properties: +### Validators ```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", + "columns": [ + { "propertyName": "Name", "order": 0 }, + { "propertyName": "Status", "order": 1 }, + { "propertyName": "Budget", "order": 2 } + ], + "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 | - -> 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 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// +``` -## UI Configuration +## Forms -### Entity-Level UI +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` | -### Property-Level UI +`hasValue` is a UI alias. At runtime, `Yes` maps to `IsNotNull`, `No` maps to `IsNull`, and `All` does not add a filter. + +## Permissions + +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) | - -#### Form Availability Values +Custom permission definitions live in the top-level `permissions` section and can be granted through the normal ABP permission management UI. -| Value | Description | -|-------|-------------| -| `Available` | Visible and editable | -| `Hidden` | Not visible in the form | -| `NotAvailable` | Visible but disabled/read-only | - -## Interceptors +## Scripts -Define JavaScript interceptors for CRUD lifecycle hooks: +### Interceptors ```json { @@ -311,33 +352,61 @@ 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). + +### 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.');" } ] } ``` -See [Custom Endpoints](custom-endpoints.md) for details. +Background workers require either `period` in milliseconds or `cronExpression`. ## Complete Example @@ -345,60 +414,84 @@ See [Custom Endpoints](custom-endpoints.md) for details. { "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 }, + { "propertyName": "Status", "order": 1 }, + { "propertyName": "Budget", "order": 2 } ], - "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,16 +499,20 @@ 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) diff --git a/docs/en/low-code/react-runtime.md b/docs/en/low-code/react-runtime.md new file mode 100644 index 0000000000..923f80fb88 --- /dev/null +++ b/docs/en/low-code/react-runtime.md @@ -0,0 +1,246 @@ +```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 requests a download token and then calls the Excel or CSV export endpoint with the current search, sorting, and filters. + +| 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 | + +Child and foreign-access pages use the matching `/children/{childEntityName}` and `/foreign-access/{sourceEntityName}` page endpoints. + +## 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 b937175c7a..f5e3e1027b 100644 --- a/docs/en/low-code/reference-entities.md +++ b/docs/en/low-code/reference-entities.md @@ -1,12 +1,16 @@ ```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 @@ -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() diff --git a/docs/en/low-code/scripting-api.md b/docs/en/low-code/scripting-api.md index a08b1107ec..422cbeb6c1 100644 --- a/docs/en/low-code/scripting-api.md +++ b/docs/en/low-code/scripting-api.md @@ -7,7 +7,13 @@ # 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. ## Unified Database API (`db`) @@ -19,9 +25,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 +39,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 +102,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 +122,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 +134,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 +150,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 +165,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 +175,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 +198,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(); @@ -200,15 +220,18 @@ All aggregations execute as SQL statements: | `groupBy(x => x.Property)` | `GROUP BY ...` | `Promise` | ```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 +260,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, @@ -258,11 +282,12 @@ var grouped = await db.query('Product') 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 +299,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 +331,199 @@ 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 | +| `fields` | `context.fields` | Field selector helpers for file operations | +| `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` | + +## 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 -Available in [interceptors](interceptors.md): +The file helpers use low-code page services so permissions, file validation, linked-blob checks, and foreign access stay consistent with the runtime. -| 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 | +| 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. ## 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 +531,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 +575,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 +591,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 +625,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 +639,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({