@dforge-core/dforge-mcp 0.2.14 → 0.2.16
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 +239 -1
- package/dist/server.js +155 -7
- package/package.json +2 -2
- package/resources/docs/conventions.md +74 -1
- package/resources/docs/dsl-reference.md +58 -1
- package/resources/schemas/entity.schema.json +80 -2
- package/resources/schemas/folders.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/action-dsl.md +113 -29
- 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 +13 -1
- package/skills/dforge-mcp-author/references/jobs.md +4 -1
- package/skills/dforge-mcp-author/references/security.md +77 -1
- package/skills/dforge-mcp-author/references/validation-checklist.md +12 -0
- package/skills/dforge-module-build/SKILL.md +13 -2
|
@@ -14,7 +14,8 @@ params:
|
|
|
14
14
|
|
|
15
15
|
canExecute:
|
|
16
16
|
# Single-expression formula returning boolean
|
|
17
|
-
#
|
|
17
|
+
# Enables/disables the button AND is re-checked server-side on execute
|
|
18
|
+
# (see "canExecute is enforced, with a fail-open subset")
|
|
18
19
|
|
|
19
20
|
onBeforeStart:
|
|
20
21
|
# Pre-queue initialization (async / background actions only)
|
|
@@ -396,6 +397,62 @@ For "who did this", prefer the platform audit columns (`created_by` /
|
|
|
396
397
|
predicates in `canExecute:` or a formula column, use `CURRENT_USER_ID()`, which
|
|
397
398
|
the server translates to an exact `::bigint` SQL literal.
|
|
398
399
|
|
|
400
|
+
> **The table above is about `execute:` (JavaScript).** The *formula* engine —
|
|
401
|
+
> `canExecute:`, formula columns, filters, reports — keeps large integer literals
|
|
402
|
+
> exact: the parser holds them as 64-bit integers and serializes anything past
|
|
403
|
+
> 2^53 as text so the browser cannot round it. `canExecute: [owner_id] = 215117312238288897`
|
|
404
|
+
> compares against the id you wrote. `CURRENT_USER_ID()` is still the better way to
|
|
405
|
+
> say "mine" — it does not hard-code an id — but a literal is no longer a
|
|
406
|
+
> correctness trap on that side.
|
|
407
|
+
|
|
408
|
+
## canExecute is enforced, with a fail-open subset
|
|
409
|
+
|
|
410
|
+
`canExecute:` is evaluated **twice**: by the browser to decide whether the button
|
|
411
|
+
is **enabled** (the action stays in the toolbar, greyed out — it is not hidden),
|
|
412
|
+
and by `action.execute` over every targeted record before any write. A
|
|
413
|
+
record in the wrong state is refused with `ACTION_EXECUTION_FAIL`, so the predicate
|
|
414
|
+
is a gate rather than a UI hint, and `canExecute: false` is the way to make an
|
|
415
|
+
action fireable only by a trigger or a scheduled job (both invoke the script engine
|
|
416
|
+
directly and bypass the check).
|
|
417
|
+
|
|
418
|
+
**Both evaluators have gaps, and both fail open**, but not the same gaps. The
|
|
419
|
+
server is a scalar walker and lets anything richer through. The client implements
|
|
420
|
+
the whole documented formula function set, so ordinary functions evaluate there
|
|
421
|
+
normally; it throws — and its `catch` **enables** the button — on ref navigation,
|
|
422
|
+
on the set aggregates `SUM` / `COUNT` / `AVG` (deliberately not formula functions;
|
|
423
|
+
counting belongs in a Generated `G` column), and on any name it does not know, a
|
|
424
|
+
typo included — a `canExecute:` is never validated against the function set at
|
|
425
|
+
install, so `TOADY()` ships and enables the button for every record.
|
|
426
|
+
`$[setting]` references are a separate case again: they do not throw, the client
|
|
427
|
+
resolver is stubbed to `null`, and the server has no settings resolver — so a
|
|
428
|
+
settings-dependent guard is unenforced server-side and resolves unpredictably in
|
|
429
|
+
the browser. Read settings in `execute:` instead.
|
|
430
|
+
|
|
431
|
+
| tier | construct | client | server |
|
|
432
|
+
|---|---|---|---|
|
|
433
|
+
| 1 | **comparisons** (`=` `==` `!=` `<>` `>` `>=` `<` `<=`) between `[field]`s and literals, combined with `AND` / `OR` / `NOT`; a bare `true` / `false` | evaluates | **enforces** |
|
|
434
|
+
| 2 | arithmetic, `IN`, `BETWEEN`, any formula function (`TODAY`, `IF`, `DATEADD`, `REGEX_MATCH`, `CURRENT_USER_ID`, …) | evaluates | fail-open |
|
|
435
|
+
| 3 | ref navigation, `SUM` / `COUNT` / `AVG`, any unknown function (a typo included) | **fail-open (throws)** | fail-open |
|
|
436
|
+
| — | `$[setting]` references | **evaluates as null** (no throw) | fail-open |
|
|
437
|
+
|
|
438
|
+
**A bare boolean column is not tier 1.** The walker evaluates conditions, not
|
|
439
|
+
values — a `[field]` resolves only as an operand of a comparison — so
|
|
440
|
+
`canExecute: [is_active]` and `canExecute: NOT [is_blocked]` fail open. Write
|
|
441
|
+
`[is_active] = true` and `NOT ([is_blocked] = true)`. A bare `false` is fine: it is
|
|
442
|
+
a complete condition, which is what makes the `canExecute: false` idiom work.
|
|
443
|
+
|
|
444
|
+
Tier 1 + tier 2 is the normal shape and behaves correctly: the client evaluates the
|
|
445
|
+
whole predicate so the button state is right, and the server short-circuits `AND` /
|
|
446
|
+
`OR` so the tier-1 half is still enforced.
|
|
447
|
+
|
|
448
|
+
**Tier 3 is what breaks.** The server short-circuits, the client does not, so
|
|
449
|
+
`[status] = 'Pending' AND COUNT([lines]) > 0` on a non-`Pending` record still
|
|
450
|
+
evaluates `COUNT` in the browser, throws, and the button is **enabled** — then the
|
|
451
|
+
server refuses the call. A `canExecute:` is not validated against the function set
|
|
452
|
+
at install, so that spelling ships silently. Count with a `G` column and compare the
|
|
453
|
+
scalar; denormalise instead of navigating; put the rest in `execute:` behind
|
|
454
|
+
`error()`.
|
|
455
|
+
|
|
399
456
|
### Messaging & notifications
|
|
400
457
|
|
|
401
458
|
#### `info(message, opts?)`, `warn(message, opts?)`
|
|
@@ -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",
|
|
@@ -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
|
}
|