@dforge-core/dforge-mcp 0.2.35 → 0.2.37
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/CHANGELOG.md +41 -0
- package/dist/server.js +24 -4
- package/package.json +3 -3
- package/resources/docs/dsl-reference.md +100 -1
- package/resources/schemas/deps.schema.json +14 -2
- package/resources/schemas/settings.schema.json +31 -0
- package/resources/schemas/translations.schema.json +13 -1
- package/skills/dforge-mcp-author/SKILL.md +2 -2
- package/skills/dforge-mcp-author/references/action-dsl.md +88 -1
- package/skills/dforge-mcp-author/references/column-types.md +11 -0
- package/skills/dforge-mcp-author/references/data-views.md +39 -7
- package/skills/dforge-mcp-author/references/field-types.md +4 -4
- package/skills/dforge-mcp-author/references/flags.md +2 -1
- package/skills/dforge-mcp-author/references/formulas.md +41 -14
- package/skills/dforge-mcp-author/references/jobs.md +2 -2
- package/skills/dforge-mcp-author/references/manifest.md +4 -1
- package/skills/dforge-mcp-author/references/security.md +7 -0
- package/skills/dforge-mcp-author/references/traits.md +30 -0
- package/skills/dforge-mcp-author/references/validation-checklist.md +9 -0
- package/skills/dforge-module-build/SKILL.md +36 -14
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ All notable changes to `@dforge-core/dforge-mcp`. This project uses semver-ish
|
|
|
4
4
|
`0.1.0-rc.N` pre-release tags; the published version is set at publish time via
|
|
5
5
|
the release workflow, so committed `package.json` versions are placeholders.
|
|
6
6
|
|
|
7
|
+
## 0.2.37 — 2026-10-12
|
|
8
|
+
|
|
9
|
+
**Author references brought in line with the hand-authoring copy in dForge-core.** Content that
|
|
10
|
+
had only reached one of the two now reaches both:
|
|
11
|
+
|
|
12
|
+
- **Data views:** a grid view needs a renderable scalar column, from `dataSources[].columns` or
|
|
13
|
+
from the entity's `V` flags; the `columns` array is authoritative. The old text calling it
|
|
14
|
+
layout-only was wrong.
|
|
15
|
+
- **Formulas:** the full `NUMBER()` / `BOOLEAN()` rules, where the client and server engines
|
|
16
|
+
agree, and multi-hop navigation at SQL time with its depth cap.
|
|
17
|
+
- **Security:** don't grant a right on a reference's target just to make its name show up.
|
|
18
|
+
- **Columns and traits:** how the reference picker behaves on a large target; how to show audit
|
|
19
|
+
columns from a trait; corrected params for the `code`, `tags` and `percent` field types.
|
|
20
|
+
- **Jobs:** `paused` seeds the initial value only; an upgrade never overwrites it.
|
|
21
|
+
- **Flags:** a view column's `notes` row in the example.
|
|
22
|
+
|
|
23
|
+
## 0.2.36 — 2026-10-12
|
|
24
|
+
|
|
25
|
+
**`settings.json` gains `$constraints`**: check constraints across a module's settings, SQL over
|
|
26
|
+
setting codes (`"expression": "default_discount_pct <= max_discount_pct"`). The settings schema
|
|
27
|
+
and `dforge://schema/translations` describe it.
|
|
28
|
+
|
|
29
|
+
**Function libraries.** Helpers a module's actions share go in `logic/functions/*.dsl`; another
|
|
30
|
+
module's functions are imported under `"functions"` in `deps/<module>.json` and called as
|
|
31
|
+
`<module>.<fn>()`. `dforge_module_validate` and install check the imports.
|
|
32
|
+
|
|
33
|
+
**A broken function library is reported once, as itself**, and a `ref` or `domain` may name a
|
|
34
|
+
hyphenated module (`ref cdy-hr.department`); a qualified ref to an unknown module is refused at
|
|
35
|
+
pack and validate.
|
|
36
|
+
|
|
37
|
+
**DSL reference caught up** (`dforge://docs/dsl`, `action-dsl`): a Function libraries section
|
|
38
|
+
(contents, names, state, imports, errors), the six register reads (`getBalance` ... `getCorrespondence`;
|
|
39
|
+
the built-in count is now 42), text action params staying text, `ref` to a hyphenated module, the
|
|
40
|
+
5-minute action-definition cache after a CLI install, and `query()` CTE and write-target qualification.
|
|
41
|
+
|
|
42
|
+
**Module install reports a skip.** Installing a package whose version is already installed
|
|
43
|
+
prints `already at vN — nothing installed (use --force to reinstall)` instead of success.
|
|
44
|
+
|
|
45
|
+
**Register dimensions are never empty** (author skill): an empty dimension refuses the posting by
|
|
46
|
+
design; use a default record, not `COALESCE(…, 0)`.
|
|
47
|
+
|
|
7
48
|
## 0.2.35 — 2026-10-07
|
|
8
49
|
|
|
9
50
|
**`addDays()` moves a date field** (dForge-core #1382). `addDays([order_date], 3)` used to
|
package/dist/server.js
CHANGED
|
@@ -31002,7 +31002,7 @@ var import_node_crypto = require("crypto");
|
|
|
31002
31002
|
var fs2 = __toESM(require("fs"));
|
|
31003
31003
|
var path2 = __toESM(require("path"));
|
|
31004
31004
|
|
|
31005
|
-
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.
|
|
31005
|
+
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.29/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
|
|
31006
31006
|
function buildManifest(opts, moduleId) {
|
|
31007
31007
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
31008
31008
|
const manifest = {
|
|
@@ -31290,7 +31290,7 @@ function buildZedTasks() {
|
|
|
31290
31290
|
var fs = __toESM(require("fs"));
|
|
31291
31291
|
var path = __toESM(require("path"));
|
|
31292
31292
|
|
|
31293
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
31293
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.37/node_modules/@dforge-core/metadata/dist/index.js
|
|
31294
31294
|
var AggType = {
|
|
31295
31295
|
Sum: "sum",
|
|
31296
31296
|
Count: "count",
|
|
@@ -31530,6 +31530,26 @@ function deriveDbDatatype(fieldTypeCd, options = {}) {
|
|
|
31530
31530
|
}
|
|
31531
31531
|
return base;
|
|
31532
31532
|
}
|
|
31533
|
+
var INFO_PROPERTIES = ["period", "recorder", "recordChangesOnly"];
|
|
31534
|
+
var ACCOUNTING_PROPERTIES = [
|
|
31535
|
+
"chart",
|
|
31536
|
+
"composition",
|
|
31537
|
+
"analyticFields",
|
|
31538
|
+
"lineMode",
|
|
31539
|
+
"account",
|
|
31540
|
+
"debitAccount",
|
|
31541
|
+
"creditAccount",
|
|
31542
|
+
"debitAnalyticFields",
|
|
31543
|
+
"creditAnalyticFields",
|
|
31544
|
+
"sideDimensions"
|
|
31545
|
+
];
|
|
31546
|
+
var BALANCE_PROPERTIES = ["allowNegative", "autoCreate"];
|
|
31547
|
+
var REFUSED = {
|
|
31548
|
+
balance: [...INFO_PROPERTIES, ...ACCOUNTING_PROPERTIES],
|
|
31549
|
+
turnover: ["entity", ...INFO_PROPERTIES, ...ACCOUNTING_PROPERTIES, ...BALANCE_PROPERTIES],
|
|
31550
|
+
info: ["journal", "journalDirection", "journalDate", ...ACCOUNTING_PROPERTIES, "totals", ...BALANCE_PROPERTIES],
|
|
31551
|
+
accounting: ["entity", ...INFO_PROPERTIES, "journalDirection", ...BALANCE_PROPERTIES]
|
|
31552
|
+
};
|
|
31533
31553
|
|
|
31534
31554
|
// src/tools/_helpers.ts
|
|
31535
31555
|
function modulePaths(moduleDir) {
|
|
@@ -34602,7 +34622,7 @@ var path10 = __toESM(require("path"));
|
|
|
34602
34622
|
var fs7 = __toESM(require("fs"));
|
|
34603
34623
|
var path9 = __toESM(require("path"));
|
|
34604
34624
|
|
|
34605
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
34625
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.37/node_modules/@dforge-core/metadata/dist/dsl/index.js
|
|
34606
34626
|
var PUNCT3 = ["===", "!=="];
|
|
34607
34627
|
var PUNCT2 = [
|
|
34608
34628
|
"==",
|
|
@@ -38881,7 +38901,7 @@ var resources = [
|
|
|
38881
38901
|
{
|
|
38882
38902
|
uri: "dforge://docs/dsl",
|
|
38883
38903
|
name: "dForge action DSL reference",
|
|
38884
|
-
description: "Full reference for the dForge action DSL (logic/actions/*.dsl files): block structure (params/canExecute/onBeforeStart/execute), execution modes (single/each/batch), field-access syntax ([field], params[name], [ref].[field], records.*), all
|
|
38904
|
+
description: "Full reference for the dForge action DSL (logic/actions/*.dsl files): block structure (params/canExecute/onBeforeStart/execute), execution modes (single/each/batch), field-access syntax ([field], params[name], [ref].[field], records.*), all 42 built-in host functions, incl. register reads, plus function libraries (logic/functions/*.dsl) (query/insert/error/exit/now/sendEmail/...), supported JS subset (ES5 via Esprima/Jint), common patterns, and pitfalls. **Load this before authoring any DSL action (Phase 2 of the wizard).**",
|
|
38885
38905
|
mimeType: "text/markdown",
|
|
38886
38906
|
read: () => readVendored("docs/dsl-reference.md")
|
|
38887
38907
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforge-core/dforge-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.37",
|
|
4
4
|
"description": "MCP server for dForge module authoring. Exposes 35 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/iash44/dForge-core",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"node": ">=18"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@dforge-core/dforge-cli": "^0.2.
|
|
30
|
-
"@dforge-core/metadata": "^0.0.
|
|
29
|
+
"@dforge-core/dforge-cli": "^0.2.29",
|
|
30
|
+
"@dforge-core/metadata": "^0.0.37",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
32
|
"zod": "^4.4.3"
|
|
33
33
|
},
|
|
@@ -133,6 +133,10 @@ execute:
|
|
|
133
133
|
var maxDays = params[supplier].payment_terms
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
A `ref` names the entity by its code, qualified as `module.entity` for another module; a hyphenated module code is fine (`ref cdy-hr.department`), the schema form `cdy_hr.department` is not. Pack and install refuse a `ref` to a module that is neither this one, a declared dependency nor a system module, and install refuses one naming no installed entity.
|
|
137
|
+
|
|
138
|
+
A param declared with a text field type (`text`, `textarea`, ...) stays a string whatever JSON type the client sent: `007` keeps its zeros, and a barcode compares to a text column without a cast. No `'' + params[x]` workaround is needed. Every other param is unwrapped as before.
|
|
139
|
+
|
|
136
140
|
---
|
|
137
141
|
|
|
138
142
|
## Field access syntax
|
|
@@ -211,6 +215,8 @@ info('Reminded ' + open.length + ' open invoice(s)')
|
|
|
211
215
|
> tables. Don't assume the whole action is one transaction either — that depends
|
|
212
216
|
> on the execution path. Applies to `query()` only.
|
|
213
217
|
|
|
218
|
+
> **Table names.** Bare names are qualified with the current module's schema after `FROM` / `JOIN` / `USING` and as the target of `INSERT INTO`, `MERGE INTO`, `UPDATE`; a qualified name (`wms.stock`) is left as written. A CTE name is a source only inside the query that declares its `WITH` (its own body only under `RECURSIVE`); a write target is always the real table, so `WITH t AS (...) UPDATE t ...` updates the module's `t`.
|
|
219
|
+
|
|
214
220
|
#### `insert(entityCd, fields) → record`
|
|
215
221
|
Insert a row. Returns the inserted record as a dictionary (PKs populated, defaults applied, audit columns set).
|
|
216
222
|
|
|
@@ -707,13 +713,95 @@ if (meta != null && meta.priority == 'high') { notify([owner_id], 'High priority
|
|
|
707
713
|
|
|
708
714
|
---
|
|
709
715
|
|
|
716
|
+
### Register reads
|
|
717
|
+
|
|
718
|
+
The first argument names a **register** (`module_cd.register_cd`, as declared in that module's `registers.json`), not an entity. These only ask questions; a document posts through its `A` / `L` column.
|
|
719
|
+
|
|
720
|
+
| Function | Returns | Description |
|
|
721
|
+
|---|---|---|
|
|
722
|
+
| `getBalance('module.register', dims?, asOf?)` | Object, per resource | Balance of an accumulation register. A dimension left out of `dims` is summed over. |
|
|
723
|
+
| `getTurnover('module.register', dims, from, to)` | Object, `{in, out, net}` per resource | Movement over an interval. `dims` may be `null`; `from` and `to` are required. |
|
|
724
|
+
| `getSlice('module.register', dims, onDate?, mode?)` | Object or `null` | One record of an information register, the latest on or before `onDate`, plus `_period`. No dimension may be omitted. |
|
|
725
|
+
| `getAccountBalance('module.register', account, filter?, asOf?)` | Object, `{debit, credit, net}` per resource | Balance of one account of an accounting register. |
|
|
726
|
+
| `getAccountTurnover('module.register', account, filter, from, to)` | Object, `{debit, credit, net}` per resource | Movement of one account over an interval. |
|
|
727
|
+
| `getCorrespondence('module.register', debitAccount, creditAccount, filter, from, to)` | Object, per resource | Movement between two accounts. Needs `lineMode: "compound"`; refuses an analytic filter. |
|
|
728
|
+
|
|
729
|
+
An analytic filter is the key alone (`{ cost_center: 42 }`) or `{ entity, value }` to name one entity of a union kind.
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## Function libraries
|
|
734
|
+
|
|
735
|
+
Helpers several actions share go once in `logic/functions/*.dsl`, never pasted into each action. A library is detected by presence; `manifest.json` declares nothing.
|
|
736
|
+
|
|
737
|
+
```js
|
|
738
|
+
// logic/functions/accounting.dsl
|
|
739
|
+
var recNo = 0
|
|
740
|
+
|
|
741
|
+
function round2(x) {
|
|
742
|
+
return Math.round(x * 100) / 100
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function accountId(code) {
|
|
746
|
+
var rows = query('SELECT account_id FROM account WHERE code = @code', { code: code })
|
|
747
|
+
if (rows.length == 0) error('Account {code} does not exist.', { code: code })
|
|
748
|
+
return rows[0].account_id
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
```js
|
|
753
|
+
// logic/actions/commit_sale.dsl
|
|
754
|
+
execute:
|
|
755
|
+
[amount] = round2([amount])
|
|
756
|
+
var account = accountId(getSetting('sale_account'))
|
|
757
|
+
recNo = recNo + 1
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
- **Contents.** Top-level `function` declarations and top-level `var` / `let` / `const`; any other top-level statement is refused at install. Every built-in an action has.
|
|
761
|
+
- **No record context.** `[field]`, `[ref].[target]`, `params[...]` and `old[...]` are refused: pass values as arguments (`round2([amount])`). `[name]` standing alone is refused too, even where JavaScript would read an array; write a one-element array as `Array.of(name)`. An index (`rows[i]`) is fine.
|
|
762
|
+
- **Names.** File names use letters, digits, `_`, `-`. A library may not declare a built-in's name, a JS global (`Math`, `Date`), `schema` or a `__` name. A name is declared once across a module's libraries. An action may not declare a global of a library's name (a top-level function or variable, or a `var` outside any function): delete the local copy. Inside a function, or a block-scoped `let` / `const`, a name may repeat.
|
|
763
|
+
- **State.** Top-level variables start afresh on every run (each record of a `single` action, each batch, trigger fire and job run), so a counter like `recNo` never leaks between runs or users.
|
|
764
|
+
- **Messages.** `error()` / `warn()` / `info()` / `exit()` texts in a library are translated from the module's `messages` block like an action's.
|
|
765
|
+
- **Errors.** A runtime error names the file and line: `(logic/functions/tax.dsl, line 3)`. A library that does not compile is reported once, as itself (`Function library failed to compile:` with file and line), at pack, validate and install, not as undefined calls in the actions.
|
|
766
|
+
- **Linked at run time**, not copied into actions: changing a library changes every action that calls it.
|
|
767
|
+
- **Not available:** formulas (`F` columns, `canExecute:`) cannot call libraries; the in-app editor does not show them; Studio refuses a module that has libraries or imports.
|
|
768
|
+
|
|
769
|
+
### Another module's functions
|
|
770
|
+
|
|
771
|
+
List the functions under `"functions"` in that dependency's contract `deps/<module>.json`, then call them as `<module_with_underscores>.<fn>()`:
|
|
772
|
+
|
|
773
|
+
```json
|
|
774
|
+
{
|
|
775
|
+
"module": "cdy-accounting",
|
|
776
|
+
"version": ">=0.3.0",
|
|
777
|
+
"entities": {},
|
|
778
|
+
"functions": ["roleOf"]
|
|
779
|
+
}
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
```js
|
|
783
|
+
execute:
|
|
784
|
+
var account = cdy_accounting.roleOf([doc_type], 'stock')
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
- The namespace is the provider's code with `-` replaced by `_`, so `foo-bar` and `foo_bar` share one. Only the listed functions are in it; a call to another fails at install. Only a module in `dependencies` can be called.
|
|
788
|
+
- The function runs as its provider: unqualified tables resolve in the provider's schema, `getSetting()` reads the provider's settings, its messages use the provider's translations. User, folder, transaction and run limits are the caller's.
|
|
789
|
+
- A provider's top-level variables are its own, fresh per run.
|
|
790
|
+
- Libraries may call imported functions too, and imports chain (a provider's functions call what the provider imports). Import only what you call yourself.
|
|
791
|
+
- Install checks each imported name is a top-level function of the installed provider's libraries. A contract that imports only functions may declare `"entities": {}`.
|
|
792
|
+
- A provider upgrade that drops an imported function is not re-checked against consumers: the call finds `undefined` at run time.
|
|
793
|
+
|
|
794
|
+
**Definitions are cached.** The server reads an action's definition through a per-tenant cache (5 minutes), cleared on module install, uninstall and Studio publish. A CLI install is noticed only through a connected browser, so without one a changed action can run stale for up to 5 minutes.
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
710
798
|
## JavaScript subset
|
|
711
799
|
|
|
712
800
|
Esprima parses ES5; Jint executes.
|
|
713
801
|
|
|
714
802
|
**Supported control flow:** `if / else`, `for`, `while`, `do...while`, `switch / case / default / break / continue`, `return` (only inside `function`), `try / catch / finally`, `throw`.
|
|
715
803
|
|
|
716
|
-
**Supported declarations:** `var`, `function`. (No `let`, no `const`.)
|
|
804
|
+
**Supported declarations:** `var`, `function`. (No `let`, no `const` in actions; a function library may use top-level `let` / `const`.)
|
|
717
805
|
|
|
718
806
|
**Operators:** all standard arithmetic / comparison / logical / assignment, including ternary `? :`.
|
|
719
807
|
|
|
@@ -1060,6 +1148,15 @@ var q = query('SELECT * FROM customer WHERE name = ' + "'" + params[name] + "'",
|
|
|
1060
1148
|
var q = query('SELECT * FROM customer WHERE name = @n', { n: params[name] })
|
|
1061
1149
|
```
|
|
1062
1150
|
|
|
1151
|
+
### ❌ Library and import mistakes
|
|
1152
|
+
|
|
1153
|
+
- A **function library** (`logic/functions/*.dsl`) that reads `[field]` / `params[...]` / `old[...]`, uses `[name]` alone (write `Array.of(name)`), or has a top-level statement that is not a `function` / `var` / `let` / `const` -- all refused at install. Pass values in as arguments. See "Function libraries" above.
|
|
1154
|
+
- Keeping a **local copy** of a library function in an action (a top-level function, or a `var` of the same name outside any function) -- refused at install. Delete the copy.
|
|
1155
|
+
- Calling another module's function without importing it: list it under `"functions"` in `deps/<module>.json`, call it as `<module_with_underscores>.fn()`.
|
|
1156
|
+
- `ref cdy_hr.department` (schema form) in `params:` -- installs with no picker target. Write `ref cdy-hr.department`.
|
|
1157
|
+
- `'' + params[barcode]` to keep a text param text -- no longer needed.
|
|
1158
|
+
|
|
1159
|
+
|
|
1063
1160
|
### ❌ Forgetting to bump `[updated_at]` on writes
|
|
1064
1161
|
|
|
1065
1162
|
The `audit` trait auto-maintains `last_updated` on INSERT/UPDATE if the column exists, but only via the standard data-access path. Direct `[field] = value` writes from DSL trigger the same path, so `last_updated` updates correctly. (No action needed — listed here as reassurance.)
|
|
@@ -1085,6 +1182,8 @@ The `audit` trait auto-maintains `last_updated` on INSERT/UPDATE if the column e
|
|
|
1085
1182
|
| Send toast | `info('msg')`, `warn('msg')` |
|
|
1086
1183
|
| Send email | `sendEmail(to, subj, body)` or `sendEmail(to, templateCd, dataObj)` |
|
|
1087
1184
|
| Send inbox notification | `notify(userId, 'msg')` |
|
|
1185
|
+
| Call a shared helper | `round2([amount])` from `logic/functions/*.dsl` |
|
|
1186
|
+
| Call another module's helper | `cdy_accounting.roleOf(...)`, imported in `deps/<module>.json` |
|
|
1088
1187
|
| Add days | `addDays(date, n)` |
|
|
1089
1188
|
| Now | `now()` |
|
|
1090
1189
|
|
|
@@ -18,12 +18,24 @@
|
|
|
18
18
|
},
|
|
19
19
|
"entities": {
|
|
20
20
|
"type": "object",
|
|
21
|
-
"description": "Consumed provider entities, keyed by entity code (e.g. 'party'). At least one, except against a system module (admin, metadata, workspace, sys-billing), where {} declares a pure platform-version gate for a feature that introduced no entity.",
|
|
21
|
+
"description": "Consumed provider entities, keyed by entity code (e.g. 'party'). At least one, except against a system module (admin, metadata, workspace, sys-billing), where {} declares a pure platform-version gate for a feature that introduced no entity, or when 'functions' imports at least one function.",
|
|
22
22
|
"additionalProperties": { "$ref": "#/definitions/entityContract" }
|
|
23
|
+
},
|
|
24
|
+
"functions": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"description": "The provider's DSL library functions (logic/functions/*.dsl) the consumer's actions call, as <provider code with '-' replaced by '_'>.<name>(...). Only listed names are reachable, and install checks the installed provider declares each. See docs/business-logic/dsl-functions.md.",
|
|
27
|
+
"items": { "type": "string", "pattern": "^[A-Za-z_$][A-Za-z0-9_$]*$" },
|
|
28
|
+
"uniqueItems": true,
|
|
29
|
+
"minItems": 1
|
|
23
30
|
}
|
|
24
31
|
},
|
|
25
32
|
"if": {
|
|
26
|
-
"not": {
|
|
33
|
+
"not": {
|
|
34
|
+
"anyOf": [
|
|
35
|
+
{ "properties": { "module": { "enum": ["admin", "metadata", "workspace", "sys-billing"] } }, "required": ["module"] },
|
|
36
|
+
{ "required": ["functions"] }
|
|
37
|
+
]
|
|
38
|
+
}
|
|
27
39
|
},
|
|
28
40
|
"then": {
|
|
29
41
|
"properties": { "entities": { "minProperties": 1 } }
|
|
@@ -4,10 +4,41 @@
|
|
|
4
4
|
"title": "dForge Module Settings",
|
|
5
5
|
"description": "Module-level settings definitions: a flat map of setting code → setting definition. Settings are folder-scoped at runtime (values inherit through the folder tree) and surface in the Module Settings UI.",
|
|
6
6
|
"type": "object",
|
|
7
|
+
"propertyNames": {
|
|
8
|
+
"pattern": "^(\\$constraints|[^$].*)$",
|
|
9
|
+
"description": "A setting code cannot start with '$'; $constraints is the only reserved key."
|
|
10
|
+
},
|
|
11
|
+
"properties": {
|
|
12
|
+
"$constraints": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"description": "Checks across this module's settings, keyed by constraint name. The settings are the columns of one virtual row, so each is an entity check constraint: SQL over setting codes. settings.set and settings.setMany run every rule naming a written setting, at the folder written and at each folder below it that overrides a setting the rule names; install refuses defaults that fail one. NULL passes, as in a CHECK.",
|
|
15
|
+
"additionalProperties": {
|
|
16
|
+
"$ref": "#/$defs/constraint"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
7
20
|
"additionalProperties": {
|
|
8
21
|
"$ref": "#/$defs/setting"
|
|
9
22
|
},
|
|
10
23
|
"$defs": {
|
|
24
|
+
"constraint": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"required": ["type", "expression"],
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"properties": {
|
|
29
|
+
"type": {
|
|
30
|
+
"const": "check"
|
|
31
|
+
},
|
|
32
|
+
"expression": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "Boolean over this module's settings in SQL CHECK syntax, e.g. \"default_discount_pct <= max_discount_pct\": comparisons, IS [NOT] NULL, [NOT] IN, [NOT] BETWEEN, regex matches (~ ~* !~ !~*) against a string literal, AND/OR/NOT, + - * /, literals and setting codes (bare or \"quoted\"). No functions, casts or [field] brackets; IS NULL rather than = null. The settings it names are found in it: writing any of them re-checks the rule."
|
|
35
|
+
},
|
|
36
|
+
"message": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "Violation message. Localizable via translations/<locale>.json under settings.$constraints.<name>.message."
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
11
42
|
"setting": {
|
|
12
43
|
"type": "object",
|
|
13
44
|
"properties": {
|
|
@@ -46,7 +46,19 @@
|
|
|
46
46
|
},
|
|
47
47
|
"settings": {
|
|
48
48
|
"type": "object",
|
|
49
|
-
"description": "Setting code (settings.json) → label and description.",
|
|
49
|
+
"description": "Setting code (settings.json) → label and description; $constraints → constraint name → message.",
|
|
50
|
+
"properties": {
|
|
51
|
+
"$constraints": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"properties": {
|
|
57
|
+
"message": { "type": "string" }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
50
62
|
"additionalProperties": {
|
|
51
63
|
"$ref": "#/$defs/labelDesc"
|
|
52
64
|
}
|
|
@@ -71,9 +71,9 @@ Full descriptions are on the tools themselves — this is the map.
|
|
|
71
71
|
| Cross-cutting | `dforge_dependency_add`, `dforge_diagram_add` |
|
|
72
72
|
|
|
73
73
|
**Composite tools first.** A relation, a roll-up, and a status column each span
|
|
74
|
-
several coordinated keys, and hand-assembling them from `
|
|
74
|
+
several coordinated keys, and hand-assembling them from `dforge_entity_field_add` is
|
|
75
75
|
the top source of broken modules. The composite tools emit the whole shape and
|
|
76
|
-
can't express the broken variants. Reach for `
|
|
76
|
+
can't express the broken variants. Reach for `dforge_entity_field_add` only for a
|
|
77
77
|
plain scalar column.
|
|
78
78
|
|
|
79
79
|
## Writing files: preview vs `apply`
|
|
@@ -59,6 +59,10 @@ params:
|
|
|
59
59
|
|
|
60
60
|
Types are a subset of column field types: `text`, `textarea`, `number`, `currency`, `percent`, `checkbox`, `date`, `datetime`, `dropdown`, `lookup`, `user`.
|
|
61
61
|
|
|
62
|
+
A `ref` names the entity by its code, qualified as `module.entity` for another module; a hyphenated module code is fine (`ref cdy-hr.department`), the schema form `cdy_hr.department` is not. Pack and install refuse a `ref` to a module that is neither this one, a declared dependency nor a system module, and install refuses one naming no installed entity.
|
|
63
|
+
|
|
64
|
+
A param declared with a text field type (`text`, `textarea`, ...) stays a string whatever JSON type the client sent: `007` keeps its zeros, and a barcode compares to a text column without a cast. No `'' + params[x]` workaround is needed. Every other param is unwrapped as before.
|
|
65
|
+
|
|
62
66
|
Access in execute block: `params[param_name]` (bracket syntax, NOT dot syntax).
|
|
63
67
|
|
|
64
68
|
#### Dropdown params: label the options, or borrow a domain
|
|
@@ -298,6 +302,21 @@ var queued = select('comm.message', {
|
|
|
298
302
|
- `query()` remains the escape hatch for anything this shape can't express: aggregates, multi-hop
|
|
299
303
|
joins, CTEs, `FOR UPDATE`.
|
|
300
304
|
|
|
305
|
+
### Register reads
|
|
306
|
+
|
|
307
|
+
The first argument names a **register** (`module_cd.register_cd`, as declared in that module's `registers.json`), not an entity. These only ask questions; a document posts through its `A` / `L` column.
|
|
308
|
+
|
|
309
|
+
| Function | Returns | Description |
|
|
310
|
+
|---|---|---|
|
|
311
|
+
| `getBalance('module.register', dims?, asOf?)` | Object, per resource | Balance of an accumulation register. A dimension left out of `dims` is summed over. |
|
|
312
|
+
| `getTurnover('module.register', dims, from, to)` | Object, `{in, out, net}` per resource | Movement over an interval. `dims` may be `null`; `from` and `to` are required. |
|
|
313
|
+
| `getSlice('module.register', dims, onDate?, mode?)` | Object or `null` | One record of an information register, the latest on or before `onDate`, plus `_period`. No dimension may be omitted. |
|
|
314
|
+
| `getAccountBalance('module.register', account, filter?, asOf?)` | Object, `{debit, credit, net}` per resource | Balance of one account of an accounting register. |
|
|
315
|
+
| `getAccountTurnover('module.register', account, filter, from, to)` | Object, `{debit, credit, net}` per resource | Movement of one account over an interval. |
|
|
316
|
+
| `getCorrespondence('module.register', debitAccount, creditAccount, filter, from, to)` | Object, per resource | Movement between two accounts. Needs `lineMode: "compound"`; refuses an analytic filter. |
|
|
317
|
+
|
|
318
|
+
An analytic filter is the key alone (`{ cost_center: 42 }`) or `{ entity, value }` to name one entity of a union kind.
|
|
319
|
+
|
|
301
320
|
### Messaging
|
|
302
321
|
|
|
303
322
|
| Function | Description |
|
|
@@ -392,6 +411,67 @@ error('Insufficient stock. Available: {available}, requested: {requested}',
|
|
|
392
411
|
> (undefined in `execute:`) that the server translates to an exact `::bigint` SQL literal.
|
|
393
412
|
|
|
394
413
|
|
|
414
|
+
## Function libraries
|
|
415
|
+
|
|
416
|
+
Helpers several actions share go once in `logic/functions/*.dsl`, never pasted into each action. A library is detected by presence; `manifest.json` declares nothing.
|
|
417
|
+
|
|
418
|
+
```js
|
|
419
|
+
// logic/functions/accounting.dsl
|
|
420
|
+
var recNo = 0
|
|
421
|
+
|
|
422
|
+
function round2(x) {
|
|
423
|
+
return Math.round(x * 100) / 100
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function accountId(code) {
|
|
427
|
+
var rows = query('SELECT account_id FROM account WHERE code = @code', { code: code })
|
|
428
|
+
if (rows.length == 0) error('Account {code} does not exist.', { code: code })
|
|
429
|
+
return rows[0].account_id
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
```js
|
|
434
|
+
// logic/actions/commit_sale.dsl
|
|
435
|
+
execute:
|
|
436
|
+
[amount] = round2([amount])
|
|
437
|
+
var account = accountId(getSetting('sale_account'))
|
|
438
|
+
recNo = recNo + 1
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
- **Contents.** Top-level `function` declarations and top-level `var` / `let` / `const`; any other top-level statement is refused at install. Every built-in an action has.
|
|
442
|
+
- **No record context.** `[field]`, `[ref].[target]`, `params[...]` and `old[...]` are refused: pass values as arguments (`round2([amount])`). `[name]` standing alone is refused too, even where JavaScript would read an array; write a one-element array as `Array.of(name)`. An index (`rows[i]`) is fine.
|
|
443
|
+
- **Names.** File names use letters, digits, `_`, `-`. A library may not declare a built-in's name, a JS global (`Math`, `Date`), `schema` or a `__` name. A name is declared once across a module's libraries. An action may not declare a global of a library's name (a top-level function or variable, or a `var` outside any function): delete the local copy. Inside a function, or a block-scoped `let` / `const`, a name may repeat.
|
|
444
|
+
- **State.** Top-level variables start afresh on every run (each record of a `single` action, each batch, trigger fire and job run), so a counter like `recNo` never leaks between runs or users.
|
|
445
|
+
- **Messages.** `error()` / `warn()` / `info()` / `exit()` texts in a library are translated from the module's `messages` block like an action's.
|
|
446
|
+
- **Errors.** A runtime error names the file and line: `(logic/functions/tax.dsl, line 3)`. A library that does not compile is reported once, as itself (`Function library failed to compile:` with file and line), at pack, validate and install, not as undefined calls in the actions.
|
|
447
|
+
- **Linked at run time**, not copied into actions: changing a library changes every action that calls it.
|
|
448
|
+
- **Not available:** formulas (`F` columns, `canExecute:`) cannot call libraries; the in-app editor does not show them; Studio refuses a module that has libraries or imports.
|
|
449
|
+
|
|
450
|
+
### Another module's functions
|
|
451
|
+
|
|
452
|
+
List the functions under `"functions"` in that dependency's contract `deps/<module>.json`, then call them as `<module_with_underscores>.<fn>()`:
|
|
453
|
+
|
|
454
|
+
```json
|
|
455
|
+
{
|
|
456
|
+
"module": "cdy-accounting",
|
|
457
|
+
"version": ">=0.3.0",
|
|
458
|
+
"entities": {},
|
|
459
|
+
"functions": ["roleOf"]
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
```js
|
|
464
|
+
execute:
|
|
465
|
+
var account = cdy_accounting.roleOf([doc_type], 'stock')
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
- The namespace is the provider's code with `-` replaced by `_`, so `foo-bar` and `foo_bar` share one. Only the listed functions are in it; a call to another fails at install. Only a module in `dependencies` can be called.
|
|
469
|
+
- The function runs as its provider: unqualified tables resolve in the provider's schema, `getSetting()` reads the provider's settings, its messages use the provider's translations. User, folder, transaction and run limits are the caller's.
|
|
470
|
+
- A provider's top-level variables are its own, fresh per run.
|
|
471
|
+
- Libraries may call imported functions too, and imports chain (a provider's functions call what the provider imports). Import only what you call yourself.
|
|
472
|
+
- Install checks each imported name is a top-level function of the installed provider's libraries. A contract that imports only functions may declare `"entities": {}`.
|
|
473
|
+
- A provider upgrade that drops an imported function is not re-checked against consumers: the call finds `undefined` at run time.
|
|
474
|
+
|
|
395
475
|
## Real examples from the codebase
|
|
396
476
|
|
|
397
477
|
### Simple field update (status transition)
|
|
@@ -650,6 +730,8 @@ In `ui/actions.json`:
|
|
|
650
730
|
| `orderNum` | Recommended | Display order in the action menu |
|
|
651
731
|
| `isAsync` | Optional | `true` **permits** background execution — it does not force it. A parameterless action is queued directly; one with params is offered to the user as both "Run" and "Run in Background". Queued runs get a completion notification |
|
|
652
732
|
|
|
733
|
+
**Definitions are cached.** The server reads an action's definition through a per-tenant cache (5 minutes), cleared on module install, uninstall and Studio publish. A CLI install is noticed only through a connected browser, so without one a changed action can run stale for up to 5 minutes.
|
|
734
|
+
|
|
653
735
|
## Atomicity
|
|
654
736
|
|
|
655
737
|
`isTransacted: true` makes the run **one transaction**, in both execution modes.
|
|
@@ -710,7 +792,7 @@ be either.
|
|
|
710
792
|
> and `ON COMMIT DROP` on any temp table. Only `query()` can do any of this — the
|
|
711
793
|
> structured built-ins emit plain DML — and nothing checks it statically.
|
|
712
794
|
|
|
713
|
-
1. **
|
|
795
|
+
1. **Bare table names are qualified automatically** with the current module's schema after `FROM` / `JOIN` / `USING`, and as the target of `INSERT INTO`, `MERGE INTO` and `UPDATE`. A name already qualified (`wms.stock_movement`) is left as written, so qualify only another module's table. Left alone: words that name no table (`IS DISTINCT FROM`, `EXTRACT(year FROM d)`, `FOR UPDATE`, `DO UPDATE SET`, `SELECT ... INTO var`), built-in set functions (`generate_series`, `unnest`), literals and comments. A CTE name is a source only inside the query that declares its `WITH` (and in its own body only under `RECURSIVE`); a write target is always the real table, so `WITH t AS (...) UPDATE t ...` updates the module's `t`. `;`-separated statements are each qualified. (Module codes with a hyphen, e.g. `crm-fin`, cannot be schema-qualified by hand in raw SQL; use the structured built-ins, which accept `module.entity`.)
|
|
714
796
|
2. **Use `@param` placeholders** for values — NEVER concatenate user input into SQL strings. The engine auto-parameterizes.
|
|
715
797
|
3. **SELECT, INSERT, UPDATE, DELETE all work** via `query()`. For reads, it returns row arrays. For writes, it executes the statement.
|
|
716
798
|
4. **`insert()` is preferred over `query('INSERT ...')`**, `update()`/`delete()` over `query('UPDATE/DELETE ...')`, and `select()` over `query('SELECT ...')` for module entities — raw SQL skips auto-fill (PKs, audit, number sequences) and metadata validation.
|
|
@@ -731,6 +813,11 @@ be either.
|
|
|
731
813
|
- Calling `TODAY()` or `NOW()` (uppercase) inside `execute:` — **wrong**, they're undefined there and install fails with `'TODAY' is not defined`. Use lowercase **`now()`** in `execute:`; `TODAY()`/`NOW()` are formula-only (`canExecute:`, formula columns).
|
|
732
814
|
- Forgetting that `insert()` returns the full row — you can use `quote.quote_id` immediately after insert.
|
|
733
815
|
- Using `[field]` inside a `query()` SQL string — **wrong**. `[field]` is resolved in JS scope, not inside SQL strings. Pass values as `@param` arguments.
|
|
816
|
+
- A **function library** (`logic/functions/*.dsl`) that reads `[field]` / `params[...]` / `old[...]`, uses `[name]` alone (write `Array.of(name)`), or has a top-level statement that is not a `function` / `var` / `let` / `const` -- all refused at install. Pass values in as arguments. See *Function libraries*.
|
|
817
|
+
- Keeping a **local copy** of a library function in an action (a top-level function, or a `var` of the same name outside any function) -- refused at install. Delete the copy.
|
|
818
|
+
- Calling another module's function without importing it: list it under `"functions"` in `deps/<module>.json`, call it as `<module_with_underscores>.fn()`.
|
|
819
|
+
- `ref cdy_hr.department` (schema form) in `params:` -- installs with no picker target. Write `ref cdy-hr.department`.
|
|
820
|
+
- `'' + params[barcode]` to keep a text param text -- no longer needed.
|
|
734
821
|
|
|
735
822
|
## Internal API (advanced)
|
|
736
823
|
|
|
@@ -101,6 +101,17 @@ A reference entry may declare `onDelete` and/or `onUpdate` to emit an `ON DELETE
|
|
|
101
101
|
|
|
102
102
|
**Why two columns?** The FK stores the actual value. The Reference column configures how the value is rendered in the UI (as a typeahead picker showing the target's `toString`). They must be kept in sync — column order in JSON doesn't matter but `orderNum` determines UI ordering.
|
|
103
103
|
|
|
104
|
+
### How the picker behaves on a large target
|
|
105
|
+
|
|
106
|
+
Opening the picker loads the **first 200 rows** of the target entity — enough to browse, cheap on a table of any size. Typing does *not* filter those 200 rows: the query goes to the server as a case-insensitive match over the columns named in the target's `toString`, so a target with 200 000 rows is as pickable as one with 20. Every whitespace-separated term must match at least one of those columns, which is what lets a `toString` of `"{first_name} {last_name}"` answer "John Smith".
|
|
107
|
+
|
|
108
|
+
Two consequences for the entity you point at:
|
|
109
|
+
|
|
110
|
+
- **Put the columns people search by in `toString`.** They are the only ones the picker searches. A target whose `toString` is `"{code}"` cannot be found by name, however prominent the name column is elsewhere.
|
|
111
|
+
- **A `toString` built only from formula columns can't be searched server-side.** Neither formula nor set columns translate to a `WHERE` clause, so the picker falls back to narrowing the 200 rows it already loaded — rows past that point stay unreachable. Name at least one stored column in the template.
|
|
112
|
+
|
|
113
|
+
`refFilter` and folder row filters still apply: they are AND-ed onto the search, so a filtered-out row does not surface just because the user typed its name.
|
|
114
|
+
|
|
104
115
|
## Set columns (`columnType: "S"`)
|
|
105
116
|
|
|
106
117
|
Virtual 1:N backwards reference. Shows a grid of related records on the parent's detail view.
|
|
@@ -60,26 +60,58 @@ Lives in: `ui/data_views.json`
|
|
|
60
60
|
}
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
## Critical rule — the
|
|
63
|
+
## Critical rule — the view needs something to draw
|
|
64
64
|
|
|
65
|
-
A grid-style view
|
|
65
|
+
A grid-style view resolves its columns one of two ways, and **one of them has to produce a renderable scalar column** — anything that isn't a set (`columnType: "S"`, a 1:N collection with no per-row value):
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
1. **The source names its own columns.** `dataSources[].columns` wins outright — see [the section below](#the-columns-array-is-authoritative). Flags don't enter into it, so this route works even when every field on the entity is internal.
|
|
68
|
+
2. **It doesn't, and the entity's `V` flags decide.** With no `columns` array the view falls back to the entity's **visible scalar columns** — fields whose `flags` include `V` and whose `columnType` isn't `S`.
|
|
69
|
+
|
|
70
|
+
With neither, the view renders the runtime empty state *"No visible columns configured for this entity."* — a broken screen the user only hits when they open the menu item. `dforge_module_validate` / `dforge_module_pack` **reject** that before install.
|
|
68
71
|
|
|
69
72
|
```jsonc
|
|
70
|
-
// WRONG — grid
|
|
73
|
+
// WRONG — grid falls back to flags, and no field has "V"
|
|
71
74
|
"fields": {
|
|
72
|
-
"code": { "fieldTypeCd": "text", "flags": "EM" }, // E=editable, M
|
|
73
|
-
"lines": { "columnType": "S", "flags": "
|
|
75
|
+
"code": { "fieldTypeCd": "text", "flags": "EM" }, // E=editable, M=… but no V
|
|
76
|
+
"lines": { "columnType": "S", "flags": "VEM" } // visible, but a SET — doesn't count for a grid
|
|
74
77
|
}
|
|
75
|
-
// RIGHT — at least one visible scalar field
|
|
78
|
+
// RIGHT (route 2) — at least one visible scalar field
|
|
76
79
|
"fields": {
|
|
77
80
|
"code": { "fieldTypeCd": "text", "flags": "VEM" } // V present, not a set → satisfies the grid
|
|
78
81
|
}
|
|
82
|
+
// ALSO RIGHT (route 1) — the entity's fields stay internal, the view says what it shows
|
|
83
|
+
"dataSources": [
|
|
84
|
+
{ "entityCode": "audit_log", "columns": ["created_date", "created_by_user"] }
|
|
85
|
+
]
|
|
79
86
|
```
|
|
80
87
|
|
|
88
|
+
Practical guidance: reach for route 2. Flags are what every other surface reads — the card, the Fields popover, quick-add — so an entity whose real business fields are invisible is usually a mistake, even when one view papers over it. Route 1 is for columns that are *deliberately* internal, audit columns above all.
|
|
89
|
+
|
|
81
90
|
This applies to every column-rendering view type (grid, list, kanban, calendar, gallery, tree-grid, master-detail, and the default). The column-agnostic types — `diagram`, `matrix`, `library` — draw from `viewConfig`/relationships instead, so they're **exempt** from this rule.
|
|
82
91
|
|
|
92
|
+
## The `columns` array is authoritative
|
|
93
|
+
|
|
94
|
+
A column you name in `dataSources[].columns` renders **even if its flags omit `V`**. The list is your statement of what this view shows, so it wins over the entity's default visibility — that's how the internal (`flags: "I"`) columns the `audit` / `audit-full` traits contribute get onto a grid:
|
|
95
|
+
|
|
96
|
+
```jsonc
|
|
97
|
+
{
|
|
98
|
+
"entityCode": "trip",
|
|
99
|
+
"columns": ["trip_number", "status", "created_by_user"] // renders Created By, read-only
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A promoted column stays **read-only** unless its flags also carry `E`, and it shows up in the Fields popover like any other column.
|
|
104
|
+
|
|
105
|
+
What `columns` can't do — each fails `dforge_module_validate` / `dforge_module_pack`, instead of dropping silently:
|
|
106
|
+
|
|
107
|
+
| Entry | Why it's rejected |
|
|
108
|
+
|-------|-------------------|
|
|
109
|
+
| A code the entity doesn't define | Nothing to render — usually a typo (`avatar` for `avatar_image`) |
|
|
110
|
+
| A set column (`columnType: "S"`) | A 1:N collection has no per-row value for a column |
|
|
111
|
+
| A nav path (`account.company_name`) | The list takes plain column codes — add a Reference (`R`) or Formula (`F`) column to the entity and list that |
|
|
112
|
+
|
|
113
|
+
The `matrix` / `diagram` / `library` types are exempt here too: their `columns` entry feeds axes and config, not a header row.
|
|
114
|
+
|
|
83
115
|
See [flags.md](flags.md) for the flag string, [column-types.md](column-types.md) for `columnType` (`R`/`S`/`F`).
|
|
84
116
|
|
|
85
117
|
## View types
|
|
@@ -24,10 +24,10 @@ Source of truth: `server/database/system-modules/metadata/seed-data/field_types.
|
|
|
24
24
|
| `textarea` | `string` | Multi-line plain text. | `text` | `defaultLines`, `maxLines` |
|
|
25
25
|
| `markdown` | `string` | Markdown editor (Write/Preview tabs), rendered sanitized. | `text` | — |
|
|
26
26
|
| `richtext` | `string` | Rich text WYSIWYG editor, stores sanitized HTML. | `text` | — |
|
|
27
|
-
| `code` | `string` | Code editor
|
|
27
|
+
| `code` | `string` | Code editor (CodeMirror, dForge DSL grammar). | `text` | `defaultLines` |
|
|
28
28
|
| `dropdown` | `string` | Fixed list of options. | `varchar` | `options: ["a","b",…]` or `[{value,label,icon,color}]` |
|
|
29
29
|
| `flags` | `string` | Multiple checkboxes with letter flags. | `varchar` | `optionSets`, `style: "buttons"` |
|
|
30
|
-
| `tags` | `string` | Free-form tag list. | `varchar` or `text` |
|
|
30
|
+
| `tags` | `string` | Free-form comma-separated tag list. | `varchar` or `text` | `options` (optional suggestions) |
|
|
31
31
|
| `color` | `string` | Color picker. | `varchar(20)` | — |
|
|
32
32
|
| `hidden` | `string` | Internal, never shown in UI. | `varchar` | — |
|
|
33
33
|
|
|
@@ -37,7 +37,7 @@ Source of truth: `server/database/system-modules/metadata/seed-data/field_types.
|
|
|
37
37
|
|---|---|---|---|---|
|
|
38
38
|
| `number` | `number` | Numeric value. | `int`, `bigint`, `numeric` | `min`, `max`, `scale` |
|
|
39
39
|
| `currency` | `number` | Monetary value. Precision 2. | `numeric(18,2)` | `currency: "USD"`, `min`, `max` |
|
|
40
|
-
| `percent` | `number` | Percentage
|
|
40
|
+
| `percent` | `number` | Percentage. Precision 2. | `numeric(5,2)` | `min`, `max` |
|
|
41
41
|
|
|
42
42
|
## Boolean-backed
|
|
43
43
|
|
|
@@ -72,7 +72,7 @@ Source of truth: `server/database/system-modules/metadata/seed-data/field_types.
|
|
|
72
72
|
| `user` | `cuid` | `D` | User picker. Writes a user ID directly, no paired FK needed. |
|
|
73
73
|
| `grid` | `set` | `S` | Detail grid of related records (1:N backwards reference). Used with `link` declaring the relation. |
|
|
74
74
|
| `list` | `set` | `S` | Detail list — same 1:N set as `grid`, rendered as a card list instead of a table. |
|
|
75
|
-
| `entitylink` | `json` | `D` | Polymorphic link to any entity
|
|
75
|
+
| `entitylink` | `json` | `D` | Polymorphic link to any entity. Stores `{entityId, entity, pk: {col: "val"}, description?}` — `pk` is a per-column map (values as strings), there is no `id` key. Populate from action DSL with the `entityLink('entityCd', record, description?)` built-in; pass a qualified entity code (see `action-dsl.md`). |
|
|
76
76
|
|
|
77
77
|
## JSON
|
|
78
78
|
|
|
@@ -57,7 +57,8 @@ Because a view writes no DDL, the requiredness `M` produces there is runtime-onl
|
|
|
57
57
|
"party_id": {},
|
|
58
58
|
"party_name": { "flags": "V" },
|
|
59
59
|
"vat_no": { "flags": "VEM" },
|
|
60
|
-
"salary": { "flags": "" }
|
|
60
|
+
"salary": { "flags": "" },
|
|
61
|
+
"notes": { "flags": "VE", "isNullable": true }
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
}
|
|
@@ -129,8 +129,12 @@ The whole date family evaluates identically **client-side and SQL-time** (report
|
|
|
129
129
|
### Conversion and context functions
|
|
130
130
|
|
|
131
131
|
- `TEXT(v)` — as text (null becomes `''`)
|
|
132
|
-
- `NUMBER(v)` — as a number, or null when the
|
|
133
|
-
|
|
132
|
+
- `NUMBER(v)` — as a number, or null when the text isn't one. "Is a number" means an optional
|
|
133
|
+
sign, digits and an optional fraction/exponent — a blank string, `'0x10'` and `'Infinity'`
|
|
134
|
+
are all null, because SQL would reject them too
|
|
135
|
+
- `BOOLEAN(v)` — null is false; a number is false only at **zero**; text is false only when
|
|
136
|
+
**empty**; anything else is true. Note that text and numbers differ: the string `'0'` is
|
|
137
|
+
true (it isn't empty) while the number `0` is false
|
|
134
138
|
- `JSON_GET(json, 'key' [, type])` — read a key out of a JSON column. The optional type is a
|
|
135
139
|
string literal: `'text'` (default), `'number'`/`'numeric'`, `'int'`/`'integer'`/`'bigint'`,
|
|
136
140
|
`'bool'`/`'boolean'`, `'date'`, `'datetime'`/`'timestamp'`/`'timestamptz'`
|
|
@@ -143,21 +147,44 @@ report selects it, or when it is used in a filter or sort. **Every function list
|
|
|
143
147
|
implemented in both**, so a column reads the same in a grid and in a report; a test executes
|
|
144
148
|
each one against PostgreSQL and diffs the result against the client runtime.
|
|
145
149
|
|
|
146
|
-
|
|
150
|
+
Four deliberate points of agreement, because the engines disagree by nature:
|
|
147
151
|
|
|
148
|
-
- **
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
- **Comparison is null-safe and two-valued.** `null = null` is true and `[x] != null` is true
|
|
153
|
+
for any non-null `x` — there is no SQL "unknown" third answer that an enclosing `IF`/`AND`
|
|
154
|
+
would then read as false. `IF([completed_date] != null AND …, 100, 0)` therefore scores the
|
|
155
|
+
same in a grid and in a report. (At SQL time `=`/`!=` become `IS NOT DISTINCT FROM` /
|
|
156
|
+
`IS DISTINCT FROM`.) `ISNULL([x])` remains the clearer spelling of an explicit null check.
|
|
157
|
+
|
|
158
|
+
- **Out-of-domain math is null, not an error.** `SQRT(-1)`, `LOG(0)`, `ASIN(2)`, `ATANH(1)`,
|
|
159
|
+
`EXP(1000)`, `POW(0, -1)`, `POW(-8, 0.5)`, `POW(10, 1000)`, `MOD(x, 0)` — JavaScript would
|
|
160
|
+
answer NaN or Infinity and PostgreSQL would raise (failing the whole report). Both return
|
|
161
|
+
null, so the cell renders blank. A result too *small* to represent is the one exception:
|
|
162
|
+
`POW(10, -1000)` is **0** on both sides, because zero is the answer, not "no answer".
|
|
151
163
|
- **String indexing is JavaScript-flavoured, not SQL's.** `SUBSTRING(s, start, end)` and
|
|
152
164
|
`MID(s, start, len)` take a **0-based** start, and `SUBSTRING`'s third argument is an **end
|
|
153
|
-
offset**, not a length.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
165
|
+
offset**, not a length. A negative offset clamps to 0, and reversed bounds **swap** —
|
|
166
|
+
`SUBSTRING(s, 4, 1)` is the same as `SUBSTRING(s, 1, 4)`. `INDEX_OF` / `LAST_INDEX_OF`
|
|
167
|
+
return a 0-based index, or `-1`.
|
|
168
|
+
- **Comparison never invents a number.** `[a] = [b]` reads both sides as numbers only when
|
|
169
|
+
both genuinely are (a number, or text SQL would also accept as one). A blank string is not
|
|
170
|
+
zero, so `NULLIF([note], '')` leaves a note of `'0'` alone, and `[qty] = ''` is false. The
|
|
171
|
+
same comparator backs `=`, `!=`, `<`, `>`, `<=`, `>=` and `BETWEEN`, so they can't disagree
|
|
172
|
+
with each other.
|
|
173
|
+
|
|
174
|
+
Three known remaining differences:
|
|
175
|
+
|
|
176
|
+
- `CONCAT` on a numeric column renders PostgreSQL's declared scale (`-3.0`) where the client
|
|
177
|
+
renders the JavaScript number (`-3`). Use `FORMAT`/`TEXT` when the exact rendering matters.
|
|
178
|
+
- `MIN`/`MAX` are **number** functions. Handed a non-numeric argument the client silently
|
|
179
|
+
drops it, while a report raises on the type mismatch. Wrap the argument in `NUMBER()` if a
|
|
180
|
+
column might not be numeric.
|
|
181
|
+
- `NUMBER('1e309')` is null client-side — the magnitude exceeds what a JavaScript number can
|
|
182
|
+
hold, and every non-finite result in the client is null. PostgreSQL's `numeric` has a far
|
|
183
|
+
wider range, so a report keeps the value.
|
|
184
|
+
|
|
185
|
+
`CURRENT_USER_ID()` needs the requesting user. It resolves in grids, cards, reports, filters,
|
|
186
|
+
sorts and `select()` — anywhere a request has one — but not in a context with no user, such as
|
|
187
|
+
a scheduled job.
|
|
161
188
|
|
|
162
189
|
### Unknown functions
|
|
163
190
|
|
|
@@ -37,7 +37,7 @@ Lives in: `logic/jobs.json` at the module root. Schema: [`docs/schemas/jobs.sche
|
|
|
37
37
|
| `timeZone` | No | tenant tz | IANA name. Falls back to `auth.tenant.time_zone`. |
|
|
38
38
|
| `params` | No | `{}` | Static parameters passed to the action as `__params`. Manifest-fixed — an admin cannot change them. For tenant-configurable values use module settings, resolved from the job's folder (below). |
|
|
39
39
|
| `enabled` | No | `true` | `false` fully disables — no scheduler, no manual trigger. |
|
|
40
|
-
| `paused` | No | `false` | Soft pause. Scheduler skips, but "Run now" still works.
|
|
40
|
+
| `paused` | No | `false` | Soft pause. Scheduler skips, but "Run now" still works. Seeds the initial value only — upgrades never overwrite it. |
|
|
41
41
|
| `description` | No | | Human-readable description. |
|
|
42
42
|
|
|
43
43
|
**Hard cap: 50 jobs per module.**
|
|
@@ -152,7 +152,7 @@ Full source: [`modules/chore/`](../../../modules/chore/).
|
|
|
152
152
|
- **Sub-minute cron.** The scheduler ticks every 60 s. Anything finer is dishonest — declare a minute cron.
|
|
153
153
|
- **Renaming `code` between versions.** Treats it as a new job and leaves the old `scheduled_job` row orphaned until uninstall (actually reaped on upgrade — but you lose `job_run` history). Keep `code` stable.
|
|
154
154
|
- **"Schedule this 5 minutes from now" pattern.** There is no per-call `schedule()`. Use a cron-scan job that watches a queue table — see [memory: cron-scan pattern](../../../docs/business-logic/jobs.md#when-to-use-a-job-vs-a-trigger).
|
|
155
|
-
- **Confusing `enabled` and `paused`.** `enabled: false` → "Run now" no-ops. `paused: true` → cron skips but "Run now" works. Use `paused` for incident response; use `enabled: false` when you mean "turn this off entirely".
|
|
155
|
+
- **Confusing `enabled` and `paused`.** `enabled: false` → "Run now" no-ops. `paused: true` → cron skips but "Run now" works. Use `paused` for incident response; use `enabled: false` when you mean "turn this off entirely". Only `enabled` is rewritten on upgrade — `paused` is operator state, so shipping `paused: true` in a new version does nothing to tenants that already have the job.
|
|
156
156
|
|
|
157
157
|
## Reference
|
|
158
158
|
|
|
@@ -143,10 +143,13 @@ Every entry in `dependencies` needs a contract file at `deps/<module_code>.json`
|
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
- `version` must be the same range as the manifest's dependency entry.
|
|
146
|
-
- `entities` needs at least one entry, except against a system module
|
|
146
|
+
- `entities` needs at least one entry, except against a system module (where `{}` is a pure platform-version gate) or a contract that imports only `functions`. Each entry needs a `pk` and a non-empty `use`.
|
|
147
147
|
- `columns` is optional — omit it when you touch no specific column of that entity. Declare only real storage columns you read, write or join on, at their **storage class** (`int8`, `varchar`, `numeric`, `bool`, `date`, `timestamptz`, …; a `cuid` PK is `int8`). Never declare a virtual column (`R`/`S`/`F`) or a column your own extension adds.
|
|
148
148
|
- `use` tokens are `<kind>:<symbol>` where kind is `ref`, `extends`, `formula`, `action`, `view`, `report`, `print` or `role`. `extends` must be fully qualified: `extends:parties.party`.
|
|
149
149
|
|
|
150
|
+
- `functions` (optional) lists the library functions of that dependency you call: `"functions": ["roleOf"]`, called as `<module_with_underscores>.roleOf()`. Install checks each is a top-level function of the installed provider's libraries. A contract that imports only functions may declare `"entities": {}`. See *Function libraries* in `action-dsl.md`.
|
|
151
|
+
- `logic/functions/*.dsl` (function libraries) is detected by presence. No `manifest.json` key declares it, and `actions` does not list it.
|
|
152
|
+
|
|
150
153
|
Full schema: `dforge://schema/deps`.
|
|
151
154
|
|
|
152
155
|
## Extension entities
|
|
@@ -145,6 +145,13 @@ installer resolves those within this module only, so a dotted one silently grant
|
|
|
145
145
|
}
|
|
146
146
|
```
|
|
147
147
|
8. **Folder access** is granted separately — roles can reference folder codes, but folder definitions themselves live in `ui/folders.json`.
|
|
148
|
+
9. **Don't grant a right on a reference's target just to make the name show up.** A reference column
|
|
149
|
+
resolves its display value for anyone who can read the record carrying it — a read-only driver role
|
|
150
|
+
with `S` on `trip` sees each trip's Vehicle name without any grant on `vehicle`. Grant `S` on the
|
|
151
|
+
target only when the role should genuinely *browse* that entity, open its records, or when a view
|
|
152
|
+
or formula navigates **through** the reference to another of its columns (`vehicle.plate_no`) —
|
|
153
|
+
that still requires the right. Granting it for the name alone unlocks the entity as a full catalog
|
|
154
|
+
everywhere, which is strictly more than intended.
|
|
148
155
|
|
|
149
156
|
## Folders (`ui/folders.json`)
|
|
150
157
|
|
|
@@ -42,6 +42,8 @@ Adds **two timestamp columns** (no user tracking):
|
|
|
42
42
|
|
|
43
43
|
Both columns have `flags: "I"` (internal — not shown in UI by default, managed by the platform).
|
|
44
44
|
|
|
45
|
+
To show one, name it in a data view's `dataSources[].columns` — an explicitly listed column renders (read-only) regardless of its flags. See [Showing audit columns](#showing-audit-columns) below.
|
|
46
|
+
|
|
45
47
|
**Important**: `audit` does NOT add `created_by` / `updated_by` user columns. For user tracking, use `audit-full`.
|
|
46
48
|
|
|
47
49
|
### `audit-full`
|
|
@@ -59,6 +61,34 @@ Use `audit-full` when you need to know **who** created/modified a record, not ju
|
|
|
59
61
|
|
|
60
62
|
**You don't need to declare both `audit` and `audit-full`** — `audit-full` inherits from `audit` automatically.
|
|
61
63
|
|
|
64
|
+
### Showing audit columns
|
|
65
|
+
|
|
66
|
+
Trait columns are internal by default, not off-limits. Two ways to surface them:
|
|
67
|
+
|
|
68
|
+
1. **Per view (usual case)** — list the column in `ui/data_views.json`. A column named in `dataSources[].columns` renders whatever its flags say, so this is all it takes:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"entityCode": "trip",
|
|
73
|
+
"columns": ["trip_number", "status", "created_by_user", "last_updated_by_user"]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
It renders **read-only** (the trait flags carry no `'E'`) and shows up in the Fields popover so users can toggle it off. Use the Reference columns (`created_by_user`) for a readable name — `created_by` is the raw cuid FK.
|
|
78
|
+
|
|
79
|
+
2. **Everywhere, or editable** — redeclare the field on the entity with your own flags. An explicit field definition wins over the trait's, as long as the type matches:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
"created_by_user": {
|
|
83
|
+
"columnType": "R",
|
|
84
|
+
"fieldTypeCd": "lookup",
|
|
85
|
+
"flags": "V",
|
|
86
|
+
"orderNum": 9904,
|
|
87
|
+
"description": "Created By",
|
|
88
|
+
"link": { "entity": "user", "thisKey": "created_by", "otherKey": "user_id" }
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
62
92
|
> **⚠️ `audit-full` + seed data needs handling.** `created_by` and `last_updated_by` are
|
|
63
93
|
> **required** (NOT NULL) cuid FKs to `user`, with **no default formula**. At runtime the platform
|
|
64
94
|
> fills them from the current user, but at **seed** time there is no current user, so a seeded
|
|
@@ -135,6 +135,15 @@ For each action:
|
|
|
135
135
|
- [ ] Referenced entities exist
|
|
136
136
|
- [ ] Referenced parameters are declared in `params:`
|
|
137
137
|
|
|
138
|
+
## Function libraries (`logic/functions/*.dsl`)
|
|
139
|
+
|
|
140
|
+
- [ ] Only top-level `function` declarations and `var` / `let` / `const`; no other top-level statement
|
|
141
|
+
- [ ] No `[field]`, `[ref].[target]`, `params[...]`, `old[...]`, and no `[name]` standing alone (use `Array.of(name)`); values come in as arguments
|
|
142
|
+
- [ ] No name that is a built-in, a JS global, `schema` or starts with `__`; each name declared once across the libraries
|
|
143
|
+
- [ ] No action keeps a local copy of a library function
|
|
144
|
+
- [ ] Every imported function is listed under `"functions"` in `deps/<module>.json`, exists as a top-level function in the provider, and is called as `<module_with_underscores>.fn()`
|
|
145
|
+
- [ ] A `ref` to another module in `params:` uses the module code (`ref cdy-hr.department`) and the module is a declared dependency
|
|
146
|
+
|
|
138
147
|
## Settings
|
|
139
148
|
|
|
140
149
|
- [ ] `settings.json` exists if the module has any settings
|
|
@@ -10,8 +10,15 @@ module artifacts.
|
|
|
10
10
|
|
|
11
11
|
**Before each phase:** run `dforge_module_inspect` and parse
|
|
12
12
|
`files["_inspect.json"]` — never plan a patch against what you assume is there.
|
|
13
|
+
For Phase 1, first present the proposed entity inventory based only on context
|
|
14
|
+
already provided and get approval before any tool call; if that context is
|
|
15
|
+
insufficient, ask the user for the missing information without calling tools.
|
|
16
|
+
Immediately after approval, run the inspection before any file changes.
|
|
13
17
|
**After each phase:** record it with
|
|
14
18
|
`dforge_module_plan({ action: "complete_phase", moduleDir, phase: "N", note: "..." })`.
|
|
19
|
+
If `dforge_module_inspect` or `dforge_module_plan` fails or is unavailable, stop,
|
|
20
|
+
report the tool name and error to the user, and do not proceed to the next phase
|
|
21
|
+
or patch files.
|
|
15
22
|
|
|
16
23
|
## Loading policy — one reference per step, at the step
|
|
17
24
|
|
|
@@ -34,6 +41,7 @@ flags, or column patterns.
|
|
|
34
41
|
| Hide columns from a user type (column-level security) | `dforge://reference/security` (§Column-level security), `dforge://example/column-security/entities/product.json`, `dforge://example/column-security/ui/folders.json` | — |
|
|
35
42
|
| Add a menu | `dforge://reference/menus`, `dforge://example/ui/menus.json` | — |
|
|
36
43
|
| Add an action | `dforge://reference/action-dsl`, `dforge://example/logic/actions/mark_done.dsl`, `dforge://example/ui/actions.json` | `dforge://docs/dsl` |
|
|
44
|
+
| Share helpers between actions, or call another module's functions | `dforge://reference/action-dsl` (§Function libraries): `logic/functions/*.dsl`, imports in `deps/<module>.json` | `dforge://schema/deps` |
|
|
37
45
|
| Add a trigger | — | `dforge://schema/triggers`; condition syntax in `dforge://docs/dsl` |
|
|
38
46
|
| Add a scheduled job | `dforge://reference/jobs` | `dforge://schema/jobs` |
|
|
39
47
|
| Add a webhook | — | `dforge://schema/webhooks` |
|
|
@@ -67,6 +75,12 @@ Enough to author inline; load the linked reference for the full detail.
|
|
|
67
75
|
size/precision. Common fixes: `number` not `integer`/`float`; `phone` not
|
|
68
76
|
`phoneNumber`; `date` not `datePicker`; `timestamptz` not `datetime`; `bool`
|
|
69
77
|
not `boolean`; `varchar`/`text` not `string`. → `field-types`
|
|
78
|
+
- **Register dimensions are never empty.** An empty dimension refuses the
|
|
79
|
+
posting by design; do not make one optional or add a `COALESCE([x_id], 0)`
|
|
80
|
+
technical key. Variants: one `variant_id` dimension, with a default variant
|
|
81
|
+
for products without options. Untracked batch/series: a seeded default
|
|
82
|
+
record, not `0`. Optional analytics: accounting analytic slots or plain
|
|
83
|
+
columns. Ad hoc slices by an optional attribute: a report, not a dimension.
|
|
70
84
|
- **Formula columns** (`columnType: "F"`): `baseDatatypeCd` required, no
|
|
71
85
|
`dbDatatype`, `flags: "V"`. → `formulas`
|
|
72
86
|
- **Roll-up totals** over a child set are Generated (`G`) columns with
|
|
@@ -126,7 +140,8 @@ Proposed entities (N):
|
|
|
126
140
|
Loop with the user until they explicitly approve.
|
|
127
141
|
|
|
128
142
|
**Pre-scaffold consistency checks** against `docs/DESIGN.md` — if any fail,
|
|
129
|
-
surface it and go back to Phase
|
|
143
|
+
surface it and go back to Phase 0 and re-run the design step that produced
|
|
144
|
+
`docs/DESIGN.md`; don't silently adjust the design:
|
|
130
145
|
|
|
131
146
|
1. Every FK in the relationship map has a corresponding field on the child.
|
|
132
147
|
2. Every action's `canExecute` guard references a status value that exists.
|
|
@@ -147,13 +162,15 @@ approval, then patch. Reach for the **composite tools first**:
|
|
|
147
162
|
|
|
148
163
|
**Field batching rule** — the only Phase 1 exception to one-at-a-time. A field
|
|
149
164
|
is batchable only if ALL of: scalar primitive (string/integer/decimal/boolean/
|
|
150
|
-
date), no FK or reference, no formula, and
|
|
151
|
-
|
|
152
|
-
actions, or
|
|
165
|
+
date), no FK or reference, no formula, and its nullability is stated explicitly
|
|
166
|
+
by the user (required = M flag, optional = VE flag). Everything else goes one
|
|
167
|
+
at a time. This never justifies batching entities, views, roles, actions, or
|
|
168
|
+
reports.
|
|
153
169
|
|
|
154
170
|
**Extension entities last.** `extends: "module.entity"`, `toString: null`,
|
|
155
171
|
dotted manifest key. Snapshot the base entity's fields with
|
|
156
|
-
`dforge_module_inspect` on the dependency dir when
|
|
172
|
+
`dforge_module_inspect` on the dependency dir when the dependency module
|
|
173
|
+
directory exists in the current workspace (check the `deps/<module>.json` path);
|
|
157
174
|
otherwise document the known base fields and note that drift tracking is the
|
|
158
175
|
user's responsibility.
|
|
159
176
|
|
|
@@ -209,7 +226,7 @@ must set `jobClass: 'long_running'`. `dforge_module_validate` flags violations.
|
|
|
209
226
|
|
|
210
227
|
**3a. Default grids first.** Every entity gets a `viewType: "grid"` view with
|
|
211
228
|
`dataSources: [{ entityCode, columns: [...] }]`. The scaffolder already wrote one
|
|
212
|
-
per entity, so you'll often `
|
|
229
|
+
per entity, so you'll often `dforge_view_modify` rather than `dforge_view_add`.
|
|
213
230
|
|
|
214
231
|
View codes are semantic — the entity name (`feedback_item`), a plural
|
|
215
232
|
(`invoices`), or descriptive (`invoices_kanban`). **Never use the literal code
|
|
@@ -279,12 +296,15 @@ stated trigger; every report's params sit on a dataset.
|
|
|
279
296
|
## Phase 5 — Security (5a required)
|
|
280
297
|
|
|
281
298
|
1. **Inspect first.** The scaffolder pre-created `<code>.admin` with `SIUDC` on
|
|
282
|
-
every entity declared at scaffold time.
|
|
283
|
-
(`
|
|
299
|
+
every entity declared at scaffold time. Preserve this scaffold-generated
|
|
300
|
+
baseline role; don't re-create it (`dforge_role_add` fails on an existing
|
|
301
|
+
code; use `dforge_role_right_set` to amend). It is not an inferred user type.
|
|
284
302
|
2. **Derive roles from the intake's user types and verbs.** Re-read the user-type
|
|
285
303
|
sentences in `docs/REQUIREMENTS.md`. One role per distinct user type, named
|
|
286
|
-
`<code>.<user-type>`. **Forbidden:**
|
|
287
|
-
the user didn't ask for.
|
|
304
|
+
`<code>.<user-type>`. **Forbidden:** adding generic admin/contributor/viewer
|
|
305
|
+
roles or a matrix the user didn't ask for. The scaffold-generated
|
|
306
|
+
`<code>.admin` baseline is the sole exception; include its existing rights
|
|
307
|
+
when showing the actual rights matrix.
|
|
288
308
|
3. **Reflect the role list back** before computing rights. Get confirmation.
|
|
289
309
|
4. **Show the rights matrix as a table** (rows = entities/actions/reports,
|
|
290
310
|
columns = roles, cells = rights strings), each cell explained by the verb it
|
|
@@ -332,16 +352,18 @@ When a later phase exposes a problem in an earlier one, in order:
|
|
|
332
352
|
3. **Identify the target phase + decision.**
|
|
333
353
|
4. **Get user sign-off**, including cascading impacts.
|
|
334
354
|
5. **Apply the smallest tool that fits** — this overrides the phase labels:
|
|
335
|
-
`
|
|
336
|
-
`
|
|
355
|
+
`dforge_entity_field_add` over `dforge_entity_add`;
|
|
356
|
+
`dforge_entity_field_rename` over remove+add;
|
|
357
|
+
`dforge_role_right_set` over `dforge_role_add`;
|
|
358
|
+
`dforge_view_modify` over remove+add.
|
|
337
359
|
6. **Re-run `dforge_module_inspect`**, fix knock-on impacts, resume.
|
|
338
360
|
|
|
339
361
|
**Multi-trigger rule:** if several phases expose gaps at once, resolve the
|
|
340
362
|
**earliest-phase** gap first, complete its backtrack, re-inspect, then
|
|
341
363
|
re-evaluate.
|
|
342
364
|
|
|
343
|
-
**Rename and delete are handled for you** — `
|
|
344
|
-
`
|
|
365
|
+
**Rename and delete are handled for you** — `dforge_entity_field_rename`,
|
|
366
|
+
`dforge_entity_field_remove`, `dforge_entity_rename`, `dforge_entity_delete` each propagate the
|
|
345
367
|
cascade in one call. **Always apply the response's `deletes` as well as
|
|
346
368
|
`files`** (or just pass `apply: true`, which guarantees it). Each tool surfaces
|
|
347
369
|
what it could NOT auto-fix — reports, translations, menu labels, DSL bodies,
|