@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,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "ERP Plugin Developer Docs",
|
|
3
|
+
"audience": ["plugin-developer", "application-user"],
|
|
4
|
+
"audienceNote": "Historically this field was the single string \"tenant\" (per-page frontmatter `audience: tenant|all` is the field that actually gates bundle/build inclusion — see BUNDLE_ALLOWLIST in tools/erp-cli/erp.mjs — this top-level field is informational only, read by no code path). It's now an array because this site genuinely serves two different readers as of the Applications section: plugin-developer (Quickstart/Concepts/Guides/Reference/Tutorial/Recipes/Troubleshooting — build plugins on the platform) and application-user (Applications — use a shipped business-application screen like HCM's Employee Profile). Every individual page still declares its own `audience: tenant` frontmatter regardless of which readership it's written for; \"tenant\" there means \"the customer org\", which is accurate for both readers.",
|
|
5
|
+
"diataxis": ["quickstart", "concepts", "guides", "reference", "tutorial", "recipes", "troubleshooting", "applications"],
|
|
6
|
+
"nav": [
|
|
7
|
+
{
|
|
8
|
+
"section": "Quickstart",
|
|
9
|
+
"items": [
|
|
10
|
+
{ "title": "Build and publish a one-page plugin", "path": "quickstart/index.md" }
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"section": "Concepts",
|
|
15
|
+
"items": [
|
|
16
|
+
{ "title": "Platform overview", "path": "concepts/overview.md" },
|
|
17
|
+
{ "title": "What are the artifact types?", "path": "concepts/artifact-types.md" },
|
|
18
|
+
{ "title": "How does a plugin get installed?", "path": "concepts/plugin-lifecycle.md" },
|
|
19
|
+
{ "title": "How do the SDK modes work (monorepo vs connected)?", "path": "concepts/sdk-modes.md" },
|
|
20
|
+
{ "title": "How does a page get its data?", "path": "concepts/page-data-flow.md" },
|
|
21
|
+
{ "title": "Glossary", "path": "concepts/glossary.md" }
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"section": "Guides",
|
|
26
|
+
"items": [
|
|
27
|
+
{ "title": "Set up the SDK and connect to an environment", "path": "guides/set-up-the-sdk.md" },
|
|
28
|
+
{ "title": "Create a plugin from scratch", "path": "guides/create-a-plugin.md" },
|
|
29
|
+
{ "title": "Add an entity", "path": "guides/add-an-entity.md" },
|
|
30
|
+
{ "title": "Build a page", "path": "guides/build-a-page.md" },
|
|
31
|
+
{ "title": "Wire a page's data", "path": "guides/wire-a-pages-data.md" },
|
|
32
|
+
{ "title": "Add a data provider, data view, or data service", "path": "guides/add-a-data-provider.md" },
|
|
33
|
+
{ "title": "Add a create / edit form", "path": "guides/add-a-create-edit-form.md" },
|
|
34
|
+
{ "title": "Add a KPI or aggregation", "path": "guides/add-a-kpi.md" },
|
|
35
|
+
{ "title": "Add an approval workflow", "path": "guides/add-an-approval-workflow.md" },
|
|
36
|
+
{ "title": "Add business rules and expressions", "path": "guides/add-business-rules.md" },
|
|
37
|
+
{ "title": "Add a scheduled reminder job", "path": "guides/add-a-reminder-job.md" },
|
|
38
|
+
{ "title": "Add a cross-row aggregation job", "path": "guides/add-an-aggregation-job.md" },
|
|
39
|
+
{ "title": "Add a cadence-generator job", "path": "guides/add-a-cadence-job.md" },
|
|
40
|
+
{ "title": "Add a compliance-sweep job", "path": "guides/add-a-compliance-job.md" },
|
|
41
|
+
{ "title": "Seed config and reference data on install", "path": "guides/seed-data-on-install.md" },
|
|
42
|
+
{ "title": "Add menus", "path": "guides/add-menus.md" },
|
|
43
|
+
{ "title": "Add translations (i18n)", "path": "guides/add-i18n.md" },
|
|
44
|
+
{ "title": "Make a plugin work on desktop, tablet, and mobile", "path": "guides/responsive-plugin.md" },
|
|
45
|
+
{ "title": "Add a custom block (a \"widget\" you build yourself)", "path": "guides/add-a-custom-block.md" },
|
|
46
|
+
{ "title": "Build a plugin with custom React + Java code (L4)", "path": "guides/build-a-code-plugin.md" },
|
|
47
|
+
{ "title": "Validate and test a plugin", "path": "guides/validate-and-test.md" },
|
|
48
|
+
{ "title": "Publish and upgrade a plugin", "path": "guides/publish-and-upgrade.md" },
|
|
49
|
+
{ "title": "Build a tenant extension service (L5)", "path": "guides/build-a-tenant-extension-service.md" },
|
|
50
|
+
{ "title": "Build a Service-mode plugin in Python or Node.js", "path": "guides/build-a-polyglot-service-plugin.md" },
|
|
51
|
+
{ "title": "Use the MCP server with an AI agent", "path": "guides/use-the-mcp-server.md" },
|
|
52
|
+
{ "title": "Expose a plugin operation as an AI tool", "path": "guides/expose-a-plugin-operation-as-an-ai-tool.md" }
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"section": "Reference",
|
|
57
|
+
"items": [
|
|
58
|
+
{ "title": "Schema reference", "path": "reference/index.md" }
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"section": "Tutorial",
|
|
63
|
+
"items": [
|
|
64
|
+
{ "title": "Overview: build the Office Equipment module", "path": "tutorial/index.md" },
|
|
65
|
+
{ "title": "1. Create the plugin", "path": "tutorial/01-create-the-plugin.md" },
|
|
66
|
+
{ "title": "2. The equipment category entity + seeded data", "path": "tutorial/02-category-entity-and-seed.md" },
|
|
67
|
+
{ "title": "3. The equipment register entity", "path": "tutorial/03-equipment-entity.md" },
|
|
68
|
+
{ "title": "4. The equipment register page", "path": "tutorial/04-equipment-register-page.md" },
|
|
69
|
+
{ "title": "5. The checkout entity, page, and lookups", "path": "tutorial/05-checkout-entity-and-page.md" },
|
|
70
|
+
{ "title": "6. The checkout approval workflow", "path": "tutorial/06-checkout-approval-workflow.md" },
|
|
71
|
+
{ "title": "7. The return-due reminder job", "path": "tutorial/07-return-due-reminder-job.md" },
|
|
72
|
+
{ "title": "8. Menus, i18n, validate, publish", "path": "tutorial/08-menus-i18n-publish.md" }
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"section": "Recipes",
|
|
77
|
+
"items": [
|
|
78
|
+
{ "title": "Recipe index", "path": "recipes/index.md" },
|
|
79
|
+
{ "title": "A searchable related-record lookup field", "path": "recipes/related-record-lookup-field.md" },
|
|
80
|
+
{ "title": "A resolved reference column in a grid", "path": "recipes/resolved-reference-grid-column.md" },
|
|
81
|
+
{ "title": "A KPI dashboard page", "path": "recipes/kpi-dashboard-page.md" },
|
|
82
|
+
{ "title": "A master-detail page with a child grid", "path": "recipes/master-detail-page.md" },
|
|
83
|
+
{ "title": "An entity with an approval workflow", "path": "recipes/entity-with-approval-workflow.md" },
|
|
84
|
+
{ "title": "An N-days-before reminder job", "path": "recipes/n-days-before-reminder.md" },
|
|
85
|
+
{ "title": "A cross-row aggregation into a summary entity", "path": "recipes/cross-row-aggregation.md" },
|
|
86
|
+
{ "title": "A scheduled record-generation cadence", "path": "recipes/scheduled-record-generation.md" },
|
|
87
|
+
{ "title": "A condition-based compliance flag", "path": "recipes/condition-based-compliance-flag.md" },
|
|
88
|
+
{ "title": "A cross-plugin offboarding fan-out", "path": "recipes/cross-plugin-offboarding-fanout.md" },
|
|
89
|
+
{ "title": "A per-row document artifact generator", "path": "recipes/per-row-document-generator.md" },
|
|
90
|
+
{ "title": "Install-lifecycle data seeding", "path": "recipes/install-lifecycle-data-seeding.md" }
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"section": "Troubleshooting",
|
|
95
|
+
"items": [
|
|
96
|
+
{ "title": "Known gotchas", "path": "troubleshooting/known-gotchas.md" },
|
|
97
|
+
{ "title": "Publishing and install errors", "path": "troubleshooting/publishing-errors.md" },
|
|
98
|
+
{ "title": "\"My grid shows 0 rows\"", "path": "troubleshooting/grid-shows-zero-rows.md" },
|
|
99
|
+
{ "title": "\"My scheduled job never runs\"", "path": "troubleshooting/job-never-runs.md" }
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"section": "Applications",
|
|
104
|
+
"items": [
|
|
105
|
+
{ "title": "HCM: Employee Profile", "path": "applications/hcm/employee-profile.md" }
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Add a cadence-generator job
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a cadence-generator job
|
|
7
|
+
|
|
8
|
+
## What you're doing
|
|
9
|
+
|
|
10
|
+
"Create new records on a schedule." A quarterly equipment audit, a monthly
|
|
11
|
+
review, a follow-up task N days after some event. **No Java.** One
|
|
12
|
+
`entity_cadence_config` row, run by the generic `engine-entity.cadence-generator`
|
|
13
|
+
job, shipped as seed data.
|
|
14
|
+
|
|
15
|
+
## Two modes
|
|
16
|
+
|
|
17
|
+
| Mode | One tick creates | Set |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| **standalone** | one new record every `interval_days` | `target_entity`, `interval_days`, `template_json`, `dedupe_target_field` |
|
|
20
|
+
| **source-driven** | one record per *due* row of a source entity | also `source_entity`, `source_date_field`, `due_within_days`, `field_map_json` |
|
|
21
|
+
|
|
22
|
+
## The complete example — standalone quarterly audit
|
|
23
|
+
|
|
24
|
+
`spk-assembly/metadata/seed-data/office-equipment-cadence-configs.json`
|
|
25
|
+
(standalone example,
|
|
26
|
+
[real file](../examples/standalone-plugin/spk-assembly/metadata/seed-data/office-equipment-cadence-configs.json)):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"entity": "entity_cadence_config",
|
|
31
|
+
"keyFields": ["cadence_code"],
|
|
32
|
+
"source": "office-equipment",
|
|
33
|
+
"rows": [
|
|
34
|
+
{
|
|
35
|
+
"cadence_code": "equipment-audit-schedule",
|
|
36
|
+
"target_entity": "oeq_audit",
|
|
37
|
+
"interval_days": 90,
|
|
38
|
+
"dedupe_target_field": "cadence_marker",
|
|
39
|
+
"template_json": {
|
|
40
|
+
"audit_number": "AUDIT-${date}-${ts}",
|
|
41
|
+
"audit_name": "Scheduled Equipment Audit ${date}",
|
|
42
|
+
"status": "DRAFT"
|
|
43
|
+
},
|
|
44
|
+
"seeded_by": "office-equipment",
|
|
45
|
+
"active": true
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Line by line
|
|
52
|
+
|
|
53
|
+
- **`cadence_code`** — unique id; the natural key.
|
|
54
|
+
- **`target_entity`** — the entity to create rows in.
|
|
55
|
+
- **`interval_days`** — a new row is created only if the last one is older than
|
|
56
|
+
this many days (tracked in `last_generated_at`).
|
|
57
|
+
- **`dedupe_target_field`** — a column on the target that the job stamps to
|
|
58
|
+
recognise its own creations and avoid double-generating in one window.
|
|
59
|
+
- **`template_json`** — the field map for the new row. String values support
|
|
60
|
+
substitutions: `${date}` (today), `${ts}` (a timestamp), `${uuid}`, and (in
|
|
61
|
+
source-driven mode) `${sourceId}`.
|
|
62
|
+
|
|
63
|
+
### Source-driven variant
|
|
64
|
+
|
|
65
|
+
Add:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
"source_entity": "oeq_equipment",
|
|
69
|
+
"source_date_field": "warranty_expiry_date",
|
|
70
|
+
"due_within_days": 30,
|
|
71
|
+
"field_map_json": { "equipment_id": "id", "equipment_tag": "asset_tag" },
|
|
72
|
+
"dedupe_target_field": "equipment_id"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
→ for every `oeq_equipment` whose `warranty_expiry_date` is within 30 days and
|
|
76
|
+
which doesn't already have a target row (deduped on `equipment_id`), create one,
|
|
77
|
+
copying `id`→`equipment_id` and `asset_tag`→`equipment_tag`.
|
|
78
|
+
|
|
79
|
+
## The register rule
|
|
80
|
+
|
|
81
|
+
`spk-assembly/metadata/rules/ensure_cadence_generator_job_registered.json`:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"entityType": "entity_cadence_config",
|
|
86
|
+
"name": "ensure_cadence_generator_job_registered",
|
|
87
|
+
"triggerEvent": "AFTER_CREATE",
|
|
88
|
+
"conditions": null,
|
|
89
|
+
"actions": "[{\"type\": \"EXECUTE_SERVICE\", \"service\": \"ensureEntityCadenceGeneratorJobRegistered\"}]",
|
|
90
|
+
"priority": 10,
|
|
91
|
+
"active": true
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Ground yourself first
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
erp schema pull entity-cadence-config
|
|
99
|
+
erp examples patterns --kind jobs # "scheduled-record-generation-cadence"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## How to verify it worked
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
erp api get "/api/v1/jobs/engine-entity.cadence-generator"
|
|
106
|
+
# → { "status": "ENABLED", ... }
|
|
107
|
+
|
|
108
|
+
erp api post "/api/v1/jobs/engine-entity.cadence-generator/execute" --body "{}"
|
|
109
|
+
erp api get "/api/v1/entities/oeq_audit/records/query?size=5"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{ "rows": [
|
|
114
|
+
{ "audit_number": "AUDIT-2026-09-10-1757492940", "audit_name": "Scheduled Equipment Audit 2026-09-10",
|
|
115
|
+
"status": "DRAFT", "cadence_marker": "equipment-audit-schedule" }
|
|
116
|
+
], "total": 1 }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Run it again immediately — no new row (interval not elapsed). That's correct.
|
|
120
|
+
|
|
121
|
+
## Common mistakes
|
|
122
|
+
|
|
123
|
+
| Symptom | Cause | Fix |
|
|
124
|
+
| --- | --- | --- |
|
|
125
|
+
| a new row every single tick | `interval_days` missing or 0 | set it |
|
|
126
|
+
| duplicate rows | `dedupe_target_field` not set, or not a stable column | in source-driven mode use the source-id column |
|
|
127
|
+
| `${date}` appears literally in a non-string field | substitution only works in string template values | keep templated values as strings |
|
|
128
|
+
| job not registered | no register rule shipped | ship it |
|
|
129
|
+
|
|
130
|
+
## What to read next
|
|
131
|
+
|
|
132
|
+
- [Add a compliance-sweep job](./add-a-compliance-job.md)
|
|
133
|
+
- [Seed config and reference data on install](./seed-data-on-install.md)
|
|
134
|
+
- Recipe: [scheduled record-generation cadence](../recipes/scheduled-record-generation.md)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Add a compliance-sweep job
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a compliance-sweep job
|
|
7
|
+
|
|
8
|
+
## What you're doing
|
|
9
|
+
|
|
10
|
+
"Find rows that break a policy and flag them — or raise an escalation." More
|
|
11
|
+
expressive than a [reminder job](./add-a-reminder-job.md): the condition is a
|
|
12
|
+
**boolean expression** over the row's fields (plus `now` and an optional
|
|
13
|
+
`related_count`), not just one date column. **No Java.** One
|
|
14
|
+
`entity_compliance_config` row, run by `engine-entity.compliance-sweep`, shipped
|
|
15
|
+
as seed data.
|
|
16
|
+
|
|
17
|
+
Use this instead of a reminder job whenever the condition is more than "one date
|
|
18
|
+
vs now".
|
|
19
|
+
|
|
20
|
+
## Two outcomes
|
|
21
|
+
|
|
22
|
+
| Outcome | Set |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| **FLAG** — set a field on the offending row | `set_field`, `set_value` |
|
|
25
|
+
| **ESCALATION** — raise a row in a separate entity, deduped by source id | `escalation_entity`, `escalation_dedupe_field`, `field_map_json`, `escalation_template_json` |
|
|
26
|
+
|
|
27
|
+
## The complete example — flag un-acknowledged checkouts
|
|
28
|
+
|
|
29
|
+
`spk-assembly/metadata/seed-data/office-equipment-compliance-configs.json`
|
|
30
|
+
(standalone example,
|
|
31
|
+
[real file](../examples/standalone-plugin/spk-assembly/metadata/seed-data/office-equipment-compliance-configs.json)):
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"entity": "entity_compliance_config",
|
|
36
|
+
"keyFields": ["compliance_code"],
|
|
37
|
+
"source": "office-equipment",
|
|
38
|
+
"rows": [
|
|
39
|
+
{
|
|
40
|
+
"compliance_code": "checkout-acknowledgement-compliance",
|
|
41
|
+
"source_entity": "oeq_checkout",
|
|
42
|
+
"when_status_in": "CHECKED_OUT,DUE_SOON,OVERDUE",
|
|
43
|
+
"condition_expr": "custody_acknowledged != true && daysBetween(checkout_date, now()) > 7",
|
|
44
|
+
"set_field": "compliance_flag",
|
|
45
|
+
"set_value": "NON_COMPLIANT",
|
|
46
|
+
"seeded_by": "office-equipment",
|
|
47
|
+
"active": true
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Line by line
|
|
54
|
+
|
|
55
|
+
- **`compliance_code`** — unique id; natural key.
|
|
56
|
+
- **`source_entity`** — table to evaluate.
|
|
57
|
+
- **`when_status_in`** — optional status pre-filter.
|
|
58
|
+
- **`condition_expr`** — a boolean expression in the platform
|
|
59
|
+
`ExpressionEvaluator` language. In scope: the row's own fields, `now`, and (if
|
|
60
|
+
configured) `related_count`. Whitelisted functions only — `daysBetween`,
|
|
61
|
+
`now`, `coalesce`, arithmetic, `&&`, `||`, `!`, comparisons. **No** ternary,
|
|
62
|
+
**no** `in`, **no** `^`. A row where the expression is **true** is
|
|
63
|
+
non-compliant.
|
|
64
|
+
- **`set_field` / `set_value`** — FLAG outcome. Idempotent: a row already equal
|
|
65
|
+
to `set_value` is skipped.
|
|
66
|
+
|
|
67
|
+
### Related-count variant (cross-record)
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
"related_count_entity": "oeq_license_assignment",
|
|
71
|
+
"related_count_key_field": "license_id",
|
|
72
|
+
"related_count_status_in": "ACTIVE",
|
|
73
|
+
"condition_expr": "related_count > entitlement_quantity"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
→ counts `ACTIVE` assignments whose `license_id` equals the source row's id,
|
|
77
|
+
binds it as `related_count`, and flags over-allocated licences.
|
|
78
|
+
|
|
79
|
+
### Escalation variant
|
|
80
|
+
|
|
81
|
+
Replace `set_field`/`set_value` with:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
"escalation_entity": "oeq_compliance_case",
|
|
85
|
+
"escalation_dedupe_field": "checkout_id",
|
|
86
|
+
"field_map_json": { "checkout_id": "id", "employee_id": "employee_id" },
|
|
87
|
+
"escalation_template_json": { "status": "OPEN", "severity": "HIGH" }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
→ one `oeq_compliance_case` per offending checkout, deduped on `checkout_id`.
|
|
91
|
+
|
|
92
|
+
## The register rule
|
|
93
|
+
|
|
94
|
+
`spk-assembly/metadata/rules/ensure_compliance_sweep_job_registered.json`:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"entityType": "entity_compliance_config",
|
|
99
|
+
"name": "ensure_compliance_sweep_job_registered",
|
|
100
|
+
"triggerEvent": "AFTER_CREATE",
|
|
101
|
+
"conditions": null,
|
|
102
|
+
"actions": "[{\"type\": \"EXECUTE_SERVICE\", \"service\": \"ensureEntityComplianceSweepJobRegistered\"}]",
|
|
103
|
+
"priority": 10,
|
|
104
|
+
"active": true
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Ground yourself first
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
erp schema pull entity-compliance-config
|
|
112
|
+
erp examples patterns --kind jobs # "condition-based-compliance-flagging"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## How to verify it worked
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
erp api get "/api/v1/jobs/engine-entity.compliance-sweep"
|
|
119
|
+
# → { "status": "ENABLED", ... }
|
|
120
|
+
|
|
121
|
+
erp api post "/api/v1/jobs/engine-entity.compliance-sweep/execute" --body "{}"
|
|
122
|
+
erp api get "/api/v1/entities/oeq_checkout/records/query?size=10"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Rows that break the policy now carry `"compliance_flag": "NON_COMPLIANT"`.
|
|
126
|
+
|
|
127
|
+
## Common mistakes
|
|
128
|
+
|
|
129
|
+
| Symptom | Cause | Fix |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| expression parse error | used ternary / `in` / `^` | rewrite with whitelisted operators |
|
|
132
|
+
| nothing flagged | `condition_expr` logic inverted (true = compliant) | true must mean *non*-compliant |
|
|
133
|
+
| `related_count` is always 0 | `related_count_key_field` isn't the FK back to the source id | fix the FK column name |
|
|
134
|
+
| escalations duplicate | `escalation_dedupe_field` not the source-id column | must be a numeric column holding the source id |
|
|
135
|
+
|
|
136
|
+
## What to read next
|
|
137
|
+
|
|
138
|
+
- [Add business rules and expressions](./add-business-rules.md) — same expression language
|
|
139
|
+
- Recipe: [condition-based compliance flag](../recipes/condition-based-compliance-flag.md)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Add a create / edit form
|
|
3
|
+
audience: tenant
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a create / edit form
|
|
7
|
+
|
|
8
|
+
## What you're doing
|
|
9
|
+
|
|
10
|
+
Adding a dialog to a list page that creates (or edits) a row. On this platform a
|
|
11
|
+
"form" for a list page is not a separate artifact — it's a `core.dialog` block on
|
|
12
|
+
the page, containing input blocks bound to page state, plus a Save button whose
|
|
13
|
+
action chain `POST`s (or `PUT`s) to the entity.
|
|
14
|
+
|
|
15
|
+
## The pattern
|
|
16
|
+
|
|
17
|
+
1. **A "New" button** clears the form's page-state keys and opens the dialog.
|
|
18
|
+
2. **Each field** is an input block (`core.text-input`, `core.select`,
|
|
19
|
+
`core.date-picker`, `core.number-input`, `core.checkbox`, `core.lookup`)
|
|
20
|
+
whose `value` binds a page-state key and whose `committed` event writes
|
|
21
|
+
`${event.new}` back to that key.
|
|
22
|
+
3. **Save** calls `POST /api/v1/entities/<name>/records` with a `params` object
|
|
23
|
+
built from the page-state keys, closes the dialog, toggles the grid refresh,
|
|
24
|
+
and shows a toast.
|
|
25
|
+
|
|
26
|
+
## The complete example
|
|
27
|
+
|
|
28
|
+
From the tutorial's `equipment-register.json`. The "New" button:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"blockType": "core.button",
|
|
33
|
+
"properties": { "labelKey": { "source": "static", "value": "office-equipment.equipment-register.newBtn" },
|
|
34
|
+
"variant": { "source": "static", "value": "contained" } },
|
|
35
|
+
"events": { "clicked": { "source": "action-chain", "actions": [
|
|
36
|
+
{ "id": "a0", "order": 0, "type": "setValue", "config": { "field": "page.formAssetTag", "value": null } },
|
|
37
|
+
{ "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.formName", "value": null } },
|
|
38
|
+
{ "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.formCategoryId", "value": null } },
|
|
39
|
+
{ "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.formStatus", "value": "AVAILABLE" } },
|
|
40
|
+
{ "id": "a4", "order": 4, "type": "setValue", "config": { "field": "page.formDialogOpen", "value": true } }
|
|
41
|
+
] } }
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
One text field inside the dialog:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"blockType": "core.text-input",
|
|
50
|
+
"properties": { "value": { "source": "binding", "binding": { "scope": "page", "key": "formAssetTag" } } },
|
|
51
|
+
"events": { "committed": { "source": "action-chain", "actions": [
|
|
52
|
+
{ "id": "a0", "order": 0, "type": "setValue",
|
|
53
|
+
"config": { "field": "page.formAssetTag", "value": "${event.new}" } }
|
|
54
|
+
] } }
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The Save button:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"blockType": "core.button",
|
|
63
|
+
"properties": { "labelKey": { "source": "static", "value": "office-equipment.equipment-register.form.saveBtn" },
|
|
64
|
+
"variant": { "source": "static", "value": "contained" } },
|
|
65
|
+
"events": { "clicked": { "source": "action-chain", "actions": [
|
|
66
|
+
{ "id": "a0", "order": 0, "type": "callApi",
|
|
67
|
+
"config": { "connectionRef": "self", "path": "/api/v1/entities/oeq_equipment/records", "httpMethod": "POST",
|
|
68
|
+
"params": {
|
|
69
|
+
"asset_tag": "${page.formAssetTag}",
|
|
70
|
+
"name": "${page.formName}",
|
|
71
|
+
"category_id": "${page.formCategoryId}",
|
|
72
|
+
"status": "${page.formStatus}"
|
|
73
|
+
} },
|
|
74
|
+
"output": "createResult" },
|
|
75
|
+
{ "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.formDialogOpen", "value": false } },
|
|
76
|
+
{ "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.gridRefreshTick", "value": "${!page.gridRefreshTick}" } },
|
|
77
|
+
{ "id": "a3", "order": 3, "type": "showToast", "config": { "message": "Saved." } }
|
|
78
|
+
] } }
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Line by line — the rules that bite
|
|
83
|
+
|
|
84
|
+
- **`committed` fires with `event.new`.** The value the user just entered is
|
|
85
|
+
`${event.new}`. **Not** `event.value`, **not** `event.record`. This is true for
|
|
86
|
+
text inputs, selects, date pickers, checkboxes, and `core.lookup`.
|
|
87
|
+
- **The handler must write back to state.** `committed` → `setValue page.<key> =
|
|
88
|
+
${event.new}`. Without this the input shows what you type but page state never
|
|
89
|
+
updates, and Save sends nulls.
|
|
90
|
+
- **Save reads from `${page.<key>}`** in the `params` object — the same keys the
|
|
91
|
+
inputs write and the "New" button clears.
|
|
92
|
+
- **Clear the keys in the "New" button**, not on dialog close — so re-opening
|
|
93
|
+
always starts blank (or with defaults like `status: "AVAILABLE"`).
|
|
94
|
+
- **Booleans and numbers** flow through as their JSON types when you `setValue`
|
|
95
|
+
a real `true`/`10`; only `defaultValue` in an *entity* file must be a string.
|
|
96
|
+
|
|
97
|
+
## Editing an existing row
|
|
98
|
+
|
|
99
|
+
Same dialog. The grid's `rowClicked` event fetches the record and populates the
|
|
100
|
+
same page-state keys, then opens the dialog:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
"events": { "rowClicked": { "source": "action-chain", "actions": [
|
|
104
|
+
{ "id": "a0", "order": 0, "type": "callApi",
|
|
105
|
+
"config": { "connectionRef": "self", "path": "/api/v1/entities/oeq_equipment/records/${event.id}", "httpMethod": "GET", "params": {} },
|
|
106
|
+
"output": "detail" },
|
|
107
|
+
{ "id": "a1", "order": 1, "type": "setValue", "config": { "field": "page.formAssetTag", "value": "${detail.asset_tag}" } },
|
|
108
|
+
{ "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.editingId", "value": "${event.id}" } },
|
|
109
|
+
{ "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.formDialogOpen", "value": true } }
|
|
110
|
+
] } }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Save then branches on `page.editingId` — `PUT .../records/${page.editingId}` if
|
|
114
|
+
set, `POST` if not. (`${event.id}` is the clicked row's id.)
|
|
115
|
+
|
|
116
|
+
## A lookup field (foreign key)
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"blockType": "core.lookup",
|
|
121
|
+
"properties": {
|
|
122
|
+
"value": { "source": "binding", "binding": { "scope": "page", "key": "formCategoryId" } },
|
|
123
|
+
"recordType": { "source": "static", "value": "OeqCategory" },
|
|
124
|
+
"optionsSourceKey": { "source": "static", "value": "oeq-category-search" }
|
|
125
|
+
},
|
|
126
|
+
"events": { "committed": { "source": "action-chain", "actions": [
|
|
127
|
+
{ "id": "a0", "order": 0, "type": "setValue",
|
|
128
|
+
"config": { "field": "page.formCategoryId", "value": "${event.new}" } }
|
|
129
|
+
] } }
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`optionsSourceKey` is the `name` of a `search` Data Service. `recordType` is a
|
|
134
|
+
documentation-only label. `${event.new}` is the picked record's id. See
|
|
135
|
+
[the lookup recipe](../recipes/related-record-lookup-field.md).
|
|
136
|
+
|
|
137
|
+
## How to verify it worked
|
|
138
|
+
|
|
139
|
+
Publish, then drive it from the API the same way the Save chain does:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
erp api post "/api/v1/entities/oeq_equipment/records" \
|
|
143
|
+
--body '{"asset_tag":"LAP-001","name":"Dell Latitude 7440","category_id":1,"status":"AVAILABLE"}'
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{ "id": 1, "created_by": "you@example.com", "asset_tag": "LAP-001",
|
|
148
|
+
"name": "Dell Latitude 7440", "status": "AVAILABLE", "warranty_alert": false }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
If that works, the dialog works.
|
|
152
|
+
|
|
153
|
+
## Common mistakes
|
|
154
|
+
|
|
155
|
+
| Symptom | Cause | Fix |
|
|
156
|
+
| --- | --- | --- |
|
|
157
|
+
| Save sends `null` for a field the user filled in | `committed` handler missing or reads `event.value` | write `setValue page.<key> = ${event.new}` |
|
|
158
|
+
| dialog re-opens with last row's data | keys cleared on close instead of on "New" | clear in the "New" button chain |
|
|
159
|
+
| grid doesn't show the new row | no `refreshTrigger` toggle in the Save chain | add `setValue page.gridRefreshTick = ${!page.gridRefreshTick}` |
|
|
160
|
+
| lookup saves a name, not an id | bound the label instead of `${event.new}` | `${event.new}` is the id |
|
|
161
|
+
| `POST` 400s on an enum field | value not one of `typeParams.enumValues` | check the entity's enum list |
|
|
162
|
+
|
|
163
|
+
## What to read next
|
|
164
|
+
|
|
165
|
+
- [Add a data provider, data view, or data service](./add-a-data-provider.md)
|
|
166
|
+
- [Add an approval workflow](./add-an-approval-workflow.md) — a Submit button
|
|
167
|
+
- Recipe: [master-detail page with a child grid](../recipes/master-detail-page.md)
|