Skip to content

Commit 631e400

Browse files
authored
Merge pull request #25610 from abpframework/salihozkara/lowcode-docs
Update low-code descriptor documentation
2 parents 00041fa + a23d04f commit 631e400

26 files changed

Lines changed: 1826 additions & 740 deletions
Lines changed: 167 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,149 +1,237 @@
11
```json
22
//[doc-seo]
33
{
4-
"Description": "Define custom REST API endpoints with JavaScript handlers in the ABP Low-Code System. Create dynamic APIs without writing C# controllers."
4+
"Description": "Define JavaScript-backed custom REST endpoints in the ABP Low-Code System without writing custom .NET controllers."
55
}
66
```
77

88
# Custom Endpoints
99

10-
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.
10+
> **Preview:** Custom endpoint descriptors and scripting helpers are part of the preview Low-Code System. Names, validation rules, and runtime behavior may change before general availability.
1111
12-
## Defining Endpoints
12+
Use generated page CRUD APIs for normal dynamic entity pages. Custom endpoints are an advanced option for exposing small model-owned REST APIs that do not map to standard list, get, create, update, delete, export, file, or attachment operations.
13+
14+
Custom endpoints are defined in JSON descriptor files or through the Low-Code Designer. Each endpoint executes server-side JavaScript and is registered as an ASP.NET Core route.
1315

14-
Add endpoints to the `endpoints` array in `model.json`:
16+
## Defining Endpoints
1517

1618
```json
1719
{
1820
"endpoints": [
1921
{
20-
"name": "GetProductStats",
21-
"route": "/api/custom/products/stats",
22+
"name": "GetCampaignStats",
23+
"route": "/api/custom/campaigns/stats",
2224
"method": "GET",
23-
"description": "Get product statistics",
24-
"requireAuthentication": false,
25-
"javascript": "var count = await db.count('LowCodeDemo.Products.Product');\nreturn ok({ totalProducts: count });"
25+
"description": "Get campaign statistics",
26+
"requireAuthentication": true,
27+
"requiredPermissions": ["Acme.Campaigns"],
28+
"javascript": "var count = await db.count('Acme.Campaigns.Campaign');\nreturn ok({ totalCampaigns: count });"
2629
}
2730
]
2831
}
2932
```
3033

31-
### Endpoint Descriptor
34+
## Endpoint Descriptor
3235

3336
| Field | Type | Default | Description |
3437
|-------|------|---------|-------------|
35-
| `name` | string | **Required** | Unique endpoint name |
36-
| `route` | string | **Required** | URL route pattern (supports `{parameters}`) |
37-
| `method` | string | `"GET"` | HTTP method: `GET`, `POST`, `PUT`, `DELETE` |
38-
| `javascript` | string | **Required** | JavaScript handler code |
39-
| `description` | string | null | Description for documentation |
40-
| `requireAuthentication` | bool | `true` | Require authenticated user |
41-
| `requiredPermissions` | string[] | null | Required permission names |
38+
| `name` | string | Required | Unique endpoint identifier |
39+
| `route` | string | Required | URL route pattern; must start with `/` and can contain `{parameters}` |
40+
| `method` | string | `GET` | `GET`, `POST`, `PUT`, `DELETE`, or `PATCH` |
41+
| `javascript` | string | Required | JavaScript handler code |
42+
| `description` | string | null | Optional designer/documentation text |
43+
| `requireAuthentication` | bool | `true` | Whether the caller must be authenticated |
44+
| `requiredPermissions` | string[] | null | Permission names required to call the endpoint |
4245

43-
## Route Parameters
46+
Permission checks require an authorized user even when `requireAuthentication` is set to `false`. Keep endpoints authenticated by default and use `requireAuthentication: false` only for intentionally public APIs without `requiredPermissions`.
4447

45-
Use `{paramName}` syntax in the route. Access values via the `route` object:
48+
## Route and Request Data
49+
50+
Use `{paramName}` syntax for route parameters. Endpoint scripts can read request data through globals:
51+
52+
| Variable | Description |
53+
|----------|-------------|
54+
| `request` | Full request object |
55+
| `route` | Route values, for example `route.id` |
56+
| `params` | Alias for `route` |
57+
| `query` | Query string values, for example `query.q` |
58+
| `body` | Parsed request body |
59+
| `headers` | Selected safe request headers |
4660

4761
```json
4862
{
49-
"name": "GetProductById",
50-
"route": "/api/custom/products/{id}",
63+
"name": "GetCampaignById",
64+
"route": "/api/custom/campaigns/{id}",
5165
"method": "GET",
52-
"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 });"
66+
"javascript": "var campaign = await db.get('Acme.Campaigns.Campaign', route.id);\nif (!campaign) { return notFound('Campaign not found'); }\nreturn ok({ id: campaign.Id, name: campaign.Name });"
5367
}
5468
```
5569

