@dforge-core/dforge-mcp 0.2.15 → 0.2.17

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/dist/server.js +157 -9
  3. package/package.json +3 -3
  4. package/resources/docs/conventions.md +74 -1
  5. package/resources/schemas/deps.schema.json +1 -1
  6. package/resources/schemas/entity.schema.json +80 -2
  7. package/resources/schemas/folders.schema.json +1 -1
  8. package/resources/schemas/manifest.schema.json +1 -1
  9. package/resources/schemas/menus.schema.json +1 -1
  10. package/resources/schemas/triggers.schema.json +1 -1
  11. package/skills/dforge-mcp-author/examples/column-security/README.md +67 -0
  12. package/skills/dforge-mcp-author/examples/column-security/entities/product.json +79 -0
  13. package/skills/dforge-mcp-author/examples/column-security/manifest.json +23 -0
  14. package/skills/dforge-mcp-author/examples/column-security/security/roles.json +16 -0
  15. package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +35 -0
  16. package/skills/dforge-mcp-author/examples/column-security/ui/data_views.json +20 -0
  17. package/skills/dforge-mcp-author/examples/column-security/ui/folders.json +23 -0
  18. package/skills/dforge-mcp-author/examples/column-security/ui/menus.json +21 -0
  19. package/skills/dforge-mcp-author/examples/simple-todo/manifest.json +8 -12
  20. package/skills/dforge-mcp-author/references/column-types.md +3 -2
  21. package/skills/dforge-mcp-author/references/data-views.md +2 -0
  22. package/skills/dforge-mcp-author/references/flags.md +41 -9
  23. package/skills/dforge-mcp-author/references/formulas.md +7 -0
  24. package/skills/dforge-mcp-author/references/security.md +77 -1
  25. package/skills/dforge-mcp-author/references/validation-checklist.md +2 -0
  26. package/skills/dforge-module-build/SKILL.md +20 -3
@@ -213,7 +213,7 @@ Lives in: `ui/folders.json`
213
213
 
214
214
  | Property | Type | Description |
215
215
  |---|---|---|
216
- | `viewName` | string | Default data view (usually `"default"`) |
216
+ | `viewName` | string | Entity view to bind — a key under the entity's `views` (see [Column-level security](#column-level-security-entity-views)), **not** a `ui/data_views.json` code. `"default"` means "no view" (full column set). |
217
217
  | `quickAdd` | boolean | Show "+" quick-add button (`true` for main entities, `false` for detail/line items) |
218
218
  | `rowFilter` | object | Row-level filter scoping which records are visible in this folder |
219
219
 
@@ -226,6 +226,82 @@ Row filters use a compact filter object with:
226
226
 
227
227
  This is how **row-level security** works in the module package — subfolders with `rowFilter` restrict which rows are visible. Users assigned to "Central Warehouse" only see stock where `warehouse_id = 2001`.
228
228
 
