@dforge-core/dforge-mcp 0.2.16 → 0.2.18
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 +115 -0
- package/dist/server.js +6 -6
- package/package.json +3 -3
- package/resources/docs/dsl-reference.md +14 -0
- package/resources/schemas/deps.schema.json +1 -1
- package/resources/schemas/entity.schema.json +49 -5
- package/resources/schemas/manifest.schema.json +1 -1
- package/resources/schemas/menus.schema.json +1 -1
- package/resources/schemas/reports.schema.json +1 -1
- package/resources/schemas/triggers.schema.json +1 -1
- package/skills/dforge-mcp-author/references/action-dsl.md +1 -1
- package/skills/dforge-module-build/SKILL.md +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,121 @@ 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.18
|
|
8
|
+
|
|
9
|
+
Every counter a module updates — an on-hand quantity, a running balance, a used
|
|
10
|
+
credit — was being written as a literal the script had read a statement earlier.
|
|
11
|
+
Two callers read 10, both write 5, and one decrement is gone with nothing logged.
|
|
12
|
+
`update()` has always accepted `{ inc: n }` for exactly this; nothing here said so,
|
|
13
|
+
so nothing generated it.
|
|
14
|
+
|
|
15
|
+
### Changed — `update()` documents relative values
|
|
16
|
+
|
|
17
|
+
`update('wms.stock', [stock_id], { quantity: { inc: -params[qty] } })` adds `n` to
|
|
18
|
+
the column's **current** value, computed by the database under the row lock the
|
|
19
|
+
`UPDATE` already takes. The DSL reference gains a section under `update()` and the
|
|
20
|
+
`dforge-mcp-author` action-DSL table gains the rule in the `update()` row: use it for
|
|
21
|
+
every counter, balance and quantity — a literal is correct only for a value that does
|
|
22
|
+
not depend on what is already stored.
|
|
23
|
+
|
|
24
|
+
`inc` must be a number and the object's only key; `{ inrc: 5 }` throws rather than
|
|
25
|
+
writing nothing. Not available for extension columns, whose row may not exist yet.
|
|
26
|
+
|
|
27
|
+
Documentation only — `dforge_action_check` never parsed `update()` payloads, so
|
|
28
|
+
`{ inc: n }` validated before this and still does.
|
|
29
|
+
|
|
30
|
+
### Changed — `@dforge-core/metadata` → `0.0.22`, `resources/schemas/` re-vendored
|
|
31
|
+
|
|
32
|
+
**A column code may no longer start with `_`.** `entity.schema.json`'s field pattern
|
|
33
|
+
(and the per-view override pattern) tightened from `^[a-zA-Z_][a-zA-Z0-9_]*$` to
|
|
34
|
+
`^[a-zA-Z][a-zA-Z0-9_]*$`, because the platform reserves the prefix: the record route
|
|
35
|
+
reads `_`-prefixed query parameters as app state (`_layout`) rather than key columns,
|
|
36
|
+
and the print-template context injects `_fmt`, `_raw`, `_color`, `_link`, `_today` and
|
|
37
|
+
`_settings` alongside bare column names. **This can reject metadata that packed
|
|
38
|
+
before** — rename the column; a leading underscore was never addressable end to end.
|
|
39
|
+
|
|
40
|
+
**`constraints` is now a specified object instead of a bare `type: object`.** Each
|
|
41
|
+
entry is `unique` (needs `fields`, and `expression` is refused — a unique constraint
|
|
42
|
+
is its key columns) or `check` (needs `expression`; its optional `fields` does not
|
|
43
|
+
build anything and is reported to the client as the fields to highlight, since an
|
|
44
|
+
identifier inside a string literal is indistinguishable from a column name). `columns`
|
|
45
|
+
survives as a **deprecated** alias for `fields` on a unique constraint — still built,
|
|
46
|
+
still reported, warned about at install. `message` overrides the generated violation
|
|
47
|
+
text and localizes under `entities.<entity>.constraints.<name>.message`. Editors now
|
|
48
|
+
complete this and catch a `check` with no expression at authoring time.
|
|
49
|
+
|
|
50
|
+
Two description corrections in the same bump: a **`reports.json` `entityCd` pointing
|
|
51
|
+
outside the declaring module must be qualified `module.entity`** — `entity_cd` is
|
|
52
|
+
unique per module, not per tenant, and the installer stores the id it resolves once,
|
|
53
|
+
which `report.run` then reads for the life of the row; a bare code that several
|
|
54
|
+
installed modules define is rejected at install unless one of them is this module or a
|
|
55
|
+
declared dependency. And **an `extends` module cannot declare `views`** — a view lists
|
|
56
|
+
the complete visible column set and an extension knows only the columns it adds — which
|
|
57
|
+
`entity.schema.json` now says next to the rule (already shipped in 0.2.16) that a folder
|
|
58
|
+
naming an undeclared view fails the install.
|
|
59
|
+
|
|
60
|
+
Also arriving with `0.0.21`, platform-side and needing nothing from a module:
|
|
61
|
+
`number`, `currency` and `percent` no longer ship `min`/`max` field-type defaults. The
|
|
62
|
+
client merged those under a column's own `params` and validated the result, so every
|
|
63
|
+
plain `number` column was silently capped at 10000 and refused negatives
|
|
64
|
+
([dForge-core#947](https://github.com/iash44/dForge-core/issues/947)) — an odometer, a
|
|
65
|
+
cargo weight, a negative variance. A numeric column is bounded only when its own
|
|
66
|
+
`params` say so, which is unchanged and remains the way to state a real range.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- `@dforge-core/dforge-cli` → `0.2.16`.
|
|
71
|
+
|
|
72
|
+
Nothing else is rejected that was accepted before; apart from the `_`-prefixed column
|
|
73
|
+
codes above, `dforge_module_validate` is unchanged.
|
|
74
|
+
|
|
75
|
+
## 0.2.17
|
|
76
|
+
|
|
77
|
+
A bridge module's `async: false` trigger **can** roll back its host module's write,
|
|
78
|
+
and that is intended (platform issue #999). It is the same coupling a mandatory
|
|
79
|
+
extension column already creates — mark one `M` and the host's own `data.insert`
|
|
80
|
+
is refused with *"Missing required columns: …"* for a field the host knows nothing
|
|
81
|
+
about. A bridge that can add a required field can add a required *condition*, and
|
|
82
|
+
cross-module invariants are what bridge modules exist to enforce.
|
|
83
|
+
|
|
84
|
+
What changed platform-side is that the veto now says who issued it, so the guidance
|
|
85
|
+
here says what to point it at.
|
|
86
|
+
|
|
87
|
+
### Changed — `async: false` is a refusal mechanism, not a reaction
|
|
88
|
+
|
|
89
|
+
`dforge_trigger_add`'s `async` description and `dforge-module-build` Phase 2b both
|
|
90
|
+
said only that a failure "rolls back the original change" — true, and read by an
|
|
91
|
+
authoring agent as a footnote about transactions rather than as the decision it is.
|
|
92
|
+
Both now state that the rollback reaches **another module's** entity, and the rule
|
|
93
|
+
that follows: pick `async: false` to enforce an invariant, not merely to react,
|
|
94
|
+
because a bug in the action blocks the host's workflow exactly as effectively as a
|
|
95
|
+
real violation. Keep such an action to the check and put the rest behind
|
|
96
|
+
`try`/`catch` or in an `async: true` trigger.
|
|
97
|
+
|
|
98
|
+
### Changed — a blocked write names the module that blocked it
|
|
99
|
+
|
|
100
|
+
Trigger failures now report error code **`TRIGGER_ACTION_FAIL`** (the other action
|
|
101
|
+
phases keep `ACTION_EXECUTION_FAIL`), rendered as *Blocked by "crm-fin" (automation
|
|
102
|
+
"on_invoice_posted"): Credit limit exceeded.* in the caller's locale, with the
|
|
103
|
+
action's own `error()` text passed through. The module named is the one that
|
|
104
|
+
**declared the trigger**, not the one owning the action — a bridge routinely
|
|
105
|
+
subscribes its own trigger to an action living elsewhere, and what refused the write
|
|
106
|
+
is the subscription. Nothing to change in a module for this; it is what an author
|
|
107
|
+
should expect to see when their trigger fires.
|
|
108
|
+
|
|
109
|
+
Nothing is rejected that was accepted before — `dforge_module_validate` is unchanged.
|
|
110
|
+
|
|
111
|
+
### Changed
|
|
112
|
+
|
|
113
|
+
- `@dforge-core/metadata` → `0.0.20`; `resources/schemas/` re-vendored. Besides
|
|
114
|
+
`triggers.schema.json`'s `async` above, that brings three description fixes from
|
|
115
|
+
platform work that landed alongside: `deps.schema.json` on how a target's `columns`
|
|
116
|
+
list treats a column your own `extends` file adds, `manifest.schema.json` warning
|
|
117
|
+
against sharing one `schema` between modules that extend other modules' entities
|
|
118
|
+
(extension columns live in `"{schema}"."{entity}_ext"`, which collides), and
|
|
119
|
+
`menus.schema.json` spelling out that `folders` takes flat folder codes
|
|
120
|
+
(`logistics_all_trips`), not paths.
|
|
121
|
+
|
|
7
122
|
## 0.2.16
|
|
8
123
|
|
|
9
124
|
Entity views — the platform's **column-level security** — became reachable from a
|
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.16/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 = {
|
|
@@ -31214,7 +31214,7 @@ function buildZedSettings() {
|
|
|
31214
31214
|
var fs = __toESM(require("fs"));
|
|
31215
31215
|
var path = __toESM(require("path"));
|
|
31216
31216
|
|
|
31217
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
31217
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.22/node_modules/@dforge-core/metadata/dist/index.js
|
|
31218
31218
|
var AggType = {
|
|
31219
31219
|
Sum: "sum",
|
|
31220
31220
|
Count: "count",
|
|
@@ -31231,9 +31231,9 @@ var fieldTypes = [
|
|
|
31231
31231
|
{ cd: "phone", name: "Phone", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31232
31232
|
{ cd: "url", name: "URL", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31233
31233
|
{ cd: "textarea", name: "Multiline Text", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31234
|
-
{ cd: "number", name: "Number", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: null, useStringInFilter: false
|
|
31235
|
-
{ cd: "currency", name: "Currency", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: 2, useStringInFilter: false
|
|
31236
|
-
{ cd: "percent", name: "Percent", columnType: "D", baseDatatypeCd: "number", supportedAgg: "CAM", maxLen: null, precision: 2, useStringInFilter: false
|
|
31234
|
+
{ cd: "number", name: "Number", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: null, useStringInFilter: false },
|
|
31235
|
+
{ cd: "currency", name: "Currency", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: 2, useStringInFilter: false },
|
|
31236
|
+
{ cd: "percent", name: "Percent", columnType: "D", baseDatatypeCd: "number", supportedAgg: "CAM", maxLen: null, precision: 2, useStringInFilter: false },
|
|
31237
31237
|
{ cd: "checkbox", name: "Checkbox", columnType: "D", baseDatatypeCd: "bool", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false },
|
|
31238
31238
|
{ cd: "date", name: "Date", columnType: "D", baseDatatypeCd: "date", supportedAgg: "CM", maxLen: null, precision: null, useStringInFilter: false },
|
|
31239
31239
|
{ cd: "datetime", name: "Date & Time", columnType: "D", baseDatatypeCd: "timestamp", supportedAgg: "CM", maxLen: null, precision: null, useStringInFilter: false },
|
|
@@ -36248,7 +36248,7 @@ var triggerAddSchema = {
|
|
|
36248
36248
|
"STATIC params only \u2014 the literal values given here. Nothing is injected at runtime; there is no record_id param. The affected record arrives as record context ([field]), its pre-change values as old[field]."
|
|
36249
36249
|
),
|
|
36250
36250
|
async: external_exports.boolean().default(true).describe(
|
|
36251
|
-
"When true (the platform default), the action runs in background \u2014 recommended for slow actions. When false, it runs in the caller's transaction, so a failing action ROLLS BACK the user's original write."
|
|
36251
|
+
"When true (the platform default), the action runs in background \u2014 recommended for slow actions. When false, it runs in the caller's transaction, so a failing action ROLLS BACK the user's original write. That applies to a trigger on ANOTHER module's entity too: a bridge module CAN veto its host's write, the same way a mandatory extension column already refuses one. So async:false is a refusal mechanism \u2014 pick it to enforce an invariant, not merely to react, because a bug in the action blocks the host's workflow just as effectively as a real violation. The rollback is reported to the user as TRIGGER_ACTION_FAIL, naming the blocking module and trigger."
|
|
36252
36252
|
)
|
|
36253
36253
|
};
|
|
36254
36254
|
function triggerAdd(args) {
|
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.18",
|
|
4
4
|
"description": "MCP server for dForge module authoring. Exposes 34 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",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"install-skills": "node scripts/install-skills.mjs"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@dforge-core/dforge-cli": "^0.2.
|
|
39
|
-
"@dforge-core/metadata": "^0.0.
|
|
38
|
+
"@dforge-core/dforge-cli": "^0.2.16",
|
|
39
|
+
"@dforge-core/metadata": "^0.0.22",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
41
41
|
"zod": "^4.4.3"
|
|
42
42
|
},
|
|
@@ -264,6 +264,20 @@ coerced to the PK storage type (snowflake-as-string binds `int8`). A null key
|
|
|
264
264
|
update('comm.message', msg.message_id, { status: 'sent', sent_at: now() })
|
|
265
265
|
```
|
|
266
266
|
|
|
267
|
+
**Relative changes — `{ inc: n }`.** A field value may be `{ inc: n }` instead of a
|
|
268
|
+
literal, which adds `n` to the column's **current** value. Use it for every counter,
|
|
269
|
+
balance or on-hand quantity:
|
|
270
|
+
|
|
271
|
+
```dsl
|
|
272
|
+
// The DATABASE subtracts, under the row lock the UPDATE already takes
|
|
273
|
+
update('wms.stock', [stock_id], { quantity: { inc: -params[qty] } })
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
A literal can only carry a number the script read *before* the statement ran, so two
|
|
277
|
+
callers that both read 10 both write 5 and one decrement is silently lost. `inc` must
|
|
278
|
+
be a number and the object's only key — `{ inrc: 5 }` throws rather than writing
|
|
279
|
+
nothing. Not available for extension columns (their row may not exist yet).
|
|
280
|
+
|
|
267
281
|
#### `delete(entityCd, key) → int`
|
|
268
282
|
Delete the row(s) matched by `key`; returns rows affected. Same key shapes and
|
|
269
283
|
rules as `update()`.
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"columns": {
|
|
42
42
|
"type": "object",
|
|
43
|
-
"description": "Provider columns the consumer actually reads, writes, or joins on. Omit entirely for extension-only targets that touch no existing provider column.",
|
|
43
|
+
"description": "Provider columns the consumer actually reads, writes, or joins on. Omit entirely for extension-only targets that touch no existing provider column. A column your OWN 'extends' file adds to the provider needs no entry either — the platform exempts those and validates them separately — and a VIRTUAL extension field (columnType R/S/F: the lookup beside an FK, a reverse set, a formula) must never be listed at all: it is metadata, never a provider storage column, so no storage class can ever match and the install-time satisfaction check rejects the declaration. Declare the FK you add (e.g. trip_id), never the lookup beside it (trip).",
|
|
44
44
|
"additionalProperties": { "$ref": "#/definitions/columnContract" }
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
},
|
|
50
50
|
"fields": {
|
|
51
51
|
"type": "object",
|
|
52
|
-
"description": "Column definitions keyed by column code. Column codes must start with a
|
|
52
|
+
"description": "Column definitions keyed by column code. Column codes must start with a LETTER and contain only letters, digits, and underscores. A leading `_` is reserved by the platform — the record route treats `_`-prefixed query parameters as app state (`_layout`) rather than key columns, and the print template context injects `_fmt`, `_raw`, `_color`, `_link`, `_today` and `_settings` alongside bare column names. A leading `-` is reserved for the sort descending prefix in `view_json.order`.",
|
|
53
53
|
"propertyNames": {
|
|
54
|
-
"pattern": "^[a-zA-
|
|
54
|
+
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$"
|
|
55
55
|
},
|
|
56
56
|
"additionalProperties": {
|
|
57
57
|
"$ref": "#/$defs/field"
|
|
@@ -59,7 +59,51 @@
|
|
|
59
59
|
},
|
|
60
60
|
"constraints": {
|
|
61
61
|
"type": "object",
|
|
62
|
-
"description": "Database constraints
|
|
62
|
+
"description": "Database constraints, keyed by constraint name. Each becomes a real Postgres constraint; a violation is reported to the client under UNIQUE_VIOLATION or CHECK_VIOLATION with the constraint name, the owning entity and its declared fields.",
|
|
63
|
+
"additionalProperties": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"required": ["type"],
|
|
66
|
+
"allOf": [
|
|
67
|
+
{
|
|
68
|
+
"if": { "required": ["type"], "properties": { "type": { "const": "unique" } } },
|
|
69
|
+
"then": {
|
|
70
|
+
"anyOf": [{ "required": ["fields"] }, { "required": ["columns"] }],
|
|
71
|
+
"properties": { "expression": false }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"if": { "required": ["type"], "properties": { "type": { "const": "check" } } },
|
|
76
|
+
"then": { "required": ["expression"] }
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"properties": {
|
|
80
|
+
"type": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"enum": ["unique", "check"],
|
|
83
|
+
"description": "Constraint kind"
|
|
84
|
+
},
|
|
85
|
+
"fields": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"items": { "type": "string" },
|
|
88
|
+
"description": "Columns the constraint governs. Required for 'unique' - it IS the key. Optional for 'check', where it does not build the constraint but is reported to the client as the fields to highlight - the expression is never parsed for this, because an identifier inside a string literal is indistinguishable from a column name."
|
|
89
|
+
},
|
|
90
|
+
"columns": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"items": { "type": "string" },
|
|
93
|
+
"deprecated": true,
|
|
94
|
+
"deprecationMessage": "Deprecated: use 'fields'. The constraint is still created and its violation still reports the columns - the alias is read as a fallback on a unique constraint - but 'fields' is the documented spelling, the only one a check constraint reads, and the only one that will keep working. Module install warns about this.",
|
|
95
|
+
"description": "DEPRECATED alias for 'fields' on a unique constraint - use 'fields' instead. Accepted when building the constraint and read as a fallback when reporting a violation."
|
|
96
|
+
},
|
|
97
|
+
"expression": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"description": "SQL boolean expression the row must satisfy. Required for 'check', and not valid on 'unique' - a unique constraint is defined by its key columns, not an expression."
|
|
100
|
+
},
|
|
101
|
+
"message": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": "Violation message shown to the user; overrides the generated default. Localizable via translations/<locale>.json under entities.<entity>.constraints.<name>.message"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
63
107
|
},
|
|
64
108
|
"references": {
|
|
65
109
|
"type": "object",
|
|
@@ -98,7 +142,7 @@
|
|
|
98
142
|
},
|
|
99
143
|
"views": {
|
|
100
144
|
"type": "object",
|
|
101
|
-
"description": "Entity views keyed by view name — column-level security. Unrelated to 'isView'/'viewSql' (a SQL-view-backed entity) and to ui/data_views.json (grids, kanban, calendars). A folder binds one view per entity via ui/folders.json (entities.<code>.viewName); users working in that folder see ONLY the columns the view lists. Entities without views
|
|
145
|
+
"description": "Entity views keyed by view name — column-level security. Unrelated to 'isView'/'viewSql' (a SQL-view-backed entity) and to ui/data_views.json (grids, kanban, calendars). A folder binds one view per entity via ui/folders.json (entities.<code>.viewName); users working in that folder see ONLY the columns the view lists. Entities without views show the entity's full column set; a folder naming a view that isn't declared here FAILS the install, since resolving it to nothing would show every column \u2014 the opposite of what naming a view asks for. A view can only be declared by the entity's own module: an extension ('extends') declaring 'views' is rejected, because a view lists the complete column set and an extension knows only the columns it adds.",
|
|
102
146
|
"propertyNames": {
|
|
103
147
|
"pattern": "^[a-zA-Z_][a-zA-Z0-9_-]*$",
|
|
104
148
|
"maxLength": 100
|
|
@@ -120,7 +164,7 @@
|
|
|
120
164
|
"description": "Column code -> per-view overrides. Columns absent from this map are hidden in a folder bound to this view. Use {} (or null) to expose a column with no overrides. Must include the entity's primary key column(s) — records are addressed by them; hide the key from the UI by omitting 'V' from its flags instead.",
|
|
121
165
|
"minProperties": 1,
|
|
122
166
|
"propertyNames": {
|
|
123
|
-
"pattern": "^[a-zA-
|
|
167
|
+
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$"
|
|
124
168
|
},
|
|
125
169
|
"additionalProperties": {
|
|
126
170
|
"$ref": "#/$defs/entityViewColumn"
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"schemaName": {
|
|
51
51
|
"type": "string",
|
|
52
|
-
"description": "Database schema name override (defaults to module code, system modules use 'dForge')"
|
|
52
|
+
"description": "Database schema name override (defaults to module code, system modules use 'dForge'). Do NOT share one schema between modules that extend other modules' entities: an extension's columns live in \"{schemaName}\".\"{entity}_ext\", named for the schema rather than the module, and nothing records which module wrote a column — so install rejects a module that both shares its schema with another installed module and declares 'extends'. Leave unset (the module code, which is unique) for bridge modules."
|
|
53
53
|
},
|
|
54
54
|
"system": {
|
|
55
55
|
"type": "boolean",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"folders": {
|
|
24
24
|
"type": "array",
|
|
25
25
|
"items": { "type": "string" },
|
|
26
|
-
"description": "
|
|
26
|
+
"description": "Flat folder codes this menu is linked to (module code for the root, module code + '_' + each nesting segment for children, e.g. 'logistics_all_trips'; the full path, the module-relative path, and a folder's own key when unique in the tree also resolve). Omit to link the menu to every folder the module declares. Scopes the folder-to-menu link only — every folder in ui/folders.json is created either way. A code matching no folder, or a key two branches share, fails the install."
|
|
27
27
|
},
|
|
28
28
|
"items": {
|
|
29
29
|
"type": "object",
|
|
@@ -273,7 +273,7 @@
|
|
|
273
273
|
"properties": {
|
|
274
274
|
"entityCd": {
|
|
275
275
|
"type": "string",
|
|
276
|
-
"description": "Source entity code"
|
|
276
|
+
"description": "Source entity code. Qualify it as `module.entity` for anything outside the declaring module: entity_cd is unique per module, not per tenant, and the installer stores the id it resolves once — report.run reads that id for the life of the row. A bare code that several installed modules define is rejected at install unless one of them is this module or a declared dependency."
|
|
277
277
|
},
|
|
278
278
|
"columns": {
|
|
279
279
|
"type": "array",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"async": {
|
|
51
51
|
"type": "boolean",
|
|
52
52
|
"default": true,
|
|
53
|
-
"description": "When true (the default), the action runs in the background — the triggering transaction commits before the action starts. Recommended for slow actions (emails, external API calls); background failures are logged, not surfaced to the caller. When false, the action runs synchronously inside the same transaction as the triggering insert/update/delete: if the action fails (e.g. calls error()), the whole mutation rolls back and the error is returned to the caller — so an async:false trigger can undo the user's write. Keep async:false actions fast; they run on the request's transaction."
|
|
53
|
+
"description": "When true (the default), the action runs in the background — the triggering transaction commits before the action starts. Recommended for slow actions (emails, external API calls); background failures are logged, not surfaced to the caller. When false, the action runs synchronously inside the same transaction as the triggering insert/update/delete: if the action fails (e.g. calls error()), the whole mutation rolls back and the error is returned to the caller — so an async:false trigger can undo the user's write. That holds for a trigger on ANOTHER module's entity too (the bridge shape): a bridge can veto its host's write, the same way a mandatory extension column already refuses one. Use it to enforce an invariant, not merely to react — the error names the blocking module and trigger (code TRIGGER_ACTION_FAIL), and a bug in the action blocks the host's workflow just as effectively as a real violation. Keep async:false actions fast; they run on the request's transaction."
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"additionalProperties": false
|
|
@@ -226,7 +226,7 @@ var note = params[note] // no quotes around param name in this DSL
|
|
|
226
226
|
|---|---|---|
|
|
227
227
|
| `insert('entity', { field: value, … })` | Inserted row object | Insert a new record. Returns the full row including auto-filled columns (PK, audit, number sequences). Access returned fields with `result.field_name`. |
|
|
228
228
|
| `select('entity', opts?)` | Array of row objects | Structured multi-row read — see "select()" below. `opts` = `{columns, filter, orderBy, limit, offset}`, all optional. Prefer over `query()` for row-shaped reads. |
|
|
229
|
-
| `update('entity', key, { field: value, … })` | Rows affected (number) | Update the row(s) matched by `key`. Refreshes update-phase server defaults (audit `last_updated` / `last_updated_by`) and coerces values like `insert()`. |
|
|
229
|
+
| `update('entity', key, { field: value, … })` | Rows affected (number) | Update the row(s) matched by `key`. Refreshes update-phase server defaults (audit `last_updated` / `last_updated_by`) and coerces values like `insert()`. A value may be `{ inc: n }` instead of a literal — adds `n` to the column's CURRENT value, so concurrent callers accumulate instead of overwriting. Use it for every counter / balance / quantity. |
|
|
230
230
|
| `delete('entity', key)` | Rows affected (number) | Delete the row(s) matched by `key`. |
|
|
231
231
|
| `query('SQL', { arg: value })` | Array of row objects | Execute parameterized SQL. Use `@argName` placeholders. Schema-qualified table names (`crm.contact`). Access row fields with `row.field_name` or `row['field_name']`. **Escape hatch** — prefer `select()`/`update()`/`delete()` when the structured shape fits. |
|
|
232
232
|
| `getRecord('entity', key)` | Row object | Fetch a single record by key. Access fields with `rec.field_name` (or `rec.get('field_name')`). **Throws** a localized "not found" error if absent. |
|
|
@@ -187,7 +187,13 @@ before committing, then `dforge_action_add`. One action per turn.
|
|
|
187
187
|
|
|
188
188
|
**2b Triggers** reference actions that must already exist. `async: true` runs
|
|
189
189
|
after the transaction commits (use for anything slow); `async: false` runs
|
|
190
|
-
in-transaction, so a failure rolls back the original change
|
|
190
|
+
in-transaction, so a failure rolls back the original change — **including on
|
|
191
|
+
another module's entity**. A bridge's `async: false` trigger vetoes its host's
|
|
192
|
+
write, exactly as a mandatory extension column already refuses one, so pick it
|
|
193
|
+
to enforce an invariant, not merely to react: a bug in the action blocks the
|
|
194
|
+
host's workflow as effectively as a real violation. Keep such an action to the
|
|
195
|
+
check itself and put everything else behind `try`/`catch` or in an
|
|
196
|
+
`async: true` trigger.
|
|
191
197
|
|
|
192
198
|
**2c Jobs** run as the system user with **no current record** — the action must
|
|
193
199
|
not use `[field]` syntax. Wrap a record-context action in a thin job action that
|