Browse Source

Add Low-Code System documentation

Add comprehensive Low-Code System documentation and navigation. Introduces new docs under docs/en/low-code (index.md, fluent-api.md, model-json.md, scripting-api.md, interceptors.md, custom-endpoints.md, reference-entities.md, foreign-access.md) and updates docs/en/docs-nav.json to include a "Low-Code System" section linking these pages.
pull/24925/head
SALİH ÖZKARA 1 month ago
parent
commit
ff0714c51d
  1. 38
      docs/en/docs-nav.json
  2. 131
      docs/en/low-code/custom-endpoints.md
  3. 439
      docs/en/low-code/fluent-api.md
  4. 137
      docs/en/low-code/foreign-access.md
  5. 228
      docs/en/low-code/index.md
  6. 183
      docs/en/low-code/interceptors.md
  7. 382
      docs/en/low-code/model-json.md
  8. 128
      docs/en/low-code/reference-entities.md
  9. 428
      docs/en/low-code/scripting-api.md

38
docs/en/docs-nav.json

@ -1981,6 +1981,44 @@
} }
] ]
}, },
{
"text": "Low-Code System",
"items": [
{
"text": "Overview",
"path": "low-code",
"isIndex": true
},
{
"text": "Fluent API & Attributes",
"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": "Interceptors",
"path": "low-code/interceptors.md"
},
{
"text": "Scripting API",
"path": "low-code/scripting-api.md"
},
{
"text": "Custom Endpoints",
"path": "low-code/custom-endpoints.md"
},
{
"text": "Foreign Access",
"path": "low-code/foreign-access.md"
}
]
},
{ {
"text": "Solution Templates", "text": "Solution Templates",
"items": [ "items": [

131
docs/en/low-code/custom-endpoints.md

@ -0,0 +1,131 @@
```json
//[doc-seo]
{
"Description": "Define custom REST API endpoints with JavaScript handlers in the ABP Low-Code Module. Create dynamic APIs without writing C# 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.
## Defining Endpoints
Add endpoints to the `endpoints` array in `model.json`:
```json
{
"endpoints": [
{
"name": "GetProductStats",
"route": "/api/custom/products/stats",
"method": "GET",
"description": "Get product statistics",
"requireAuthentication": false,
"javascript": "var count = await db.count('LowCodeDemo.Products.Product');\nreturn ok({ totalProducts: count });"
}
]
}
```
### 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 |
## Route Parameters
Use `{paramName}` syntax in the route. Access values via the `route` object:
```json
{
"name": "GetProductById",
"route": "/api/custom/products/{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 Context
Inside custom endpoint scripts, you have access to:
### Request Context
| Variable | Description |
|----------|-------------|
| `route` | Route parameter values (e.g., `route.id`) |
| `query` | Query string parameters (e.g., `query.q`, `query.page`) |
| `body` | Request body (for POST/PUT) |
| `user` | Current user (same as `context.currentUser` in interceptors) |
### Response Helpers
| Function | HTTP Status | Description |
|----------|-------------|-------------|
| `ok(data)` | 200 | Success response with data |
| `notFound(message)` | 404 | Not found response |
| `badRequest(message)` | 400 | Bad request response |
### Database API
The full [Scripting API](scripting-api.md) (`db` object) is available for querying and mutating data.
## Examples
### Get Statistics
```json
{
"name": "GetProductStats",
"route": "/api/custom/products/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 });"
}
```
### Search with Query Parameters
```json
{
"name": "SearchCustomers",
"route": "/api/custom/customers/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 })));"
}
```
### Dashboard Summary
```json
{
"name": "GetDashboardSummary",
"route": "/api/custom/dashboard",
"method": "GET",
"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' });"
}
```
## Authentication and Authorization
| Setting | Behavior |
|---------|----------|
| `requireAuthentication: false` | Endpoint is publicly accessible |
| `requireAuthentication: true` | User must be authenticated |
| `requiredPermissions: ["MyApp.Products"]` | User must have the specified permissions |
## See Also
* [Scripting API](scripting-api.md)
* [Interceptors](interceptors.md)
* [model.json Structure](model-json.md)

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

@ -0,0 +1,439 @@
```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."
}
```
# Fluent API & Attributes
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.
## Quick Start
### Step 1: Define an Entity
````csharp
[DynamicEntity]
[DynamicEntityUI(PageTitle = "Products")]
public class Product
{
[DynamicPropertyUnique]
public string Name { get; set; }
[DynamicPropertyUI(DisplayName = "Unit Price")]
public decimal Price { get; set; }
public int StockCount { get; set; }
public DateTime? ReleaseDate { get; set; }
}
````
### Step 2: Register the Assembly
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
AbpDynamicEntityConfig.SourceAssemblies.Add(
new DynamicEntityAssemblyInfo(typeof(YourDomainModule).Assembly)
);
}
````
### Step 3: Add Migration and Run
```bash
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.
### Step 4: Add Relationships
````csharp
[DynamicEntity]
[DynamicEntityUI(PageTitle = "Orders")]
public class Order
{
[DynamicForeignKey("MyApp.Customers.Customer", "Name", ForeignAccess.Edit)]
public Guid CustomerId { get; set; }
public decimal TotalAmount { get; set; }
public bool IsDelivered { get; set; }
}
[DynamicEntity(Parent = "MyApp.Orders.Order")]
public class OrderLine
{
[DynamicForeignKey("MyApp.Products.Product", "Name")]
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal Amount { get; set; }
}
````
The `Order` page now has a foreign key dropdown for Customer, and `OrderLine` is managed as a nested child inside the Order detail modal.
## Three-Layer Configuration System
The Low-Code System uses a layered configuration model. From lowest to highest priority:
1. **Code Layer** — C# classes with `[DynamicEntity]` and other attributes
2. **JSON Layer**`model.json` file (see [model.json Structure](model-json.md))
3. **Fluent Layer**`AbpDynamicEntityConfig.EntityConfigurations`
A `DefaultsLayer` runs last to fill in any missing values with conventions.
> When the same entity or property is configured in multiple layers, the higher-priority layer wins.
## C# Attributes Reference
### `[DynamicEntity]`
Marks a class as a dynamic entity. The entity name is derived from the class namespace and name.
````csharp
[DynamicEntity]
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
````
Use the `Parent` property for parent-child (master-detail) relationships:
````csharp
[DynamicEntity(Parent = "MyApp.Orders.Order")]
public class OrderLine
{
public Guid ProductId { get; set; }
public int Quantity { get; set; }
}
````
### `[DynamicEntityUI]`
Configures entity-level UI. Entities with `PageTitle` get a menu item and a dedicated page:
````csharp
[DynamicEntity]
[DynamicEntityUI(PageTitle = "Product Management")]
public class Product
{
// ...
}
````
### `[DynamicForeignKey]`
Defines a foreign key relationship on a `Guid` property:
````csharp
[DynamicForeignKey("MyApp.Customers.Customer", "Name", ForeignAccess.Edit)]
public Guid CustomerId { get; set; }
````
| Parameter | Description |
|-----------|-------------|
| `entityName` | Full name of the referenced entity (or [reference entity](reference-entities.md)) |
| `displayPropertyName` | Property to show in lookups |
| `access` | `ForeignAccess.None`, `ForeignAccess.View`, or `ForeignAccess.Edit` (see [Foreign Access](foreign-access.md)) |
### `[DynamicPropertyUI]`
Controls property visibility and behavior in the UI:
````csharp
[DynamicPropertyUI(
DisplayName = "Registration Number",
IsAvailableOnListing = true,
IsAvailableOnDataTableFiltering = true,
CreationFormAvailability = EntityPropertyUIFormAvailability.Hidden,
EditingFormAvailability = EntityPropertyUIFormAvailability.NotAvailable,
QuickLookOrder = 100
)]
public string RegistrationNumber { get; set; }
````
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `DisplayName` | string | null | Custom label for the property |
| `IsAvailableOnListing` | bool | `true` | Show in data grid |
| `IsAvailableOnDataTableFiltering` | bool | `true` | Show in filter panel |
| `CreationFormAvailability` | enum | `Available` | Visibility on create form |
| `EditingFormAvailability` | enum | `Available` | Visibility on edit form |
| `QuickLookOrder` | int | `-2` | Order in quick-look panel |
### `[DynamicPropertyServerOnly]`
Hides a property from API clients entirely. It is stored in the database but never returned to the client:
````csharp
[DynamicPropertyServerOnly]
public string InternalNotes { get; set; }
````
### `[DynamicPropertySetByClients]`
Controls whether clients can set this property value. Useful for computed or server-assigned fields:
````csharp
[DynamicPropertySetByClients(Allow = false)]
public string RegistrationNumber { get; set; }
````
### `[DynamicPropertyUnique]`
Marks a property as requiring unique values across all records:
````csharp
[DynamicPropertyUnique]
public string ProductCode { get; set; }
````
### `[DynamicEntityCommandInterceptor]`
Defines JavaScript interceptors on a class for CRUD lifecycle hooks:
````csharp
[DynamicEntity]
[DynamicEntityCommandInterceptor(
"Create",
InterceptorType.Pre,
"if(!context.commandArgs.data['Name']) { globalError = 'Name is required!'; }"
)]
[DynamicEntityCommandInterceptor(
"Delete",
InterceptorType.Post,
"context.log('Deleted: ' + context.commandArgs.entityId);"
)]
public class Organization
{
public string Name { get; set; }
}
````
> The `Name` parameter must be one of: `"Create"`, `"Update"`, or `"Delete"`. Multiple interceptors can be added to the same class (`AllowMultiple = true`).
See [Interceptors](interceptors.md) for the full JavaScript context API.
### `[DynamicEnum]`
Marks an enum for use in dynamic entity properties:
````csharp
[DynamicEnum]
public enum OrganizationType
{
Corporate = 0,
Enterprise = 1,
Startup = 2,
Consulting = 3
}
````
Reference in an entity:
````csharp
[DynamicEntity]
[DynamicEntityUI(PageTitle = "Organizations")]
public class Organization
{
public string Name { get; set; }
public OrganizationType OrganizationType { get; set; }
}
````
## Fluent API
The Fluent API has the **highest priority** in the configuration system. Use `AbpDynamicEntityConfig.EntityConfigurations` to override any attribute or JSON setting programmatically.
### Basic Usage
Configure in your Domain module's `ConfigureServices`:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
AbpDynamicEntityConfig.EntityConfigurations.Configure(
"MyApp.Products.Product",
entity =>
{
entity.SetDisplayProperty("Name");
entity.ConfigureProperty("Price", prop =>
{
prop.SetRequired(true);
prop.SetUI(ui =>
{
ui.SetDisplayName("Unit Price");
ui.SetCreationFormAvailability(EntityPropertyUIFormAvailability.Available);
});
});
entity.ConfigureProperty("InternalNotes", prop =>
{
prop.SetServerOnly(true);
});
}
);
}
````
### Entity Configuration Methods
| Method | Description |
|--------|-------------|
| `SetDisplayProperty(name)` | Set the display property for lookups |
| `SetParent(entityName)` | Set parent entity for nesting |
| `SetUI(action)` | Configure entity-level UI |
| `ConfigureProperty(name, action)` | Configure a specific property |
| `AddInterceptor(name, type, js)` | Add a JavaScript interceptor |
### Property Configuration Methods
| Method | Description |
|--------|-------------|
| `SetRequired(bool)` | Mark as required |
| `SetUnique(bool)` | Mark as unique |
| `SetServerOnly(bool)` | Hide from clients |
| `SetAllowSetByClients(bool)` | Allow client writes |
| `SetForeignKey(entityName, displayProp, access)` | Configure foreign key |
| `SetUI(action)` | Configure property UI |
## Assembly Registration
Register assemblies containing `[DynamicEntity]` classes:
````csharp
AbpDynamicEntityConfig.SourceAssemblies.Add(
new DynamicEntityAssemblyInfo(typeof(MyDomainModule).Assembly)
);
````
You can also register entity types directly:
````csharp
AbpDynamicEntityConfig.DynamicEntityTypes.Add(typeof(Product));
AbpDynamicEntityConfig.DynamicEnumTypes.Add(typeof(OrganizationType));
````
## Combining with model.json
Attributes and model.json work together seamlessly. A common pattern:
1. **Define core entities** with C# attributes (compile-time safety)
2. **Add additional entities** via model.json (no recompilation needed)
3. **Fine-tune configuration** with Fluent API (overrides everything)
The three-layer system merges all definitions:
```
Fluent API (highest) > JSON (model.json) > Code (Attributes) > Defaults (lowest)
```
For example, if an attribute sets `[DynamicPropertyUnique]` and model.json sets `"isUnique": false`, the JSON value wins because JSON layer has higher priority than Code layer.
## End-to-End Example
A complete e-commerce-style entity setup:
````csharp
// Enum
[DynamicEnum]
public enum OrderStatus
{
Pending = 0,
Processing = 1,
Shipped = 2,
Delivered = 3
}
// Customer entity
[DynamicEntity]
[DynamicEntityUI(PageTitle = "Customers")]
public class Customer
{
[DynamicPropertyUnique]
public string Name { get; set; }
[DynamicPropertyUI(DisplayName = "Phone Number", QuickLookOrder = 100)]
public string Telephone { get; set; }
[DynamicForeignKey("Volo.Abp.Identity.IdentityUser", "UserName")]
public Guid? UserId { get; set; }
[DynamicPropertyServerOnly]
public string InternalNotes { get; set; }
}
// Product entity
[DynamicEntity]
[DynamicEntityUI(PageTitle = "Products")]
public class Product
{
[DynamicPropertyUnique]
public string Name { get; set; }
public decimal Price { get; set; }
public int StockCount { get; set; }
}
// Order entity with child OrderLine
[DynamicEntity]
[DynamicEntityUI(PageTitle = "Orders")]
[DynamicEntityCommandInterceptor(
"Update",
InterceptorType.Pre,
@"if(context.commandArgs.data['IsDelivered']) {
if(!context.currentUser.roles.includes('admin')) {
globalError = 'Only admins can mark as delivered!';
}
}"
)]
public class Order
{
[DynamicForeignKey("MyApp.Customers.Customer", "Name", ForeignAccess.Edit)]
public Guid CustomerId { get; set; }
public decimal TotalAmount { get; set; }
public bool IsDelivered { get; set; }
public OrderStatus Status { get; set; }
}
[DynamicEntity(Parent = "MyApp.Orders.Order")]
public class OrderLine
{
[DynamicForeignKey("MyApp.Products.Product", "Name")]
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal Amount { get; set; }
}
````
Register everything in your Domain module:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
AbpDynamicEntityConfig.SourceAssemblies.Add(
new DynamicEntityAssemblyInfo(typeof(MyDomainModule).Assembly)
);
// Reference existing ABP entities
AbpDynamicEntityConfig.ReferencedEntityList.Add<IdentityUser>("UserName");
}
````
This gives you four auto-generated pages (Customers, Products, Orders with nested OrderLines), complete with permissions, menu items, foreign key lookups, and interceptor-based business rules.
## See Also
* [model.json Structure](model-json.md)
* [Reference Entities](reference-entities.md)
* [Interceptors](interceptors.md)

137
docs/en/low-code/foreign-access.md

@ -0,0 +1,137 @@
```json
//[doc-seo]
{
"Description": "Control access to related entities through foreign key relationships using Foreign Access in the ABP Low-Code Module."
}
```
# Foreign Access
Foreign Access controls how related entities can be accessed through foreign key relationships. It determines whether users can view or manage related data directly from the referenced entity's UI.
## Access Levels
The `ForeignAccess` enum defines three levels:
| Level | Value | Description |
|-------|-------|-------------|
| `None` | 0 | No access from the referenced entity side. The relationship exists only for lookups. |
| `View` | 1 | Read-only access. Users can view related records from the referenced entity's action menu. |
| `Edit` | 2 | Full CRUD access. Users can create, update, and delete related records from the referenced entity's action menu. |
## Configuring with Attributes
Use the third parameter of `[DynamicForeignKey]`:
````csharp
[DynamicEntity]
public class Order
{
[DynamicForeignKey("MyApp.Customers.Customer", "Name", ForeignAccess.Edit)]
public Guid CustomerId { get; set; }
}
````
## Configuring with Fluent API
````csharp
AbpDynamicEntityConfig.EntityConfigurations.Configure(
"MyApp.Orders.Order",
entity =>
{
entity.ConfigureProperty("CustomerId", prop =>
{
prop.SetForeignKey("MyApp.Customers.Customer", "Name", ForeignAccess.Edit);
});
}
);
````
## Configuring in model.json
Set the `access` field on a foreign key property:
```json
{
"name": "CustomerId",
"foreignKey": {
"entityName": "LowCodeDemo.Customers.Customer",
"displayPropertyName": "Name",
"access": "edit"
}
}
```
### Examples from the Demo Application
**Edit access** — Orders can be managed from the Customer page:
```json
{
"name": "LowCodeDemo.Orders.Order",
"properties": [
{
"name": "CustomerId",
"foreignKey": {
"entityName": "LowCodeDemo.Customers.Customer",
"access": "edit"
}
}
]
}
```
**View access** — Visited countries are viewable from the Country page:
```json
{
"name": "LowCodeDemo.Customers.VisitedCountry",
"parent": "LowCodeDemo.Customers.Customer",
"properties": [
{
"name": "CountryId",
"foreignKey": {
"entityName": "LowCodeDemo.Countries.Country",
"access": "view"
}
}
]
}
```
## UI Behavior
When foreign access is configured:
### `ForeignAccess.View`
An **action menu item** appears on the referenced entity's data grid row (e.g., a "Visited Countries" item on the Country row). Clicking it opens a read-only modal showing related records.
### `ForeignAccess.Edit`
An **action menu item** appears on the referenced 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.
### `ForeignAccess.None`
No action menu item is added. The foreign key exists only for data integrity and lookup display.
## Permission Control
Foreign access actions respect the **entity permissions** of the related entity. For example, if a user does not have the `Delete` permission for `Order`, the delete button will not appear in the foreign access modal, even if the access level is `Edit`.
## How It Works
The `ForeignAccessRelation` class stores the relationship metadata:
* Source entity (the entity with the foreign key)
* Target entity (the referenced entity)
* Foreign key property name
* Access level
The `DynamicEntityAppService` checks these relations when building entity actions and filtering data.
## See Also
* [model.json Structure](model-json.md)
* [Reference Entities](reference-entities.md)
* [Fluent API & Attributes](fluent-api.md)

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

@ -0,0 +1,228 @@
```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."
}
```
# Low-Code System
The ABP Low-Code System lets you define entities using C# attributes or Fluent API and automatically generates:
* **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)
No need to write entity classes, DTOs, application services, repositories, or UI pages manually.
## Why Low-Code?
Traditionally, adding a new entity to an ABP application requires:
1. Entity class in Domain
2. DbContext configuration in EF Core
3. DTOs in Application.Contracts
4. AppService in Application
5. Controller in HttpApi
6. Razor/Blazor pages in UI
7. Permissions, menu items, localization
**With Low-Code, a single C# class replaces all of the above:**
````csharp
[DynamicEntity]
[DynamicEntityUI(PageTitle = "Products")]
public class Product
{
[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.
## Getting Started
### 1. Install NuGet Packages
| Package | Layer |
|---------|-------|
| `Volo.Abp.LowCode.Domain.Shared` | Domain.Shared |
| `Volo.Abp.LowCode.Domain` | Domain |
| `Volo.Abp.LowCode.Application.Contracts` | Application.Contracts |
| `Volo.Abp.LowCode.Application` | Application |
| `Volo.Abp.LowCode.HttpApi` | HttpApi |
| `Volo.Abp.LowCode.HttpApi.Client` | HttpApi.Client |
| `Volo.Abp.LowCode.EntityFrameworkCore` | EF Core |
| `Volo.Abp.LowCode.Blazor` | Blazor UI (SSR) |
| `Volo.Abp.LowCode.Blazor.Server` | Blazor Server |
| `Volo.Abp.LowCode.Blazor.WebAssembly` | Blazor WebAssembly |
| `Volo.Abp.LowCode.Installer` | Auto module discovery |
### 2. Add Module Dependencies
````csharp
[DependsOn(
typeof(AbpLowCodeApplicationModule),
typeof(AbpLowCodeEntityFrameworkCoreModule),
typeof(AbpLowCodeHttpApiModule),
typeof(AbpLowCodeBlazorModule)
)]
public class YourModule : AbpModule
{
}
````
### 3. Register Your Assembly
In your Domain module, register the assembly that contains your `[DynamicEntity]` classes:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
AbpDynamicEntityConfig.SourceAssemblies.Add(
new DynamicEntityAssemblyInfo(typeof(YourDomainModule).Assembly)
);
}
````
### 4. Configure DbContext
Call `ConfigureDynamicEntities()` in your `DbContext`:
````csharp
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigureDynamicEntities();
}
````
### 5. Define Your First Entity
````csharp
[DynamicEntity]
[DynamicEntityUI(PageTitle = "Customers")]
public class Customer
{
public string Name { get; set; }
[DynamicPropertyUI(DisplayName = "Phone Number")]
public string Telephone { get; set; }
[DynamicForeignKey("Volo.Abp.Identity.IdentityUser", "UserName")]
public Guid? UserId { get; set; }
}
````
### 6. Add Migration and Run
```bash
dotnet ef migrations add Added_Customer
dotnet ef database update
```
Start your application — the Customer page is ready.
## Two Ways to Define Entities
### C# Attributes (Recommended)
Define entities as C# classes with attributes. You get compile-time checking, IntelliSense, and refactoring support:
````csharp
[DynamicEntity]
[DynamicEntityUI(PageTitle = "Orders")]
public class Order
{
[DynamicForeignKey("MyApp.Customers.Customer", "Name", ForeignAccess.Edit)]
public Guid CustomerId { get; set; }
public decimal TotalAmount { get; set; }
public bool IsDelivered { get; set; }
}
[DynamicEntity(Parent = "MyApp.Orders.Order")]
public class OrderLine
{
[DynamicForeignKey("MyApp.Products.Product", "Name")]
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal Amount { get; set; }
}
````
See [Fluent API & Attributes](fluent-api.md) for the full attribute reference.
### model.json (Declarative)
Alternatively, define entities in a JSON file without writing C# classes:
```json
{
"entities": [
{
"name": "MyApp.Customers.Customer",
"displayProperty": "Name",
"properties": [
{ "name": "Name", "isRequired": true },
{ "name": "Telephone", "ui": { "displayName": "Phone Number" } }
],
"ui": { "pageTitle": "Customers" }
}
]
}
```
See [model.json Structure](model-json.md) for the full specification.
> Both approaches can be combined. The [three-layer configuration system](fluent-api.md#configuration-priority) merges Attributes, JSON, and Fluent API with clear priority rules.
## Key Features
| Feature | Description | Documentation |
|---------|-------------|---------------|
| **Attributes & Fluent API** | Define entities with C# attributes and configure programmatically | [Fluent API & Attributes](fluent-api.md) |
| **model.json** | Declarative entity definitions in JSON | [model.json Structure](model-json.md) |
| **Reference Entities** | Link to existing entities like `IdentityUser` | [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 entities from the parent's UI | [Foreign Access](foreign-access.md) |
## Internals
### Domain Layer
* `DynamicModelManager`: Singleton managing all entity metadata with a layered configuration architecture (Code > JSON > Fluent > Defaults).
* `EntityDescriptor`: Entity definition with properties, foreign keys, interceptors, and UI configuration.
* `EntityPropertyDescriptor`: Property definition with type, validation, UI settings, and foreign key info.
* `IDynamicEntityRepository`: Repository for dynamic entity CRUD operations.
### Application Layer
* `DynamicEntityAppService`: CRUD operations for all dynamic entities.
* `DynamicEntityUIAppService`: UI definitions, menu items, and page configurations.
* `DynamicPermissionDefinitionProvider`: Auto-generates permissions per entity.
* `CustomEndpointExecutor`: Executes JavaScript-based custom endpoints.
### Database Providers
**Entity Framework Core**: Dynamic entities are configured as EF Core [shared-type entities](https://learn.microsoft.com/en-us/ef/core/modeling/entity-types?tabs=fluent-api#shared-type-entity-types) via the `ConfigureDynamicEntities()` extension method.
## See Also
* [Fluent API & Attributes](fluent-api.md)
* [model.json Structure](model-json.md)
* [Scripting API](scripting-api.md)

183
docs/en/low-code/interceptors.md

@ -0,0 +1,183 @@
```json
//[doc-seo]
{
"Description": "Add custom business logic to dynamic entity CRUD operations using Interceptors in the ABP Low-Code Module. Validate, transform, and react to data changes with JavaScript."
}
```
# Interceptors
Interceptors allow you to run custom JavaScript code before or after Create, Update, and Delete operations on dynamic entities.
## Interceptor Types
| Command | Type | When Executed |
|---------|------|---------------|
| `Create` | `Pre` | Before entity creation — validation, default values |
| `Create` | `Post` | After entity creation — notifications, related data |
| `Update` | `Pre` | Before entity update — validation, authorization |
| `Update` | `Post` | After entity update — sync, notifications |
| `Delete` | `Pre` | Before entity deletion — dependency checks |
| `Delete` | `Post` | After entity deletion — cleanup |
## Defining Interceptors with Attributes
Use the `[DynamicEntityCommandInterceptor]` attribute on a C# class:
````csharp
[DynamicEntity]
[DynamicEntityCommandInterceptor(
"Create",
InterceptorType.Pre,
"if(!context.commandArgs.data['Name']) { globalError = 'Name is required!'; }"
)]
[DynamicEntityCommandInterceptor(
"Create",
InterceptorType.Post,
"context.log('Entity created: ' + context.commandArgs.entityId);"
)]
public class Organization
{
public string Name { get; set; }
}
````
The `Name` parameter must be one of: `"Create"`, `"Update"`, or `"Delete"`. This maps directly to the CRUD command being intercepted. Multiple interceptors can be added to the same class (`AllowMultiple = true`).
## 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"` or `"Post"` |
| `javascript` | string | JavaScript code to execute |
## JavaScript Context
Inside interceptor scripts, you have access to:
### `context.commandArgs`
| Property | Type | Description |
|----------|------|-------------|
| `data` | object | Entity data dictionary (for Create/Update) |
| `entityId` | string | Entity ID (for Update/Delete) |
| `getValue(name)` | function | Get a property value |
| `setValue(name, value)` | function | Set a property value (Pre-interceptors only) |
### `context.currentUser`
| Property | Type | Description |
|----------|------|-------------|
| `isAuthenticated` | bool | Whether user is logged in |
| `userName` | string | Username |
| `email` | string | Email address |
| `roles` | string[] | User's role names |
| `id` | string | User ID |
### `context.emailSender`
| Method | Description |
|--------|-------------|
| `sendAsync(to, subject, body)` | Send an email |
### `context.log(message)`
Log a message (use instead of `console.log`).
### `db` (Database API)
Full access to the [Scripting API](scripting-api.md) for querying and mutating data.
### `globalError`
Set this variable to a string to **abort** the operation and return an error:
```javascript
globalError = 'Cannot delete this entity!';
```
## Examples
### Pre-Create: Validation
```json
{
"commandName": "Create",
"type": "Pre",
"javascript": "if(!context.commandArgs.data['Name']) {\n globalError = 'Organization name is required!';\n}"
}
```
### Post-Create: Email Notification
```json
{
"commandName": "Create",
"type": "Post",
"javascript": "if(context.currentUser.isAuthenticated && context.emailSender) {\n await context.emailSender.sendAsync(\n context.currentUser.email,\n 'New Order Created',\n 'Order total: $' + context.commandArgs.data['TotalAmount']\n );\n}"
}
```
### Pre-Update: Role-Based Authorization
```json
{
"commandName": "Update",
"type": "Pre",
"javascript": "if(context.commandArgs.data['IsDelivered']) {\n if(!context.currentUser.roles.includes('admin')) {\n globalError = 'Only administrators can mark orders as delivered!';\n }\n}"
}
```
### Pre-Delete: Business Rule Check
```json
{
"commandName": "Delete",
"type": "Pre",
"javascript": "var project = await db.get('LowCodeDemo.Projects.Project', context.commandArgs.entityId);\nif(project.Budget > 100000) {\n globalError = 'Cannot delete high-budget projects!';\n}"
}
```
### Pre-Update: Negative Value Check
```json
{
"commandName": "Update",
"type": "Pre",
"javascript": "if(context.commandArgs.data['Quantity'] < 0) {\n globalError = 'Quantity cannot be negative!';\n}"
}
```
### Pre-Create: Self-Reference Check
```json
{
"commandName": "Update",
"type": "Pre",
"javascript": "if(context.commandArgs.data.ParentCategoryId === context.commandArgs.entityId) {\n globalError = 'A category cannot be its own parent!';\n}"
}
```
## See Also
* [Scripting API](scripting-api.md)
* [model.json Structure](model-json.md)
* [Custom Endpoints](custom-endpoints.md)

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

@ -0,0 +1,382 @@
```json
//[doc-seo]
{
"Description": "Define dynamic entities using model.json in the ABP Low-Code Module. Learn about entity properties, enums, foreign keys, validators, UI configuration, and migration requirements."
}
```
# 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.
## File Location
Place your `model.json` in a `_Dynamic` folder inside your **Domain** project:
```
YourApp.Domain/
└── _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:
```json
{
"$schema": "path/to/model.schema.json",
"entities": []
}
```
## Top-Level Structure
The `model.json` file has three root sections:
```json
{
"$schema": "...",
"enums": [],
"entities": [],
"endpoints": []
}
```
| Section | Description |
|---------|-------------|
| `enums` | Enum type definitions |
| `entities` | Entity definitions with properties, foreign keys, interceptors, and UI |
| `endpoints` | Custom REST API endpoints with JavaScript handlers |
## Enum Definitions
Define enums that can be used as property types:
```json
{
"enums": [
{
"name": "LowCodeDemo.Organizations.OrganizationType",
"values": [
{ "name": "Corporate", "value": 0 },
{ "name": "Enterprise", "value": 1 },
{ "name": "Startup", "value": 2 },
{ "name": "Consulting", "value": 3 }
]
}
]
}
```
Reference enums in entity properties using the `enumType` field:
```json
{
"name": "OrganizationType",
"enumType": "LowCodeDemo.Organizations.OrganizationType"
}
```
## Entity Definition
Each entity has the following structure:
```json
{
"name": "LowCodeDemo.Products.Product",
"displayProperty": "Name",
"parent": null,
"properties": [],
"foreignKeys": [],
"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" }
]
}
```
Multi-level nesting is supported (e.g., `Order > OrderLine > ShipmentItem > ShipmentTracking`).
## Property Definition
```json
{
"name": "Price",
"type": "decimal",
"isRequired": true,
"isUnique": false,
"isMappedToDbField": true,
"serverOnly": false,
"allowSetByClients": true,
"enumType": null,
"foreignKey": null,
"validators": [],
"ui": {}
}
```
### 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 |
| `boolean` | True/false |
| `Guid` | GUID/UUID |
| `Enum` | Enum type (requires `enumType` field) |
### Property Flags
| 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 |
### Foreign Key Properties
Define a foreign key relationship inline on a property:
```json
{
"name": "CustomerId",
"foreignKey": {
"entityName": "LowCodeDemo.Customers.Customer",
"displayPropertyName": "Name",
"access": "edit"
}
}
```
| Attribute | Description |
|-----------|-------------|
| `entityName` | **Required.** Full name of the referenced entity |
| `displayPropertyName` | Property to display in lookups (defaults to entity's `displayProperty`) |
| `access` | [Foreign access](foreign-access.md) level: `"none"`, `"view"`, or `"edit"` |
### Validators
Add validation rules to properties:
```json
{
"name": "EmailAddress",
"validators": [
{ "type": "required" },
{ "type": "emailAddress" },
{ "type": "minLength", "length": 5 },
{ "type": "maxLength", "length": 255 }
]
}
```
| Validator | Parameters | Description |
|-----------|------------|-------------|
| `required` | — | Value is required |
| `minLength` | `length` | Minimum string length |
| `maxLength` | `length` | Maximum string length |
| `emailAddress` | — | Must be a valid email |
| `range` | `min`, `max` | Numeric range |
## UI Configuration
### Entity-Level UI
```json
{
"ui": {
"pageTitle": "Products"
}
}
```
> Only entities with `ui.pageTitle` get a menu item and a dedicated page in the UI.
### Property-Level UI
```json
{
"name": "RegistrationNumber",
"ui": {
"displayName": "Registration Number",
"isAvailableOnDataTable": true,
"isAvailableOnDataTableFiltering": true,
"creationFormAvailability": "Hidden",
"editingFormAvailability": "NotAvailable",
"quickLookOrder": 100
}
}
```
| 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
| Value | Description |
|-------|-------------|
| `Available` | Visible and editable |
| `Hidden` | Not visible in the form |
| `NotAvailable` | Visible but disabled/read-only |
## Interceptors
Define JavaScript interceptors for CRUD lifecycle hooks:
```json
{
"interceptors": [
{
"commandName": "Create",
"type": "Pre",
"javascript": "if(!context.commandArgs.data['Name']) { globalError = 'Name is required!'; }"
}
]
}
```
See [Interceptors](interceptors.md) for details.
## Endpoints
Define custom REST endpoints with JavaScript handlers:
```json
{
"endpoints": [
{
"name": "GetProductStats",
"route": "/api/custom/products/stats",
"method": "GET",
"requireAuthentication": false,
"javascript": "var count = await db.count('Products.Product'); return ok({ total: count });"
}
]
}
```
See [Custom Endpoints](custom-endpoints.md) for details.
## Complete Example
```json
{
"enums": [
{
"name": "ShipmentStatus",
"values": [
{ "name": "Pending", "value": 0 },
{ "name": "Shipped", "value": 2 },
{ "name": "Delivered", "value": 4 }
]
}
],
"entities": [
{
"name": "LowCodeDemo.Products.Product",
"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": "LowCodeDemo.Orders.Order",
"displayProperty": "Id",
"properties": [
{
"name": "CustomerId",
"foreignKey": {
"entityName": "LowCodeDemo.Customers.Customer",
"access": "edit"
}
},
{ "name": "TotalAmount", "type": "decimal" },
{ "name": "IsDelivered", "type": "boolean" }
],
"interceptors": [
{
"commandName": "Create",
"type": "Post",
"javascript": "context.log('Order created: ' + context.commandArgs.entityId);"
}
],
"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" }
]
}
]
}
```
## Migration Requirements
When you modify `model.json`, you need database migrations for the changes to take effect:
* **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}`
> The same migration requirement applies when using [C# Attributes](fluent-api.md). Any change to entity structure requires an EF Core migration.
## See Also
* [Fluent API & Attributes](fluent-api.md)
* [Interceptors](interceptors.md)
* [Custom Endpoints](custom-endpoints.md)
* [Scripting API](scripting-api.md)

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

@ -0,0 +1,128 @@
```json
//[doc-seo]
{
"Description": "Link dynamic entities to existing C# entities like IdentityUser using Reference Entities in the ABP Low-Code Module."
}
```
# Reference Entities
Reference Entities allow you to create foreign key relationships from dynamic entities to **existing (static) C# entities** that are already defined in your application or in ABP modules.
## Overview
Dynamic entities defined via [Attributes](fluent-api.md) or [model.json](model-json.md) normally reference other dynamic entities. However, you may need to link to entities like ABP's `IdentityUser`, `Tenant`, or your own C# entity classes. Reference entities make this possible.
Unlike dynamic entities, reference entities are **read-only** from the Low-Code Module's perspective — they don't get CRUD pages or APIs. They are used solely for:
* **Foreign key lookups** — dropdown selection in UI forms
* **Display values** — showing the referenced entity's display property in grids
* **Query support** — querying via the [Scripting API](scripting-api.md)
## Registering Reference Entities
Register reference entities in your Domain module's `ConfigureServices` using `AbpDynamicEntityConfig.ReferencedEntityList`:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
AbpDynamicEntityConfig.ReferencedEntityList.Add<IdentityUser>(
"UserName" // Default display property
);
AbpDynamicEntityConfig.ReferencedEntityList.Add<IdentityUser>(
"UserName", // Default display property
"UserName", // Exposed properties
"Email",
"PhoneNumber"
);
}
````
### `Add<TEntity>` Method
````csharp
public void Add<TEntity>(
string defaultDisplayProperty,
params string[] properties
) where TEntity : class, IEntity<Guid>
````
| Parameter | Description |
|-----------|-------------|
| `defaultDisplayProperty` | Property name used as display value in lookups |
| `properties` | Additional properties to expose (optional) |
> The entity type must implement `IEntity<Guid>`.
## Using Reference Entities in model.json
Reference a registered entity in a foreign key definition:
```json
{
"name": "UserId",
"foreignKey": {
"entityName": "Volo.Abp.Identity.IdentityUser"
}
}
```
The entity name must match the CLR type's full name. The module automatically detects that this is a reference entity and uses the registered `ReferenceEntityDescriptor`.
## Using Reference Entities with Attributes
Use the `[DynamicForeignKey]` attribute on a Guid property:
````csharp
[DynamicEntity]
public class Customer
{
[DynamicForeignKey("Volo.Abp.Identity.IdentityUser", "UserName")]
public Guid? UserId { get; set; }
}
````
## How It Works
The `ReferenceEntityDescriptor` class stores metadata about the reference entity:
* `Name` — Full CLR type name
* `Type` — The actual CLR type
* `DefaultDisplayPropertyName` — Display property for lookups
* `Properties` — List of `ReferenceEntityPropertyDescriptor` entries
When a foreign key points to a reference entity, the `ForeignKeyDescriptor` populates its `ReferencedEntityDescriptor` and `ReferencedDisplayPropertyDescriptor` instead of the standard `EntityDescriptor` fields.
## Querying Reference Entities in Scripts
Reference entities can be queried via the [Scripting API](scripting-api.md):
```javascript
// Query reference entity in interceptor or custom endpoint
var user = await db.get('Volo.Abp.Identity.IdentityUser', userId);
if (user) {
context.log('User: ' + user.UserName);
}
```
## Limitations
* **Read-only**: Reference entities do not get CRUD operations, permissions, or UI pages.
* **No child entities**: You cannot define a reference entity as a parent in parent-child relationships.
* **Guid keys only**: Reference entities must have `Guid` primary keys (`IEntity<Guid>`).
* **Explicit registration required**: Each reference entity must be registered in code before use.
## Common Reference Entities
| Entity | Name for `entityName` | Typical Display Property |
|--------|----------------------|--------------------------|
| ABP Identity User | `Volo.Abp.Identity.IdentityUser` | `UserName` |
| ABP Identity Role | `Volo.Abp.Identity.IdentityRole` | `Name` |
| ABP Tenant | `Volo.Saas.Tenant` | `Name` |
## See Also
* [model.json Structure](model-json.md)
* [Foreign Access](foreign-access.md)
* [Fluent API & Attributes](fluent-api.md)

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