229
+ ## Column-level security (entity views)
230
+
231
+ > Worked example: `dforge://example/column-security/` — one entity, two folders, two
232
+ > column sets. Its README walks the overrides and the ways a view fails the install.
233
+
234
+ Layer 2 of the model. A **entity view** is a named subset of an entity's columns, declared on the entity itself; a folder binds one per entity. Users working in that folder see **only** the columns the view lists — the rest are neither visible nor queryable there.
235
+
236
+ Two halves, both required:
237
+
238
+ | File | Key | Effect |
239
+ |---|---|---|
240
+ | `entities/<entity>.json` | `views.<viewName>.columns` | Declares the view and its column set |
241
+ | `ui/folders.json` | `entities.<code>.viewName` | Binds that view for this entity in this folder |
242
+
243
+ ```json
244
+ {
245
+ "fields": { "product_id": {}, "name": {}, "price": {}, "cost": {}, "quantity": {} },
246
+ "views": {
247
+ "accountant": {
248
+ "columns": {
249
+ "product_id": {},
250
+ "name": { "flags": "V" },
251
+ "price": { "flags": "VE" },
252
+ "cost": { "flags": "V" }
253
+ }
254
+ },
255
+ "storekeeper": {
256
+ "columns": {
257
+ "product_id": {},
258
+ "name": { "flags": "V" },
259
+ "quantity": { "flags": "VE" }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ ```
265
+
266
+ ```json
267
+ "entities": { "product": { "viewName": "accountant", "quickAdd": true } }
268
+ ```
269
+
270
+ The storekeeper folder cannot see `price` or `cost` at all — not on screen, not through a filter, not in an export.
271
+
272
+ **Per-column overrides** (all optional, each falls back to the entity-level value): `flags`, `orderNum`, `isNullable`, `editMask`, `displayFmt`, `refFilter`, `params`, `formula`. `{}` means "expose this column, override nothing".
273
+
274
+ > **An empty override is a value, not an absence.** The runtime merges view over entity with `COALESCE`, which skips NULL only — so `"flags": ""` means "no flags in this view" (a column neither visible nor editable) and `"params": {}` suppresses the entity-level params rather than inheriting them. Omit the key entirely to inherit — `"flags": null` fails schema validation. See `flags.md` for the flag letters.
275
+
276
+ > **`M` works in a view override too**, folding into that override's `isNullable`. Since a view writes no DDL, the requiredness is runtime-only — which is how a column nullable in the database is made mandatory inside one view: `{ "flags": "VEM" }`. The fold is one-directional: dropping `M` does not loosen the column, because `isNullable` still inherits. See `flags.md` → Per-view overrides.
277
+
278
+ `formula` lets one view compute a column differently from the entity — same DSL as a field's `formula`, and **only on a column the entity declares as `columnType: "F"`**; elsewhere the install fails, because there that field holds the SQL default and a view cannot change a physical column's `DEFAULT`. There is no per-view default value for a normal column. See `formulas.md`.
279
+
280
+ ### Rules the installer enforces
281
+
282
+ Each of these is an install-time error, and each is silent at runtime if it slips through — which is why `dforge_module_validate` checks them offline:
283
+
284
+ - A column absent from `columns` is **hidden**. That is the mechanism, not an oversight.
285
+ - A view must list the entity's **primary key** — records are addressed by it. To keep it off screen, drop `V` from its flags instead of omitting it.
286
+ - A column code that doesn't exist on the entity, a view with **no columns**, the same column listed twice under different casing, or two views on one entity whose names differ only by case (a folder binds case-insensitively, so only one could ever be reached).
287
+ - Views must be declared on the entity's **own** module. An `extends` extension declaring `views` is rejected — a view enumerates the complete column set, which an extension does not own.
288
+ - A `viewName` that no view declares, or one naming a view with no columns, **fails the install**. It does not fall back: the fallback is the entity's full column set, so a typo would quietly *unrestrict* the folder.
289
+ - Columns removed from a view are reaped on upgrade — that is the security change the author asked for. Whole views are not: `entity_view` also holds tenant views from the card-layout editor, and the installer cannot tell them apart.
290
+
291
+ ### `"default"` is the one exempt name
292
+
293
+ `"viewName": "default"` means **no view** (full column set) and declares nothing. It is what every shipped module writes, and what the runtime auto-creates for an entity reached without a folder binding — so it is exempt from the checks above. A module that really declares a view named `default` still gets it bound.
294
+
295
+ ### Don't confuse the three "views"
296
+
297
+ | Term | Where | What it is |
298
+ |---|---|---|
299
+ | **Entity view** | `entities/*.json` → `views` | Column-level security (this section) |
300
+ | **Data view** | `ui/data_views.json` | A grid / kanban / calendar over an entity (`data-views.md`) |
301
+ | **SQL view** | `entities/*.json` → `isView` + `viewSql` | An entity backed by a SQL view instead of a table |
302
+
303
+ `viewName` in `folders.json` binds the first. Menus' `dataViewCode` references the second.
304
+
229
305
  ### When to use subfolders
230
306
 
231
307
  - **Multi-location modules** (warehouses, offices, branches) — one subfolder per location, filtered by location FK
@@ -105,6 +105,8 @@ For each entity:
105
105
  ## Security
106
106
 
107
107
  - [ ] `security/roles.json` exists (at least one role)
