Browse Source

docs: refresh low-code documentation

pull/25780/head
SALİH ÖZKARA 3 months ago
parent
commit
3a5d4f96d3
  1. 4
      docs/en/docs-nav.json
  2. 287
      docs/en/low-code/code-integration.md
  3. 61
      docs/en/low-code/dashboards.md
  4. 4
      docs/en/low-code/designer.md
  5. 1
      docs/en/low-code/fluent-api.md
  6. BIN
      docs/en/low-code/images/designer-forms.png
  7. BIN
      docs/en/low-code/images/designer-page-filters.png
  8. BIN
      docs/en/low-code/images/runtime-create-form.png
  9. BIN
      docs/en/low-code/images/runtime-dashboard.png
  10. BIN
      docs/en/low-code/images/runtime-data-grid.png
  11. BIN
      docs/en/low-code/images/runtime-filters-has-value.png
  12. BIN
      docs/en/low-code/images/runtime-filters.png
  13. 8
      docs/en/low-code/index.md
  14. 87
      docs/en/low-code/mcp.md
  15. 126
      docs/en/low-code/model-json.md
  16. 30
      docs/en/low-code/page-groups.md
  17. 8
      docs/en/low-code/react-runtime.md
  18. 3
      docs/en/low-code/reference-entities.md
  19. 4
      docs/en/low-code/script-actions.md
  20. 82
      docs/en/low-code/scripting-api.md

4
docs/en/docs-nav.json

