@sparkerp/plugin-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/bundle/blocks.json +12229 -0
- package/bundle/catalog.json +32475 -0
- package/bundle/docs/applications/hcm/employee-profile.md +144 -0
- package/bundle/docs/concepts/artifact-types.md +80 -0
- package/bundle/docs/concepts/glossary.md +124 -0
- package/bundle/docs/concepts/overview.md +88 -0
- package/bundle/docs/concepts/page-data-flow.md +84 -0
- package/bundle/docs/concepts/plugin-lifecycle.md +81 -0
- package/bundle/docs/concepts/sdk-modes.md +71 -0
- package/bundle/docs/docs.json +109 -0
- package/bundle/docs/guides/add-a-cadence-job.md +134 -0
- package/bundle/docs/guides/add-a-compliance-job.md +139 -0
- package/bundle/docs/guides/add-a-create-edit-form.md +167 -0
- package/bundle/docs/guides/add-a-custom-block.md +225 -0
- package/bundle/docs/guides/add-a-data-provider.md +216 -0
- package/bundle/docs/guides/add-a-kpi.md +154 -0
- package/bundle/docs/guides/add-a-reminder-job.md +181 -0
- package/bundle/docs/guides/add-an-aggregation-job.md +153 -0
- package/bundle/docs/guides/add-an-approval-workflow.md +222 -0
- package/bundle/docs/guides/add-an-entity.md +170 -0
- package/bundle/docs/guides/add-business-rules.md +147 -0
- package/bundle/docs/guides/add-i18n.md +121 -0
- package/bundle/docs/guides/add-menus.md +149 -0
- package/bundle/docs/guides/build-a-code-plugin.md +141 -0
- package/bundle/docs/guides/build-a-page.md +213 -0
- package/bundle/docs/guides/build-a-polyglot-service-plugin.md +201 -0
- package/bundle/docs/guides/build-a-tenant-extension-service.md +149 -0
- package/bundle/docs/guides/create-a-plugin.md +172 -0
- package/bundle/docs/guides/expose-a-plugin-operation-as-an-ai-tool.md +114 -0
- package/bundle/docs/guides/index.md +53 -0
- package/bundle/docs/guides/publish-and-upgrade.md +152 -0
- package/bundle/docs/guides/responsive-plugin.md +117 -0
- package/bundle/docs/guides/seed-data-on-install.md +127 -0
- package/bundle/docs/guides/set-up-the-sdk.md +120 -0
- package/bundle/docs/guides/use-the-mcp-server.md +104 -0
- package/bundle/docs/guides/validate-and-test.md +119 -0
- package/bundle/docs/guides/wire-a-pages-data.md +153 -0
- package/bundle/docs/quickstart/index.md +118 -0
- package/bundle/docs/recipes/condition-based-compliance-flag.md +70 -0
- package/bundle/docs/recipes/cross-plugin-offboarding-fanout.md +68 -0
- package/bundle/docs/recipes/cross-row-aggregation.md +67 -0
- package/bundle/docs/recipes/entity-with-approval-workflow.md +72 -0
- package/bundle/docs/recipes/index.md +40 -0
- package/bundle/docs/recipes/install-lifecycle-data-seeding.md +76 -0
- package/bundle/docs/recipes/kpi-dashboard-page.md +92 -0
- package/bundle/docs/recipes/master-detail-page.md +84 -0
- package/bundle/docs/recipes/n-days-before-reminder.md +72 -0
- package/bundle/docs/recipes/per-row-document-generator.md +65 -0
- package/bundle/docs/recipes/related-record-lookup-field.md +74 -0
- package/bundle/docs/recipes/resolved-reference-grid-column.md +63 -0
- package/bundle/docs/recipes/scheduled-record-generation.md +68 -0
- package/bundle/docs/reference/action.md +18 -0
- package/bundle/docs/reference/block-definition.md +35 -0
- package/bundle/docs/reference/block-instance.md +33 -0
- package/bundle/docs/reference/composite-pack-manifest.md +33 -0
- package/bundle/docs/reference/connector-definition.md +36 -0
- package/bundle/docs/reference/data-service-definition.md +22 -0
- package/bundle/docs/reference/data-view-definition.md +26 -0
- package/bundle/docs/reference/entity-aggregation-config.md +38 -0
- package/bundle/docs/reference/entity-cadence-config.md +33 -0
- package/bundle/docs/reference/entity-compliance-config.md +37 -0
- package/bundle/docs/reference/entity-cross-plugin-action-config.md +37 -0
- package/bundle/docs/reference/entity-definition.md +18 -0
- package/bundle/docs/reference/entity-document-generator-config.md +35 -0
- package/bundle/docs/reference/entity-rule-definition.md +29 -0
- package/bundle/docs/reference/entity-status-date-sweep-config.md +27 -0
- package/bundle/docs/reference/form.md +35 -0
- package/bundle/docs/reference/index.md +37 -0
- package/bundle/docs/reference/menu.md +26 -0
- package/bundle/docs/reference/mobile-nav.md +22 -0
- package/bundle/docs/reference/page-template.md +29 -0
- package/bundle/docs/reference/page.md +44 -0
- package/bundle/docs/reference/plugin-manifest.md +54 -0
- package/bundle/docs/reference/plugin-seed-data.md +21 -0
- package/bundle/docs/reference/print-template-definition.md +22 -0
- package/bundle/docs/reference/provider-definition.md +22 -0
- package/bundle/docs/reference/report-definition.md +22 -0
- package/bundle/docs/reference/rule.md +30 -0
- package/bundle/docs/reference/workflow-definition.md +28 -0
- package/bundle/docs/troubleshooting/grid-shows-zero-rows.md +82 -0
- package/bundle/docs/troubleshooting/job-never-runs.md +89 -0
- package/bundle/docs/troubleshooting/known-gotchas.md +193 -0
- package/bundle/docs/troubleshooting/publishing-errors.md +119 -0
- package/bundle/docs/tutorial/01-create-the-plugin.md +157 -0
- package/bundle/docs/tutorial/02-category-entity-and-seed.md +94 -0
- package/bundle/docs/tutorial/03-equipment-entity.md +121 -0
- package/bundle/docs/tutorial/04-equipment-register-page.md +153 -0
- package/bundle/docs/tutorial/05-checkout-entity-and-page.md +116 -0
- package/bundle/docs/tutorial/06-checkout-approval-workflow.md +139 -0
- package/bundle/docs/tutorial/07-return-due-reminder-job.md +129 -0
- package/bundle/docs/tutorial/08-menus-i18n-publish.md +123 -0
- package/bundle/docs/tutorial/index.md +72 -0
- package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-aggregation-configs.json +18 -0
- package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-cadence-configs.json +9 -0
- package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-compliance-configs.json +74 -0
- package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-cross-plugin-configs.json +9 -0
- package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-document-configs.json +9 -0
- package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-sweep-configs.json +260 -0
- package/bundle/examples/backend/modules/hcm-compensation/spk-assembly/metadata/entities/hcm_salary_planning_entry.json +41 -0
- package/bundle/examples/backend/modules/hcm-compensation/spk-assembly/metadata/page/salary-planning.json +1 -0
- package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/page/promotions.json +10944 -0
- package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.movement.json +44 -0
- package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.promotion-acknowledge.json +19 -0
- package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.resignation.json +29 -0
- package/bundle/examples/example-patterns.mjs +94 -0
- package/bundle/manifest.json +14 -0
- package/bundle/schemas/action.schema.json +102 -0
- package/bundle/schemas/block-definition.schema.json +222 -0
- package/bundle/schemas/block-instance.schema.json +139 -0
- package/bundle/schemas/composite-pack-manifest.schema.json +160 -0
- package/bundle/schemas/connector-definition.schema.json +29 -0
- package/bundle/schemas/data-service-definition.schema.json +64 -0
- package/bundle/schemas/data-view-definition.schema.json +148 -0
- package/bundle/schemas/entity-aggregation-config.schema.json +32 -0
- package/bundle/schemas/entity-cadence-config.schema.json +27 -0
- package/bundle/schemas/entity-compliance-config.schema.json +31 -0
- package/bundle/schemas/entity-cross-plugin-action-config.schema.json +31 -0
- package/bundle/schemas/entity-definition.schema.json +126 -0
- package/bundle/schemas/entity-document-generator-config.schema.json +29 -0
- package/bundle/schemas/entity-rule-definition.schema.json +75 -0
- package/bundle/schemas/entity-status-date-sweep-config.schema.json +21 -0
- package/bundle/schemas/form.schema.json +281 -0
- package/bundle/schemas/menu.schema.json +106 -0
- package/bundle/schemas/mobile-nav.schema.json +53 -0
- package/bundle/schemas/page-template.schema.json +46 -0
- package/bundle/schemas/page.schema.json +292 -0
- package/bundle/schemas/plugin-manifest.schema.json +139 -0
- package/bundle/schemas/plugin-seed-data.schema.json +24 -0
- package/bundle/schemas/print-template-definition.schema.json +90 -0
- package/bundle/schemas/provider-definition.schema.json +27 -0
- package/bundle/schemas/report-definition.schema.json +96 -0
- package/bundle/schemas/rule.schema.json +82 -0
- package/bundle/schemas/workflow-definition.schema.json +42 -0
- package/bundle/validators/block-engine.mjs +23222 -0
- package/bundle/validators/page-engine.mjs +21645 -0
- package/erp-cli/authoring-root.mjs +344 -0
- package/erp-cli/erp.mjs +4065 -0
- package/erp-cli/example-patterns.mjs +94 -0
- package/erp-mcp-server/index.mjs +818 -0
- package/package.json +27 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A KPI dashboard page
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# A KPI dashboard page
|
|
7
|
+
|
|
8
|
+
## When you need this
|
|
9
|
+
|
|
10
|
+
A page (or the top of a list page) showing several headline numbers computed
|
|
11
|
+
server-side, fetched in **one** call, refreshed after writes.
|
|
12
|
+
|
|
13
|
+
## The pieces
|
|
14
|
+
|
|
15
|
+
### One `count` Data Service per metric
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"name": "oeq-equipment-count-available",
|
|
20
|
+
"definition": {
|
|
21
|
+
"operation": "count",
|
|
22
|
+
"source": { "kind": "entity", "entityName": "oeq_equipment" },
|
|
23
|
+
"filters": [ { "field": "status", "operator": "eq", "value": "AVAILABLE" } ],
|
|
24
|
+
"parameters": []
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### One `composite` Data Service wrapping them
|
|
30
|
+
|
|
31
|
+
`metadata/data_service/oeq-equipment-kpis.json`
|
|
32
|
+
([real file](../tutorial/example-plugin/spk-assembly/metadata/data_service/oeq-equipment-kpis.json)):
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"name": "oeq-equipment-kpis",
|
|
37
|
+
"definition": {
|
|
38
|
+
"operation": "composite",
|
|
39
|
+
"source": { "kind": "entity", "entityName": "oeq_equipment" },
|
|
40
|
+
"steps": [
|
|
41
|
+
{ "as": "total", "service": "oeq-equipment-count-total", "parameters": {} },
|
|
42
|
+
{ "as": "available", "service": "oeq-equipment-count-available", "parameters": {} },
|
|
43
|
+
{ "as": "assigned", "service": "oeq-equipment-count-assigned", "parameters": {} }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### The page mount loader
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
"events": { "mounted": { "source": "action-chain", "actions": [
|
|
53
|
+
{ "id": "a0", "order": 0, "type": "callApi",
|
|
54
|
+
"config": { "connectionRef": "self", "path": "/api/v1/data-services/oeq-equipment-kpis/execute", "httpMethod": "POST", "params": {} }, "output": "kpi" },
|
|
55
|
+
{ "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.kpiTotal", "value": "${kpi.results.total.value}" } },
|
|
56
|
+
{ "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.kpiAvailable", "value": "${kpi.results.available.value}" } },
|
|
57
|
+
{ "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.kpiAssigned", "value": "${kpi.results.assigned.value}" } }
|
|
58
|
+
] } }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### The cards
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"blockType": "core.kpi-card",
|
|
66
|
+
"properties": {
|
|
67
|
+
"value": { "source": "binding", "binding": { "scope": "page", "key": "kpiTotal" } },
|
|
68
|
+
"labelKey": { "source": "static", "value": "office-equipment.equipment-register.kpi.total" },
|
|
69
|
+
"format": { "source": "static", "value": "number" },
|
|
70
|
+
"colorToken": { "source": "static", "value": "primary" }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## The rule that bites
|
|
76
|
+
|
|
77
|
+
`count` → `${out.value}`. `composite` → **`${out.results.<step>.value}`**. Never
|
|
78
|
+
`.total` (that's the unrelated list-pagination envelope).
|
|
79
|
+
|
|
80
|
+
Real response from the tutorial:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{ "results": { "total": { "value": 0 }, "available": { "value": 0 }, "assigned": { "value": 0 } } }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To refresh after a write, append the same `callApi` + `setValue` steps to the
|
|
87
|
+
Save action chain.
|
|
88
|
+
|
|
89
|
+
## Related
|
|
90
|
+
|
|
91
|
+
- [Add a KPI or aggregation](../guides/add-a-kpi.md)
|
|
92
|
+
- [A cross-row aggregation into a summary entity](./cross-row-aggregation.md) — for numbers stored *back* into a table
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A master-detail page with a child grid
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# A master-detail page with a child grid
|
|
7
|
+
|
|
8
|
+
## When you need this
|
|
9
|
+
|
|
10
|
+
A list where clicking a row opens a panel showing that record **plus** a list of
|
|
11
|
+
its children — a category and its equipment types, a maintenance record and its
|
|
12
|
+
line items, a checkout and its history.
|
|
13
|
+
|
|
14
|
+
## The shape
|
|
15
|
+
|
|
16
|
+
1. **The master grid** — a normal `core.grid` wired via `metadata.dataSource`.
|
|
17
|
+
2. **`rowClicked`** on the grid runs an action chain that:
|
|
18
|
+
- fetches the master record → `page.detailRecord`;
|
|
19
|
+
- calls a **parameterized Data Service** for the children, passing the master
|
|
20
|
+
id → `page.childItems`;
|
|
21
|
+
- opens `page.detailDialogOpen`.
|
|
22
|
+
3. **A `core.dialog`** bound to `page.detailDialogOpen` containing an overview
|
|
23
|
+
card bound to `page.detailRecord.*` and a `core.list` (or nested `core.grid`)
|
|
24
|
+
bound to `page.childItems`.
|
|
25
|
+
|
|
26
|
+
## The `rowClicked` chain
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
"events": { "rowClicked": { "source": "action-chain", "actions": [
|
|
30
|
+
{ "id": "a0", "order": 0, "type": "callApi",
|
|
31
|
+
"config": { "connectionRef": "self", "path": "/api/v1/entities/oeq_category/records/${event.id}", "httpMethod": "GET", "params": {} },
|
|
32
|
+
"output": "detail" },
|
|
33
|
+
{ "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.detailRecord", "value": "${detail}" } },
|
|
34
|
+
{ "id": "a2", "order": 2, "type": "callApi",
|
|
35
|
+
"config": { "connectionRef": "self", "path": "/api/v1/data-services/oeq-type-list/execute", "httpMethod": "POST",
|
|
36
|
+
"params": { "parameters": { "parentId": "${event.id}" } } },
|
|
37
|
+
"output": "children" },
|
|
38
|
+
{ "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.childItems", "value": "${children.rows}" } },
|
|
39
|
+
{ "id": "a4", "order": 4, "type": "setValue", "config": { "field": "page.detailDialogOpen", "value": true } }
|
|
40
|
+
] } }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## The child Data Service
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"name": "oeq-type-list",
|
|
48
|
+
"definition": {
|
|
49
|
+
"operation": "search",
|
|
50
|
+
"source": { "kind": "entity", "entityName": "oeq_type" },
|
|
51
|
+
"filters": [ { "field": "category_id", "operator": "eq", "value": "${param.parentId}" } ],
|
|
52
|
+
"parameters": [ { "name": "parentId", "type": "string" } ]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## The child list block
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"blockType": "core.list",
|
|
62
|
+
"properties": {
|
|
63
|
+
"items": { "source": "binding", "binding": { "scope": "page", "key": "childItems" } },
|
|
64
|
+
"primaryPath": { "source": "static", "value": "name" },
|
|
65
|
+
"secondaryPath": { "source": "static", "value": "code" }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## The rules that bite
|
|
71
|
+
|
|
72
|
+
- **`${event.id}`** is the clicked master row's id — available in `rowClicked`.
|
|
73
|
+
- A parameterized Data Service is called with args nested under
|
|
74
|
+
**`parameters`**: `{ "params": { "parameters": { "parentId": "..." } } }`.
|
|
75
|
+
- Bind the child list to `${children.rows}` (or `.items` / `.records` — check
|
|
76
|
+
the service's response envelope).
|
|
77
|
+
- To let the panel **add** a child, put a small form + "Add" button in the same
|
|
78
|
+
dialog whose chain `POST`s to the child entity then re-runs the child Data
|
|
79
|
+
Service. The tutorial's Equipment Catalog page does exactly this.
|
|
80
|
+
|
|
81
|
+
## Related
|
|
82
|
+
|
|
83
|
+
- [Build a page](../guides/build-a-page.md)
|
|
84
|
+
- [Add a data provider, data view, or data service](../guides/add-a-data-provider.md)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: An N-days-before reminder job
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# An N-days-before reminder job
|
|
7
|
+
|
|
8
|
+
## When you need this
|
|
9
|
+
|
|
10
|
+
"Remind me N days before `<date>`." Warranty expiring in 30 days, an audit due in
|
|
11
|
+
7, a checkout due back in 3. Zero Java — one `entity_status_date_sweep_config`
|
|
12
|
+
row with `day_offset` set, shipped as seed data.
|
|
13
|
+
|
|
14
|
+
## The file
|
|
15
|
+
|
|
16
|
+
`metadata/seed-data/office-equipment-sweep-configs.json`
|
|
17
|
+
([real file](../tutorial/example-plugin/spk-assembly/metadata/seed-data/office-equipment-sweep-configs.json)):
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"entity": "entity_status_date_sweep_config",
|
|
22
|
+
"keyFields": ["entity_name", "date_field", "set_status_to", "day_offset"],
|
|
23
|
+
"source": "office-equipment",
|
|
24
|
+
"rows": [
|
|
25
|
+
{
|
|
26
|
+
"entity_name": "oeq_checkout",
|
|
27
|
+
"status_field": "status",
|
|
28
|
+
"when_status_in": "CHECKED_OUT",
|
|
29
|
+
"date_field": "due_date",
|
|
30
|
+
"compare_op": "lte",
|
|
31
|
+
"day_offset": 3,
|
|
32
|
+
"set_status_to": "DUE_SOON",
|
|
33
|
+
"active": true,
|
|
34
|
+
"seeded_by": "office-equipment"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How to read it
|
|
41
|
+
|
|
42
|
+
`compare_op: "lte"` + `day_offset: 3` → the job fires for a `CHECKED_OUT`
|
|
43
|
+
checkout whose `due_date` is **on or before `now + 3 days`** — i.e. "within 3
|
|
44
|
+
days of due". `day_offset: 30` = "within 30 days". `day_offset: 0` (the default)
|
|
45
|
+
= "the date has already passed".
|
|
46
|
+
|
|
47
|
+
Use a **distinct target status per window** if you want 30/14/7-day reminders
|
|
48
|
+
that are independently queryable — e.g. `DUE_IN_30`, `DUE_IN_14`, `DUE_SOON`.
|
|
49
|
+
|
|
50
|
+
## The rules that bite
|
|
51
|
+
|
|
52
|
+
- **Flip a status value, not a boolean flag.** The generic job passes
|
|
53
|
+
`set_status_to` as a string; writing `"true"` to a boolean column fails.
|
|
54
|
+
- Include `day_offset` in `keyFields` so a 30-day and a 7-day rule for the same
|
|
55
|
+
date column are distinct on re-install.
|
|
56
|
+
- Also ship `metadata/entities/entity_status_date_sweep_config.json` (additive
|
|
57
|
+
fields only). The job auto-registers on the first seeded row via a
|
|
58
|
+
platform-shipped rule — you don't ship a register rule for *this* job.
|
|
59
|
+
|
|
60
|
+
## Verified
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
$ erp api post "/api/v1/jobs/engine-entity.status-date-sweep/execute" --body "{}"
|
|
64
|
+
{ "executionId": 534 }
|
|
65
|
+
$ erp api get "/api/v1/entities/oeq_checkout/records/2"
|
|
66
|
+
{ "checkout_number": "CO-100", "status": "OVERDUE" } # was CHECKED_OUT, due_date in the past
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Related
|
|
70
|
+
|
|
71
|
+
- [Add a scheduled reminder job](../guides/add-a-reminder-job.md)
|
|
72
|
+
- Tutorial chapter 7: [the return-due reminder job](../tutorial/07-return-due-reminder-job.md)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A per-row document artifact generator
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# A per-row document artifact generator
|
|
7
|
+
|
|
8
|
+
## When you need this
|
|
9
|
+
|
|
10
|
+
"Generate a document per completed record and stash the file" — an audit report
|
|
11
|
+
per finished audit, a handover certificate per completed checkout. Zero Java —
|
|
12
|
+
one `entity_document_generator_config` row run by
|
|
13
|
+
`engine-entity.document-generator`.
|
|
14
|
+
|
|
15
|
+
## The file
|
|
16
|
+
|
|
17
|
+
`metadata/seed-data/office-equipment-document-configs.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"entity": "entity_document_generator_config",
|
|
22
|
+
"keyFields": ["document_code"],
|
|
23
|
+
"source": "office-equipment",
|
|
24
|
+
"rows": [
|
|
25
|
+
{
|
|
26
|
+
"document_code": "equipment-audit-report",
|
|
27
|
+
"source_entity": "oeq_audit",
|
|
28
|
+
"when_status_in": "COMPLETED",
|
|
29
|
+
"target_file_field": "report_file_ref_id",
|
|
30
|
+
"format": "json",
|
|
31
|
+
"cabinet_id": 42,
|
|
32
|
+
"child_entity": "oeq_audit_item",
|
|
33
|
+
"child_match_field": "audit_id",
|
|
34
|
+
"file_name_template": "equipment-audit-${id}-${date}.json",
|
|
35
|
+
"active": true
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## How to read it
|
|
42
|
+
|
|
43
|
+
For each `oeq_audit` in status `COMPLETED` that doesn't already have a file
|
|
44
|
+
reference in `report_file_ref_id` (a **text** column — its non-blank state is the
|
|
45
|
+
dedupe marker), the job builds a structured document from the audit row plus its
|
|
46
|
+
`oeq_audit_item` children, stores it in DMS cabinet `42`, and writes the file id
|
|
47
|
+
back into `report_file_ref_id`.
|
|
48
|
+
|
|
49
|
+
## The limitation to know
|
|
50
|
+
|
|
51
|
+
The platform's PDF render path only renders synthetic sample data via the Studio
|
|
52
|
+
app — it can't be driven headless against a real record. So this job emits a
|
|
53
|
+
**structured-data artifact** (`json` or `csv`), not a rendered PDF.
|
|
54
|
+
`template_ref` is reserved for when real-record binding ships.
|
|
55
|
+
|
|
56
|
+
## Also ship
|
|
57
|
+
|
|
58
|
+
- `cabinet_id` must be a real DMS cabinet id in your tenant.
|
|
59
|
+
- The shared `entity_document_generator_config.json` (additive) and an
|
|
60
|
+
`AFTER_CREATE` register rule calling
|
|
61
|
+
`ensureEntityDocumentGeneratorJobRegistered`.
|
|
62
|
+
|
|
63
|
+
## Related
|
|
64
|
+
|
|
65
|
+
- `erp examples patterns --kind jobs` → `per-row-document-artifact-generation`
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A searchable related-record lookup field
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# A searchable related-record lookup field
|
|
7
|
+
|
|
8
|
+
## When you need this
|
|
9
|
+
|
|
10
|
+
Any form field whose value is another entity's id — `category_id`,
|
|
11
|
+
`equipment_id`, `employee_id`. Use a `core.lookup` block, never a plain text or
|
|
12
|
+
number input that makes the user type a raw id. (An entire module built by AI
|
|
13
|
+
agents once shipped every FK as a bare number input because this pattern was
|
|
14
|
+
undiscoverable.)
|
|
15
|
+
|
|
16
|
+
## The three files
|
|
17
|
+
|
|
18
|
+
### 1. A Data View producing `{ value, label }`
|
|
19
|
+
|
|
20
|
+
`metadata/data_view/oeq-category-select-options-view.json`
|
|
21
|
+
([real file](../tutorial/example-plugin/spk-assembly/metadata/data_view/oeq-category-select-options-view.json)) —
|
|
22
|
+
selects `t.id AS value` and a concatenated label. The output columns **must** be
|
|
23
|
+
named `value` and `label`.
|
|
24
|
+
|
|
25
|
+
### 2. A `search` Data Service over that view
|
|
26
|
+
|
|
27
|
+
`metadata/data_service/oeq-category-search.json`
|
|
28
|
+
([real file](../tutorial/example-plugin/spk-assembly/metadata/data_service/oeq-category-search.json)):
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"name": "oeq-category-search",
|
|
33
|
+
"definition": {
|
|
34
|
+
"operation": "search",
|
|
35
|
+
"source": { "kind": "dataView", "dataViewName": "oeq-category-select-options-view" },
|
|
36
|
+
"filters": [ { "field": "t.category_name", "operator": "contains", "value": "${param.search}" } ],
|
|
37
|
+
"parameters": [ { "name": "search", "type": "string" } ]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. The `core.lookup` block on the page
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"blockType": "core.lookup",
|
|
47
|
+
"properties": {
|
|
48
|
+
"value": { "source": "binding", "binding": { "scope": "page", "key": "formCategoryId" } },
|
|
49
|
+
"recordType": { "source": "static", "value": "OeqCategory" },
|
|
50
|
+
"optionsSourceKey": { "source": "static", "value": "oeq-category-search" }
|
|
51
|
+
},
|
|
52
|
+
"events": { "committed": { "source": "action-chain", "actions": [
|
|
53
|
+
{ "id": "a0", "order": 0, "type": "setValue",
|
|
54
|
+
"config": { "field": "page.formCategoryId", "value": "${event.new}" } }
|
|
55
|
+
] } }
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## The rules
|
|
60
|
+
|
|
61
|
+
- `optionsSourceKey` = the `name` of the `search` Data Service. This is the real
|
|
62
|
+
wiring.
|
|
63
|
+
- `recordType` = a documentation-only label; it doesn't route anything.
|
|
64
|
+
- `committed` fires with **`event.new`** = the picked record's **id**. Not
|
|
65
|
+
`event.value`, not the label.
|
|
66
|
+
- If the target entity has no search-shaped Data Service yet, build files 1 and 2
|
|
67
|
+
first — don't fall back to a text input.
|
|
68
|
+
- For a small, static option set, `core.select` with inline `options` is simpler
|
|
69
|
+
and needs none of this.
|
|
70
|
+
|
|
71
|
+
## Related
|
|
72
|
+
|
|
73
|
+
- [Add a create / edit form](../guides/add-a-create-edit-form.md)
|
|
74
|
+
- [A resolved reference column in a grid](./resolved-reference-grid-column.md)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A resolved reference column in a grid
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# A resolved reference column in a grid
|
|
7
|
+
|
|
8
|
+
## When you need this
|
|
9
|
+
|
|
10
|
+
A grid column (or a detail-view field) that should show a person's or record's
|
|
11
|
+
**name**, but the entity only stores the **id**. Don't bind the raw `*_id` — bind
|
|
12
|
+
the automatically-resolved `*_id_label` sibling.
|
|
13
|
+
|
|
14
|
+
## The one line on the entity field
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"fieldName": "employee_id",
|
|
19
|
+
"label": "\"Employee Id\"",
|
|
20
|
+
"dataType": "long",
|
|
21
|
+
"flags": "{\"reference\":{\"entityType\":\"Employee\"}}"
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That `flags` string turns on id → display-name resolution platform-wide. From
|
|
26
|
+
then on, **every** read of the entity —
|
|
27
|
+
`GET .../records/query` (the grid) and `GET .../records/{id}` (single record) —
|
|
28
|
+
gains a sibling key `employee_id_label`, resolved in one batched call per page
|
|
29
|
+
load, not per row.
|
|
30
|
+
|
|
31
|
+
**Verified live** in the tutorial:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
$ erp api post "/api/v1/entities/oeq_checkout/records" --body '{"...":"...","employee_id":1,"...":"..."}'
|
|
35
|
+
{ "id": 1, "employee_id": 1, "employee_id_label": "Aria Chen", ... }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## The grid column
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{ "name": "employee_id_label", "type": "string",
|
|
42
|
+
"headerKey": "office-equipment.equipment-checkout.column.employee" }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Bind `employee_id_label`, not `employee_id`.
|
|
46
|
+
|
|
47
|
+
## The rules
|
|
48
|
+
|
|
49
|
+
- `entityType` must be a **registered** provider — one that exposes a
|
|
50
|
+
`batch-names` endpoint. `"Employee"` (backed by the HCM employee plugin) is the
|
|
51
|
+
only one shipped today. An unregistered `entityType` simply produces no
|
|
52
|
+
`_label` key — no error.
|
|
53
|
+
- For a foreign key to **your own** entity (no registered provider), denormalize
|
|
54
|
+
instead: store `category_name` next to `category_id` and write both on create.
|
|
55
|
+
That's what the tutorial's `oeq_equipment` does.
|
|
56
|
+
- You can add the flag to a field after the fact via
|
|
57
|
+
`PUT /api/v1/entities/{id}/fields/{fieldId}` — the `_label` key appears on the
|
|
58
|
+
next read.
|
|
59
|
+
|
|
60
|
+
## Related
|
|
61
|
+
|
|
62
|
+
- [Add an entity](../guides/add-an-entity.md)
|
|
63
|
+
- [A searchable related-record lookup field](./related-record-lookup-field.md)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A scheduled record-generation cadence
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# A scheduled record-generation cadence
|
|
7
|
+
|
|
8
|
+
## When you need this
|
|
9
|
+
|
|
10
|
+
"Create new records on a schedule." A quarterly audit, a monthly review, a
|
|
11
|
+
follow-up task per due row. Zero Java — one `entity_cadence_config` row run by
|
|
12
|
+
`engine-entity.cadence-generator`.
|
|
13
|
+
|
|
14
|
+
## Standalone mode — one record per interval
|
|
15
|
+
|
|
16
|
+
`metadata/seed-data/office-equipment-cadence-configs.json`
|
|
17
|
+
([real file](../examples/standalone-plugin/spk-assembly/metadata/seed-data/office-equipment-cadence-configs.json)):
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"entity": "entity_cadence_config",
|
|
22
|
+
"keyFields": ["cadence_code"],
|
|
23
|
+
"source": "office-equipment",
|
|
24
|
+
"rows": [
|
|
25
|
+
{
|
|
26
|
+
"cadence_code": "equipment-audit-schedule",
|
|
27
|
+
"target_entity": "oeq_audit",
|
|
28
|
+
"interval_days": 90,
|
|
29
|
+
"dedupe_target_field": "cadence_marker",
|
|
30
|
+
"template_json": {
|
|
31
|
+
"audit_number": "AUDIT-${date}-${ts}",
|
|
32
|
+
"audit_name": "Scheduled Equipment Audit ${date}",
|
|
33
|
+
"status": "DRAFT"
|
|
34
|
+
},
|
|
35
|
+
"active": true
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Every 90 days (tracked via `last_generated_at`) one `oeq_audit` row is created
|
|
42
|
+
from `template_json`. String substitutions: `${date}`, `${ts}`, `${uuid}`.
|
|
43
|
+
|
|
44
|
+
## Source-driven mode — one record per due source row
|
|
45
|
+
|
|
46
|
+
Add:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
"source_entity": "oeq_equipment",
|
|
50
|
+
"source_date_field": "warranty_expiry_date",
|
|
51
|
+
"due_within_days": 30,
|
|
52
|
+
"field_map_json": { "equipment_id": "id", "equipment_tag": "asset_tag" },
|
|
53
|
+
"dedupe_target_field": "equipment_id"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
→ for every `oeq_equipment` whose warranty expires within 30 days and which
|
|
57
|
+
doesn't already have a generated row (deduped on `equipment_id`), create one,
|
|
58
|
+
copying the mapped fields. `${sourceId}` is available in `template_json`.
|
|
59
|
+
|
|
60
|
+
## Also ship
|
|
61
|
+
|
|
62
|
+
- the target entity, the shared `entity_cadence_config.json` (additive), and
|
|
63
|
+
- `metadata/rules/ensure_cadence_generator_job_registered.json`
|
|
64
|
+
(`AFTER_CREATE` → `ensureEntityCadenceGeneratorJobRegistered`).
|
|
65
|
+
|
|
66
|
+
## Related
|
|
67
|
+
|
|
68
|
+
- [Add a cadence-generator job](../guides/add-a-cadence-job.md)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: action schema
|
|
3
|
+
audience: tenant
|
|
4
|
+
generated: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# `action` schema
|
|
8
|
+
|
|
9
|
+
A page-JSON/UI action-chain definition (frontend action engine — distinct from the server-side Action Engine, see ai/patterns/action-engine-server-side.md).
|
|
10
|
+
|
|
11
|
+
Pull the full JSON Schema: `erp schema pull action` · MCP: `erp_get_schema {"name":"action"}`
|
|
12
|
+
|
|
13
|
+
_This schema's root is not a plain object (see the full schema for its shape)._
|
|
14
|
+
|
|
15
|
+
## Named sub-definitions
|
|
16
|
+
|
|
17
|
+
`actionDefinition`, `retryPolicy`, `errorStrategy`
|
|
18
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: block-definition schema
|
|
3
|
+
audience: tenant
|
|
4
|
+
generated: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# `block-definition` schema
|
|
8
|
+
|
|
9
|
+
A block TYPE's own declared property/event contract (what core.grid, core.container, etc. accept).
|
|
10
|
+
|
|
11
|
+
Pull the full JSON Schema: `erp schema pull block-definition` · MCP: `erp_get_schema {"name":"block-definition"}`
|
|
12
|
+
|
|
13
|
+
## Top-level properties
|
|
14
|
+
|
|
15
|
+
| Property | Type | Required | Notes |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| `contractVersion` | — | yes | |
|
|
18
|
+
| `type` | string | yes | Globally unique block-type id, dot-namespaced (e.g. core.text-input, acme-hr.org-chart). |
|
|
19
|
+
| `version` | string | yes | Semver. Instances pin the version they were authored against; minor/patch upgrades apply automatically, major upgrades require an explicit migration (W1-05). |
|
|
20
|
+
| `publisher` | string | yes | The Phase 21 marketplace publisher id. |
|
|
21
|
+
| `inputClass` | boolean | | True for input-class blocks: the block inherits the validation-message surface (invalid state + message slot + aria-invalid/aria-describedby) from the base contract, which Phase 6's validation pipeline populates (round 2). |
|
|
22
|
+
| `properties` | array | yes | |
|
|
23
|
+
| `inputs` | array | | |
|
|
24
|
+
| `outputs` | array | | |
|
|
25
|
+
| `events` | array | yes | |
|
|
26
|
+
| `permissions` | object | yes | Which permission hook points the block honors (W2-05). Enforcement lands in Phase 11; the hooks are in the render path from day one. |
|
|
27
|
+
| `a11y` | object | yes | |
|
|
28
|
+
| `designer` | object | yes | Block-level designer metadata: what Phase 10's palette and property panel render for a block they have never seen (gap audit). |
|
|
29
|
+
| `slots` | array | | Container-class blocks only: named child slots and what they may contain. |
|
|
30
|
+
| `composition` | array | | 2026-07-18: present only on tenant-authored composite Block definitions (Studio's Block Designer) - the block's own fixed internal content, an array of frozen @erp/block-engine BlockInstanceConfig, validated against the live BlockRegistry at mount time exactly like a page item's or a container's own children. undefined for every core/plugin block. |
|
|
31
|
+
|
|
32
|
+
## Named sub-definitions
|
|
33
|
+
|
|
34
|
+
`identifier`, `i18nKey`, `propertyType`, `valueSourceKind`, `selectOption`, `propertySpec`, `portSpec`, `eventSpec`
|
|
35
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: block-instance schema
|
|
3
|
+
audience: tenant
|
|
4
|
+
generated: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# `block-instance` schema
|
|
8
|
+
|
|
9
|
+
One block INSTANCE inside a page (instanceId/blockType/properties/events) — same shape validate-page-properties.mjs checks.
|
|
10
|
+
|
|
11
|
+
Pull the full JSON Schema: `erp schema pull block-instance` · MCP: `erp_get_schema {"name":"block-instance"}`
|
|
12
|
+
|
|
13
|
+
## Top-level properties
|
|
14
|
+
|
|
15
|
+
| Property | Type | Required | Notes |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| `contractVersion` | — | yes | |
|
|
18
|
+
| `instanceId` | string | yes | The block's instance identity - also its Phase 4 block-scope scopeKey. |
|
|
19
|
+
| `blockType` | string | yes | |
|
|
20
|
+
| `blockVersion` | string | yes | The version this instance was authored against (the pin, W1-05). |
|
|
21
|
+
| `properties` | object | yes | |
|
|
22
|
+
| `events` | object | | |
|
|
23
|
+
| `outputs` | object | | Optional output redirection: where each output port writes. Defaults to the block's own block-scope entry named after the port. |
|
|
24
|
+
| `children` | object | | Container-class blocks only: slot name to nested instance configs. |
|
|
25
|
+
| `a11yLabelKey` | string | | W10 (additive, 2026-07-16): the authored label's translation key for this placed instance (a Phase 6 field's labelKey), resolved through the runtime i18n resolver into the node's accessible name; takes precedence over the block definition's labelFrom default. |
|
|
26
|
+
| `name` | string | | 2026-07-18: an author-facing display name (e.g. "Header block"), never read at render/runtime — a Studio authoring aid only. |
|
|
27
|
+
| `className` | string | | 2026-07-17: a framework-specific class name applied to the rendered root, the universal escape hatch alongside style. |
|
|
28
|
+
| `style` | object | | 2026-07-17: raw inline style properties applied to the rendered root, the universal escape hatch alongside className. |
|
|
29
|
+
|
|
30
|
+
## Named sub-definitions
|
|
31
|
+
|
|
32
|
+
`boundKey`, `propertyValue`, `eventWiring`
|
|
33
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: composite-pack-manifest schema
|
|
3
|
+
audience: tenant
|
|
4
|
+
generated: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# `composite-pack-manifest` schema
|
|
8
|
+
|
|
9
|
+
The newer composite-pack / Country-or-Industry-Pack / Code-Plugin-style manifest contract (contractVersion/publisher/distribution/12 extension-point contributions) — a genuinely different artifact type from an ordinary .spk plugin.json (use 'plugin-manifest' for that).
|
|
10
|
+
|
|
11
|
+
Pull the full JSON Schema: `erp schema pull composite-pack-manifest` · MCP: `erp_get_schema {"name":"composite-pack-manifest"}`
|
|
12
|
+
|
|
13
|
+
## Top-level properties
|
|
14
|
+
|
|
15
|
+
| Property | Type | Required | Notes |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| `contractVersion` | — | yes | |
|
|
18
|
+
| `id` | `#/$defs/identifier` | yes | |
|
|
19
|
+
| `version` | `#/$defs/semver` | yes | |
|
|
20
|
+
| `publisher` | string | yes | |
|
|
21
|
+
| `displayNameKey` | string | yes | |
|
|
22
|
+
| `descriptionKey` | string | yes | |
|
|
23
|
+
| `distribution` | enum | yes | one of: `locked`, `editable` |
|
|
24
|
+
| `dependencies` | array | | |
|
|
25
|
+
| `contributions` | `#/$defs/contributions` | yes | |
|
|
26
|
+
| `hasUpgradeMigration` | boolean | | |
|
|
27
|
+
| `hasDataErasureHook` | boolean | | |
|
|
28
|
+
| `frontendBundle` | object | | 2026-09-03, closes the dynamic-frontend-plugin-loading gap (ai/domains/ai-native-developer-platform-gap-analysis.md): a compiled @erp/* frontend bundle a runtime loader fetches from the real, generic engine-file content endpoint (GET /api/v1/files/{fileId}/content) and dynamically import()s, instead of the code shipping inside a host application's own build. Optional and additive - absent on every manifest that predates this field (every real plugin in the fleet today except this pass's own proof plugin), which a frontend loader must treat as a cheap, silent no-op. |
|
|
29
|
+
|
|
30
|
+
## Named sub-definitions
|
|
31
|
+
|
|
32
|
+
`identifier`, `semver`, `versionRange`, `dependency`, `argKind`, `actionTypeContribution`, `functionContribution`, `idDescription`, `networkScopedContribution`, `scheduledJobContribution`, `themeContribution`, `menuItemContribution`, `localeBundleContribution`, `contributions`
|
|
33
|
+
|