@ -0,0 +1,428 @@
```json
//[doc-seo]
{
"Description": "Server-side JavaScript Scripting API for ABP Low-Code Module. Query, filter, aggregate data and perform CRUD operations with database-level execution."
}
```
# Scripting API
The Low-Code Module 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.
## Unified Database API (`db`)
The `db` object is the main entry point for all data operations.
### Key Design Principles
* **Immutable Query Builder** — each query method returns a new builder instance. The original is never modified.
* **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.
```javascript
// Immutable pattern — each call creates a new builder
var baseQuery = 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
```
## Query API
### Basic Queries
```javascript
var products = await db.query('LowCodeDemo.Products.Product')
.where(x => x.Price > 100)
.orderBy(x => x.Price)
.take(10)
.toList();
var result = await db.query('LowCodeDemo.Products.Product')
.where(x => x.Price > 100 && x.Price < 500)
.where(x => x.StockCount > 0)
.orderByDescending(x => x.Price)
.skip(10)
.take(20)
.toList();
```
### Query Methods
| Method | Description | Returns |
|--------|-------------|---------|
| `where(x => condition)` | Filter results | `QueryBuilder` |
| `orderBy(x => x.Property)` | Sort ascending | `QueryBuilder` |
| `orderByDescending(x => x.Property)` | Sort descending | `QueryBuilder` |
| `thenBy(x => x.Property)` | Secondary sort ascending | `QueryBuilder` |
| `thenByDescending(x => x.Property)` | Secondary sort descending | `QueryBuilder` |
| `skip(n)` | Skip n records | `QueryBuilder` |
| `take(n)` | Take n records | `QueryBuilder` |
| `reverse()` | Reverse sort order | `QueryBuilder` |
| `toList()` | Execute and return array | `Promise<object[]>` |
| `count()` | Return count | `Promise<number>` |
| `any()` | Check if any matches exist | `Promise<boolean>` |
| `all(x => condition)` | Check if all records match | `Promise<boolean>` |
| `isEmpty()` | Check if no results | `Promise<boolean>` |
| `isSingle()` | Check if exactly one result | `Promise<boolean>` |
| `first()` / `firstOrDefault()` | Return first match or null | `Promise<object\|null>` |
| `last()` / `lastOrDefault()` | Return last match or null | `Promise<object\|null>` |
| `single()` / `singleOrDefault()` | Return single match or null | `Promise<object\|null>` |
| `elementAt(index)` | Return element at index | `Promise<object\|null>` |
| `select(x => projection)` | Project to custom shape | `QueryBuilder` |
| `join(entity, alias, condition)` | Inner join | `QueryBuilder` |
| `leftJoin(entity, alias, condition)` | Left join | `QueryBuilder` |
| `chunk(size)` | Split into chunks | `Promise<object[][]>` |
### Supported Operators in Lambda
| Category | Operators |
|----------|-----------|
| Comparison | `===`, `!==`, `>`, `>=`, `<`, `<=` |
| Logical | `&&`, `\|\|`, `!` |
| Arithmetic | `+`, `-`, `*`, `/`, `%` |
| String | `startsWith()`, `endsWith()`, `includes()`, `trim()`, `toLowerCase()`, `toUpperCase()` |
| Array | `array.includes(x.Property)` — translates to SQL `IN` |
| Math | `Math.round()`, `Math.floor()`, `Math.ceil()`, `Math.abs()`, `Math.sqrt()`, `Math.pow()`, `Math.sign()`, `Math.truncate()` |
| Null | `!= null`, `=== null` |
### Variable Capture
External variables are captured and passed as parameters:
```javascript
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();
```
### Contains / IN Operator
```javascript
var targetPrices = [50, 100, 200];
var products = await db.query('Entity')
.where(x => targetPrices.includes(x.Price))
.toList();
```
### Select Projection
```javascript
var projected = await db.query('LowCodeDemo.Products.Product')
.where(x => x.Price > 0)
.select(x => ({ ProductName: x.Name, ProductPrice: x.Price }))
.toList();
```
## Joins
### Explicit Joins
```javascript
var orderLines = await db.query('LowCodeDemo.Orders.OrderLine')
.join('LowCodeDemo.Products.Product', 'p', (ol, p) => ol.ProductId === p.Id)
.take(10)
.toList();
// Access joined data via alias
orderLines.forEach(line => {
var product = line.p;
context.log(product.Name + ': $' + line.Amount);
});
```
### Left Join
```javascript
var orders = await db.query('LowCodeDemo.Orders.Order')
.leftJoin('LowCodeDemo.Products.Product', 'p', (o, p) => o.CustomerId === p.Id)
.toList();
orders.forEach(order => {
if (order.p) {
context.log('Has match: ' + order.p.Name);
}
});
```
### LINQ-Style Join
```javascript
db.query('Order')
.join('LowCodeDemo.Products.Product',
o => o.ProductId,
p => p.Id)
```
### Join with Filtered Query
```javascript
var expensiveProducts = db.query('Product').where(p => p.Price > 100);
var orders = await db.query('OrderLine')
.join(expensiveProducts,
ol => ol.ProductId,
p => p.Id)
.toList();
```
## Set Operations
Set operations execute at the database level using SQL:
| Method | SQL Equivalent | Description |
|--------|---------------|-------------|
| `union(query)` | `UNION` | Combine, remove duplicates |
| `concat(query)` | `UNION ALL` | Combine, keep duplicates |
| `intersect(query)` | `INTERSECT` | Elements in both |
| `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 bestDeals = await cheap.intersect(popular).toList();
var underrated = await cheap.except(popular).toList();
```
## Aggregation Methods
All aggregations execute as SQL statements:
| Method | SQL | Returns |
|--------|-----|---------|
| `sum(x => x.Property)` | `SELECT SUM(...)` | `Promise<number>` |
| `average(x => x.Property)` | `SELECT AVG(...)` | `Promise<number>` |
| `min(x => x.Property)` | `SELECT MIN(...)` | `Promise<any>` |
| `max(x => x.Property)` | `SELECT MAX(...)` | `Promise<any>` |
| `distinct(x => x.Property)` | `SELECT DISTINCT ...` | `Promise<any[]>` |
| `groupBy(x => x.Property)` | `GROUP BY ...` | `Promise<GroupResult[]>` |
```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);
```
### GroupBy with Select
```javascript
var grouped = await db.query('Product')
.groupBy(x => x.Category)
.select(g => ({
Category: g.Key,
Count: g.count(),
TotalPrice: g.sum(x => x.Price),
AvgPrice: g.average(x => x.Price),
MinPrice: g.min(x => x.Price),
MaxPrice: g.max(x => x.Price)
}))
.toList();
```
### GroupBy Aggregation Methods
| Method | SQL |
|--------|-----|
| `g.Key` | Group key value |
| `g.count()` | `COUNT(*)` |
| `g.sum(x => x.Prop)` | `SUM(prop)` |
| `g.average(x => x.Prop)` | `AVG(prop)` |
| `g.min(x => x.Prop)` | `MIN(prop)` |
| `g.max(x => x.Prop)` | `MAX(prop)` |
| `g.toList()` | Get group items |
| `g.take(n).toList()` | Get first n items |
### GroupBy with Items
```javascript
var grouped = await db.query('Product')
.groupBy(x => x.Category)
.select(g => ({
Category: g.Key,
Count: g.count(),
Items: g.take(10).toList()
}))
.toList();
```
### GroupBy Security Limits
| Limit | Default | Description |
|-------|---------|-------------|
| `MaxGroupCount` | 100 | Maximum groups |
| `MaxItemsPerGroup` | 50 | Items per group |
| `MaxTotalGroupedItems` | 1000 | Total items across groups |
## Math Functions
Math functions translate to SQL functions (ROUND, FLOOR, CEILING, ABS, etc.):
```javascript
var products = await db.query('Product')
.where(x => Math.round(x.Price) > 100)
.toList();
var result = await db.query('Product')
.where(x => Math.abs(x.Balance) < 10 && Math.floor(x.Rating) >= 4)
.toList();
```
## CRUD API
Direct CRUD methods on the `db` object:
| Method | Description | Returns |
|--------|-------------|---------|
| `db.get(entityName, id)` | Get by ID | `Promise<object\|null>` |
| `db.getList(entityName, take?)` | Get list | `Promise<object[]>` |
| `db.getCount(entityName)` | Get count | `Promise<number>` |
| `db.insert(entityName, entity)` | Insert new | `Promise<object>` |
| `db.update(entityName, entity)` | Update existing | `Promise<object>` |
| `db.delete(entityName, id)` | Delete by ID | `Promise<void>` |
```javascript
// Get by ID
var product = await db.get('LowCodeDemo.Products.Product', id);
// Insert
var newProduct = await db.insert('LowCodeDemo.Products.Product', {
Name: 'New Product',
Price: 99.99,
StockCount: 100
});
// Update
var updated = await db.update('LowCodeDemo.Products.Product', {
Id: existingId,
Name: 'Updated Name',
Price: 149.99
});
// Delete
await db.delete('LowCodeDemo.Products.Product', id);
```
## Context Object
Available in [interceptors](interceptors.md):
| Property | Type | Description |
|----------|------|-------------|
| `context.commandArgs` | object | Command arguments (data, entityId) |
| `context.commandArgs.getValue(name)` | function | Get property value |
| `context.commandArgs.setValue(name, value)` | function | Set property value |
| `context.currentUser` | object | Current user (isAuthenticated, userName, email, roles, id) |
| `context.emailSender` | object | Email sending (`sendAsync(to, subject, body)`) |
| `context.log(msg)` | function | Logging |
## Security
### Sandbox Constraints
| Constraint | Default | Configurable |
|------------|---------|--------------|
| Script Timeout | 10 seconds | Yes |
| Max Statements | 10,000 | Yes |
| Memory Limit | 10 MB | Yes |
| Recursion Depth | 100 levels | Yes |
| CLR Access | Disabled | No |
### Query Security Limits
| Limit | Default | Description |
|-------|---------|-------------|
| MaxExpressionNodes | 100 | Max AST nodes per expression |
| MaxExpressionDepth | 10 | Max nesting depth |
| MaxNavigationDepth | 5 | Max navigation property depth |
| MaxLimit (take) | 1000 | Max records per query |
| DefaultLimit | 100 | Default if `take()` not specified |
| MaxArraySize (includes) | 100 | Max array size for IN operations |
### Property Whitelist
Only properties defined in the entity model can be queried. Accessing undefined properties throws a `SecurityException`.
### SQL Injection Protection
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();
```
### Blocked Features
The following are **not allowed** inside lambda expressions: `typeof`, `instanceof`, `in`, bitwise operators, `eval()`, `Function()`, `new RegExp()`, `new Date()`, `console.log()`, `setTimeout()`, `globalThis`, `window`, `__proto__`, `constructor`, `prototype`, `Reflect`, `Proxy`, `Symbol`.
## Error Handling
```javascript
// Abort operation with error
if (!context.commandArgs.getValue('Email').includes('@')) {
throw new Error('Valid email is required');
}
// Try-catch for safe execution
try {
var products = await db.query('Entity').where(x => x.Price > 0).toList();
} catch (error) {
context.log('Query failed: ' + error.message);
}
```
## Best Practices
1. **Use specific filters** — avoid querying all records without `where()`
2. **Set limits** — always use `take()` to limit results
3. **Validate early** — check inputs at the start of scripts
4. **Use `first()` for single results** — instead of `toList()[0]`
5. **Keep scripts focused** — one responsibility per interceptor
6. **Use `context.log()`** — never `console.log()`
7. **Handle nulls** — check for null before property access
## Examples
### Inventory Check on Order Creation
```javascript
// Pre-create interceptor for Order
var productId = context.commandArgs.getValue('ProductId');
var quantity = context.commandArgs.getValue('Quantity');
var product = await db.query('LowCodeDemo.Products.Product')
.where(x => x.Id === productId)
.first();
if (!product) { throw new Error('Product not found'); }
if (product.StockCount < quantity) { throw new Error('Insufficient stock'); }
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')
.where(x => x.IsDelivered === true).count();
var revenue = await db.query('LowCodeDemo.Orders.Order')
.where(x => x.IsDelivered === true).sum(x => x.TotalAmount);
return ok({
orders: totalOrders,
delivered: delivered,
revenue: revenue
});
```
## See Also
* [Interceptors](interceptors.md)
* [Custom Endpoints](custom-endpoints.md)
* [model.json Structure](model-json.md)
Loading…
Cancel
Save