56-
## JavaScript Context
70+
For non-GET requests, `body` is parsed when a body is present and remains subject to the configured request size limit. The `headers` object intentionally contains only selected request headers such as `Content-Type`, `Accept`, `Accept-Language`, and `X-Requested-With`.
71+
72+
## Response Helpers
73+
74+
Endpoint scripts can return plain data, an endpoint response object, or one of the response helpers.
75+
76+
| Function | HTTP status | Response kind |
77+
|----------|-------------|---------------|
78+
| `ok(data, headers?)` | 200 | JSON |
79+
| `okText(text, contentType?)` | 200 | Text |
80+
| `okBinary(base64Data, contentType?)` | 200 | Binary from base64 |
81+
| `created(data, headers?)` | 201 | JSON |
82+
| `noContent()` | 204 | Empty |
83+
| `badRequest(message)` | 400 | JSON error |
84+
| `unauthorized(message)` | 401 | JSON error |
85+
| `forbidden(message)` | 403 | JSON error |
86+
| `notFound(message)` | 404 | JSON error |
87+
| `error(message)` | 500 | JSON error |
88+
89+
For custom status codes or response metadata, return an object with response fields:
90+
91+
```javascript
92+
return {
93+
statusCode: 202,
94+
kind: 'json',
95+
data: { accepted: true },
96+
headers: { 'x-trace-id': guid() }
97+
};
98+
```
5799

58-
Inside custom endpoint scripts, you have access to:
100+
Response kinds are:
59101

60-
### Request Context
102+
| Kind | Description |
103+
|------|-------------|
104+
| `json` | JSON serialization, default |
105+
| `text` | Plain text |
106+
| `binaryBase64` | Base64-encoded binary payload |
61107

62-
| Variable | Description |
63-
|----------|-------------|
64-
| `request` | Full request object |
65-
| `route` | Route parameter values (e.g., `route.id`) |
66-
| `params` | Alias for route parameters |
67-
| `query` | Query string parameters (e.g., `query.q`, `query.page`) |
68-
| `body` | Request body (for POST/PUT) |
69-
| `headers` | Request headers |
70-
| `user` | Current user (same as `context.currentUser` in [Interceptors](interceptors.md)) |
71-
| `email` | Email sender (same as `context.emailSender` in [Interceptors](interceptors.md)) |
72-
73-
### Response Helpers
74-
75-
| Function | HTTP Status | Description |
76-
|----------|-------------|-------------|
77-
| `ok(data)` | 200 | Success response with data |
78-
| `created(data)` | 201 | Created response with data |
79-
| `noContent()` | 204 | No content response |
80-
| `badRequest(message)` | 400 | Bad request response |
81-
| `unauthorized(message)` | 401 | Unauthorized response |
82-
| `forbidden(message)` | 403 | Forbidden response |
83-
| `notFound(message)` | 404 | Not found response |
84-
| `error(message)` | 500 | Internal server error response |
85-
| `response(statusCode, data, error)` | Custom | Custom status code response |
86-
87-
### Logging
88-
89-
| Function | Description |
90-
|----------|-------------|
91-
| `log(message)` | Log an informational message |
92-
| `logWarning(message)` | Log a warning message |
93-
| `logError(message)` | Log an error message |
108+
## Script Services
94109

95-
### Database API
110+
Custom endpoint scripts use the same common [Scripting API](scripting-api.md) services as other low-code scripts:
96111

97-
The full [Scripting API](scripting-api.md) (`db` object) is available for querying and mutating data.
112+
| Service | Example |
113+
|---------|---------|
114+
| `db` | Query or mutate dynamic entities |
115+
| `user` / `currentUser` | Read current user information |
116+
| `tenant` / `currentTenant` | Read tenant information |
117+
| `auth` / `authorization` | Check permissions |
118+
| `settings`, `features`, `config` | Read allowed settings, features, and app configuration |
119+
| `http` | Call allowed outbound HTTP services |
120+
| `email` | Send or queue email |
121+
| `events`, `jobs` | Publish distributed events or enqueue background jobs |
122+
| `files`, `images`, `attachments` | Work with low-code files and record attachments |
123+
| `log`, `logWarning`, `logError` | Write logs |
98124

99125
## Examples
100126

101-
### Get Statistics
127+
### Statistics
102128

103129
```json
104130
{
105-
"name": "GetProductStats",
106-
"route": "/api/custom/products/stats",
131+
"name": "GetCampaignStats",
132+
"route": "/api/custom/campaigns/stats",
107133
"method": "GET",
108-
"requireAuthentication": false,
109-
"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 });"
134+
"requireAuthentication": true,
135+
"javascript": "var campaignQuery = await db.query('Acme.Campaigns.Campaign');\nvar total = await campaignQuery.count();\nvar active = await campaignQuery.where(c => c.Status === 1).count();\nreturn ok({ total: total, active: active });"
110136
}
111137
```
112138

