@dforge-core/dforge-mcp 0.2.0 → 0.2.2

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 CHANGED
@@ -4,6 +4,193 @@ 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.2
8
+
9
+ Mirrors the platform change that gives the `M` column flag its meaning — in the
10
+ docs, in the schemas, and in the tools that emit flags — and catches up the
11
+ formula and `entityLink()` references with the platform work since 0.2.1.
12
+
13
+ ### Fixed — the relation tools emitted `NOT NULL` for every optional relation
14
+
15
+ The `M` change turned an inert flag into a physical constraint, which made three
16
+ places that hard-coded it actively wrong. All three now take `M` from whether the
17
+ relation is actually required, and put it on **both** halves in step:
18
+
19
+ - **`dforge_entity_reference_add`** emitted the hidden FK as `flags: "EM"`
20
+ unconditionally, whatever `required` said. Post-change that made every optional
21
+ relation's FK column `NOT NULL` while its own Reference half said `"VE"` — a pair
22
+ that contradicts itself. It now emits FK `"EM"` + Reference `"VEM"` when
23
+ `required`, and FK `"E"` + Reference `"VE"` when not. Reusing a half-built FK
24
+ normalizes to the same shape, and drops a lingering `"isNullable": true` when the
25
+ relation is required rather than emitting a pair the platform rejects at pack.
26
+ - **`dforge_dbml_import` / `dforge_module_import`** did the same for every imported
27
+ FK. They now read the source column's nullability — a DBML `[not null]` on the FK
28
+ is carried onto the reference (the FK column itself is dropped from `columns`, so
29
+ it was being lost) and `required` is accepted explicitly on a table-spec reference.
30
+ - **`isHiddenFk()`** required `M` to recognize the hidden half of a pair, so an
31
+ optional FK (`"E"`) became invisible to every rule keyed off it. `M` is not part
32
+ of the shape — it marks the relation required.
33
+ - **Set columns** are emitted `"VE"`, not `"VEM"`: `M` is inert on a virtual column,
34
+ but it reads as "this grid must be non-empty", which nothing enforces.
35
+
36
+ ### Added — the validator catches the `M` / `isNullable` contradiction
37
+
38
+ `dforge_module_validate` (and the authoring-time field schema) now error on a field
39
+ declaring `M` together with `"isNullable": true`, naming the field. The platform
40
+ rejects this at pack time; catching it here says which column while you can still
41
+ see it. On a virtual column the message says so instead of offering both fixes.
42
+
43
+ ### Changed — the `M` column flag now means "required" (platform mirror)
44
+
45
+ - **`M` resolves to `isNullable: false` at install.** The flag was documented as
46
+ "mandatory" but was inert — nothing on the server or the client read it, so a
47
+ column declared `"flags": "VEM"` rendered without a red asterisk and saved
48
+ empty. It is now folded into `isNullable`, the platform's single notion of
49
+ required, and everything downstream keys off that: `NOT NULL` in generated
50
+ DDL, the `data.insert` required-column check, the client asterisk.
51
+ - **Declaring `M` together with `"isNullable": true` is now a hard error** at
52
+ pack time, naming the entity and every offending field — it is a contradiction
53
+ only the author can resolve. **This repo shipped exactly that combination** in
54
+ the extension example in `resources/docs/conventions.md` (`customer_id`, `"flags":
55
+ "EM"` with `"isNullable": true`), so a module authored from that pattern could
56
+ not pack. Corrected to `"flags": "E"`, matching the real `crm-fin` module.
57
+ - **The FK+Reference rule no longer prescribes `M` by default.** A hidden FK is
58
+ `flags: "E"`, or `"EM"` when the relationship is genuinely required; omitting
59
+ both leaves the column nullable, which is the default — so the examples no
60
+ longer spell out `"isNullable": true` either. Keep the visible Reference in
61
+ step (`VE` / `VEM`): `M` is inert on a virtual column, so `VEM` over an
62
+ optional FK changes nothing but reads as a required field to the next author,
63
+ which is precisely how the broken example above went unnoticed.
64
+ - **An upgrade now reconciles nullability in both directions.** Previously
65
+ neither direction reached an existing table: `ADD COLUMN IF NOT EXISTS` no-ops
66
+ on a column that is already there, and the only nullability statement emitted
67
+ was `DROP NOT NULL`, and that only for an *explicit* `isNullable: true`. So
68
+ adding `M` to a shipped field silently did nothing on upgraded tenants (it
69
+ worked on a fresh install, which goes through `CREATE TABLE`), and merely
70
+ deleting `M` left the constraint standing, failing later inserts with a raw
71
+ Postgres 23502.
72
+ - **Authoring consequence:** adding `M` to a field that existing tenant rows
73
+ leave empty now **aborts the upgrade**, rolled back, naming the column and its
74
+ NULL row count — rather than committing a tenant whose API enforces a rule its
75
+ data already breaks. Give the column a `params.serverDefault` if the install
76
+ should backfill it. Removing `M` is always safe.
77
+ - **`params.serverDefault` now works on a required column.** The platform's
78
+ required-for-insert check excluded `formula` and `numberSequence` but not
79
+ `serverDefault`, so an insert omitting such a column was rejected as missing
80
+ even though the INSERT would have supplied the value. An `on: "update"`
81
+ default stays required — it does not fire on insert.
82
+ - **Also corrected in `conventions.md`:** the hidden-FK examples carried
83
+ `"fieldTypeCd": "hidden"`, which makes install warn that a `cuid` column
84
+ (group *number*) is bound to a *text* field type and can misbind filter/DML
85
+ values at runtime (`operator does not exist: bigint = text`). A hidden FK is
86
+ `dbDatatype: "cuid"` + `flags` — no `fieldTypeCd`.
87
+
88
+ ### Changed — formula navigation at SQL time is no longer single-hop
89
+
90
+ `formulas.md` and `column-types.md` still described the report/query translator as
91
+ single-hop-only, and told authors to keep report-bound formulas to one hop. That
92
+ stopped being true upstream:
93
+
94
+ - **Multi-hop `[a].[b].[c]` translates**, one `LEFT JOIN` per hop, each with the
95
+ same soft-deny rights check as a reference display column. A nav path may land on
96
+ a *formula* column of the referenced entity, whose own hops are joined off that
97
+ alias — which is what makes a two-level formula reachable through a reference.
98
+ - **Hops are counted from the query's root** and capped at `MaxNavigationDepth`, so
99
+ the hops spent reaching a formula reduce the budget left for its own navigation.
100
+ - **What is still unsupported changed shape:** `$[Setting]` references, paths past
101
+ the cap, **extension-table columns** (they live in a separate 1:1 table needing
102
+ its own JOIN), and misauthored paths. All come back NULL with a response warning.
103
+ - **Warning wording follows the clause** — a SELECT slot goes empty, an ORDER BY
104
+ term is dropped so rows come back in a different order, a WHERE condition is
105
+ dropped so *more* rows come back than were asked for. Documented, because the
106
+ third one is the only failure that silently widens a result set.
107
+ - **Comparison is null-safe and two-valued** in every context: `null = null` is
108
+ true, `[x] != null` is true for any non-null `x`, and SQL-time `=`/`!=` translate
109
+ to `IS NOT DISTINCT FROM` / `IS DISTINCT FROM` so a card and a report agree.
110
+ - Corrected in both files: nav resolution walks N:1 references only, **never a 1:N
111
+ set**, however many hops it is given — the reason `SUM([set].[field])` in an `F`
112
+ column renders empty. That was previously attributed to the single-hop limit.
113
+
114
+ ### Changed — `entityLink()` stores the entity id and captions from `toString`
115
+
116
+ `entity_cd` is only unique per module, so storing the bare code meant a link built
117
+ as `entityLink('fin.invoice', …)` re-resolved at click time against the *reader's*
118
+ folder module and could land on another module's `invoice`. It now stores the
119
+ resolved entity id. The display caption comes from the entity's `toString`
120
+ evaluated against the record (an explicit `description` still wins) — previously
121
+ the entity's own description, which names the table, not the record. Documented in
122
+ `action-dsl.md`, `field-types.md` and the `dforge://reference/dsl` resource, with
123
+ the reminder to pass a qualified code.
124
+
125
+ ### Changed — dependencies
126
+
127
+ - **`@dforge-core/metadata` `^0.0.10` → `^0.0.12`**, and the vendored schemas
128
+ re-generated from it. `entity.schema.json` and `triggers.schema.json` were stale:
129
+ they still described `M` as plain "Mandatory" with no mention of `isNullable`, and
130
+ omitted that DSL `insert()`/`update()`/`delete()`/`query()` writes to *other*
131
+ records raise no trigger events.
132
+ - **`@dforge-core/dforge-cli` `^0.2.7` → `^0.2.10`.**
133
+
134
+ ## 0.2.1
135
+
136
+ Adds the column-domain schema that `dforge_module_inspect` already reported on,
137
+ and corrects three areas where the schemas and reference docs described
138
+ behavior the platform doesn't have.
139
+
140
+ ### Added — column domains
141
+
142
+ - **`resources/schemas/domains.schema.json`** — `domains.json` had no schema, so
143
+ the one file that defines reusable semantic types was the only module file
144
+ authored blind. A domain bundles a base datatype, a control, sizing and a
145
+ shared option list under one code. Structural fields (`dbDatatype`,
146
+ `baseDatatypeCd`, `fieldTypeCd`, `maxLen`, `precision`) are materialized onto
147
+ each consuming column at install; `params` — most importantly `options` —
148
+ stays on the domain and resolves at metadata-load time, which is what lets a
149
+ shared list be authored and translated exactly once.
150
+ - **`entity.schema.json` gains `domain`** plus a *Domain-backed column* branch in
151
+ the column `oneOf`. Referencing a domain and restating what it owns is
152
+ rejected at install, so the schema now refuses the same combination rather
153
+ than letting it through to a failed install.
154
+
155
+ ### Fixed — trigger semantics were documented wrong
156
+
157
+ `triggers.schema.json` and `dforge_trigger_add` both described an invocation
158
+ contract the runtime doesn't implement, and agents wrote actions against it:
159
+
160
+ - **There is no injected `record_id` param.** The trigger binds the affected
161
+ record to the action as *record context* — the action reads it with `[field]`
162
+ and its PK with `[pk_column]`, exactly like a UI-invoked action, with
163
+ pre-change values as `old[field]` on update/delete/status_change. `params` is
164
+ static-only: the literal values written in the trigger, nothing else.
165
+ - **The action's `entityCode` must equal the trigger's `entity`**, and its
166
+ `executionMode` must not be `batch` — both enforced at pack time. The old
167
+ description said the opposite, warning the action must *not* rely on the
168
+ triggering record.
169
+ - **`condition` rejects reference navigation.** The event-time evaluator reads
170
+ only columns of the changed row, so `[ref].[target]` resolves to null and the
171
+ trigger misfires. Navigate inside the action, or denormalise onto the watched
172
+ entity.
173
+ - **`async` defaults to `true`**, matching the platform. The tool defaulted it to
174
+ `false`, so every trigger added without an explicit `async` ran synchronously
175
+ inside the user's transaction — where a failing action rolls back their write.
176
+
177
+ ### Fixed — actions are not reliably atomic
178
+
179
+ - **`isTransacted` does not open a transaction.** It only decides whether the
180
+ first failing record aborts the run or the loop continues. Whether writes made
181
+ before a failure survive depends on the execution path: `auditHistory` on the
182
+ entity, `single` vs `batch`, sync vs background. `action-dsl.md` now tabulates
183
+ all six combinations, and `dsl-reference.md` no longer claims `error()` rolls
184
+ back the whole transaction. This is a platform defect rather than a contract —
185
+ actions are intended to be atomic — so both documents say to validate before
186
+ writing anything, and not to design around either outcome.
187
+ - **`query()` runs on a pooled, shared connection**, which nothing checks
188
+ statically and neither document mentioned: `SET LOCAL` not `SET`,
189
+ `pg_advisory_xact_lock()` not `pg_advisory_lock()`, `ON COMMIT DROP` on temp
190
+ tables.
191
+ - Documented `old[fieldName]`, and the single-hop limit on `[ref].[target]` —
192
+ `[a].[b].[c]` is a compile error, not a slow path.
193
+
7
194
  ## 0.2.0
8
195
 
9
196
  Tools go from 27 → 34; the single wizard skill splits into a router plus three