@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,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Wire a page's data
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Wire a page's data
|
|
7
|
+
|
|
8
|
+
## What you're doing
|
|
9
|
+
|
|
10
|
+
You have a page with a `core.grid` block and it shows **zero rows and no error
|
|
11
|
+
message**. This is the single most common confusion on the platform. This guide
|
|
12
|
+
explains the one chain that connects a grid to a table.
|
|
13
|
+
|
|
14
|
+
## The mental model
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
page.metadata.dataSource ──names──▶ a Data Provider file ──points at──▶ a REST base path
|
|
18
|
+
"oeq-equipment-provider" metadata/provider/ /api/v1/entities/oeq_equipment/records
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Three things must line up **exactly**:
|
|
22
|
+
|
|
23
|
+
1. The page's top-level **`metadata.dataSource`** is a string.
|
|
24
|
+
2. That string equals the **`name`** field of a file in
|
|
25
|
+
`spk-assembly/metadata/provider/`.
|
|
26
|
+
3. That provider's **`definition.basePath`** is a real REST path that supports
|
|
27
|
+
`search`.
|
|
28
|
+
|
|
29
|
+
The grid block itself has **no** data configuration. It renders whatever the
|
|
30
|
+
page's `dataSource` provider returns.
|
|
31
|
+
|
|
32
|
+
## The complete example
|
|
33
|
+
|
|
34
|
+
Page (`equipment-register.json`), last two lines of the file:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
"metadata": { "dataSource": "oeq-equipment-provider" },
|
|
38
|
+
"modules": ["office-equipment"]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Provider (`spk-assembly/metadata/provider/oeq-equipment-provider.json`) —
|
|
43
|
+
[the real file](../tutorial/example-plugin/spk-assembly/metadata/provider/oeq-equipment-provider.json):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"name": "oeq-equipment-provider",
|
|
48
|
+
"description": "Data Provider for oeq_equipment (grid + CRUD).",
|
|
49
|
+
"definition": {
|
|
50
|
+
"kind": "rest",
|
|
51
|
+
"connectionRef": "self",
|
|
52
|
+
"basePath": "/api/v1/entities/oeq_equipment/records",
|
|
53
|
+
"supports": ["search", "get", "create", "update"]
|
|
54
|
+
},
|
|
55
|
+
"metadata": {},
|
|
56
|
+
"modules": []
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The string `"oeq-equipment-provider"` appears in **both** files, spelled
|
|
61
|
+
identically. That is the whole wiring.
|
|
62
|
+
|
|
63
|
+
## Line by line
|
|
64
|
+
|
|
65
|
+
- **`metadata.dataSource`** — page-level. Not inside `definition`, not on any
|
|
66
|
+
block. A grid with no matching `dataSource` shows nothing.
|
|
67
|
+
- **provider `name`** — the lookup key. Must match `dataSource` character for
|
|
68
|
+
character.
|
|
69
|
+
- **`kind: "rest"`** — the provider calls a REST endpoint.
|
|
70
|
+
- **`connectionRef: "self"`** — this ERP's own backend. (External systems use a
|
|
71
|
+
named connection.)
|
|
72
|
+
- **`basePath`** — for an entity you own, it's always
|
|
73
|
+
`/api/v1/entities/<entityName>/records`. The Entity Engine serves `search` at
|
|
74
|
+
`<basePath>/query`, `get` at `<basePath>/{id}`, `create` at `POST <basePath>`,
|
|
75
|
+
`update` at `PUT <basePath>/{id}`.
|
|
76
|
+
- **`supports`** — which of those the grid/dialogs may use.
|
|
77
|
+
|
|
78
|
+
## Filtering the grid
|
|
79
|
+
|
|
80
|
+
The grid's `externalFilter` property binds a page state object. Its keys become
|
|
81
|
+
query filters:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
"externalFilter": { "source": "binding", "binding": { "scope": "page", "key": "gridExternalFilter" } }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A search input then writes into it:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
"events": { "committed": { "source": "action-chain", "actions": [
|
|
91
|
+
{ "id": "a0", "order": 0, "type": "setValue",
|
|
92
|
+
"config": { "field": "page.gridExternalFilter.search", "value": "${event.new}" } }
|
|
93
|
+
] } }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Now typing in the box filters the grid on `search`; picking a status filters on
|
|
97
|
+
`status`. (`${event.new}` — the committed value — see
|
|
98
|
+
[Add a create/edit form](./add-a-create-edit-form.md).)
|
|
99
|
+
|
|
100
|
+
## Refreshing the grid after a write
|
|
101
|
+
|
|
102
|
+
Bind `refreshTrigger` to a page value and flip it at the end of a save chain:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{ "id": "a2", "order": 2, "type": "setValue",
|
|
106
|
+
"config": { "field": "page.gridRefreshTick", "value": "${!page.gridRefreshTick}" } }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## When you need a computed / joined read
|
|
110
|
+
|
|
111
|
+
If the grid needs columns from more than one table, or server-side aggregation,
|
|
112
|
+
the provider's `basePath` can point at a **Data Service** or **Data View**
|
|
113
|
+
instead of a raw entity. See [Add a data provider, data view, or data
|
|
114
|
+
service](./add-a-data-provider.md).
|
|
115
|
+
|
|
116
|
+
## How to verify it worked
|
|
117
|
+
|
|
118
|
+
The semantic linter catches the mismatch before you publish:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
erp plugin test office-equipment/spk-assembly
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If the wiring is broken you get:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
PAGE-DS-001 equipment-register.json: core.grid present but metadata.dataSource
|
|
128
|
+
"oeq-equipmnt-provider" does not match any metadata/provider/*.json name
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
When it's right, `erp plugin test` is silent on `PAGE-DS-001` and after publish:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
erp api get "/api/v1/entities/oeq_equipment/records/query?size=3"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
returns real rows — which is exactly what the grid will show.
|
|
138
|
+
|
|
139
|
+
## Common mistakes
|
|
140
|
+
|
|
141
|
+
| Symptom | Cause | Fix |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| grid empty, no error | `dataSource` typo / absent / put on the grid block | move it to page `metadata.dataSource`, match the provider `name` |
|
|
144
|
+
| grid empty but the entity has rows | provider `basePath` wrong | for your own entity it's `/api/v1/entities/<name>/records` |
|
|
145
|
+
| grid loads once, never refreshes after save | no `refreshTrigger` binding | bind it and toggle it at the end of the save chain |
|
|
146
|
+
| filter box does nothing | `externalFilter` not bound, or `committed` writes the wrong key | bind `gridExternalFilter`; write `page.gridExternalFilter.<key>` |
|
|
147
|
+
| "0 rows" only for some users | row-level permissions | expected — check the user's role |
|
|
148
|
+
|
|
149
|
+
## What to read next
|
|
150
|
+
|
|
151
|
+
- [Add a data provider, data view, or data service](./add-a-data-provider.md)
|
|
152
|
+
- [Add a create / edit form](./add-a-create-edit-form.md)
|
|
153
|
+
- Troubleshooting: [My grid shows 0 rows](../troubleshooting/grid-shows-zero-rows.md)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Build and publish a one-page plugin
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Quickstart: a one-page plugin, live
|
|
7
|
+
|
|
8
|
+
By the end of this page you will have a plugin installed on your ERP tenant with
|
|
9
|
+
one working page reachable from the app menu. Every command is real against the
|
|
10
|
+
current `erp` CLI.
|
|
11
|
+
|
|
12
|
+
## 0. Prerequisites
|
|
13
|
+
|
|
14
|
+
- Node.js 20+.
|
|
15
|
+
- The plugin SDK on your `PATH` (the `erp` command). Until the SDK is published
|
|
16
|
+
to npm you run it directly: `node <sdk>/tools/erp-cli/erp.mjs`. Substitute that
|
|
17
|
+
for `erp` below.
|
|
18
|
+
- The base URL of your ERP (e.g. `https://erp.example.com`).
|
|
19
|
+
|
|
20
|
+
## 1. Point the CLI at your ERP and log in
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
erp env use prod --base-url https://erp.example.com
|
|
24
|
+
erp login
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`erp login` opens your browser, you authorize the CLI, and the session is written
|
|
28
|
+
to `~/.erp/config.json`. Confirm it:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
erp whoami
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 2. Sync the authoring bundle (optional but recommended)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
erp env sync
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This pulls your ERP's schema/catalog/example/doc bundle into
|
|
41
|
+
`~/.erp/cache/`. If your ERP does not expose the bundle endpoint yet, this
|
|
42
|
+
no-ops and the SDK falls back to the bundle shipped inside the SDK package —
|
|
43
|
+
you can keep going.
|
|
44
|
+
|
|
45
|
+
## 3. Wire up the MCP server for your IDE agent
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
erp mcp install
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Writes a `.mcp.json` in the current directory pointing at the
|
|
52
|
+
`erp-plugin-language` MCP server, so Claude Code / Cursor / Codex can call
|
|
53
|
+
`erp_get_schema`, `erp_list_blocks`, `erp_platform_catalog`,
|
|
54
|
+
`erp_search_docs`, `erp_examples_patterns`, `erp_validate_page`, and the rest.
|
|
55
|
+
|
|
56
|
+
## 4. Scaffold the plugin
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
erp plugin create acme-hello --name "Acme Hello" --type business-app
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This creates `acme-hello/` with a `spk-assembly/` tree (`plugin.json`,
|
|
63
|
+
`metadata/page/`, `metadata/menu/`, `metadata/i18n/`, …).
|
|
64
|
+
|
|
65
|
+
> **No Java needed.** The scaffold leaves `plugin.json`'s `mainClass` `null` and
|
|
66
|
+
> that is exactly right for a pure-JSON plugin — the platform loads it through a
|
|
67
|
+
> built-in generic entry point. You compile a `Plugin` class only if your plugin
|
|
68
|
+
> ships its own Java behaviour. See
|
|
69
|
+
> [Create a plugin from scratch](../guides/create-a-plugin.md).
|
|
70
|
+
|
|
71
|
+
Author one page under `spk-assembly/metadata/page/hello.json`. Ground yourself
|
|
72
|
+
first — do not guess the shape:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
erp schema pull page # the page.json contract
|
|
76
|
+
erp blocks list --category display # what widgets exist
|
|
77
|
+
erp examples patterns --kind page # curated real page shapes
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Give the page a menu entry:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
erp menu create acme-hello acme-hello.main --display-name "Hello" --route /hello
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 5. Validate before you ship
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
erp plugin validate acme-hello/spk-assembly
|
|
90
|
+
erp plugin test acme-hello/spk-assembly # semantic lints (i18n keys, grid data-source, …)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Fix anything flagged. These are the same checks the platform runs at install
|
|
94
|
+
time — a clean run here means a clean install.
|
|
95
|
+
|
|
96
|
+
## 6. Build and publish
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
erp plugin build acme-hello/spk-assembly -o acme-hello.spk
|
|
100
|
+
erp plugin publish acme-hello.spk
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 7. See it live
|
|
104
|
+
|
|
105
|
+
Open your ERP, navigate to the app that owns the module, and you'll find the
|
|
106
|
+
**Hello** menu item. The page URL is
|
|
107
|
+
`/app/<app>/<module>/acme-hello/hello` (the route pattern in your menu file is
|
|
108
|
+
*not* the browser URL — see `troubleshooting/known-gotchas.md`).
|
|
109
|
+
|
|
110
|
+
## Next
|
|
111
|
+
|
|
112
|
+
- [The tutorial](../tutorial/index.md) — build and publish a real 3-page module
|
|
113
|
+
end to end, every step verified.
|
|
114
|
+
- [Guides](../guides/index.md) — every task as its own page.
|
|
115
|
+
- [concepts/overview.md](../concepts/overview.md) — how the pieces fit together.
|
|
116
|
+
- [reference/](../reference/index.md) — every authoring artifact's JSON Schema.
|
|
117
|
+
- [troubleshooting/known-gotchas.md](../troubleshooting/known-gotchas.md) — the
|
|
118
|
+
mistakes everyone makes once.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A condition-based compliance flag
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# A condition-based compliance flag
|
|
7
|
+
|
|
8
|
+
## When you need this
|
|
9
|
+
|
|
10
|
+
"Find rows that break a policy and flag them" — where the policy is more than one
|
|
11
|
+
date vs now. Un-acknowledged custody after 7 days, over-allocated licences,
|
|
12
|
+
missing documentation. Zero Java — one `entity_compliance_config` row run by
|
|
13
|
+
`engine-entity.compliance-sweep`.
|
|
14
|
+
|
|
15
|
+
## The file
|
|
16
|
+
|
|
17
|
+
`metadata/seed-data/office-equipment-compliance-configs.json`
|
|
18
|
+
([real file](../examples/standalone-plugin/spk-assembly/metadata/seed-data/office-equipment-compliance-configs.json)):
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"entity": "entity_compliance_config",
|
|
23
|
+
"keyFields": ["compliance_code"],
|
|
24
|
+
"source": "office-equipment",
|
|
25
|
+
"rows": [
|
|
26
|
+
{
|
|
27
|
+
"compliance_code": "checkout-acknowledgement-compliance",
|
|
28
|
+
"source_entity": "oeq_checkout",
|
|
29
|
+
"when_status_in": "CHECKED_OUT,DUE_SOON,OVERDUE",
|
|
30
|
+
"condition_expr": "custody_acknowledged != true && daysBetween(checkout_date, now()) > 7",
|
|
31
|
+
"set_field": "compliance_flag",
|
|
32
|
+
"set_value": "NON_COMPLIANT",
|
|
33
|
+
"active": true
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
A row where `condition_expr` is **true** is non-compliant; the job sets
|
|
40
|
+
`compliance_flag = "NON_COMPLIANT"` on it (idempotent — a row already flagged is
|
|
41
|
+
skipped).
|
|
42
|
+
|
|
43
|
+
## The expression language
|
|
44
|
+
|
|
45
|
+
The platform `ExpressionEvaluator` — the same one that powers formula fields and
|
|
46
|
+
cross-field validation. In scope: the row's fields, `now`, and (if configured)
|
|
47
|
+
`related_count`. Allowed: `&&`, `||`, `!`, comparisons, arithmetic, and
|
|
48
|
+
whitelisted functions (`daysBetween`, `now`, `coalesce`, …). **Not allowed:**
|
|
49
|
+
ternary `?:`, `in`, `^`.
|
|
50
|
+
|
|
51
|
+
## Variants
|
|
52
|
+
|
|
53
|
+
- **Related-count** (cross-record) — add `related_count_entity`,
|
|
54
|
+
`related_count_key_field` (FK back to the source id), `related_count_status_in`;
|
|
55
|
+
then use `related_count` in the expression, e.g.
|
|
56
|
+
`related_count > entitlement_quantity`.
|
|
57
|
+
- **Escalation** (raise a row in another entity) — replace `set_field`/`set_value`
|
|
58
|
+
with `escalation_entity`, `escalation_dedupe_field` (a numeric column holding
|
|
59
|
+
the source id), `field_map_json`, `escalation_template_json`.
|
|
60
|
+
|
|
61
|
+
## Also ship
|
|
62
|
+
|
|
63
|
+
- the shared `entity_compliance_config.json` (additive), and
|
|
64
|
+
- `metadata/rules/ensure_compliance_sweep_job_registered.json`
|
|
65
|
+
(`AFTER_CREATE` → `ensureEntityComplianceSweepJobRegistered`).
|
|
66
|
+
|
|
67
|
+
## Related
|
|
68
|
+
|
|
69
|
+
- [Add a compliance-sweep job](../guides/add-a-compliance-job.md)
|
|
70
|
+
- [Add business rules and expressions](../guides/add-business-rules.md) — same expression language
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A cross-plugin offboarding fan-out
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# A cross-plugin offboarding fan-out
|
|
7
|
+
|
|
8
|
+
## When you need this
|
|
9
|
+
|
|
10
|
+
"When an employee is separated, auto-create the follow-up records for everything
|
|
11
|
+
they hold" — asset return requests, licence reclamations, access revocations. The
|
|
12
|
+
trigger entity can belong to **another plugin** entirely. Zero Java, zero
|
|
13
|
+
cross-plugin HTTP — one `entity_cross_plugin_action_config` row run by
|
|
14
|
+
`engine-entity.cross-plugin-action`.
|
|
15
|
+
|
|
16
|
+
## The file
|
|
17
|
+
|
|
18
|
+
`metadata/seed-data/office-equipment-cross-plugin-configs.json`:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"entity": "entity_cross_plugin_action_config",
|
|
23
|
+
"keyFields": ["action_code"],
|
|
24
|
+
"source": "office-equipment",
|
|
25
|
+
"rows": [
|
|
26
|
+
{
|
|
27
|
+
"action_code": "equipment-separation-return",
|
|
28
|
+
"trigger_entity": "employee",
|
|
29
|
+
"trigger_status_field": "employment_status",
|
|
30
|
+
"trigger_status_in": "TERMINATED,SEPARATED",
|
|
31
|
+
"trigger_key_field": "id",
|
|
32
|
+
"related_entity": "oeq_checkout",
|
|
33
|
+
"related_match_field": "employee_id",
|
|
34
|
+
"related_status_in": "APPROVED,CHECKED_OUT,DUE_SOON,OVERDUE",
|
|
35
|
+
"create_entity": "oeq_return",
|
|
36
|
+
"create_dedupe_field": "checkout_id",
|
|
37
|
+
"create_template_json": { "status": "REQUESTED", "reason": "EMPLOYEE_SEPARATION" },
|
|
38
|
+
"create_field_map_json": { "equipment_id": "equipment_id", "employee_id": "employee_id" },
|
|
39
|
+
"related_set_field": "status",
|
|
40
|
+
"related_set_value": "RETURN_PENDING",
|
|
41
|
+
"active": true
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## How to read it
|
|
48
|
+
|
|
49
|
+
When an `employee` row's `employment_status` becomes `TERMINATED` or
|
|
50
|
+
`SEPARATED`, the job finds every `oeq_checkout` for that `employee_id` in an
|
|
51
|
+
active status, creates one `oeq_return` per checkout (deduped on `checkout_id`,
|
|
52
|
+
which **must be a numeric/long column**), copies the mapped fields, applies the
|
|
53
|
+
template, and sets the source checkout's status to `RETURN_PENDING`.
|
|
54
|
+
|
|
55
|
+
## The rules that bite
|
|
56
|
+
|
|
57
|
+
- `create_dedupe_field` must be a numeric column holding the related row's id —
|
|
58
|
+
it's how the job avoids double-creating on the next tick.
|
|
59
|
+
- The `trigger_entity` can be owned by any plugin; you just need its name and
|
|
60
|
+
the status field.
|
|
61
|
+
- Ship the shared `entity_cross_plugin_action_config.json` (additive) and an
|
|
62
|
+
`AFTER_CREATE` register rule calling
|
|
63
|
+
`ensureEntityCrossPluginActionJobRegistered`.
|
|
64
|
+
|
|
65
|
+
## Related
|
|
66
|
+
|
|
67
|
+
- `erp examples patterns --kind jobs` → `cross-plugin-offboarding-fanout`
|
|
68
|
+
- [Add a cadence-generator job](../guides/add-a-cadence-job.md) — the source-driven cousin
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: A cross-row aggregation into a summary entity
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# A cross-row aggregation into a summary entity
|
|
7
|
+
|
|
8
|
+
## When you need this
|
|
9
|
+
|
|
10
|
+
"Count / sum / average many rows on a schedule and store the result." A nightly
|
|
11
|
+
inventory reconciliation, per-asset maintenance-cost totals, seats-used per
|
|
12
|
+
licence. Zero Java — one `entity_aggregation_config` row run by
|
|
13
|
+
`engine-entity.aggregation-sweep`.
|
|
14
|
+
|
|
15
|
+
## The file
|
|
16
|
+
|
|
17
|
+
`metadata/seed-data/office-equipment-aggregation-configs.json`
|
|
18
|
+
([real file](../examples/standalone-plugin/spk-assembly/metadata/seed-data/office-equipment-aggregation-configs.json)):
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"entity": "entity_aggregation_config",
|
|
23
|
+
"keyFields": ["sweep_code"],
|
|
24
|
+
"source": "office-equipment",
|
|
25
|
+
"rows": [
|
|
26
|
+
{
|
|
27
|
+
"sweep_code": "equipment-status-reconciliation",
|
|
28
|
+
"source_entity": "oeq_equipment",
|
|
29
|
+
"aggregation": "count",
|
|
30
|
+
"group_by_field": "status",
|
|
31
|
+
"target_entity": "oeq_reconciliation",
|
|
32
|
+
"target_key_field": "metric_key",
|
|
33
|
+
"target_key_prefix": "equipment_status:",
|
|
34
|
+
"target_value_field": "metric_value",
|
|
35
|
+
"target_timestamp_field": "last_reconciled_at",
|
|
36
|
+
"active": true
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Result: one `oeq_reconciliation` row per status, keyed
|
|
43
|
+
`equipment_status:AVAILABLE`, `equipment_status:ASSIGNED`, … with the count in
|
|
44
|
+
`metric_value`.
|
|
45
|
+
|
|
46
|
+
## Variants
|
|
47
|
+
|
|
48
|
+
- **`sum` / `avg` / `min` / `max`** — add `agg_field` (the numeric column).
|
|
49
|
+
- **Write onto the parent row instead of a summary entity** — set
|
|
50
|
+
`target_key_field: "id"`, leave `target_key_prefix` blank; the `group_by_field`
|
|
51
|
+
values are the parent's primary keys. (E.g. sum line-item cost onto the
|
|
52
|
+
maintenance record's own `synced_cost`.)
|
|
53
|
+
- **Filter first** — add `status_field` + `when_status_in`.
|
|
54
|
+
|
|
55
|
+
## Also ship
|
|
56
|
+
|
|
57
|
+
- `metadata/entities/oeq_reconciliation.json` (the summary entity), and the
|
|
58
|
+
shared `entity_aggregation_config.json` (additive).
|
|
59
|
+
- `metadata/rules/ensure_aggregation_sweep_job_registered.json` — an
|
|
60
|
+
`AFTER_CREATE` `EXECUTE_SERVICE` rule calling
|
|
61
|
+
`ensureEntityAggregationSweepJobRegistered` (this job does **not** self-register
|
|
62
|
+
from a platform rule — you ship it).
|
|
63
|
+
|
|
64
|
+
## Related
|
|
65
|
+
|
|
66
|
+
- [Add a cross-row aggregation job](../guides/add-an-aggregation-job.md)
|
|
67
|
+
- [A KPI dashboard page](./kpi-dashboard-page.md) — the on-demand version
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: An entity with an approval workflow + reminder job
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# An entity with an approval workflow + reminder job
|
|
7
|
+
|
|
8
|
+
## When you need this
|
|
9
|
+
|
|
10
|
+
A record with a full lifecycle: it's drafted, submitted for approval, approved,
|
|
11
|
+
acted on, and eventually chased if it goes stale. This recipe combines
|
|
12
|
+
[the approval workflow](../guides/add-an-approval-workflow.md) and
|
|
13
|
+
[the reminder job](../guides/add-a-reminder-job.md) on one entity — exactly what
|
|
14
|
+
the tutorial's `oeq_checkout` does.
|
|
15
|
+
|
|
16
|
+
## The five files
|
|
17
|
+
|
|
18
|
+
| File | Purpose |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `metadata/entities/oeq_checkout.json` | the entity; status enum includes every lifecycle value (`DRAFT`, `PENDING_APPROVAL`, `APPROVED`, `CHECKED_OUT`, `DUE_SOON`, `OVERDUE`, `RETURNED`, `REJECTED`) |
|
|
21
|
+
| `metadata/workflow/office-equipment.checkout-approval.json` | single-stage manager approval |
|
|
22
|
+
| `metadata/rules/oeq_checkout_submit_workflow.json` | `AFTER_UPDATE` rule: status → `PENDING_APPROVAL` starts the workflow; decision writes `APPROVED`/`REJECTED` back |
|
|
23
|
+
| `metadata/entities/entity_status_date_sweep_config.json` | ship the shared config entity (idempotent-additive) |
|
|
24
|
+
| `metadata/seed-data/office-equipment-sweep-configs.json` | 2 sweep configs: `CHECKED_OUT` within 3 days → `DUE_SOON`; past due → `OVERDUE` |
|
|
25
|
+
|
|
26
|
+
All five are real in
|
|
27
|
+
[`tutorial/example-plugin/`](../tutorial/example-plugin/spk-assembly/metadata/).
|
|
28
|
+
|
|
29
|
+
## The lifecycle, end to end
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
DRAFT
|
|
33
|
+
│ user: Submit for approval (PUT status = PENDING_APPROVAL)
|
|
34
|
+
▼
|
|
35
|
+
PENDING_APPROVAL ──rule──▶ workflow instance + manager task
|
|
36
|
+
│ manager approves ──callback──▶ status = APPROVED
|
|
37
|
+
▼
|
|
38
|
+
APPROVED
|
|
39
|
+
│ equipment handed over (PUT status = CHECKED_OUT)
|
|
40
|
+
▼
|
|
41
|
+
CHECKED_OUT
|
|
42
|
+
│ nightly sweep: due_date within 3 days
|
|
43
|
+
▼
|
|
44
|
+
DUE_SOON
|
|
45
|
+
│ nightly sweep: due_date passed
|
|
46
|
+
▼
|
|
47
|
+
OVERDUE
|
|
48
|
+
│ equipment returned (PUT status = RETURNED)
|
|
49
|
+
▼
|
|
50
|
+
RETURNED
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The **user** drives `DRAFT → PENDING_APPROVAL`, `APPROVED → CHECKED_OUT`,
|
|
54
|
+
`* → RETURNED` (buttons on the page). The **workflow** drives
|
|
55
|
+
`PENDING_APPROVAL → APPROVED/REJECTED`. The **sweep job** drives
|
|
56
|
+
`CHECKED_OUT → DUE_SOON → OVERDUE`. No two mechanisms fight over the same
|
|
57
|
+
transition.
|
|
58
|
+
|
|
59
|
+
## The rules that bite
|
|
60
|
+
|
|
61
|
+
- The submit rule needs the `status__previous` guard so it fires only on the
|
|
62
|
+
transition, not every later update.
|
|
63
|
+
- The `OVERDUE` sweep config's `when_status_in` includes `DUE_SOON`, so a warned
|
|
64
|
+
checkout still escalates.
|
|
65
|
+
- Sweep configs flip the **status enum**, not a boolean flag (the generic job
|
|
66
|
+
can't write a string to a boolean column).
|
|
67
|
+
|
|
68
|
+
## Related
|
|
69
|
+
|
|
70
|
+
- [Add an approval workflow](../guides/add-an-approval-workflow.md)
|
|
71
|
+
- [Add a scheduled reminder job](../guides/add-a-reminder-job.md)
|
|
72
|
+
- Tutorial chapters [6](../tutorial/06-checkout-approval-workflow.md) and [7](../tutorial/07-return-due-reminder-job.md)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Recipes
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Recipes
|
|
7
|
+
|
|
8
|
+
Short, copy-paste solutions to shapes that come up again and again. Each recipe
|
|
9
|
+
is 2–3 sentences of "when you need this" plus the real file(s). They're drawn
|
|
10
|
+
from the curated `erp examples patterns` catalog — run that any time to see the
|
|
11
|
+
live list, each entry pointing at a real shipped file:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
erp examples patterns # all
|
|
15
|
+
erp examples patterns --kind jobs # just the scheduled-job shapes
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Screens
|
|
19
|
+
|
|
20
|
+
- [A searchable related-record lookup field](./related-record-lookup-field.md)
|
|
21
|
+
- [A resolved reference column in a grid](./resolved-reference-grid-column.md)
|
|
22
|
+
- [A KPI dashboard page](./kpi-dashboard-page.md)
|
|
23
|
+
- [A master-detail page with a child grid](./master-detail-page.md)
|
|
24
|
+
|
|
25
|
+
## Logic
|
|
26
|
+
|
|
27
|
+
- [An entity with an approval workflow](./entity-with-approval-workflow.md)
|
|
28
|
+
|
|
29
|
+
## Scheduled jobs (no Java)
|
|
30
|
+
|
|
31
|
+
- [An N-days-before reminder job](./n-days-before-reminder.md)
|
|
32
|
+
- [A cross-row aggregation into a summary entity](./cross-row-aggregation.md)
|
|
33
|
+
- [A scheduled record-generation cadence](./scheduled-record-generation.md)
|
|
34
|
+
- [A condition-based compliance flag](./condition-based-compliance-flag.md)
|
|
35
|
+
- [A cross-plugin offboarding fan-out](./cross-plugin-offboarding-fanout.md)
|
|
36
|
+
- [A per-row document artifact generator](./per-row-document-generator.md)
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
- [Install-lifecycle data seeding](./install-lifecycle-data-seeding.md)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Install-lifecycle data seeding
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Install-lifecycle data seeding
|
|
7
|
+
|
|
8
|
+
## When you need this
|
|
9
|
+
|
|
10
|
+
Any time a data row must **travel with the `.spk`** instead of a manual
|
|
11
|
+
per-tenant `POST` — reference data you own, default settings, and (the important
|
|
12
|
+
case) rows in the **shared platform config entities** that make your scheduled
|
|
13
|
+
jobs exist.
|
|
14
|
+
|
|
15
|
+
## The file
|
|
16
|
+
|
|
17
|
+
`metadata/seed-data/<name>.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"entity": "oeq_category",
|
|
22
|
+
"keyFields": ["category_code"],
|
|
23
|
+
"source": "office-equipment",
|
|
24
|
+
"rows": [
|
|
25
|
+
{ "category_code": "LAPTOP", "category_name": "Laptops", "status": "ACTIVE", "seeded_by": "office-equipment" }
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- **`entity`** — must already exist (ship it in `metadata/entities/` if you own
|
|
31
|
+
it).
|
|
32
|
+
- **`keyFields`** — the natural key. On re-install a matching row is updated only
|
|
33
|
+
if a non-key value changed, else inserted. **Never duplicated.**
|
|
34
|
+
- **`source`** — your plugin id; stamped into a `seeded_by` field if present.
|
|
35
|
+
- **`rows`** — plain field maps, no tenant id.
|
|
36
|
+
|
|
37
|
+
## Verified idempotency
|
|
38
|
+
|
|
39
|
+
First install of the tutorial module:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
PluginDataSeedInstaller: office-equipment-categories.json -> entity oeq_category (tenant 2): 4 created, 0 updated, 0 unchanged
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Re-publish with one row added:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
PluginDataSeedInstaller: office-equipment-categories.json -> entity oeq_category (tenant 2): 1 created, 0 updated, 4 unchanged
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## For scheduled-job config
|
|
52
|
+
|
|
53
|
+
When the target is a shared config entity
|
|
54
|
+
(`entity_status_date_sweep_config`, `entity_aggregation_config`,
|
|
55
|
+
`entity_cadence_config`, `entity_compliance_config`, `entity_cross_plugin_action_config`,
|
|
56
|
+
`entity_document_generator_config`) also ship:
|
|
57
|
+
|
|
58
|
+
1. **the shared config entity file** — with just the additive columns your rows
|
|
59
|
+
need. `PluginEntityInstaller` is idempotent-additive: an existing table gains
|
|
60
|
+
only the missing columns.
|
|
61
|
+
2. **an `AFTER_CREATE` register rule** on that config entity calling
|
|
62
|
+
`ensure<Job>Registered`, so the first seeded row auto-registers the job. (The
|
|
63
|
+
status-date-sweep job's register rule ships with the platform; the other five
|
|
64
|
+
you ship.)
|
|
65
|
+
|
|
66
|
+
## The rules that bite
|
|
67
|
+
|
|
68
|
+
- Non-unique `keyFields` → duplicates on re-install. Pick a real key.
|
|
69
|
+
- A `null` in a key field in some rows → matches nothing / everything. Keep key
|
|
70
|
+
fields populated in every row.
|
|
71
|
+
- Seed targets an entity you forgot to ship → `SEED-001` lint / install failure.
|
|
72
|
+
|
|
73
|
+
## Related
|
|
74
|
+
|
|
75
|
+
- [Seed config and reference data on install](../guides/seed-data-on-install.md)
|
|
76
|
+
- Each job guide's "register rule" section
|