113139
### Search with Query Parameters
114140

115141
```json
116142
{
117-
"name": "SearchCustomers",
118-
"route": "/api/custom/customers/search",
143+
"name": "SearchCampaigns",
144+
"route": "/api/custom/campaigns/search",
119145
"method": "GET",
120146
"requireAuthentication": true,
121-
"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 })));"
147+
"javascript": "var q = query.q || '';\nvar campaignQuery = await db.query('Acme.Campaigns.Campaign');\nvar rows = await campaignQuery\n .where(c => c.Name.toLowerCase().includes(q.toLowerCase()))\n .take(10)\n .toList();\nreturn ok(rows.map(c => ({ id: c.Id, name: c.Name })));"
122148
}
123149
```
124150

125-
### Dashboard Summary
151+
### Create with Validation
126152

127153
```json
128154
{
129-
"name": "GetDashboardSummary",
130-
"route": "/api/custom/dashboard",
131-
"method": "GET",
155+
"name": "CreateCampaignDraft",
156+
"route": "/api/custom/campaigns/draft",
157+
"method": "POST",
132158
"requireAuthentication": true,
133-
"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' });"
159+
"requiredPermissions": ["Acme.Campaigns.Create"],
160+
"javascript": "if (!body.name) { return badRequest('Name is required.'); }\nvar record = await db.insert('Acme.Campaigns.Campaign', { Name: body.name, Status: 0 });\nreturn created({ id: record.Id, name: record.Name });"
134161
}
135162
```
136163

137-
## Authentication and Authorization
164+
## Testing Endpoint Scripts
165+
166+
The Low-Code Designer endpoint editor includes **Test JavaScript**. Use it to run the current editor content without saving it.
167+
168+
The dry-run request editor lets you provide:
169+
170+
* HTTP method
171+
* Request path
172+
* Route values
173+
* Query values
174+
* Headers
175+
* Body JSON
176+
* Outbound HTTP mocks
177+
178+
Dry-run execution evaluates the endpoint descriptor, request context, script, authentication metadata, and required permissions against the current user. It returns the same response shape that a real endpoint execution would return.
179+
180+
Side effects are captured instead of being sent to external systems:
181+
182+
| Operation | Dry-run behavior |
183+
|-----------|------------------|
184+
| Database writes | Rolled back |
185+
| Email send or queue | Captured under **Captured Side Effects** |
186+
| Event publish | Captured under **Captured Side Effects** |
187+
| Background job enqueue | Captured under **Captured Side Effects** |
188+
| Outbound HTTP | Matched against HTTP mocks |
189+
| File, image, and attachment operations | Captured without persisting files |
190+
191+
If a script calls the `http` helper and no mock matches the method and URL, the result contains a mock miss instead of sending a real HTTP request.
192+
193+
## Response Policy
194+
195+
Dynamic endpoint responses are validated by `LowCode:Scripting:EndpointResponse`.
196+
197+
```json
198+
{
199+
"LowCode": {
200+
"Scripting": {
201+
"EndpointResponse": {
202+
"MaxBodyBytes": 1048576,
203+
"AllowedContentTypes": [
204+
"application/json",
205+
"text/plain",
206+
"application/octet-stream"
207+
],
208+
"BlockedHeaders": [
209+
"Set-Cookie",
210+
"Content-Length",
211+
"Content-Type"
212+
]
213+
}
214+
}
215+
}
216+
}
217+
```
218+
219+
Default blocked headers also include hop-by-hop headers such as `Connection`, `Transfer-Encoding`, and `Upgrade`.
220+
221+
`Content-Type` is blocked as a custom response header. Choose the response kind or `contentType` field instead of setting a raw `Content-Type` header from script.
222+
223+
## Security Notes
138224

139-
| Setting | Behavior |
140-
|---------|----------|
141-
| `requireAuthentication: false` | Endpoint is publicly accessible |
142-
| `requireAuthentication: true` | User must be authenticated |
143-
| `requiredPermissions: ["MyApp.Products"]` | User must have the specified permissions |
225+
* Prefer authenticated endpoints with explicit `requiredPermissions`.
226+
* Treat endpoints with `requireAuthentication: false` and no `requiredPermissions` as public API surface.
227+
* Keep endpoint scripts small and focused.
228+
* Validate route, query, and body input before using it.
229+
* Use `take()` for list queries.
230+
* Use the configured HTTP, email, file, and response limits for untrusted integrations.
144231

145232
## See Also
146233

147234
* [Scripting API](scripting-api.md)
235+
* [Script Actions](script-actions.md)
148236
* [Interceptors](interceptors.md)
149-
* [model.json Structure](model-json.md)
237+
* [Model Descriptor Files](model-json.md)

0 commit comments

Comments
 (0)