@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.
- package/CHANGELOG.md +192 -0
- package/dist/server.js +157 -9
- package/package.json +3 -3
- package/resources/docs/conventions.md +74 -1
- package/resources/schemas/deps.schema.json +1 -1
- package/resources/schemas/entity.schema.json +80 -2
- package/resources/schemas/folders.schema.json +1 -1
- package/resources/schemas/manifest.schema.json +1 -1
- package/resources/schemas/menus.schema.json +1 -1
- package/resources/schemas/triggers.schema.json +1 -1
- package/skills/dforge-mcp-author/examples/column-security/README.md +67 -0
- package/skills/dforge-mcp-author/examples/column-security/entities/product.json +79 -0
- package/skills/dforge-mcp-author/examples/column-security/manifest.json +23 -0
- package/skills/dforge-mcp-author/examples/column-security/security/roles.json +16 -0
- package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +35 -0
- package/skills/dforge-mcp-author/examples/column-security/ui/data_views.json +20 -0
- package/skills/dforge-mcp-author/examples/column-security/ui/folders.json +23 -0
- package/skills/dforge-mcp-author/examples/column-security/ui/menus.json +21 -0
- package/skills/dforge-mcp-author/examples/simple-todo/manifest.json +8 -12
- package/skills/dforge-mcp-author/references/column-types.md +3 -2
- package/skills/dforge-mcp-author/references/data-views.md +2 -0
- package/skills/dforge-mcp-author/references/flags.md +41 -9
- package/skills/dforge-mcp-author/references/formulas.md +7 -0
- package/skills/dforge-mcp-author/references/security.md +77 -1
- package/skills/dforge-mcp-author/references/validation-checklist.md +2 -0
- package/skills/dforge-module-build/SKILL.md +20 -3
|
@@ -95,10 +95,88 @@
|
|
|
95
95
|
}
|
|
96
96
|
},
|
|
97
97
|
"additionalProperties": true
|
|
98
|
+
},
|
|
99
|
+
"views": {
|
|
100
|
+
"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 (or folders naming a view that isn't declared here) show the entity's full column set.",
|
|
102
|
+
"propertyNames": {
|
|
103
|
+
"pattern": "^[a-zA-Z_][a-zA-Z0-9_-]*$",
|
|
104
|
+
"maxLength": 100
|
|
105
|
+
},
|
|
106
|
+
"additionalProperties": {
|
|
107
|
+
"$ref": "#/$defs/entityView"
|
|
108
|
+
}
|
|
98
109
|
}
|
|
99
110
|
},
|
|
100
111
|
"additionalProperties": false,
|
|
101
112
|
"$defs": {
|
|
113
|
+
"entityView": {
|
|
114
|
+
"type": "object",
|
|
115
|
+
"description": "One entity view: the columns it exposes, plus optional per-view overrides on each.",
|
|
116
|
+
"required": ["columns"],
|
|
117
|
+
"properties": {
|
|
118
|
+
"columns": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"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
|
+
"minProperties": 1,
|
|
122
|
+
"propertyNames": {
|
|
123
|
+
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
|
|
124
|
+
},
|
|
125
|
+
"additionalProperties": {
|
|
126
|
+
"$ref": "#/$defs/entityViewColumn"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"params": {
|
|
130
|
+
"type": "object",
|
|
131
|
+
"description": "Free-form view configuration, stored on entity_view.params.",
|
|
132
|
+
"additionalProperties": true
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"additionalProperties": false
|
|
136
|
+
},
|
|
137
|
+
"entityViewColumn": {
|
|
138
|
+
"description": "Per-view overrides for one column. Every property is optional; anything omitted falls back to the entity-level value on the column itself.",
|
|
139
|
+
"type": ["object", "null"],
|
|
140
|
+
"properties": {
|
|
141
|
+
"flags": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"maxLength": 10,
|
|
144
|
+
"pattern": "^[VIEM]*$",
|
|
145
|
+
"description": "Flag letters for this column in this view — the same V/I/E/M vocabulary as a field's 'flags'. One rule, three cases: OMIT the key to inherit the entity-level flags (omission is the only inherit spelling — this field is a string, so a literal null does not validate; to inherit everything, set the whole column override to null or {}); any string, empty included, OVERRIDES them, because the runtime merges view over entity with COALESCE, which skips a missing value only; and a non-empty string REPLACES the entity letters rather than merging with them. So 'V' exposes the column read-only (visible, not editable) however the entity flagged it, and '' means 'no flags here' — neither visible nor editable. M works here as it does on a field: the installer folds it into this override's isNullable, making the column required IN THIS VIEW even when it is nullable in the database (a view writes no DDL, so the requiredness is runtime-only). Declaring M together with \"isNullable\": true is rejected at pack time. Note the fold is one-directional — DROPPING M from a view's flags does not make a column optional, because isNullable still COALESCEs to the entity value; write \"isNullable\": true to loosen it."
|
|
146
|
+
},
|
|
147
|
+
"orderNum": {
|
|
148
|
+
"type": "integer",
|
|
149
|
+
"description": "Display order within this view."
|
|
150
|
+
},
|
|
151
|
+
"isNullable": {
|
|
152
|
+
"type": "boolean",
|
|
153
|
+
"description": "Nullability override for this view."
|
|
154
|
+
},
|
|
155
|
+
"editMask": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"maxLength": 100
|
|
158
|
+
},
|
|
159
|
+
"displayFmt": {
|
|
160
|
+
"type": "string",
|
|
161
|
+
"maxLength": 100
|
|
162
|
+
},
|
|
163
|
+
"refFilter": {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"description": "Lookup filter override for a reference column in this view.",
|
|
166
|
+
"additionalProperties": true
|
|
167
|
+
},
|
|
168
|
+
"formula": {
|
|
169
|
+
"type": "string",
|
|
170
|
+
"description": "Formula override for this column in this view, in the same DSL as a field's 'formula'. Allowed only on a column the entity declares as columnType 'F'; on any other column the install FAILS (it does not silently ignore the override), 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 today. The runtime prefers it over the entity-level formula, so one view can compute a column differently from the entity. The parsed AST is regenerated at install; a formula that does not parse, calls an unimplemented function, or aggregates over a 1:N set fails the install (each renders empty at runtime)."
|
|
171
|
+
},
|
|
172
|
+
"params": {
|
|
173
|
+
"type": "object",
|
|
174
|
+
"description": "Control-specific configuration override for this view.",
|
|
175
|
+
"additionalProperties": true
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
"additionalProperties": false
|
|
179
|
+
},
|
|
102
180
|
"field": {
|
|
103
181
|
"type": "object",
|
|
104
182
|
"description": "Entity column/field definition",
|
|
@@ -129,8 +207,8 @@
|
|
|
129
207
|
},
|
|
130
208
|
"flags": {
|
|
131
209
|
"type": "string",
|
|
132
|
-
"pattern": "^[
|
|
133
|
-
"description": "
|
|
210
|
+
"pattern": "^[VIEM]*$",
|
|
211
|
+
"description": "Column flags, in any order. V=Visible and E=Editable are display/interaction; I=Identity marks a platform-managed column (never user-editable, excluded from required validation); M=Mandatory is not a runtime flag at all — it is resolved into isNullable:false at install, so it makes the column NOT NULL, is inert on virtual (R/S/F) and identity columns, and declaring it together with isNullable:true is rejected at pack time. Any other letter is rejected at pack time; there is no H flag — a column is hidden by omitting V."
|
|
134
212
|
},
|
|
135
213
|
"isPk": {
|
|
136
214
|
"type": "boolean",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"properties": {
|
|
59
59
|
"viewName": {
|
|
60
60
|
"type": "string",
|
|
61
|
-
"description": "Entity view name (data view code
|
|
61
|
+
"description": "Entity view to bind for this entity in this folder — the name of a key under the entity definition's 'views' (column-level security), NOT a data view code from ui/data_views.json. Users in this folder then see only the columns that view lists. The conventional 'default' declares nothing and means 'no view' (full column set); any other name that the entity does not declare fails the install."
|
|
62
62
|
},
|
|
63
63
|
"quickAdd": {
|
|
64
64
|
"type": "boolean",
|
|
@@ -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",
|
|
@@ -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
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# column-security — entity views example
|
|
2
|
+
|
|
3
|
+
A minimal module whose only job is to demonstrate **entity views**: dForge's
|
|
4
|
+
column-level security. One `product` entity holds both stock and pricing, and two
|
|
5
|
+
folders show two different column sets of it.
|
|
6
|
+
|
|
7
|
+
## The shape
|
|
8
|
+
|
|
9
|
+
Column-level security is always **two halves**, and both are required:
|
|
10
|
+
|
|
11
|
+
| Piece | File | Effect |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| The column set | `entities/product.json` → `views` | Declares `storekeeper` and `accountant` — each a named subset of the entity's columns |
|
|
14
|
+
| The binding | `ui/folders.json` → `entities.product.viewName` | The Warehouse folder binds `storekeeper`, the Finance folder binds `accountant` |
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
product columns: name sku quantity cost_price sale_price margin
|
|
18
|
+
|
|
19
|
+
Warehouse folder → view "storekeeper": name sku quantity
|
|
20
|
+
Finance folder → view "accountant": name sku quantity(read-only) cost_price sale_price margin
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A user working in Warehouse cannot see `cost_price` **at all** — not on screen, not
|
|
24
|
+
through a filter, not in an export. The columns a view omits are invisible *and*
|
|
25
|
+
unqueryable there. That is the mechanism: omission is the restriction.
|
|
26
|
+
|
|
27
|
+
## What each view demonstrates
|
|
28
|
+
|
|
29
|
+
- **`storekeeper`** — the plain case: list the columns this folder may see, and no
|
|
30
|
+
others. Note it still lists `product_id`; a view must include the primary key
|
|
31
|
+
because records are addressed by it.
|
|
32
|
+
- **`accountant`** — the override cases:
|
|
33
|
+
- `"quantity": { "flags": "V" }` — visible but **not** editable here, while the
|
|
34
|
+
entity itself declares `VEM`. A per-column `flags` override re-flags the column
|
|
35
|
+
for the folders bound to this view only.
|
|
36
|
+
- `"margin": { "formula": "…", "displayFmt": "N1" }` — a per-view **formula**
|
|
37
|
+
override: the entity computes margin as an absolute amount, this view computes it
|
|
38
|
+
as a percentage. Allowed only because `margin` is a `columnType: "F"` column — on
|
|
39
|
+
any other column that field holds the SQL default, so the override would be inert
|
|
40
|
+
and the install rejects it.
|
|
41
|
+
|
|
42
|
+
## Things that would fail the install
|
|
43
|
+
|
|
44
|
+
Worth knowing, because each one is silent at runtime if it slips through:
|
|
45
|
+
|
|
46
|
+
- Dropping `product_id` from a view — records the client cannot open or save.
|
|
47
|
+
- A view with **no** columns, or a column code the entity doesn't have.
|
|
48
|
+
- Two views whose names differ only by case — a folder binds case-insensitively, so
|
|
49
|
+
only one could ever be reached.
|
|
50
|
+
- A `viewName` in `folders.json` that no view declares. This does **not** fall back:
|
|
51
|
+
the fallback would be the entity's full column set, so a typo would quietly
|
|
52
|
+
*unrestrict* the folder. `"default"` is the one exempt name and means "no view".
|
|
53
|
+
|
|
54
|
+
`dforge_module_validate` checks all of these offline, before packing.
|
|
55
|
+
|
|
56
|
+
## What this example deliberately leaves out
|
|
57
|
+
|
|
58
|
+
The `product_grid` data view lists **every** column, including the prices. That is
|
|
59
|
+
correct: a data view describes the presentation, and the folder's entity view decides
|
|
60
|
+
what a user in that folder is allowed to see of it. The grid renders whichever
|
|
61
|
+
columns survive the view — you do not maintain one data view per role.
|
|
62
|
+
|
|
63
|
+
Entity views are also unrelated to the two other things called "view" here: a *data
|
|
64
|
+
view* (`ui/data_views.json`) is a grid/kanban/calendar, and `isView` + `viewSql` on an
|
|
65
|
+
entity means it is backed by a SQL view.
|
|
66
|
+
|
|
67
|
+
See `dforge://reference/security` → Column-level security for the full rules.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Products, with stock and pricing on one record",
|
|
3
|
+
"dbObject": "product",
|
|
4
|
+
"toString": "{name}",
|
|
5
|
+
"traits": ["identity", "audit"],
|
|
6
|
+
"fields": {
|
|
7
|
+
"name": {
|
|
8
|
+
"dbDatatype": "varchar",
|
|
9
|
+
"fieldTypeCd": "text",
|
|
10
|
+
"flags": "VEM",
|
|
11
|
+
"maxLen": 200,
|
|
12
|
+
"orderNum": 10,
|
|
13
|
+
"description": "Product Name"
|
|
14
|
+
},
|
|
15
|
+
"sku": {
|
|
16
|
+
"dbDatatype": "varchar",
|
|
17
|
+
"fieldTypeCd": "text",
|
|
18
|
+
"flags": "VEM",
|
|
19
|
+
"maxLen": 40,
|
|
20
|
+
"orderNum": 20,
|
|
21
|
+
"description": "SKU"
|
|
22
|
+
},
|
|
23
|
+
"quantity": {
|
|
24
|
+
"dbDatatype": "numeric(18,2)",
|
|
25
|
+
"fieldTypeCd": "number",
|
|
26
|
+
"flags": "VEM",
|
|
27
|
+
"precision": 2,
|
|
28
|
+
"orderNum": 30,
|
|
29
|
+
"description": "Quantity On Hand"
|
|
30
|
+
},
|
|
31
|
+
"cost_price": {
|
|
32
|
+
"dbDatatype": "numeric(18,2)",
|
|
33
|
+
"fieldTypeCd": "currency",
|
|
34
|
+
"flags": "VEM",
|
|
35
|
+
"precision": 2,
|
|
36
|
+
"orderNum": 40,
|
|
37
|
+
"description": "Cost Price"
|
|
38
|
+
},
|
|
39
|
+
"sale_price": {
|
|
40
|
+
"dbDatatype": "numeric(18,2)",
|
|
41
|
+
"fieldTypeCd": "currency",
|
|
42
|
+
"flags": "VEM",
|
|
43
|
+
"precision": 2,
|
|
44
|
+
"orderNum": 50,
|
|
45
|
+
"description": "Sale Price"
|
|
46
|
+
},
|
|
47
|
+
"margin": {
|
|
48
|
+
"columnType": "F",
|
|
49
|
+
"fieldTypeCd": "currency",
|
|
50
|
+
"baseDatatypeCd": "number",
|
|
51
|
+
"flags": "V",
|
|
52
|
+
"precision": 2,
|
|
53
|
+
"formula": "[sale_price] - [cost_price]",
|
|
54
|
+
"orderNum": 60,
|
|
55
|
+
"description": "Margin"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"views": {
|
|
59
|
+
"storekeeper": {
|
|
60
|
+
"columns": {
|
|
61
|
+
"product_id": {},
|
|
62
|
+
"name": {},
|
|
63
|
+
"sku": {},
|
|
64
|
+
"quantity": {}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"accountant": {
|
|
68
|
+
"columns": {
|
|
69
|
+
"product_id": {},
|
|
70
|
+
"name": {},
|
|
71
|
+
"sku": {},
|
|
72
|
+
"quantity": { "flags": "V" },
|
|
73
|
+
"cost_price": {},
|
|
74
|
+
"sale_price": {},
|
|
75
|
+
"margin": { "formula": "([sale_price] - [cost_price]) / [sale_price] * 100", "displayFmt": "N1" }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"packageFormat": 1,
|
|
3
|
+
"moduleId": "20000000-0000-0000-0000-000000000003",
|
|
4
|
+
"code": "column_security",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"dbSchemaVersion": "0.0.1",
|
|
7
|
+
"displayName": "Column Security",
|
|
8
|
+
"description": "Example module demonstrating entity views — column-level security",
|
|
9
|
+
"author": { "name": "dForge Examples" },
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"category": "Examples",
|
|
12
|
+
"tags": ["example", "security", "entity-views"],
|
|
13
|
+
"icon": "bi-eye-slash",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"admin": ">=0.0.1"
|
|
16
|
+
},
|
|
17
|
+
"supportedLocales": ["en-US"],
|
|
18
|
+
"created": "2026-08-19",
|
|
19
|
+
"updated": "2026-08-19",
|
|
20
|
+
"entities": {
|
|
21
|
+
"product": "./entities/product.json"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"column_security.storekeeper": {
|
|
3
|
+
"description": "Counts stock; never sees cost or price",
|
|
4
|
+
"rights": {
|
|
5
|
+
"product": "SIU",
|
|
6
|
+
"folder:warehouse": "E"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"column_security.accountant": {
|
|
10
|
+
"description": "Owns costing and pricing",
|
|
11
|
+
"rights": {
|
|
12
|
+
"product": "SIU",
|
|
13
|
+
"folder:finance": "E"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entities": {
|
|
3
|
+
"product": {
|
|
4
|
+
"label": "Product",
|
|
5
|
+
"fields": {
|
|
6
|
+
"name": { "label": "Product Name" },
|
|
7
|
+
"sku": { "label": "SKU" },
|
|
8
|
+
"quantity": { "label": "Quantity On Hand" },
|
|
9
|
+
"cost_price": { "label": "Cost Price" },
|
|
10
|
+
"sale_price": { "label": "Sale Price" },
|
|
11
|
+
"margin": { "label": "Margin" }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"folders": {
|
|
16
|
+
"warehouse": { "label": "Warehouse" },
|
|
17
|
+
"finance": { "label": "Finance" }
|
|
18
|
+
},
|
|
19
|
+
"roles": {
|
|
20
|
+
"column_security.storekeeper": { "label": "Storekeeper" },
|
|
21
|
+
"column_security.accountant": { "label": "Accountant" }
|
|
22
|
+
},
|
|
23
|
+
"views": {
|
|
24
|
+
"product_grid": { "label": "Products" }
|
|
25
|
+
},
|
|
26
|
+
"menus": {
|
|
27
|
+
"product_menu": {
|
|
28
|
+
"label": "Products",
|
|
29
|
+
"items": {
|
|
30
|
+
"catalog": { "label": "Catalog" },
|
|
31
|
+
"all_products": { "label": "Products" }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"product_grid": {
|
|
3
|
+
"label": "Products",
|
|
4
|
+
"description": "One grid, two column sets — whichever the folder's entity view allows",
|
|
5
|
+
"viewType": "grid",
|
|
6
|
+
"icon": "bi-box-seam",
|
|
7
|
+
"dataSources": [{
|
|
8
|
+
"entityCode": "product",
|
|
9
|
+
"columns": [
|
|
10
|
+
{ "column_cd": "name", "width": 240 },
|
|
11
|
+
{ "column_cd": "sku", "width": 120 },
|
|
12
|
+
{ "column_cd": "quantity", "width": 120 },
|
|
13
|
+
{ "column_cd": "cost_price", "width": 120 },
|
|
14
|
+
{ "column_cd": "sale_price", "width": 120 },
|
|
15
|
+
{ "column_cd": "margin", "width": 120 }
|
|
16
|
+
],
|
|
17
|
+
"order": ["name"]
|
|
18
|
+
}]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"label": "Products",
|
|
3
|
+
"description": "Product master data",
|
|
4
|
+
"icon": "bi-box-seam",
|
|
5
|
+
"children": {
|
|
6
|
+
"warehouse": {
|
|
7
|
+
"label": "Warehouse",
|
|
8
|
+
"description": "Stock keeping — no pricing",
|
|
9
|
+
"color": "#0ea5e9",
|
|
10
|
+
"entities": {
|
|
11
|
+
"product": { "viewName": "storekeeper", "quickAdd": true }
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"finance": {
|
|
15
|
+
"label": "Finance",
|
|
16
|
+
"description": "Costing and pricing",
|
|
17
|
+
"color": "#f59e0b",
|
|
18
|
+
"entities": {
|
|
19
|
+
"product": { "viewName": "accountant", "quickAdd": false }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"product_menu": {
|
|
3
|
+
"label": "Products",
|
|
4
|
+
"items": {
|
|
5
|
+
"catalog": {
|
|
6
|
+
"orderNum": 1,
|
|
7
|
+
"label": "Catalog",
|
|
8
|
+
"icon": "box-seam",
|
|
9
|
+
"children": {
|
|
10
|
+
"all_products": {
|
|
11
|
+
"itemType": "V",
|
|
12
|
+
"label": "Products",
|
|
13
|
+
"icon": "box-seam",
|
|
14
|
+
"dataViewCode": "product_grid",
|
|
15
|
+
"orderNum": 1
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
"dbSchemaVersion": "0.0.1",
|
|
7
7
|
"displayName": "Simple Todo",
|
|
8
8
|
"description": "Minimal example module demonstrating the dForge package format",
|
|
9
|
-
"author": {
|
|
9
|
+
"author": {
|
|
10
|
+
"name": "dForge Examples"
|
|
11
|
+
},
|
|
10
12
|
"license": "MIT",
|
|
11
13
|
"category": "Examples",
|
|
12
|
-
"tags": [
|
|
14
|
+
"tags": [
|
|
15
|
+
"example",
|
|
16
|
+
"todo"
|
|
17
|
+
],
|
|
13
18
|
"icon": "bi-check-square",
|
|
14
19
|
"dependencies": {
|
|
15
20
|
"admin": ">=0.0.1"
|
|
@@ -19,14 +24,5 @@
|
|
|
19
24
|
"entities": {
|
|
20
25
|
"todo_list": "./entities/todo_list.json",
|
|
21
26
|
"todo_item": "./entities/todo_item.json"
|
|
22
|
-
}
|
|
23
|
-
"dataViews": "./ui/data_views.json",
|
|
24
|
-
"menus": "./ui/menus.json",
|
|
25
|
-
"actions": "./ui/actions.json",
|
|
26
|
-
"security": {
|
|
27
|
-
"roles": "./security/roles.json"
|
|
28
|
-
},
|
|
29
|
-
"seedData": [
|
|
30
|
-
"./seed-data/01-lists.json"
|
|
31
|
-
]
|
|
27
|
+
}
|
|
32
28
|
}
|
|
@@ -36,8 +36,9 @@ Virtual N:1 lookup. No physical column. Always **paired with a hidden FK column*
|
|
|
36
36
|
The example below is a **required** relation, so both halves carry `M`. For an optional
|
|
37
37
|
one, drop it from both: FK `"E"`, Reference `"VE"`. `M` resolves to `isNullable: false`
|
|
38
38
|
at install, so it is what makes the FK column `NOT NULL` — and while `M` is inert on the
|
|
39
|
-
virtual Reference half
|
|
40
|
-
|
|
39
|
+
virtual Reference half (at the entity level; a per-view override behaves differently, see
|
|
40
|
+
`flags.md`), leaving it there over an optional FK reads as a required field to the next
|
|
41
|
+
author. `dforge_entity_reference_add({ required })` emits both halves in step.
|
|
41
42
|
|
|
42
43
|
```json
|
|
43
44
|
// Hidden FK column — physical, hidden. "E" instead of "EM" if the relation is optional.
|
|
@@ -4,6 +4,8 @@ Data views define how users see and interact with entity data. One entity can ha
|
|
|
4
4
|
|
|
5
5
|
Lives in: `ui/data_views.json`
|
|
6
6
|
|
|
7
|
+
> **Not to be confused with entity views.** An *entity view* (`views` on an entity file) is column-level **security** — which columns a folder may see at all — and is what `folders.json`'s `viewName` binds. A *data view* is a presentation of an entity, referenced by a menu's `dataViewCode`. A third thing, `isView` + `viewSql` on an entity, means the entity is backed by a SQL view. See `security.md` → Column-level security.
|
|
8
|
+
|
|
7
9
|
## Structure
|
|
8
10
|
|
|
9
11
|
```json
|
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
Column flags are single-letter codes concatenated into a string. Order doesn't matter — `"VEM"` and `"EMV"` are equivalent.
|
|
4
4
|
|
|
5
|
-
Valid flags regex: `^[
|
|
5
|
+
Valid flags regex: `^[VIEM]*$`
|
|
6
6
|
|
|
7
7
|
## Flag letters
|
|
8
8
|
|
|
9
9
|
| Letter | Name | Meaning |
|
|
10
10
|
|---|---|---|
|
|
11
11
|
| `V` | Visible | Column appears in grid / form / card views. Omit to hide from standard UI. |
|
|
12
|
-
| `I` |
|
|
12
|
+
| `I` | Identity | Auto-managed by the platform. Used on PK columns, audit timestamps, system fields. Not shown in UI, not directly editable by users, excluded from required validation. |
|
|
13
13
|
| `E` | Editable | User can modify the value in the UI. Omit for read-only. |
|
|
14
14
|
| `M` | Mandatory | Required. Resolved into `isNullable: false` at install — see below. |
|
|
15
|
-
| `H` | Hidden | Permanently hidden — never shown, not even in admin metadata views. Stronger than omitting `V`. |
|
|
16
15
|
|
|
17
|
-
**Only these
|
|
16
|
+
**Only these 4 letters are valid.** The platform rejects any other characters.
|
|
17
|
+
> **There is no `H` flag.** Earlier drafts listed one, described as "stronger than omitting `V`", and nothing ever read it — a column with `flags: "H"` is hidden because the string contains no `V`, not because it contains `H`. To hide a column, omit `V`. To hide it from specific folders (and make it unqueryable there), use an entity view. To render it as a hidden input, use `fieldTypeCd: "hidden"`.
|
|
18
|
+
|
|
19
|
+
> **The same letters override per entity view.** `views.<v>.columns.<cd>.flags` re-flags a column for the folders bound to that view, while the entity keeps its own flags everywhere else. Same `^[VIEM]*$` vocabulary — see "Per-view overrides" below. See `security.md` → Column-level security.
|
|
18
20
|
|
|
19
21
|
## `M` is the platform's one notion of "required"
|
|
20
22
|
|
|
@@ -31,7 +33,8 @@ Four consequences worth knowing before you type a flag:
|
|
|
31
33
|
entity and every offending field. It's a contradiction only you can resolve: drop
|
|
32
34
|
`M` if the column is optional, or drop `isNullable` if it is required.
|
|
33
35
|
- **`M` is inert on virtual (`R`/`S`/`F`) and identity columns** — nullability is
|
|
34
|
-
meaningless where there is no physical column.
|
|
36
|
+
meaningless where there is no physical column. (At the *entity* level; a per-view
|
|
37
|
+
override behaves differently — see "Per-view overrides".) It won't fail, but `VEM` on a
|
|
35
38
|
Reference over an *optional* FK reads as a required field to the next author, which
|
|
36
39
|
is exactly how these get out of step. Match the Reference to its FK.
|
|
37
40
|
- **Adding `M` to a shipped field aborts the upgrade** on any tenant whose existing
|
|
@@ -41,6 +44,33 @@ Four consequences worth knowing before you type a flag:
|
|
|
41
44
|
|
|
42
45
|
Omitting both `M` and `isNullable` leaves the column nullable — that is the default.
|
|
43
46
|
|
|
47
|
+
## Per-view overrides
|
|
48
|
+
|
|
49
|
+
`views.<v>.columns.<cd>.flags` re-flags one column for the folders bound to that view. The letters are the same four, and `M` means the same thing — the installer folds it into **that override's** `isNullable`.
|
|
50
|
+
|
|
51
|
+
Because a view writes no DDL, the requiredness `M` produces there is runtime-only. That is how you express **"nullable in the database, mandatory in this view"**, which no entity-level flag can say:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
"views": {
|
|
55
|
+
"compliance": {
|
|
56
|
+
"columns": {
|
|
57
|
+
"party_id": {},
|
|
58
|
+
"party_name": { "flags": "V" },
|
|
59
|
+
"vat_no": { "flags": "VEM" },
|
|
60
|
+
"salary": { "flags": "" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Three rules to keep straight:
|
|
67
|
+
|
|
68
|
+
- **The letters replace, they don't merge.** The cascade is `COALESCE(view, entity)`. A non-empty string wins outright, so `{ "flags": "V" }` is read-only there however the entity flagged the column.
|
|
69
|
+
- **`""` is a real value, omission is not.** An empty string means "no flags here" — neither visible nor editable. To inherit, leave the key out; `"flags": null` fails schema validation, since the property is typed `string`. (To inherit *everything*, set the whole column override to `{}` or `null`.)
|
|
70
|
+
- **The `M` fold is one-directional.** Dropping `M` from a view's flags does NOT make the column optional — `isNullable` still `COALESCE`s to the entity value. Write `"isNullable": true` to loosen it, and only where the database actually allows null, or the save trades a clean validation error for a raw constraint violation.
|
|
71
|
+
|
|
72
|
+
Unlike the entity level, `M` here is *not* inert on virtual or identity columns: the fold only reaches the runtime required check, so a view can mark a Reference required. (Identity columns stay excluded — they are never asked of the user.)
|
|
73
|
+
|
|
44
74
|
## Common column flag combinations
|
|
45
75
|
|
|
46
76
|
| Flags | Meaning | Use case |
|
|
@@ -50,7 +80,7 @@ Omitting both `M` and `isNullable` leaves the column nullable — that is the de
|
|
|
50
80
|
| `"V"` | Visible, read-only | Display-only, like formula columns or computed values |
|
|
51
81
|
| `"E"` | Editable, **not visible** | Hidden FK column of an *optional* relation |
|
|
52
82
|
| `"EM"` | Editable, Mandatory, **not visible** | Hidden FK column of a *required* relation |
|
|
53
|
-
| `"I"` |
|
|
83
|
+
| `"I"` | Identity | Trait-provided columns (PK, audit timestamps). Platform-managed. |
|
|
54
84
|
| `""` (empty) | No flags | Rare — column exists but is invisible and not editable |
|
|
55
85
|
|
|
56
86
|
## Rules
|
|
@@ -60,9 +90,9 @@ Omitting both `M` and `isNullable` leaves the column nullable — that is the de
|
|
|
60
90
|
2. **Visible Reference columns**: `"VE"`, or `"VEM"` when the FK is required — **keep the
|
|
61
91
|
two halves in step**. `dforge_entity_reference_add({ required })` sets both for you.
|
|
62
92
|
3. **Formula columns**: usually `"V"` (visible, read-only). The user never edits them.
|
|
63
|
-
4. **Trait columns** (PK, audit): use `"I"` (
|
|
93
|
+
4. **Trait columns** (PK, audit): use `"I"` (identity). These are set automatically by `traits: ["identity", "audit"]`.
|
|
64
94
|
5. **Don't use `I` on your own columns** — it's reserved for trait-provided and platform-managed columns.
|
|
65
|
-
6. **Don't mix `I` with `V` or `E`** —
|
|
95
|
+
6. **Don't mix `I` with `V` or `E`** — identity columns are not user-facing.
|
|
66
96
|
|
|
67
97
|
## Entity rights flags (different namespace — roles only)
|
|
68
98
|
|
|
@@ -79,7 +109,7 @@ These letters appear in role `rights` declarations, **NOT** on column flags. Don
|
|
|
79
109
|
|
|
80
110
|
So `"SIUD"` on a role grants full CRUD; `"SI"` grants read + create only.
|
|
81
111
|
|
|
82
|
-
**Note**: the `I` in column flags (
|
|
112
|
+
**Note**: the `I` in column flags (Identity) and the `I` in role rights (Insert) are different concepts using the same letter. Context makes them unambiguous — column flags go on entity fields, role rights go on `security/roles.json`.
|
|
83
113
|
|
|
84
114
|
## How uniqueness, PKs, and search work
|
|
85
115
|
|
|
@@ -101,4 +131,6 @@ These are **NOT** flag letters. They're separate properties on the column defini
|
|
|
101
131
|
- Putting `M` on a Reference whose hidden FK is optional — inert, but it misreports the field as required.
|
|
102
132
|
- Using `"VEM"` on trait-provided columns (PK, audit) — **don't**. Traits set their own flags (`"I"`).
|
|
103
133
|
- Using `"I"` on business columns — **don't**. `I` means "platform-managed"; regular business columns use `V`/`E`/`M`.
|
|
134
|
+
- Expecting a per-view `{ "flags": "VE" }` to make a column **optional** because it drops `M` — it doesn't. `isNullable` still inherits; write `"isNullable": true`.
|
|
135
|
+
- Writing `"flags": null` in a view override to inherit — fails validation. Omit the key.
|
|
104
136
|
- Using long names like `"Visible,Editable"` — **wrong**. Single letters concatenated: `"VE"`.
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
Formulas are used in:
|
|
4
4
|
|
|
5
5
|
- **Formula columns** (`columnType: "F"`) — computed values
|
|
6
|
+
- **Per-view formula overrides** (`views.<v>.columns.<cd>.formula`) — one entity view
|
|
7
|
+
computing a column differently from the entity. Allowed **only** on a column the
|
|
8
|
+
entity declares as `columnType: "F"`; on any other column the install FAILS, because
|
|
9
|
+
there that field holds the SQL default and a view cannot change a physical column's
|
|
10
|
+
`DEFAULT`. (There is no per-view default value for a normal column.) Same rules as an
|
|
11
|
+
`F` column otherwise: it must parse, call only implemented functions, and not
|
|
12
|
+
aggregate over a 1:N set. See `security.md` → Column-level security.
|
|
6
13
|
- **Action `canExecute:` blocks** — availability checks. Evaluated on both sides
|
|
7
14
|
and **enforced** by `action.execute`, but only over a narrow subset: comparisons
|
|
8
15
|
between fields and literals combined with `AND` / `OR` / `NOT`. Functions,
|