108
+ - [ ] Every entity view (`views.<v>.columns` on an entity) lists the entity's **primary key**, names only real columns, and has at least one column — each of these fails the install
109
+ - [ ] Every `viewName` in `ui/folders.json` is either `"default"` (= no view) or a view that entity actually declares — an unresolved name fails the install rather than falling back
108
110
  - [ ] Every role has `description` and `rights` (both required; no `label` field in `roles.json` — the `roles.schema.json` sets `additionalProperties: false`, so any extra field including `label` is rejected here). `description` is the English display-name fallback; **localized role labels live in the translation files** (`roles.<code>.label`) and are completeness-enforced — see Translations below.
109
111
  - [ ] The field name is `rights`, NOT `entityRights`
110
112
  - [ ] Rights strings use only valid letters: `SIUDC` for entities, `E` for actions/reports/folders
@@ -31,6 +31,7 @@ flags, or column patterns.
31
31
  | Add a grid / list view | `dforge://reference/data-views`, `dforge://example/ui/data_views.json` | — |
32
32
  | Add a matrix (pivot) view | `dforge://reference/data-views` (§Matrix), `dforge://example/matrix-budget/ui/data_views.json`, `dforge://example/matrix-budget/entities/budget_line.json` | — |
33
33
  | Add kanban / calendar / tree-grid / master-detail | `dforge://reference/data-views` | `dforge://schema/data-views` |
34
+ | 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` | — |
34
35
  | Add a menu | `dforge://reference/menus`, `dforge://example/ui/menus.json` | — |
35
36
  | Add an action | `dforge://reference/action-dsl`, `dforge://example/logic/actions/mark_done.dsl`, `dforge://example/ui/actions.json` | `dforge://docs/dsl` |
36
37
  | Add a trigger | — | `dforge://schema/triggers`; condition syntax in `dforge://docs/dsl` |
@@ -186,7 +187,13 @@ before committing, then `dforge_action_add`. One action per turn.
186
187
 
187
188
  **2b Triggers** reference actions that must already exist. `async: true` runs
188
189
  after the transaction commits (use for anything slow); `async: false` runs
189
- 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.
190
197
 
191
198
  **2c Jobs** run as the system user with **no current record** — the action must
192
199
  not use `[field]` syntax. Wrap a record-context action in a thin job action that
@@ -204,8 +211,11 @@ per entity, so you'll often `view_modify` rather than `view_add`.
204
211
 
205
212
  View codes are semantic — the entity name (`feedback_item`), a plural
206
213
  (`invoices`), or descriptive (`invoices_kanban`). **Never use the literal code
207
- `default`**; `ui/folders.json`'s `viewName: "default"` is a fallback alias the
208
- platform resolves to the entity's first declared view.
214
+ `default`.** Note that `ui/folders.json`'s `viewName` is a different axis
215
+ entirely: it binds an *entity view* (column-level security, declared under
216
+ `views` on the entity file), not a data view code. `"default"` there means "no
217
+ entity view — show the full column set", and any other name the entity does not
218
+ declare **fails the install**. See `references/security.md`.
209
219
 
210
220
  Then wire menus with `dforge_menu_add` — a section node per group, leaves
211
221
  pointing at the views.
@@ -273,6 +283,13 @@ stated trigger; every report's params sit on a dataset.
273
283
  4. **Show the rights matrix as a table** (rows = entities/actions/reports,
274
284
  columns = roles, cells = rights strings), each cell explained by the verb it
275
285
  maps to. Get sign-off.
286
+ 5. **Column-level security only when the intake asks for it.** Roles grant rights
287
+ per entity, not per field. When a user type must not see specific *columns*
288
+ (salaries, cost prices, personal data), that is an **entity view**: declare
289
+ `views.<name>.columns` on the entity and bind it with `viewName` on the folder
290
+ that user type works in. Don't reach for it otherwise — a view is the complete
291
+ column list, so it is one more list to keep in step with the fields. Rules and
292
+ the worked example: `references/security.md` → Column-level security.
276
293
  5. `dforge_role_add` for new roles; `dforge_role_right_set` per grant when
277
294
  amending an existing one.
278
295