@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.
Files changed (25) hide show
  1. package/CHANGELOG.md +239 -1
  2. package/dist/server.js +155 -7
  3. package/package.json +2 -2
  4. package/resources/docs/conventions.md +74 -1
  5. package/resources/docs/dsl-reference.md +58 -1
  6. package/resources/schemas/entity.schema.json +80 -2
  7. package/resources/schemas/folders.schema.json +1 -1
  8. package/skills/dforge-mcp-author/examples/column-security/README.md +67 -0
  9. package/skills/dforge-mcp-author/examples/column-security/entities/product.json +79 -0
  10. package/skills/dforge-mcp-author/examples/column-security/manifest.json +23 -0
  11. package/skills/dforge-mcp-author/examples/column-security/security/roles.json +16 -0
  12. package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +35 -0
  13. package/skills/dforge-mcp-author/examples/column-security/ui/data_views.json +20 -0
  14. package/skills/dforge-mcp-author/examples/column-security/ui/folders.json +23 -0
  15. package/skills/dforge-mcp-author/examples/column-security/ui/menus.json +21 -0
  16. package/skills/dforge-mcp-author/examples/simple-todo/manifest.json +8 -12
  17. package/skills/dforge-mcp-author/references/action-dsl.md +113 -29
  18. package/skills/dforge-mcp-author/references/column-types.md +3 -2
  19. package/skills/dforge-mcp-author/references/data-views.md +2 -0
  20. package/skills/dforge-mcp-author/references/flags.md +41 -9
  21. package/skills/dforge-mcp-author/references/formulas.md +13 -1
  22. package/skills/dforge-mcp-author/references/jobs.md +4 -1
  23. package/skills/dforge-mcp-author/references/security.md +77 -1
  24. package/skills/dforge-mcp-author/references/validation-checklist.md +12 -0
  25. 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
- # Determines whether the action button is enabled for current record(s)
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": "^[VIEMH]*$",
133
- "description": "Display flags: V=Visible, I=Identity, E=Editable, M=Mandatory, H=Hidden. M is resolved into isNullable:false at install, so it makes the column NOT NULL; it is inert on virtual (R/S/F) and identity columns. Declaring M together with isNullable:true is rejected at pack time."
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) to use in this folder. Common values include 'default'."
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": { "name": "dForge Examples" },
9
+ "author": {
10
+ "name": "dForge Examples"
11
+ },
10
12
  "license": "MIT",
11
13
  "category": "Examples",
12
- "tags": ["example", "todo"],
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
  }