@ -2145,6 +2145,10 @@
"text": "Reference Entities", "text": "Reference Entities",
"path": "low-code/reference-entities.md" "path": "low-code/reference-entities.md"
}, },
{
"text": "Code Integration",
"path": "low-code/code-integration.md"
},
{ {
"text": "Foreign Access", "text": "Foreign Access",
"path": "low-code/foreign-access.md" "path": "low-code/foreign-access.md"

287
docs/en/low-code/code-integration.md

@ -0,0 +1,287 @@
```json
//[doc-seo]
{
"Description": "Connect ABP Low-Code entities with regular C# code. Register reference entities for low-code lookups and access low-code entities from application code via typed repositories, DynamicEntity, or IDynamicPageAppService."
}
```
# Code Integration
> **Preview:** Low-Code integration APIs are part of the preview Low-Code System. Registration details, repository behavior, and page service contracts may change before general availability.
Use the [Low-Code Designer](designer.md) and [React Runtime](react-runtime.md) for standard CRUD screens. This page covers the integration points for the cases where low-code models and regular application code need to reach each other directly.
## Choose the Right Integration Path
| Need | Recommended approach |
|------|----------------------|
| A low-code field should point to an existing C# entity such as `IdentityUser` | Register a [reference entity](reference-entities.md) and use its CLR full name in the foreign key definition |
| Application code should work with a low-code entity that has a real C# class | Inject `IRepository<TEntity, Guid>` for that `[DynamicEntity]` class |
| Application code should work with a descriptor-only or runtime-defined low-code entity | Inject `IRepository<DynamicEntity, Guid>` and call `SetEntityName(...)` |
| Application code should reuse runtime page behavior such as low-code permissions, foreign-access checks, files, attachments, or child-page rules | Inject `IDynamicPageAppService` and work by `pageName` |
## Low-Code to Existing C# Entities
Low-code entities can point to existing entities that are not managed by the Low-Code System itself. Register them once during startup:
````csharp
AbpDynamicEntityConfig.ReferencedEntityList.Add<IdentityUser>(
"UserName",
"UserName",
"Email"
);
````
Then use the registered CLR type name in a foreign key:
````csharp
[DynamicForeignKey("Volo.Abp.Identity.IdentityUser", "UserName")]
public Guid? OwnerId { get; set; }
````
or in a descriptor file:
```json
{
"name": "OwnerId",
"foreignKey": {
"entityName": "Volo.Abp.Identity.IdentityUser"
}
}
```
Scripts can also query the same reference entity:
```javascript
var user = await db.get('Volo.Abp.Identity.IdentityUser', ownerId);
```
Reference entities are read-only from the low-code side. For the full registration model and metadata details, see [Reference Entities](reference-entities.md).
## C# Code to Low-Code Entities
### Option 1: Typed Repository for Code-Defined Dynamic Entities
If the low-code entity is declared as a normal C# class with `[DynamicEntity]` and `DynamicEntityBase`, use it like any other ABP entity. The low-code demo's `Customer` entity is a typical example:
````csharp
public class CustomerSyncService : ITransientDependency
{
private readonly IRepository<Customer, Guid> _customerRepository;
public CustomerSyncService(IRepository<Customer, Guid> customerRepository)
{
_customerRepository = customerRepository;
}
public async Task UpdateAsync(Guid id)
{
var customer = await _customerRepository.GetAsync(id);
customer.SetData("Telephone", "5550001122");
customer.SetData("CreditLimit", 119.99m);
await _customerRepository.UpdateAsync(customer);
}
}
````
This is the simplest path when a CLR type already exists. `GetData(...)` and `SetData(...)` still work for dynamic or extra properties on that typed entity.
### Option 2: Generic Repository for Descriptor-Only or Runtime-Defined Entities
If the low-code entity only exists in descriptor files or runtime metadata, inject `IRepository<DynamicEntity, Guid>`. Set the entity name before repository operations:
````csharp
public class ProductImportService : ITransientDependency
{
private readonly IRepository<DynamicEntity, Guid> _dynamicEntityRepository;
public ProductImportService(IRepository<DynamicEntity, Guid> dynamicEntityRepository)
{
_dynamicEntityRepository = dynamicEntityRepository;
}
public async Task<Guid> CreateAsync()
{
_dynamicEntityRepository.SetEntityName("LowCodeDemo.Products.Product");
var product = new DynamicEntity("LowCodeDemo.Products.Product", Guid.NewGuid())
{
["Name"] = "Road Helmet",
Data =
{
["Price"] = 125m,
["StockCount"] = 40
}
};
await _dynamicEntityRepository.InsertAsync(product);
return product.Id;
}
public async Task<decimal> GetPriceAsync(Guid id)
{
_dynamicEntityRepository.SetEntityName("LowCodeDemo.Products.Product");
var product = await _dynamicEntityRepository.GetAsync(id);
return product.GetData<decimal>("Price");
}
}
````
Call `SetEntityName(...)` again whenever you switch to another low-code entity name. After that, standard repository operations such as `GetAsync`, `GetListAsync`, `InsertAsync`, `UpdateAsync`, `DeleteAsync`, and `GetQueryableAsync` work normally.
Use `GetData(...)` and `SetData(...)` for normal read/write code. Use the indexers only when you need to target the underlying storage explicitly, such as query expressions or manual `DynamicEntity` construction.
| API | Use it for |
|-----|------------|
| `entity.GetData<T>("FieldName")` | General read API; it resolves CLR properties, mapped fields, and extra properties through one call |
| `entity.SetData("FieldName", value)` | General write API; it chooses the mapped field or `Data` dictionary automatically |
| `entity["FieldName"]` | Direct access to a dynamic property with `isMappedToDbField: true` |
| `entity.Data["FieldName"]` | Direct access to a dynamic property without `isMappedToDbField: true` |
### Query Expressions: `entity["Field"]` vs `entity.Data["Field"]`
For materialized entities, `GetData(...)` is the safest read API because it hides the storage details. For `IQueryable` expressions, use the access form that matches the property's storage shape so EF Core and the low-code query layer can translate it correctly:
| Property shape | Query form |
|----------------|------------|
| Normal CLR property | `x.PropertyName` |
| Dynamic property with `isMappedToDbField: true` | `x["PropertyName"]` |
| Dynamic property without `isMappedToDbField: true` | `x.Data["PropertyName"]` |
The low-code demo uses both patterns in the same query. In `LowCodeDemo.Orders.OrderLine`, `ProductId` is mapped to a DB field, while `Amount` stays in the `Data` dictionary:
````csharp
var orderLineQuery = await _dynamicEntityRepository
.SetEntityName("LowCodeDemo.Orders.OrderLine")
.GetQueryableAsync();
var filtered = orderLineQuery
.Where(line => line.Data["Amount"] != null && (decimal?)line.Data["Amount"] > 1000m)
.Select(line => new
{
ProductId = (Guid?)line["ProductId"],
Amount = (decimal?)line.Data["Amount"]
});
````
The same rule applies to joins. `ProductId` is joined through the entity indexer because it is mapped, while `CustomerId` on `LowCodeDemo.Orders.Order` is joined through `Data` because it is not:
````csharp
var orderLineQuery = await _dynamicEntityRepository
.SetEntityName("LowCodeDemo.Orders.OrderLine")
.GetQueryableAsync();
var orderQuery = await _dynamicEntityRepository
.SetEntityName("LowCodeDemo.Orders.Order")
.GetQueryableAsync();
var query =
from orderLine in orderLineQuery
join order in orderQuery
on (Guid?)orderLine["OrderId"] equals order.Id
select new
{
OrderId = order.Id,
CustomerId = (Guid?)order.Data["CustomerId"],
Amount = (decimal?)orderLine.Data["Amount"]
};
````
Use `GetData(...)` again after the query has been materialized into entity objects.
### Option 3: `IDynamicPageAppService` for Runtime-Like Behavior
Use `IDynamicPageAppService` when custom code should go through the same page-level behavior as the generated runtime: low-code permissions, foreign-access validation, lookup rules, file handling, attachments, export, and child records.
This API works with `pageName`, not `entityName`:
````csharp
public class ProductRuntimeService : ITransientDependency
{
private readonly IDynamicPageAppService _dynamicPageAppService;
public ProductRuntimeService(IDynamicPageAppService dynamicPageAppService)
{
_dynamicPageAppService = dynamicPageAppService;
}
public async Task<DynamicEntityDto> CreateAsync()
{
return await _dynamicPageAppService.CreateAsync(
"products",
new DynamicEntityCreateInput
{
Properties = new Dictionary<string, string?>
{
["Name"] = "Road Helmet",
["Price"] = "125",
["StockCount"] = "40"
}
}
);
}
public async Task<PagedResultDto<DynamicEntityDto>> GetListAsync()
{
return await _dynamicPageAppService.GetDataAsync(
"products",
new DynamicEntityListRequestDto
{
MaxResultCount = 20
}
);
}
}
````
Reach for this service when your custom code should behave like the runtime page rather than like a raw repository call.
## Joining Low-Code and Regular Entities in LINQ
Low-code entities can be composed with typed repositories in the same LINQ query. The low-code demo does this by joining dynamic order lines and orders with the typed `Customer` repository:
````csharp
var orderLineQuery = await _dynamicEntityRepository
.SetEntityName("LowCodeDemo.Orders.OrderLine")
.GetQueryableAsync();
var orderQuery = (await _dynamicEntityRepository
.SetEntityName("LowCodeDemo.Orders.Order")
.GetQueryableAsync())
.As<IQueryable<DynamicEntity>>();
var customerQuery = (await _customerRepository.GetQueryableAsync())
.As<IQueryable<IEntity<Guid>>>();
var query = orderLineQuery
.Where(line => line.Data["Amount"] != null && (decimal?)line.Data["Amount"] > 1000m)
.GroupJoin(
orderQuery,
orderLine => (Guid?)orderLine["OrderId"],
order => order.Id,
(orderLine, orders) => new { orderLine, orders }
)
.SelectMany(
x => x.orders.DefaultIfEmpty(),
(x, order) => new { x.orderLine, order }
)
.GroupJoin(
customerQuery,
x => (Guid?)x.order!.Data["CustomerId"],
customer => customer.Id,
(x, customers) => new { x.orderLine, x.order, customers }
);
````
This is useful when a business rule or reporting service needs both low-code entities and typed C# entities in one application-layer query.
## See Also
* [Reference Entities](reference-entities.md)
* [Attributes & Fluent API](fluent-api.md)
* [Model Descriptor Files](model-json.md)
* [React Runtime](react-runtime.md)
* [Scripting API](scripting-api.md)

61
docs/en/low-code/dashboards.md

@ -7,7 +7,7 @@
# Dashboards # Dashboards
Dashboards are low-code pages that render charts, lists, and number widgets from low-code entity data. A dashboard is still a page, so it uses the normal page name, title, icon, order, group, and permission model, but its page type is `dashboard` and its page definition carries a nested `dashboard` payload. Dashboards are low-code pages that render charts, lists, and number widgets from low-code entity data. A dashboard is still a page, so it uses the normal page name, title, icon, order, group, and permission model, but its page type is `dashboard` and its page definition carries a nested `dashboard` payload. The screenshot below shows an inventory-style dashboard from the demo app, and the JSON that follows is a simplified descriptor that uses the same layout concepts.
The runtime below was generated from a low-code dashboard page definition: The runtime below was generated from a low-code dashboard page definition:
@ -46,50 +46,47 @@ At runtime, the React UI groups those flat visualizations into rendered rows. Th
```json ```json
{ {
"name": "sales-dashboard", "name": "inventory-overview",
"title": "Sales Dashboard", "title": "Inventory Overview",
"type": "dashboard", "type": "dashboard",
"group": "analytics", "group": "inventory",
"dashboard": { "dashboard": {
"description": "Operational sales view", "description": "Operational inventory view",
"globalFilters": [
{ "type": "dateRange" }
],
"visualizations": [ "visualizations": [
{ {
"name": "sales-by-status", "name": "product-count",
"type": "chart",
"title": "Sales by Status",
"row": 0,
"order": 0,
"width": 2,
"entityName": "Acme.Sales.Order",
"chart": {
"chartType": "bar",
"xAxis": { "property": "Status" },
"yAxis": [
{ "aggregation": "count", "label": "Orders" }
]
}
},
{
"name": "totals",
"type": "numberContainer", "type": "numberContainer",
"title": "Totals", "title": "Product Count",
"row": 1, "row": 0,
"order": 0, "order": 0,
"width": 2, "width": 1,
"numberContainer": { "numberContainer": {
"items": [ "items": [
{ {
"name": "order-count", "name": "total-products",
"title": "Order Count", "title": "Total Products",
"entityName": "Acme.Sales.Order", "entityName": "Acme.Catalog.Product",
"aggregation": "count", "aggregation": "count",
"format": "number" "format": "number"
} }
] ]
} }
},
{
"name": "stock-by-product",
"type": "chart",
"title": "Stock by Product",
"row": 0,
"order": 1,
"width": 1,
"entityName": "Acme.Catalog.Product",
"chart": {
"chartType": "bar",
"xAxis": { "property": "Name" },
"yAxis": [
{ "aggregation": "sum", "property": "StockCount", "label": "Stock" }
]
}
} }
] ]
} }
@ -136,6 +133,8 @@ Number containers hold one or more number items. Each item can define:
* entity-specific filters and global date filter linkage * entity-specific filters and global date filter linkage
* click-through behavior * click-through behavior
The sample descriptor above combines a number container and a chart in the same `Inventory Overview` page.
## Filters and Interactivity ## Filters and Interactivity
Dashboards support three filter layers: Dashboards support three filter layers:

4
docs/en/low-code/designer.md

@ -109,7 +109,7 @@ For example, this form defines three fields and places those same field IDs into
"fields": [ "fields": [
{ "id": "name", "label": "Name", "type": "text", "binding": "Name" }, { "id": "name", "label": "Name", "type": "text", "binding": "Name" },
{ "id": "price", "label": "Price", "type": "money", "binding": "Price" }, { "id": "price", "label": "Price", "type": "money", "binding": "Price" },
{ "id": "is-active", "label": "Active", "type": "checkbox", "binding": "IsActive" } { "id": "stock-count", "label": "Stock Count", "type": "number", "binding": "StockCount" }
], ],
"layout": { "layout": {
"tabs": [ "tabs": [
@ -119,7 +119,7 @@ For example, this form defines three fields and places those same field IDs into
"fields": [ "fields": [
{ "fieldId": "name", "row": 0, "colSpan": 4 }, { "fieldId": "name", "row": 0, "colSpan": 4 },
{ "fieldId": "price", "row": 1, "colSpan": 2 }, { "fieldId": "price", "row": 1, "colSpan": 2 },
{ "fieldId": "is-active", "row": 1, "colSpan": 2 } { "fieldId": "stock-count", "row": 1, "colSpan": 2 }
] ]
} }
] ]

1
docs/en/low-code/fluent-api.md

@ -571,5 +571,6 @@ This gives you four auto-generated pages (Customers, Products, Orders with neste
## See Also ## See Also
* [Model Descriptor Files](model-json.md) * [Model Descriptor Files](model-json.md)
* [Code Integration](code-integration.md)
* [Reference Entities](reference-entities.md) * [Reference Entities](reference-entities.md)
* [Interceptors](interceptors.md) * [Interceptors](interceptors.md)

BIN
docs/en/low-code/images/designer-forms.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 47 KiB

BIN
docs/en/low-code/images/designer-page-filters.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 58 KiB

BIN
docs/en/low-code/images/runtime-create-form.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 46 KiB

BIN
docs/en/low-code/images/runtime-dashboard.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 35 KiB

BIN
docs/en/low-code/images/runtime-data-grid.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/en/low-code/images/runtime-filters-has-value.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 59 KiB

BIN
docs/en/low-code/images/runtime-filters.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 60 KiB

8
docs/en/low-code/index.md

@ -129,11 +129,11 @@ React low-code filters are type-aware. The runtime shows only operators that mak
* Numeric fields support equals, comparison, between, 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. * Date fields use date-friendly labels such as on, after, before, and between.
* Boolean fields use an `All / Yes / No` value selector. * Boolean fields use an `All / Yes / No` value selector.
* File and image fields use `Has value` with an `All / Yes / No` value selector. * File and image fields use the same `All / Yes / No` selector behind a `Has value` filter.
`All` means no filter is applied. `Yes` maps to non-empty values. `No` maps to empty values. In the current screenshot set, the dropdown below is shown on the `Active` boolean filter. The same three-value selector is also used by `Has value` filters on file and image fields.
![Has value filter options](images/runtime-filters-has-value.png) ![Yes/No filter options](images/runtime-filters-has-value.png)
## Export ## Export
@ -180,6 +180,7 @@ The designer stores and reads the same descriptor metadata described in the refe
| [Attributes & Fluent API](fluent-api.md) | Source-controlled C# metadata and runtime overrides | | [Attributes & Fluent API](fluent-api.md) | Source-controlled C# metadata and runtime overrides |
| [Model Descriptor Files](model-json.md) | JSON descriptor files and public descriptor schemas used by the designer and runtime | | [Model Descriptor Files](model-json.md) | JSON descriptor files and public descriptor schemas used by the designer and runtime |
| [Reference Entities](reference-entities.md) | Lookups to existing entities such as Identity users | | [Reference Entities](reference-entities.md) | Lookups to existing entities such as Identity users |
| [Code Integration](code-integration.md) | Moving between low-code models and regular C# code in both directions |
| [Foreign Access](foreign-access.md) | Access to related dynamic entities through relations | | [Foreign Access](foreign-access.md) | Access to related dynamic entities through relations |
| [Interceptors](interceptors.md) | JavaScript lifecycle logic for CRUD operations | | [Interceptors](interceptors.md) | JavaScript lifecycle logic for CRUD operations |
| [Custom Endpoints](custom-endpoints.md) | JavaScript-backed REST endpoints | | [Custom Endpoints](custom-endpoints.md) | JavaScript-backed REST endpoints |
@ -205,4 +206,5 @@ The generated pages are powered by these services:
* [Dashboards](dashboards.md) * [Dashboards](dashboards.md)
* [Page Groups](page-groups.md) * [Page Groups](page-groups.md)
* [MCP Integration](mcp.md) * [MCP Integration](mcp.md)
* [Code Integration](code-integration.md)
* [Model Descriptor Files](model-json.md) * [Model Descriptor Files](model-json.md)

87
docs/en/low-code/mcp.md

@ -21,6 +21,93 @@ The MCP surface is **runtime-only**:
Use [Model Descriptor Files](model-json.md) when you need source-controlled descriptors. Use MCP when an agent or automation needs safe, structured changes to the runtime model that the Designer can immediately show. Use [Model Descriptor Files](model-json.md) when you need source-controlled descriptors. Use MCP when an agent or automation needs safe, structured changes to the runtime model that the Designer can immediately show.
## Tool Reference
All tools below work against the **runtime database-backed** model only.
### Core Workflow Tools
| Tool | Use it for |
|------|------------|
| `lowcode_designer_get_capabilities` | Read the server's runtime-only scope, write rules, recommended workflow, and example mutation targets |
| `lowcode_designer_get_mutation_metadata` | Fetch the current `concurrencyStamp` and the valid semantic target tree before planning a write |
| `lowcode_designer_validate_mutations` | Dry-run a mutation batch and get structured validation feedback without writing |
| `lowcode_designer_apply_mutations` | Apply a validated mutation batch to the runtime model; this is the **only write tool** |
| `lowcode_designer_get_health_snapshot` | Re-check the full runtime model after changes to catch broken references or invalid layouts |
### Entity and Enum Tools
| Tool | Use it for |
|------|------------|
| `lowcode_designer_get_entities` | List runtime entities; optionally include configured reference entities |
| `lowcode_designer_get_entity` | Read one entity with properties, validators, interceptors, and child metadata |
| `lowcode_designer_get_entity_delete_impact` | Review references that would break or change if an entity is removed |
| `lowcode_designer_get_enum_types` | List available enum types |
| `lowcode_designer_get_enum` | Read one enum and its values |
| `lowcode_designer_get_enum_delete_impact` | Review what depends on an enum before removing it |
### Page, Form, and Permission Tools
| Tool | Use it for |
|------|------------|
| `lowcode_designer_get_page_groups` | List page groups used for runtime navigation folders |
| `lowcode_designer_get_pages` | List pages across page types, including dashboard pages |
| `lowcode_designer_get_page` | Read one page descriptor, including columns, filters, dashboard config, and form links |
| `lowcode_designer_get_page_permission_config` | Read the page's permission bindings before changing access rules |
| `lowcode_designer_get_forms` | List forms |
| `lowcode_designer_get_form` | Read one form with fields, flat layout placements, validations, and rules |
| `lowcode_designer_get_form_delete_impact` | Review what depends on a form before removing it |
| `lowcode_designer_get_permissions` | List runtime low-code permission definitions |
| `lowcode_designer_get_permission_delete_impact` | Review where a permission is referenced before removing it |
### Script and Action Tools
| Tool | Use it for |
|------|------------|
| `lowcode_designer_get_endpoints` | List scripted custom endpoints |
| `lowcode_designer_get_endpoint` | Read one endpoint with route, method, JavaScript, and analyzed references |
| `lowcode_designer_get_script_autocomplete_metadata` | Discover the script globals, helpers, entities, and metadata available for a script type |
| `lowcode_designer_test_script` | Dry-run endpoint, interceptor, event handler, background job, or background worker JavaScript |
| `lowcode_designer_get_script_event_handlers` | List script event handlers |
| `lowcode_designer_get_script_event_handler` | Read one event handler |
| `lowcode_designer_get_script_background_jobs` | List script background jobs |
| `lowcode_designer_get_script_background_job` | Read one background job |
| `lowcode_designer_get_script_background_worker_scheduler_capabilities` | Read whether the runtime supports dynamic worker registration and cron scheduling |
| `lowcode_designer_get_script_background_workers` | List script background workers |
| `lowcode_designer_get_script_background_worker` | Read one background worker with schedule and JavaScript |
## Common Sequences
### Change an Entity or Form
1. Read the current item with `lowcode_designer_get_entity` or `lowcode_designer_get_form`.
2. Fetch `lowcode_designer_get_mutation_metadata`.
3. Build a small mutation batch against the returned target tree.
4. Run `lowcode_designer_validate_mutations`.
5. Apply with `lowcode_designer_apply_mutations`.
6. Re-read the item and run `lowcode_designer_get_health_snapshot`.
### Change a Dashboard or Page Group
1. Read the page with `lowcode_designer_get_page` or list groups with `lowcode_designer_get_page_groups`.
2. Fetch mutation metadata and validate the exact batch.
3. Apply the change.
4. Re-open the page via `lowcode_designer_get_page` and confirm health.
### Remove a Shared Item Safely
1. Call the matching delete-impact tool first, such as `lowcode_designer_get_entity_delete_impact`, `lowcode_designer_get_enum_delete_impact`, `lowcode_designer_get_form_delete_impact`, or `lowcode_designer_get_permission_delete_impact`.
2. Fix or remove dependent references.
3. Validate the removal batch.
4. Apply and then re-check health.
### Review or Debug a Script
1. Read the action or endpoint with its `get_*` tool.
2. Fetch `lowcode_designer_get_script_autocomplete_metadata` for the relevant script type.
3. Run `lowcode_designer_test_script`.
4. Persist the change through mutations only after the dry run looks correct.
## Mental Model ## Mental Model
MCP is not a raw JSON document editor. It is a semantic mutation layer over low-code concepts such as: MCP is not a raw JSON document editor. It is a semantic mutation layer over low-code concepts such as:

126
docs/en/low-code/model-json.md

@ -21,13 +21,13 @@ YourApp.Domain/
|-- YourAppLowCodeInitializer.cs |-- YourAppLowCodeInitializer.cs
|-- model/ |-- model/
| |-- entities/ | |-- entities/
| | `-- Acme/Campaigns/Campaign.json | | `-- Acme/Catalog/Product.json
| |-- pages/ | |-- pages/
| | `-- campaigns.json | | `-- products.json
| |-- forms/ | |-- forms/
| | `-- campaign-form.json | | `-- product-form.json
| `-- permissions/ | `-- permissions/
| `-- Acme.Campaigns.json | `-- Acme.Catalog.json
`-- model-examples/ `-- model-examples/
|-- product.entity.json |-- product.entity.json
|-- product-form.form.json |-- product-form.form.json
@ -40,7 +40,7 @@ Keep the whole `_Dynamic` folder and the generated initializer in source control
Layout conventions: Layout conventions:
* `entities/` and `enums/` usually follow namespace-like folders such as `entities/Acme/Campaigns/Campaign.json`. * `entities/` and `enums/` usually follow namespace-like folders such as `entities/Acme/Catalog/Product.json`.
* `pageGroups/` usually stays flat as `pageGroups/{groupName}.json`. * `pageGroups/` usually stays flat as `pageGroups/{groupName}.json`.
* `pages/` stays flat as `pages/{pageName}.json`, even when the page belongs to a page group or is a dashboard. * `pages/` stays flat as `pages/{pageName}.json`, even when the page belongs to a page group or is a dashboard.
* Dashboard pages live in `pages/`; there is no separate dashboard descriptor folder. * Dashboard pages live in `pages/`; there is no separate dashboard descriptor folder.
@ -110,8 +110,8 @@ Use the descriptor schema directly when a descriptor is stored as its own JSON f
```json ```json
{ {
"$schema": "https://raw.githubusercontent.com/abpframework/abp/rel-10.5/schemas/low-code/definitions/entity-descriptor.schema.json", "$schema": "https://raw.githubusercontent.com/abpframework/abp/rel-10.5/schemas/low-code/definitions/entity-descriptor.schema.json",
"name": "Acme.Campaigns.Campaign", "name": "Acme.Catalog.Product",
"displayName": "Campaigns", "displayName": "Products",
"properties": [] "properties": []
} }
``` ```
@ -160,7 +160,7 @@ Define enums before properties that reference them:
{ {
"enums": [ "enums": [
{ {
"name": "Acme.Campaigns.CampaignStatus", "name": "Acme.Catalog.ProductStatus",
"values": [ "values": [
{ "name": "Draft", "value": 0 }, { "name": "Draft", "value": 0 },
{ "name": "Active", "value": 1 }, { "name": "Active", "value": 1 },
@ -178,7 +178,7 @@ Use the enum from a property with `type: "enum"` and `enumType`:
{ {
"name": "Status", "name": "Status",
"type": "enum", "type": "enum",
"enumType": "Acme.Campaigns.CampaignStatus", "enumType": "Acme.Catalog.ProductStatus",
"defaultValue": "0" "defaultValue": "0"
} }
``` ```
@ -189,8 +189,8 @@ Entities describe the persisted data model. UI is not configured with legacy pro
```json ```json
{ {
"name": "Acme.Campaigns.Campaign", "name": "Acme.Catalog.Product",
"displayName": "Campaigns", "displayName": "Products",
"displayProperty": "Name", "displayProperty": "Name",
"properties": [], "properties": [],
"crossFieldValidations": [], "crossFieldValidations": [],
@ -200,7 +200,7 @@ Entities describe the persisted data model. UI is not configured with legacy pro
| Field | Description | | Field | Description |
|-------|-------------| |-------|-------------|
| `name` | Required stable full entity name, for example `Acme.Campaigns.Campaign` | | `name` | Required stable full entity name, for example `Acme.Catalog.Product` |
| `displayName` | Default plural/screen label | | `displayName` | Default plural/screen label |
| `displayProperty` | Property shown in lookups and foreign key display values | | `displayProperty` | Property shown in lookups and foreign key display values |
| `parent` | Parent entity name for child/detail entities | | `parent` | Parent entity name for child/detail entities |
@ -213,7 +213,7 @@ Entities describe the persisted data model. UI is not configured with legacy pro
```json ```json
{ {
"name": "Budget", "name": "Price",
"type": "money", "type": "money",
"isRequired": true, "isRequired": true,
"isUnique": false, "isUnique": false,
@ -274,7 +274,7 @@ Use entity `attachments` when each record can have multiple arbitrary files:
```json ```json
{ {
"name": "Acme.Campaigns.Campaign", "name": "Acme.Catalog.Product",
"attachments": { "attachments": {
"isEnabled": true, "isEnabled": true,
"maxFileCount": 10, "maxFileCount": 10,
@ -322,25 +322,25 @@ Pages create runtime routes and menu entries. They also choose how entity data i
```json ```json
{ {
"name": "campaigns", "name": "products",
"title": "Campaigns", "title": "Products",
"icon": "fa-solid fa-bullhorn", "icon": "fa-solid fa-box",
"type": "dataGrid", "type": "dataGrid",
"entityName": "Acme.Campaigns.Campaign", "entityName": "Acme.Catalog.Product",
"group": "marketing", "group": "catalog",
"defaultFileExportMode": 0, "defaultFileExportMode": 0,
"allowFileBundleExport": true, "allowFileBundleExport": true,
"columns": [ "columns": [
{ "propertyName": "Name", "order": 0, "exportOrder": 0 }, { "propertyName": "Name", "order": 0, "exportOrder": 0 },
{ "propertyName": "Status", "order": 1, "exportOrder": 1 }, { "propertyName": "Status", "order": 1, "exportOrder": 1 },
{ "propertyName": "Budget", "order": 2, "exportOrder": 2, "exportable": false } { "propertyName": "Price", "order": 2, "exportOrder": 2, "exportable": false }
], ],
"filters": [ "filters": [
{ "propertyName": "Name", "control": "text", "defaultOperator": "contains" }, { "propertyName": "Name", "control": "text", "defaultOperator": "contains" },
{ "propertyName": "Status", "control": "select", "defaultOperator": "equal" } { "propertyName": "Status", "control": "select", "defaultOperator": "equal" }
], ],
"createFormName": "campaign-form", "createFormName": "product-form",
"editFormName": "campaign-form" "editFormName": "product-form"
} }
``` ```
@ -391,13 +391,13 @@ Use flat field placements inside each group. The current runtime and designer re
```json ```json
{ {
"name": "campaign-form", "name": "product-form",
"entityName": "Acme.Campaigns.Campaign", "entityName": "Acme.Catalog.Product",
"enableSaveAndNew": true, "enableSaveAndNew": true,
"fields": [ "fields": [
{ "id": "name", "label": "Name", "type": "text", "binding": "Name" }, { "id": "name", "label": "Name", "type": "text", "binding": "Name" },
{ "id": "status", "label": "Status", "type": "select", "binding": "Status", "enumType": "Acme.Campaigns.CampaignStatus" }, { "id": "status", "label": "Status", "type": "select", "binding": "Status", "enumType": "Acme.Catalog.ProductStatus" },
{ "id": "ownerId", "label": "Owner", "type": "lookup", "binding": "OwnerId" } { "id": "price", "label": "Price", "type": "money", "binding": "Price" }
], ],
"layout": { "layout": {
"tabs": [ "tabs": [
@ -413,7 +413,7 @@ Use flat field placements inside each group. The current runtime and designer re
"fields": [ "fields": [
{ "fieldId": "name", "row": 0, "colSpan": 4 }, { "fieldId": "name", "row": 0, "colSpan": 4 },
{ "fieldId": "status", "row": 1, "colSpan": 2 }, { "fieldId": "status", "row": 1, "colSpan": 2 },
{ "fieldId": "ownerId", "row": 1, "colSpan": 2 } { "fieldId": "price", "row": 1, "colSpan": 2 }
] ]
} }
] ]
@ -448,9 +448,9 @@ Pages can use generated defaults or explicit permission configuration:
{ {
"permissionConfig": { "permissionConfig": {
"view": "authenticated", "view": "authenticated",
"create": "Acme.Campaigns.Create", "create": "Acme.Catalog.Create",
"update": "Acme.Campaigns.Update", "update": "Acme.Catalog.Update",
"delete": "Acme.Campaigns.Delete" "delete": "Acme.Catalog.Delete"
} }
} }
``` ```
@ -481,12 +481,12 @@ See [Interceptors](interceptors.md) and [Scripting API](scripting-api.md).
{ {
"endpoints": [ "endpoints": [
{ {
"name": "GetCampaignStats", "name": "GetProductStats",
"route": "/api/custom/campaigns/stats", "route": "/api/custom/products/stats",
"method": "GET", "method": "GET",
"requireAuthentication": true, "requireAuthentication": true,
"requiredPermissions": ["Acme.Campaigns"], "requiredPermissions": ["Acme.Catalog"],
"javascript": "var count = await db.count('Acme.Campaigns.Campaign'); return ok({ total: count });" "javascript": "var count = await db.count('Acme.Catalog.Product'); return ok({ total: count });"
} }
] ]
} }
@ -500,22 +500,22 @@ See [Custom Endpoints](custom-endpoints.md).
{ {
"eventHandlers": [ "eventHandlers": [
{ {
"name": "NotifyCampaignCompleted", "name": "NotifyProductPublished",
"eventName": "Acme.Campaigns.CampaignCompleted", "eventName": "Acme.Catalog.ProductPublished",
"javascript": "log('Campaign completed: ' + eventData.id);" "javascript": "context.log('Product published: ' + eventData.id);"
} }
], ],
"backgroundJobs": [ "backgroundJobs": [
{ {
"name": "SendCampaignSummary", "name": "SendProductSummary",
"javascript": "log('Sending summary for ' + jobData.campaignId);" "javascript": "context.log('Sending summary for ' + jobData.productId);"
} }
], ],
"backgroundWorkers": [ "backgroundWorkers": [
{ {
"name": "CampaignCleanup", "name": "ProductCleanup",
"period": 3600000, "period": 3600000,
"javascript": "log('Cleaning campaign data.');" "javascript": "context.log('Cleaning product data.');"
} }
] ]
} }
@ -531,37 +531,37 @@ The complete example below shows the logical aggregate shape. Split projects sto
{ {
"enums": [ "enums": [
{ {
"name": "Acme.Campaigns.CampaignStatus", "name": "Acme.Catalog.ProductStatus",
"values": [ "values": [
{ "name": "Draft", "value": 0 }, { "name": "Draft", "value": 0 },
{ "name": "Active", "value": 1 }, { "name": "Active", "value": 1 },
{ "name": "Completed", "value": 2 } { "name": "Archived", "value": 2 }
] ]
} }
], ],
"entities": [ "entities": [
{ {
"name": "Acme.Campaigns.Campaign", "name": "Acme.Catalog.Product",
"displayName": "Campaigns", "displayName": "Products",
"displayProperty": "Name", "displayProperty": "Name",
"properties": [ "properties": [
{ "name": "Name", "type": "string", "isRequired": true, "validators": [{ "type": "maxLength", "length": 128 }] }, { "name": "Name", "type": "string", "isRequired": true, "validators": [{ "type": "maxLength", "length": 128 }] },
{ "name": "Status", "type": "enum", "enumType": "Acme.Campaigns.CampaignStatus", "defaultValue": "0" }, { "name": "Status", "type": "enum", "enumType": "Acme.Catalog.ProductStatus", "defaultValue": "0" },
{ "name": "Budget", "type": "money" }, { "name": "Price", "type": "money" },
{ "name": "StartDate", "type": "date" }, { "name": "ReleaseDate", "type": "date" },
{ "name": "CoverImage", "type": "image", "fileAllowedContentTypes": ["image/*"] } { "name": "CoverImage", "type": "image", "fileAllowedContentTypes": ["image/*"] }
] ]
} }
], ],
"forms": [ "forms": [
{ {
"name": "campaign-form", "name": "product-form",
"entityName": "Acme.Campaigns.Campaign", "entityName": "Acme.Catalog.Product",
"fields": [ "fields": [
{ "id": "name", "label": "Name", "type": "text", "binding": "Name" }, { "id": "name", "label": "Name", "type": "text", "binding": "Name" },
{ "id": "status", "label": "Status", "type": "select", "binding": "Status", "enumType": "Acme.Campaigns.CampaignStatus" }, { "id": "status", "label": "Status", "type": "select", "binding": "Status", "enumType": "Acme.Catalog.ProductStatus" },
{ "id": "budget", "label": "Budget", "type": "money", "binding": "Budget" }, { "id": "price", "label": "Price", "type": "money", "binding": "Price" },
{ "id": "startDate", "label": "Start Date", "type": "date", "binding": "StartDate" }, { "id": "releaseDate", "label": "Release Date", "type": "date", "binding": "ReleaseDate" },
{ "id": "coverImage", "label": "Cover Image", "type": "image", "binding": "CoverImage" } { "id": "coverImage", "label": "Cover Image", "type": "image", "binding": "CoverImage" }
], ],
"layout": { "layout": {
@ -578,8 +578,8 @@ The complete example below shows the logical aggregate shape. Split projects sto
"fields": [ "fields": [
{ "fieldId": "name", "row": 0, "colSpan": 4 }, { "fieldId": "name", "row": 0, "colSpan": 4 },
{ "fieldId": "status", "row": 1, "colSpan": 2 }, { "fieldId": "status", "row": 1, "colSpan": 2 },
{ "fieldId": "budget", "row": 1, "colSpan": 2 }, { "fieldId": "price", "row": 1, "colSpan": 2 },
{ "fieldId": "startDate", "row": 2, "colSpan": 2 }, { "fieldId": "releaseDate", "row": 2, "colSpan": 2 },
{ "fieldId": "coverImage", "row": 2, "colSpan": 2 } { "fieldId": "coverImage", "row": 2, "colSpan": 2 }
] ]
} }
@ -590,27 +590,27 @@ The complete example below shows the logical aggregate shape. Split projects sto
} }
], ],
"pageGroups": [ "pageGroups": [
{ "name": "marketing", "title": "Marketing", "icon": "fa-solid fa-bullhorn", "order": 10 } { "name": "catalog", "title": "Catalog", "icon": "fa-solid fa-boxes-stacked", "order": 10 }
], ],
"pages": [ "pages": [
{ {
"name": "campaigns", "name": "products",
"title": "Campaigns", "title": "Products",
"type": "dataGrid", "type": "dataGrid",
"entityName": "Acme.Campaigns.Campaign", "entityName": "Acme.Catalog.Product",
"group": "marketing", "group": "catalog",
"columns": [ "columns": [
{ "propertyName": "Name", "order": 0, "exportOrder": 0 }, { "propertyName": "Name", "order": 0, "exportOrder": 0 },
{ "propertyName": "Status", "order": 1, "exportOrder": 1 }, { "propertyName": "Status", "order": 1, "exportOrder": 1 },
{ "propertyName": "Budget", "order": 2, "exportOrder": 2, "exportable": false } { "propertyName": "Price", "order": 2, "exportOrder": 2, "exportable": false }
], ],
"filters": [ "filters": [
{ "propertyName": "Name", "control": "text", "defaultOperator": "contains" }, { "propertyName": "Name", "control": "text", "defaultOperator": "contains" },
{ "propertyName": "Status", "control": "select", "defaultOperator": "equal" }, { "propertyName": "Status", "control": "select", "defaultOperator": "equal" },
{ "propertyName": "CoverImage", "control": "exists", "defaultOperator": "hasValue" } { "propertyName": "CoverImage", "control": "exists", "defaultOperator": "hasValue" }
], ],
"createFormName": "campaign-form", "createFormName": "product-form",
"editFormName": "campaign-form" "editFormName": "product-form"
} }
] ]
} }

30
docs/en/low-code/page-groups.md

@ -29,11 +29,11 @@ Pages reference a group by name:
```json ```json
{ {
"name": "orders", "name": "products",
"title": "Orders", "title": "Products",
"type": "dataGrid", "type": "dataGrid",
"entityName": "Acme.Sales.Order", "entityName": "Acme.Catalog.Product",
"group": "sales" "group": "inventory"
} }
``` ```
@ -71,15 +71,15 @@ In split descriptor projects, a page group file in `pageGroups/` stores **one de
```json ```json
{ {
"name": "sales-reports", "name": "inventory-insights",
"title": "Reports", "title": "Insights",
"icon": "fa-solid fa-folder-tree", "icon": "fa-solid fa-folder-tree",
"order": 20, "order": 20,
"parent": "sales" "parent": "inventory"
} }
``` ```
For example, `pageGroups/sales-reports.json` would use that shape directly. For example, `pageGroups/inventory-insights.json` would use that shape directly.
If you are looking at the logical aggregate model instead of split files, the same data appears under the top-level `pageGroups` array: If you are looking at the logical aggregate model instead of split files, the same data appears under the top-level `pageGroups` array:
@ -87,17 +87,17 @@ If you are looking at the logical aggregate model instead of split files, the sa
{ {
"pageGroups": [ "pageGroups": [
{ {
"name": "sales", "name": "inventory",
"title": "Sales", "title": "Inventory",
"icon": "fa-solid fa-chart-line", "icon": "fa-solid fa-boxes-stacked",
"order": 10 "order": 10
}, },
{ {
"name": "sales-reports", "name": "inventory-insights",
"title": "Reports", "title": "Insights",
"icon": "fa-solid fa-folder-tree", "icon": "fa-solid fa-folder-tree",
"order": 20, "order": 20,
"parent": "sales" "parent": "inventory"
} }
] ]
} }
@ -111,7 +111,7 @@ Use CSS class strings for icons, for example:
* `fa-solid fa-folder` * `fa-solid fa-folder`
* `fa-solid fa-folder-tree` * `fa-solid fa-folder-tree`
* `fa-solid fa-chart-line` * `fa-solid fa-boxes-stacked`
Do not treat `icon` as an image URL or file path. Do not treat `icon` as an image URL or file path.

8
docs/en/low-code/react-runtime.md

@ -148,10 +148,12 @@ Filters are rendered as an ABP-style advanced filter area. The runtime shows all
File and image filters use a single `Has value` concept. The value selector controls whether the filter is applied: 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. * `All` does not add a filter.
* `Yes` returns records with a value. * `Yes` returns `true` for boolean filters, or records with a value for `Has value` filters.
* `No` returns records without a value. * `No` returns `false` for boolean filters, or records without a value for `Has value` filters.
![Has value options](images/runtime-filters-has-value.png) The screenshot below shows the shared selector pattern on the `Active` boolean filter.
![Yes/No filter 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. The URL keeps the existing `lcFilters` query parameter shape. The runtime maps user-friendly filter choices to the existing backend `FilterType` values.

3
docs/en/low-code/reference-entities.md

@ -13,6 +13,8 @@ Use the [Low-Code Designer](designer.md) to select reference entities after they
Reference Entities allow you to create foreign key relationships from **dynamic entities** to **existing C# entities** that live outside the Low-Code System. Reference Entities allow you to create foreign key relationships from **dynamic entities** to **existing C# entities** that live outside the Low-Code System.
If you need the opposite direction, where regular C# code reads or writes low-code entities, see [Code Integration](code-integration.md).
## Dynamic Entities vs Reference Entities ## Dynamic Entities vs Reference Entities
| | Dynamic Entities | Reference Entities | | | Dynamic Entities | Reference Entities |
@ -147,5 +149,6 @@ if (user) {
## See Also ## See Also
* [Model Descriptor Files](model-json.md) * [Model Descriptor Files](model-json.md)
* [Code Integration](code-integration.md)
* [Foreign Access](foreign-access.md) * [Foreign Access](foreign-access.md)
* [Attributes & Fluent API](fluent-api.md) * [Attributes & Fluent API](fluent-api.md)

4
docs/en/low-code/script-actions.md

@ -100,7 +100,7 @@ Event handlers run when a distributed event with the configured name is publishe
"name": "NotifyCampaignCompleted", "name": "NotifyCampaignCompleted",
"eventName": "Acme.Campaigns.CampaignCompleted", "eventName": "Acme.Campaigns.CampaignCompleted",
"description": "Logs and notifies when a campaign is completed", "description": "Logs and notifies when a campaign is completed",
"javascript": "log('Campaign completed: ' + eventData.id);\nawait email.queueAsync('ops@example.com', 'Campaign completed', eventData.id);" "javascript": "context.log('Campaign completed: ' + eventData.id);\nawait email.queueAsync('ops@example.com', 'Campaign completed', eventData.id);"
} }
] ]
} }
@ -194,7 +194,7 @@ Configure either `period` or `cronExpression`.
"name": "CampaignCleanup", "name": "CampaignCleanup",
"period": 3600000, "period": 3600000,
"description": "Runs every hour", "description": "Runs every hour",
"javascript": "var query = await db.query('Acme.Campaigns.Campaign');\nvar stale = await query.where(c => c.Status === 0).take(100).toList();\nlog('Stale draft count: ' + stale.length);" "javascript": "var query = await db.query('Acme.Campaigns.Campaign');\nvar stale = await query.where(c => c.Status === 0).take(100).toList();\ncontext.log('Stale draft count: ' + stale.length);"
} }
] ]
} }

82
docs/en/low-code/scripting-api.md

@ -177,12 +177,12 @@ orderLines.forEach(line => {
```javascript ```javascript
var orderQuery = await db.query('LowCodeDemo.Orders.Order'); var orderQuery = await db.query('LowCodeDemo.Orders.Order');
var orders = await orderQuery var orders = await orderQuery
.leftJoin('LowCodeDemo.Products.Product', 'p', (o, p) => o.CustomerId === p.Id) .leftJoin('LowCodeDemo.Customers.Customer', 'c', (o, c) => o.CustomerId === c.Id)
.toList(); .toList();
orders.forEach(order => { orders.forEach(order => {
if (order.p) { if (order.c) {
context.log('Has match: ' + order.p.Name); context.log('Has match: ' + order.c.Name);
} }
}); });
``` ```
@ -190,21 +190,21 @@ orders.forEach(order => {
### LINQ-Style Join ### LINQ-Style Join
```javascript ```javascript
var orderQuery = await db.query('Order'); var orderLineQuery = await db.query('LowCodeDemo.Orders.OrderLine');
orderQuery orderLineQuery
.join('LowCodeDemo.Products.Product', .join('LowCodeDemo.Products.Product',
o => o.ProductId, ol => ol.ProductId,
p => p.Id) p => p.Id)
``` ```
### Join with Filtered Query ### Join with Filtered Query
```javascript ```javascript
var productQuery = await db.query('Product'); var productQuery = await db.query('LowCodeDemo.Products.Product');
var expensiveProducts = productQuery.where(p => p.Price > 100); var expensiveProducts = productQuery.where(p => p.Price > 100);
var orderLineQuery = await db.query('OrderLine'); var orderLineQuery = await db.query('LowCodeDemo.Orders.OrderLine');
var orders = await orderLineQuery var orderLines = await orderLineQuery
.join(expensiveProducts, .join(expensiveProducts,
ol => ol.ProductId, ol => ol.ProductId,
p => p.Id) p => p.Id)
@ -223,12 +223,12 @@ Set operations execute at the database level using SQL:
| `except(query)` | `EXCEPT` | Elements in first, not second | | `except(query)` | `EXCEPT` | Elements in first, not second |
```javascript ```javascript
var productQuery = await db.query('Product'); var productQuery = await db.query('LowCodeDemo.Products.Product');
var cheap = productQuery.where(x => x.Price <= 100); var cheap = productQuery.where(x => x.Price <= 100);
var popular = productQuery.where(x => x.Rating > 4); var inStock = productQuery.where(x => x.StockCount > 0);
var bestDeals = await cheap.intersect(popular).toList(); var affordableInStock = await cheap.intersect(inStock).toList();
var underrated = await cheap.except(popular).toList(); var soldOutBudgetItems = await cheap.except(inStock).toList();
``` ```
## Aggregation Methods ## Aggregation Methods
@ -245,26 +245,26 @@ All aggregations execute as SQL statements:
| `groupBy(x => x.Property)` | `GROUP BY ...` | `QueryBuilder` | | `groupBy(x => x.Property)` | `GROUP BY ...` | `QueryBuilder` |
```javascript ```javascript
var productQuery = await db.query('Product'); var productQuery = await db.query('LowCodeDemo.Products.Product');
var totalValue = await productQuery.sum(x => x.Price); var totalValue = await productQuery.sum(x => x.Price);
var avgPrice = await productQuery.where(x => x.InStock).average(x => x.Price); var avgPrice = await productQuery.where(x => x.StockCount > 0).average(x => x.Price);
var cheapest = await productQuery.min(x => x.Price); var cheapest = await productQuery.min(x => x.Price);
``` ```
### GroupBy with Select ### GroupBy with Select
```javascript ```javascript
var productQuery = await db.query('Product'); var campaignQuery = await db.query('LowCodeDemo.ContentStudio.Campaign');
var grouped = await productQuery var grouped = await campaignQuery
.groupBy(x => x.Category) .groupBy(x => x.Status)
.select(g => ({ .select(g => ({
Category: g.Key, Status: g.Key,
Count: g.count(), Count: g.count(),
TotalPrice: g.sum(x => x.Price), TotalBudget: g.sum(x => x.Budget),
AvgPrice: g.average(x => x.Price), AvgBudget: g.average(x => x.Budget),
MinPrice: g.min(x => x.Price), MinBudget: g.min(x => x.Budget),
MaxPrice: g.max(x => x.Price) MaxBudget: g.max(x => x.Budget)
})) }))
.toList(); .toList();
``` ```
@ -285,11 +285,11 @@ var grouped = await productQuery
### GroupBy with Items ### GroupBy with Items
```javascript ```javascript
var productQuery = await db.query('Product'); var campaignQuery = await db.query('LowCodeDemo.ContentStudio.Campaign');
var grouped = await productQuery var grouped = await campaignQuery
.groupBy(x => x.Category) .groupBy(x => x.Status)
.select(g => ({ .select(g => ({
Category: g.Key, Status: g.Key,
Count: g.count(), Count: g.count(),
Items: g.take(10).toList() Items: g.take(10).toList()
})) }))
@ -307,13 +307,13 @@ var grouped = await productQuery
Math functions translate to SQL functions (ROUND, FLOOR, CEILING, ABS, etc.): Math functions translate to SQL functions (ROUND, FLOOR, CEILING, ABS, etc.):
```javascript ```javascript
var productQuery = await db.query('Product'); var productQuery = await db.query('LowCodeDemo.Products.Product');
var products = await productQuery var products = await productQuery
.where(x => Math.round(x.Price) > 100) .where(x => Math.round(x.Price) > 100)
.toList(); .toList();
var result = await productQuery var result = await productQuery
.where(x => Math.abs(x.Balance) < 10 && Math.floor(x.Rating) >= 4) .where(x => Math.abs(x.StockCount) < 10 && Math.floor(x.Price) >= 4)
.toList(); .toList();
``` ```
@ -442,7 +442,7 @@ Event handlers, background jobs, and background workers are configured in the De
Event handler example: Event handler example:
```javascript ```javascript
log('Received event ' + eventName); context.log('Received event ' + eventName);
if (eventData && eventData.campaignId) { if (eventData && eventData.campaignId) {
await jobs.enqueueAsync('SendCampaignSummary', { await jobs.enqueueAsync('SendCampaignSummary', {
@ -470,7 +470,7 @@ var staleCount = await campaignQuery
.where(campaign => campaign.Status === 0) .where(campaign => campaign.Status === 0)
.count(); .count();
log('Stale draft campaigns: ' + staleCount); context.log('Stale draft campaigns: ' + staleCount);
``` ```
## Service Helpers ## Service Helpers
@ -762,21 +762,21 @@ if (product.StockCount < quantity) { throw new Error('Insufficient stock'); }
context.commandArgs.setValue('TotalAmount', product.Price * quantity); context.commandArgs.setValue('TotalAmount', product.Price * quantity);
``` ```
### Sales Dashboard (Custom Endpoint) ### Inventory Overview (Custom Endpoint)
```javascript ```javascript
var orderQuery = await db.query('LowCodeDemo.Orders.Order'); var productQuery = await db.query('LowCodeDemo.Products.Product');
var totalOrders = await orderQuery.count(); var totalProducts = await productQuery.count();
var delivered = await orderQuery var productsWithStock = await productQuery
.where(x => x.IsDelivered === true).count(); .where(x => x.StockCount > 0).count();
var revenue = await orderQuery var averagePrice = await productQuery
.where(x => x.IsDelivered === true).sum(x => x.TotalAmount); .where(x => x.Price > 0).average(x => x.Price);
return ok({ return ok({
orders: totalOrders, totalProducts: totalProducts,
delivered: delivered, productsWithStock: productsWithStock,
revenue: revenue averagePrice: averagePrice
}); });
``` ```

Loading…
Cancel
Save