@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,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Add an entity
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add an entity
|
|
7
|
+
|
|
8
|
+
## What you're doing
|
|
9
|
+
|
|
10
|
+
An **entity** is a database table you declare instead of writing SQL. You list
|
|
11
|
+
its **fields** and their **data types**; the platform's Entity Engine generates
|
|
12
|
+
the `CREATE TABLE`, the CRUD REST endpoints (`/api/v1/entities/<name>/records`),
|
|
13
|
+
the grid query endpoint, and single-record reads. Zero persistence code.
|
|
14
|
+
|
|
15
|
+
One entity = one file under `spk-assembly/metadata/entities/`.
|
|
16
|
+
|
|
17
|
+
## The complete example
|
|
18
|
+
|
|
19
|
+
`spk-assembly/metadata/entities/oeq_equipment.json` from the tutorial module —
|
|
20
|
+
[the real file](../tutorial/example-plugin/spk-assembly/metadata/entities/oeq_equipment.json):
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"entity": {
|
|
25
|
+
"name": "oeq_equipment",
|
|
26
|
+
"tableName": "oeq_equipment",
|
|
27
|
+
"label": "{\"en\":\"Equipment\"}",
|
|
28
|
+
"category": "office-equipment",
|
|
29
|
+
"icon": "devices",
|
|
30
|
+
"color": "#2563EB",
|
|
31
|
+
"pkStrategy": "identity"
|
|
32
|
+
},
|
|
33
|
+
"fields": [
|
|
34
|
+
{ "fieldName": "asset_tag", "label": "\"Asset Tag\"", "dataType": "text", "nullable": false, "required": true, "isUnique": true, "isIndexed": true, "isDisplayField": true, "displayOrder": 1 },
|
|
35
|
+
{ "fieldName": "name", "label": "\"Name\"", "dataType": "text", "nullable": false, "required": true, "displayOrder": 2 },
|
|
36
|
+
{ "fieldName": "category_id", "label": "\"Category Id\"", "dataType": "long", "nullable": true, "required": false, "isIndexed": true, "displayOrder": 3 },
|
|
37
|
+
{ "fieldName": "category_name", "label": "\"Category\"", "dataType": "text", "nullable": true, "required": false, "displayOrder": 4 },
|
|
38
|
+
{ "fieldName": "serial_number", "label": "\"Serial Number\"", "dataType": "text", "nullable": true, "required": false, "displayOrder": 5 },
|
|
39
|
+
{ "fieldName": "status", "label": "\"Status\"", "dataType": "enum", "nullable": false, "required": true, "isIndexed": true, "defaultValue": "AVAILABLE", "typeParams": "{\"enumValues\":[\"AVAILABLE\",\"ASSIGNED\",\"MAINTENANCE\",\"RETIRED\"]}", "displayOrder": 6 },
|
|
40
|
+
{ "fieldName": "purchase_date", "label": "\"Purchase Date\"", "dataType": "date", "nullable": true, "required": false, "displayOrder": 7 },
|
|
41
|
+
{ "fieldName": "warranty_expiry_date", "label": "\"Warranty Expiry Date\"", "dataType": "date", "nullable": true, "required": false, "displayOrder": 8 },
|
|
42
|
+
{ "fieldName": "notes", "label": "\"Notes\"", "dataType": "text", "nullable": true, "required": false, "displayOrder": 9 }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Line by line
|
|
48
|
+
|
|
49
|
+
### The `entity` block
|
|
50
|
+
|
|
51
|
+
- **`name`** — lowercase, `snake_case`, starts with a letter. This is the table
|
|
52
|
+
name and the id in every REST path. **Prefix it** (`oeq_`) so it never
|
|
53
|
+
collides with another plugin's table.
|
|
54
|
+
- **`label`** — this column is stored as JSON in the database, so the *string*
|
|
55
|
+
must itself be valid JSON: `"{\"en\":\"Equipment\"}"` (an i18n object) — **not**
|
|
56
|
+
`"Equipment"`. A bare string fails the install with `invalid input syntax for
|
|
57
|
+
type json`. This is the single most common entity mistake.
|
|
58
|
+
- **`pkStrategy`** — `identity` gives an auto-incrementing integer `id`.
|
|
59
|
+
`snowflake` gives a distributed 64-bit id. Use `identity` unless you have a
|
|
60
|
+
reason not to.
|
|
61
|
+
- **`icon` / `color` / `category`** — cosmetic, used by Studio's explorer tree.
|
|
62
|
+
|
|
63
|
+
### Each `field`
|
|
64
|
+
|
|
65
|
+
- **`fieldName`** — `snake_case`, starts with a letter.
|
|
66
|
+
- **`label`** — **also pre-stringified JSON.** `"\"Asset Tag\""` is the JSON
|
|
67
|
+
string literal `"Asset Tag"`. A bare `"Asset Tag"` fails the install.
|
|
68
|
+
- **`dataType`** — accepted case-insensitively; every shipped file uses
|
|
69
|
+
lowercase. The ones you'll actually use:
|
|
70
|
+
|
|
71
|
+
| `dataType` | Postgres | notes |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `text` | `text` | any string, short or long. **Not `long`** — that's the 64-bit integer type. |
|
|
74
|
+
| `integer` | `int` | 32-bit |
|
|
75
|
+
| `long` | `bigint` | 64-bit; use for foreign-key id columns |
|
|
76
|
+
| `decimal` / `currency` | `numeric` | money |
|
|
77
|
+
| `boolean` | `boolean` | |
|
|
78
|
+
| `date` | `date` | |
|
|
79
|
+
| `datetime` | `timestamptz` | |
|
|
80
|
+
| `enum` | `text` + check | needs `typeParams` (below) |
|
|
81
|
+
|
|
82
|
+
- **`typeParams`** — **also a pre-stringified JSON string.** For an enum:
|
|
83
|
+
`"{\"enumValues\":[\"AVAILABLE\",\"ASSIGNED\"]}"`.
|
|
84
|
+
- **`nullable` / `required`** — a `required: true` field **must** also be
|
|
85
|
+
`nullable: false`. A `required: false` field **must** be `nullable: true`. A
|
|
86
|
+
mismatched pair produces a broken `ALTER TABLE` on a table that already has
|
|
87
|
+
rows.
|
|
88
|
+
- **`isUnique`**, **`isIndexed`** — add a unique constraint / index (scoped to
|
|
89
|
+
the tenant).
|
|
90
|
+
- **`isDisplayField: true`** — marks the human-readable "name" column. Pick
|
|
91
|
+
exactly one.
|
|
92
|
+
- **`defaultValue`** — always a string, even for numbers/booleans:
|
|
93
|
+
`"10"`, `"true"`, `"AVAILABLE"`.
|
|
94
|
+
|
|
95
|
+
### Foreign keys and automatic labels
|
|
96
|
+
|
|
97
|
+
`category_id` is a `long` holding another table's `id`. Two ways to show a name
|
|
98
|
+
instead of a raw number:
|
|
99
|
+
|
|
100
|
+
1. **Denormalize** — also store `category_name` and write both when you create a
|
|
101
|
+
row (what the tutorial does — simplest).
|
|
102
|
+
2. **`reference` flag** — add
|
|
103
|
+
`"flags": "{\"reference\":{\"entityType\":\"Employee\"}}"` to the id field.
|
|
104
|
+
Every read then gains a sibling `<field>_label` key
|
|
105
|
+
(`employee_id` → `employee_id_label`) resolved automatically, batched once per
|
|
106
|
+
page load. `entityType` must be a **registered** provider — `"Employee"` is
|
|
107
|
+
the only one shipped today. See
|
|
108
|
+
[resolved reference column](../recipes/resolved-reference-grid-column.md).
|
|
109
|
+
|
|
110
|
+
The tutorial's `oeq_checkout` entity uses the `reference` flag on `employee_id`
|
|
111
|
+
and it was verified live to return `"employee_id_label": "Aria Chen"`.
|
|
112
|
+
|
|
113
|
+
## Ground yourself first
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
erp schema pull entity-definition # the full contract
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or over MCP: `erp_get_schema {"name":"entity-definition"}`.
|
|
120
|
+
|
|
121
|
+
## Two file shapes both validate
|
|
122
|
+
|
|
123
|
+
- **`{ "entity": { ... }, "fields": [ ... ] }`** — the packaged artifact shape,
|
|
124
|
+
used in `spk-assembly/metadata/entities/`. Use this.
|
|
125
|
+
- **flat** (`{ "name": ..., "label": ..., "fields": [ ... ] }`) — the live
|
|
126
|
+
`POST /api/v1/entities` request body. Only matters if you're calling the API
|
|
127
|
+
directly.
|
|
128
|
+
|
|
129
|
+
## How to verify it worked
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
erp schema validate spk-assembly/metadata/entities/oeq_equipment.json --schema entity-definition
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
OK — spk-assembly/metadata/entities/oeq_equipment.json matches schema "entity-definition"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
After publishing, the table is live:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
erp api get "/api/v1/entities/oeq_equipment/records/query?size=5"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{ "rows": [], "total": 0 }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
And a record you create comes back with audit columns the engine added for free:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{ "id": 1, "created_by": "you@example.com", "created_at": "2026-09-10T08:08:33.282+00:00",
|
|
153
|
+
"asset_tag": "LAP-001", "name": "Dell Latitude 7440", "status": "AVAILABLE" }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Common mistakes
|
|
157
|
+
|
|
158
|
+
| Symptom | Cause | Fix |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| install fails `invalid input syntax for type json` | `label` is a bare string | make it `"{\"en\":\"...\"}"` (entity) or `"\"...\""` (field) |
|
|
161
|
+
| a field silently missing after install | `required:false` without `nullable:true` on a table with rows | pair them correctly, bump version, republish |
|
|
162
|
+
| big text truncated / rejected | `dataType: "long"` | that's the integer type; use `"text"` |
|
|
163
|
+
| enum column rejects every value | `typeParams` is a nested object, not a string | pre-stringify it |
|
|
164
|
+
| grid shows a raw number for a person | bound to `employee_id` not `employee_id_label` | add the `reference` flag and bind the `_label` |
|
|
165
|
+
|
|
166
|
+
## What to read next
|
|
167
|
+
|
|
168
|
+
- [Add a data provider](./add-a-data-provider.md) — so a grid can read the table
|
|
169
|
+
- [Build a page](./build-a-page.md)
|
|
170
|
+
- [Seed reference data on install](./seed-data-on-install.md)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Add business rules and expressions
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add business rules and expressions
|
|
7
|
+
|
|
8
|
+
## What you're doing
|
|
9
|
+
|
|
10
|
+
Attaching declarative logic to an entity's lifecycle: reject an invalid save,
|
|
11
|
+
compute a field, kick off a job or workflow. One rule = one file under
|
|
12
|
+
`spk-assembly/metadata/rules/`, evaluated by the Rule Engine at a lifecycle
|
|
13
|
+
point.
|
|
14
|
+
|
|
15
|
+
## When each action type fires
|
|
16
|
+
|
|
17
|
+
| `triggerEvent` | fires | typical use |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `BEFORE_CREATE` / `BEFORE_UPDATE` | before the write | `REJECT` (validation), `SET_VALUE` (computed/defaulted field) |
|
|
20
|
+
| `AFTER_CREATE` | after insert | `EXECUTE_SERVICE` (register a job on first row), `START_WORKFLOW` |
|
|
21
|
+
| `AFTER_UPDATE` | after update | `START_WORKFLOW` on a status transition |
|
|
22
|
+
| `AFTER_DELETE` | after delete | cleanup fan-out |
|
|
23
|
+
|
|
24
|
+
## The complete example — a validation + a computed field
|
|
25
|
+
|
|
26
|
+
`spk-assembly/metadata/rules/oeq_checkout_dates.json` (a standalone example,
|
|
27
|
+
[real file](../examples/standalone-plugin/spk-assembly/metadata/rules/oeq_checkout_dates.json)):
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"entityType": "oeq_checkout",
|
|
32
|
+
"name": "oeq_checkout_dates",
|
|
33
|
+
"description": "Due date must be on or after the checkout date.",
|
|
34
|
+
"triggerEvent": "BEFORE_CREATE",
|
|
35
|
+
"conditions": { "field": "due_date", "op": "lt", "value": "${record.checkout_date}" },
|
|
36
|
+
"actions": [
|
|
37
|
+
{ "type": "REJECT", "message": "Due date cannot be before the checkout date." }
|
|
38
|
+
],
|
|
39
|
+
"priority": 10,
|
|
40
|
+
"active": true
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> In a `metadata/rules/*.json` file `conditions` and `actions` may be authored as
|
|
45
|
+
> real JSON (as above) **or** as pre-stringified JSON (as the workflow-trigger
|
|
46
|
+
> rule does). Both install. The stringified form matches what the live API
|
|
47
|
+
> stores and returns.
|
|
48
|
+
|
|
49
|
+
## Line by line
|
|
50
|
+
|
|
51
|
+
- **`entityType`** — the entity `name` this rule fires against.
|
|
52
|
+
- **`triggerEvent`** — one of the six lifecycle points above.
|
|
53
|
+
- **`conditions`** — a condition tree. A leaf is `{ field, op, value }`. Compose
|
|
54
|
+
with `{ "all": [...] }`, `{ "any": [...] }`, `{ "not": {...} }`.
|
|
55
|
+
- **operators:** `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `in`,
|
|
56
|
+
`is_null`, `is_not_null`. `is_null` / `is_not_null` take no `value`.
|
|
57
|
+
- **`${record.<field>}`** in a `value` reads another field of the same row.
|
|
58
|
+
- **`${record.<field>__previous`}`** (in an `AFTER_UPDATE` rule) is the value
|
|
59
|
+
before the update.
|
|
60
|
+
- **`actions`** — at least one:
|
|
61
|
+
|
|
62
|
+
| `type` | fields | effect |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `REJECT` | `message` | abort the write, surface `message` to the caller |
|
|
65
|
+
| `SET_VALUE` | `field`, `value` | assign a field before the write |
|
|
66
|
+
| `EXECUTE_SERVICE` | `service` (a registered handler bean) | run a named platform service |
|
|
67
|
+
| `START_WORKFLOW` | `workflowName`, `config`, `callbackUrl` | see [Add an approval workflow](./add-an-approval-workflow.md) |
|
|
68
|
+
|
|
69
|
+
- **`priority`** — lower first. Put `REJECT`/`SET_VALUE` validation rules at `10`,
|
|
70
|
+
workflow triggers at `20`.
|
|
71
|
+
|
|
72
|
+
## Computed fields — two ways
|
|
73
|
+
|
|
74
|
+
1. **A `SET_VALUE` rule** (above) — imperative, runs at a lifecycle point.
|
|
75
|
+
2. **A formula field on the entity** — declarative, computed on every read, no
|
|
76
|
+
physical column:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{ "fieldName": "days_out", "label": "\"Days Out\"", "dataType": "formula",
|
|
80
|
+
"flags": "{\"formula\":\"daysBetween(checkout_date, now())\"}" }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The formula language is the platform `ExpressionEvaluator` — whitelisted
|
|
84
|
+
functions only (`daysBetween`, `now`, `coalesce`, arithmetic, comparisons).
|
|
85
|
+
**No** ternary, **no** `in`, **no** `^`. The same evaluator powers
|
|
86
|
+
compliance-sweep conditions (see [Add a compliance-sweep job](./add-a-compliance-job.md)).
|
|
87
|
+
|
|
88
|
+
## Registering a job on the first config row
|
|
89
|
+
|
|
90
|
+
The pattern jobs use to auto-register themselves — an unconditional
|
|
91
|
+
`AFTER_CREATE` `EXECUTE_SERVICE`:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"entityType": "entity_aggregation_config",
|
|
96
|
+
"name": "ensure_aggregation_sweep_job_registered",
|
|
97
|
+
"triggerEvent": "AFTER_CREATE",
|
|
98
|
+
"conditions": null,
|
|
99
|
+
"actions": "[{\"type\": \"EXECUTE_SERVICE\", \"service\": \"ensureEntityAggregationSweepJobRegistered\"}]",
|
|
100
|
+
"priority": 10,
|
|
101
|
+
"active": true
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`conditions: null` means "always". See [Seed data on install](./seed-data-on-install.md).
|
|
106
|
+
|
|
107
|
+
## Ground yourself first
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
erp schema pull entity-rule-definition
|
|
111
|
+
erp examples patterns --kind entities
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## How to verify it worked
|
|
115
|
+
|
|
116
|
+
After publishing:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
erp api get "/api/v1/entity-rules?entityType=oeq_checkout"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
lists your rule with `"active": true`. Then trigger it:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
erp api post "/api/v1/entities/oeq_checkout/records" \
|
|
126
|
+
--body '{"checkout_number":"CO-9","equipment_id":1,"employee_id":1,"checkout_date":"2026-09-10","due_date":"2026-09-01","status":"DRAFT"}'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{ "error": "Due date cannot be before the checkout date." }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Common mistakes
|
|
134
|
+
|
|
135
|
+
| Symptom | Cause | Fix |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| a `!= null` / `!=null` comparison never matches | action-engine quirk | use `is_not_null` |
|
|
138
|
+
| rule fires on every update, not just the transition | no `__previous` guard | add `{ "field": "status__previous", "op": "neq", "value": "..." }` |
|
|
139
|
+
| `SET_VALUE` doesn't stick | used `AFTER_*` (write already happened) | use `BEFORE_CREATE` / `BEFORE_UPDATE` |
|
|
140
|
+
| formula field errors | used ternary / `in` / unsupported function | rewrite with whitelisted functions only |
|
|
141
|
+
| install rejects the rule file | `actions` is a bare object, not an array/string | `actions` is an array (or a stringified array) |
|
|
142
|
+
|
|
143
|
+
## What to read next
|
|
144
|
+
|
|
145
|
+
- [Add an approval workflow](./add-an-approval-workflow.md)
|
|
146
|
+
- [Add a compliance-sweep job](./add-a-compliance-job.md)
|
|
147
|
+
- [Seed config and reference data on install](./seed-data-on-install.md)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Add translations (i18n)
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add translations (i18n)
|
|
7
|
+
|
|
8
|
+
## What you're doing
|
|
9
|
+
|
|
10
|
+
Every user-visible string on a page is an **i18n key**, not literal text. Page
|
|
11
|
+
titles, button labels, column headers, KPI labels, enum option labels — all
|
|
12
|
+
keys. You supply the actual words in `spk-assembly/metadata/i18n/en.json` (and
|
|
13
|
+
`fr.json`, `de.json`, … for other locales).
|
|
14
|
+
|
|
15
|
+
If a key has no entry, the UI renders the raw key
|
|
16
|
+
(`office-equipment.equipment-register.title`) — ugly, and flagged by the linter.
|
|
17
|
+
|
|
18
|
+
## The naming convention
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
<pluginId>.<page-name>.<what>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Examples from the tutorial:
|
|
25
|
+
|
|
26
|
+
| Key | Value |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `office-equipment.equipment-register.title` | `Equipment Register` |
|
|
29
|
+
| `office-equipment.equipment-register.newBtn` | `New` |
|
|
30
|
+
| `office-equipment.equipment-register.column.asset_tag` | `Asset Tag` |
|
|
31
|
+
| `office-equipment.equipment-register.kpi.available` | `Available` |
|
|
32
|
+
| `office-equipment.equipment-register.status.AVAILABLE` | `Available` |
|
|
33
|
+
| `office-equipment.equipment-checkout.status.OVERDUE` | `Overdue` |
|
|
34
|
+
| `office-equipment.application.title` | `Office Equipment` |
|
|
35
|
+
| `office-equipment.module.title` | `Equipment` |
|
|
36
|
+
|
|
37
|
+
## The complete example
|
|
38
|
+
|
|
39
|
+
`spk-assembly/metadata/i18n/en.json` (excerpt — the
|
|
40
|
+
[real file](../tutorial/example-plugin/spk-assembly/metadata/i18n/en.json) has
|
|
41
|
+
~70 keys):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"office-equipment.application.title": "Office Equipment",
|
|
46
|
+
"office-equipment.module.title": "Equipment",
|
|
47
|
+
"office-equipment.equipment-register.title": "Equipment Register",
|
|
48
|
+
"office-equipment.equipment-register.subtitle": "Office Equipment tutorial module",
|
|
49
|
+
"office-equipment.equipment-register.newBtn": "New",
|
|
50
|
+
"office-equipment.equipment-register.exportBtn": "Export CSV",
|
|
51
|
+
"office-equipment.equipment-register.kpi.total": "Total",
|
|
52
|
+
"office-equipment.equipment-register.kpi.available": "Available",
|
|
53
|
+
"office-equipment.equipment-register.kpi.assigned": "Assigned",
|
|
54
|
+
"office-equipment.equipment-register.column.asset_tag": "Asset Tag",
|
|
55
|
+
"office-equipment.equipment-register.column.name": "Name",
|
|
56
|
+
"office-equipment.equipment-register.column.status": "Status",
|
|
57
|
+
"office-equipment.equipment-register.status.AVAILABLE": "Available",
|
|
58
|
+
"office-equipment.equipment-register.status.ASSIGNED": "Assigned",
|
|
59
|
+
"office-equipment.equipment-register.form.title": "Equipment",
|
|
60
|
+
"office-equipment.equipment-register.form.asset_tag": "Asset Tag",
|
|
61
|
+
"office-equipment.equipment-register.form.saveBtn": "Save",
|
|
62
|
+
"office-equipment.equipment-register.form.cancelBtn": "Cancel"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It's a flat object — dotted keys, string values. No nesting.
|
|
67
|
+
|
|
68
|
+
## Line by line — where each key type comes from
|
|
69
|
+
|
|
70
|
+
| In a page file | i18n key referenced |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `definition.title` | the page title |
|
|
73
|
+
| a block's `properties.textKey` / `labelKey` (with `source: "static"`) | any static label |
|
|
74
|
+
| a grid column's `headerKey` | column header |
|
|
75
|
+
| a `core.select` option's `labelKey` | dropdown option label |
|
|
76
|
+
| `designer.displayNameKey` / `descriptionKey` | Studio explorer label |
|
|
77
|
+
| `metadata/application/*.json` `titleKey`, `metadata/module/*.json` `titleKey` | app / module name |
|
|
78
|
+
|
|
79
|
+
Every one of those must have a matching entry in `en.json`.
|
|
80
|
+
|
|
81
|
+
## Adding another language
|
|
82
|
+
|
|
83
|
+
Add `fr.json` with the same keys, French values. Missing keys in `fr.json` fall
|
|
84
|
+
back to `en.json`. The ERP picks the file by the user's locale.
|
|
85
|
+
|
|
86
|
+
## Keeping it in sync automatically
|
|
87
|
+
|
|
88
|
+
You don't have to hand-track keys. A small script that walks your page/menu/app
|
|
89
|
+
files collecting every `*Key` / `headerKey` / static `title` value, then writes
|
|
90
|
+
`en.json` with a humanised default for any missing key, is the practical
|
|
91
|
+
approach — the tutorial's build uses exactly this. The linter is your safety
|
|
92
|
+
net.
|
|
93
|
+
|
|
94
|
+
## How to verify it worked
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
erp plugin test office-equipment/spk-assembly
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
A missing key is reported as:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
I18N-001 equipment-register.json: i18n key
|
|
104
|
+
"office-equipment.equipment-register.kpi.total" not found in metadata/i18n/en.json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A clean run means every referenced key resolves.
|
|
108
|
+
|
|
109
|
+
## Common mistakes
|
|
110
|
+
|
|
111
|
+
| Symptom | Cause | Fix |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| UI shows `office-equipment.x.y` literally | key missing from `en.json` | add it (linter tells you which) |
|
|
114
|
+
| `en.json` is nested objects | wrong shape | flat `{ "a.b.c": "text" }` |
|
|
115
|
+
| enum chips show the raw enum value | no `...status.<VALUE>` keys | add one per enum value |
|
|
116
|
+
| translation not applied | `fr.json` missing keys, or wrong locale on the user | keys fall back to `en`; check the user's locale |
|
|
117
|
+
|
|
118
|
+
## What to read next
|
|
119
|
+
|
|
120
|
+
- [Build a page](./build-a-page.md)
|
|
121
|
+
- [Validate and test a plugin](./validate-and-test.md)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Add menus
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add menus
|
|
7
|
+
|
|
8
|
+
## What you're doing
|
|
9
|
+
|
|
10
|
+
Putting your pages in the ERP's navigation. A **menu** is a tree of nodes, each
|
|
11
|
+
pointing at a route. One file under `spk-assembly/metadata/menu/`.
|
|
12
|
+
|
|
13
|
+
There is a second, easy-to-miss step: a **master menu scaffold** file that some
|
|
14
|
+
deployments keep as the merged sidenav. If your plugin's module belongs to a
|
|
15
|
+
larger application (e.g. HCM), you update that too — see below.
|
|
16
|
+
|
|
17
|
+
## The complete example
|
|
18
|
+
|
|
19
|
+
`spk-assembly/metadata/menu/office-equipment-menu.json` —
|
|
20
|
+
[real file](../tutorial/example-plugin/spk-assembly/metadata/menu/office-equipment-menu.json):
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"name": "office-equipment-menu",
|
|
25
|
+
"description": "Navigation contributed by the Office Equipment plugin.",
|
|
26
|
+
"definition": {
|
|
27
|
+
"contractVersion": 1,
|
|
28
|
+
"name": "office-equipment-menu",
|
|
29
|
+
"nodes": [
|
|
30
|
+
{
|
|
31
|
+
"id": "office-equipment.equipment",
|
|
32
|
+
"name": "equipment",
|
|
33
|
+
"displayName": "Office Equipment",
|
|
34
|
+
"sequence": 0,
|
|
35
|
+
"navigation": { "route": null, "openMode": "current-tab" },
|
|
36
|
+
"visibility": { "visible": true, "enabled": true, "devices": [] },
|
|
37
|
+
"color": null,
|
|
38
|
+
"children": [
|
|
39
|
+
{
|
|
40
|
+
"id": "office-equipment.equipment.equipment-register",
|
|
41
|
+
"name": "equipment-register",
|
|
42
|
+
"displayName": "Equipment Register",
|
|
43
|
+
"sequence": 0,
|
|
44
|
+
"children": [],
|
|
45
|
+
"navigation": { "route": "/office-equipment/equipment-register", "openMode": "current-tab" },
|
|
46
|
+
"visibility": { "visible": true, "enabled": true, "devices": [] },
|
|
47
|
+
"color": null
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "office-equipment.equipment.equipment-catalog",
|
|
51
|
+
"name": "equipment-catalog",
|
|
52
|
+
"displayName": "Equipment Catalog",
|
|
53
|
+
"sequence": 1,
|
|
54
|
+
"children": [],
|
|
55
|
+
"navigation": { "route": "/office-equipment/equipment-catalog", "openMode": "current-tab" },
|
|
56
|
+
"visibility": { "visible": true, "enabled": true, "devices": [] },
|
|
57
|
+
"color": null
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "office-equipment.equipment.equipment-checkout",
|
|
61
|
+
"name": "equipment-checkout",
|
|
62
|
+
"displayName": "Equipment Checkout",
|
|
63
|
+
"sequence": 2,
|
|
64
|
+
"children": [],
|
|
65
|
+
"navigation": { "route": "/office-equipment/equipment-checkout", "openMode": "current-tab" },
|
|
66
|
+
"visibility": { "visible": true, "enabled": true, "devices": [] },
|
|
67
|
+
"color": null
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"modules": ["office-equipment"]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Line by line
|
|
78
|
+
|
|
79
|
+
- **`nodes`** — the tree. A parent node with `navigation.route: null` is a group
|
|
80
|
+
header; its `children` are the clickable items.
|
|
81
|
+
- **`id`** — dotted, unique, conventionally `<pluginId>.<group>.<page>`.
|
|
82
|
+
- **`name`** — a short slug.
|
|
83
|
+
- **`displayName`** — the label shown. (For a translated label use
|
|
84
|
+
`displayNameKey` with an i18n key instead.)
|
|
85
|
+
- **`sequence`** — sort order among siblings.
|
|
86
|
+
- **`navigation.route`** — the page's `route.pattern`, *exactly*. This is the
|
|
87
|
+
internal routing key, not the browser URL.
|
|
88
|
+
- **`visibility.devices`** — `[]` means all devices; `["mobile"]` restricts.
|
|
89
|
+
- **`modules`** — the module id(s) this menu contributes to. Match your pages'
|
|
90
|
+
`modules`.
|
|
91
|
+
|
|
92
|
+
## Scaffold with the CLI
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
erp menu create office-equipment office-equipment-menu \
|
|
96
|
+
--display-name "Equipment Register" --route /office-equipment/equipment-register --icon devices
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
produces a valid one-node menu you then flesh out. (Run it from PowerShell on
|
|
100
|
+
Windows, or the leading `/` in `--route` gets mangled by Git Bash.)
|
|
101
|
+
|
|
102
|
+
## The master-scaffold sync note
|
|
103
|
+
|
|
104
|
+
Some applications (the HCM suite is the canonical case) keep a single
|
|
105
|
+
hand-maintained "master menu" file — e.g. `hcm-foundation-main-menu.json` — that
|
|
106
|
+
is the merged sidenav shown whether or not a given plugin is installed. If your
|
|
107
|
+
module attaches to such an application, a new page needs its node added in **two
|
|
108
|
+
places**:
|
|
109
|
+
|
|
110
|
+
1. your own `metadata/menu/<plugin>-menu.json` (above), and
|
|
111
|
+
2. the application's master menu scaffold.
|
|
112
|
+
|
|
113
|
+
Skipping (2) means the item is missing from the merged sidenav for anyone who
|
|
114
|
+
sees the app but hasn't installed your plugin. A **standalone** plugin that ships
|
|
115
|
+
its own `metadata/application/*.json` (like the tutorial) has no master scaffold
|
|
116
|
+
to sync — one menu file is enough.
|
|
117
|
+
|
|
118
|
+
## Ground yourself first
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
erp schema pull menu
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## How to verify it worked
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
erp schema validate spk-assembly/metadata/menu/office-equipment-menu.json --schema menu
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
OK — ... matches schema "menu"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
After publishing, open the ERP and confirm the group and its items appear in the
|
|
135
|
+
sidenav under your application, and each item navigates to the right page.
|
|
136
|
+
|
|
137
|
+
## Common mistakes
|
|
138
|
+
|
|
139
|
+
| Symptom | Cause | Fix |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| menu item 404s | `navigation.route` doesn't match the page's `route.pattern` | copy it exactly |
|
|
142
|
+
| item appears but page is blank | page's `modules` array doesn't include this module | align them |
|
|
143
|
+
| item missing from the merged app sidenav | didn't sync the master menu scaffold | add the node there too |
|
|
144
|
+
| `erp menu create` route is a Windows path | Git Bash `/` mangling | run from PowerShell / `MSYS_NO_PATHCONV=1` |
|
|
145
|
+
|
|
146
|
+
## What to read next
|
|
147
|
+
|
|
148
|
+
- [Add translations (i18n)](./add-i18n.md)
|
|
149
|
+
- [Make a plugin work on desktop, tablet, and mobile](./responsive-plugin.md)
|