@dforge-core/dforge-mcp 0.1.0-rc.9 → 0.1.1
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 +132 -0
- package/README.md +84 -27
- package/dist/server.js +2957 -616
- package/docs/creating-modules.md +11 -4
- package/package.json +11 -6
- package/resources/docs/conventions.md +22 -16
- package/resources/docs/dsl-reference.md +767 -0
- package/resources/schemas/entity.schema.json +8 -1
- package/resources/schemas/print-templates.schema.json +82 -0
- package/resources/schemas/reports.schema.json +4 -0
- package/resources/schemas/triggers.schema.json +59 -0
- package/skills/dforge-mcp-author/SKILL.md +269 -69
- package/skills/dforge-mcp-author/examples/matrix-budget/README.md +43 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/entities/budget_category.json +24 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/entities/budget_line.json +56 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/manifest.json +30 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/security/roles.json +9 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/seed-data/01-categories.json +8 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/ui/data_views.json +42 -0
- package/skills/dforge-mcp-author/examples/simple-todo/README.md +38 -0
- package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_item.json +83 -0
- package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_list.json +43 -0
- package/skills/dforge-mcp-author/examples/simple-todo/logic/actions/mark_done.dsl +6 -0
- package/skills/dforge-mcp-author/examples/simple-todo/manifest.json +32 -0
- package/skills/dforge-mcp-author/examples/simple-todo/security/roles.json +10 -0
- package/skills/dforge-mcp-author/examples/simple-todo/seed-data/01-lists.json +17 -0
- package/skills/dforge-mcp-author/examples/simple-todo/ui/actions.json +11 -0
- package/skills/dforge-mcp-author/examples/simple-todo/ui/data_views.json +35 -0
- package/skills/dforge-mcp-author/examples/simple-todo/ui/menus.json +28 -0
- package/skills/dforge-mcp-author/references/action-dsl.md +397 -0
- package/skills/dforge-mcp-author/references/column-types.md +168 -0
- package/skills/dforge-mcp-author/references/conventions.md +177 -0
- package/skills/dforge-mcp-author/references/data-migration.md +270 -0
- package/skills/dforge-mcp-author/references/data-views.md +243 -0
- package/skills/dforge-mcp-author/references/excel-import.md +61 -0
- package/skills/dforge-mcp-author/references/field-types.md +144 -0
- package/skills/dforge-mcp-author/references/filters.md +326 -0
- package/skills/dforge-mcp-author/references/flags.md +73 -0
- package/skills/dforge-mcp-author/references/formulas.md +206 -0
- package/skills/dforge-mcp-author/references/jobs.md +149 -0
- package/skills/dforge-mcp-author/references/manifest.md +123 -0
- package/skills/dforge-mcp-author/references/menus.md +164 -0
- package/skills/dforge-mcp-author/references/number-sequences.md +117 -0
- package/skills/dforge-mcp-author/references/print-templates.md +159 -0
- package/skills/dforge-mcp-author/references/queries.md +312 -0
- package/skills/dforge-mcp-author/references/reports.md +398 -0
- package/skills/dforge-mcp-author/references/schema-import.md +331 -0
- package/skills/dforge-mcp-author/references/security.md +244 -0
- package/skills/dforge-mcp-author/references/settings.md +120 -0
- package/skills/dforge-mcp-author/references/traits.md +153 -0
- package/skills/dforge-mcp-author/references/translations.md +158 -0
- package/skills/dforge-mcp-author/references/validation-checklist.md +183 -0
- package/skills/dforge-mcp-author/scripts/xlsx_to_model.py +198 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Schema Import Reference
|
|
2
|
+
|
|
3
|
+
When the user provides an existing database schema (DBML file, SQL DDL, or a description of their current tables), convert it into a dForge module. This is one of the most valuable things you can do — it turns hours of manual work into a few minutes of conversation.
|
|
4
|
+
|
|
5
|
+
## Supported inputs
|
|
6
|
+
|
|
7
|
+
| Input | How to handle |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `.dbml` file | Parse the DBML syntax directly — tables, columns, refs, enums, notes |
|
|
10
|
+
| `.sql` file (DDL) | Parse `CREATE TABLE`, `FOREIGN KEY`, `CHECK`, `UNIQUE`, column types, comments |
|
|
11
|
+
| Pasted table description | Treat as informal spec, ask clarifying questions |
|
|
12
|
+
| ERD image / diagram | Describe what you see and confirm with the user |
|
|
13
|
+
|
|
14
|
+
## Conversion process
|
|
15
|
+
|
|
16
|
+
### Step 1: Identify entities
|
|
17
|
+
|
|
18
|
+
Each table in the source becomes a dForge entity. Apply naming conventions:
|
|
19
|
+
|
|
20
|
+
- Table name → entity `dbObject` (lowercase `snake_case`, singular)
|
|
21
|
+
- `CUSTOMERS` → `customer`, `tbl_OrderLines` → `order_line`
|
|
22
|
+
- Ask the user if naming is ambiguous
|
|
23
|
+
|
|
24
|
+
### Step 2: Map columns
|
|
25
|
+
|
|
26
|
+
For each column, determine `fieldTypeCd`, `baseDatatypeCd`, `dbDatatype`, `flags`, and params.
|
|
27
|
+
|
|
28
|
+
> **The source SQL type is authoritative — always map from it, never from the column name alone.** A column named `floor`, `level`, `count`, `code` could be an integer or a string; only the source type tells you which. Using the wrong `dbDatatype` causes `ENTITY_COLUMN_IMPORT_ERROR` type-mismatch on install. Name heuristics in the table below are secondary signals applied only when no source type is available (e.g. pasted informal descriptions).
|
|
29
|
+
|
|
30
|
+
**Type mapping table:**
|
|
31
|
+
|
|
32
|
+
| Source type | `fieldTypeCd` | `baseDatatypeCd` | `dbDatatype` | Notes |
|
|
33
|
+
|---|---|---|---|---|
|
|
34
|
+
| `varchar(n)`, `char(n)`, `text` | `text` | `string` | `varchar` | Set `maxLen` from `n` |
|
|
35
|
+
| Column named `*email*` | `email` | `string` | `varchar` | Name heuristic |
|
|
36
|
+
| Column named `*phone*`, `*tel*`, `*mobile*` | `phone` | `string` | `varchar` | Name heuristic |
|
|
37
|
+
| Column named `*url*`, `*website*` | `url` | `string` | `varchar` | Name heuristic |
|
|
38
|
+
| `text` (large / multiline context) | `textarea` | `string` | `text` | |
|
|
39
|
+
| `int`, `integer`, `int4`, `smallint` | `number` | `number` | `int` | |
|
|
40
|
+
| `bigint`, `int8` | `number` | `number` | `bigint` | |
|
|
41
|
+
| `numeric(p,s)`, `decimal(p,s)` | `number` | `number` | `numeric` | Set `params.scale` |
|
|
42
|
+
| Column named `*price*`, `*amount*`, `*cost*`, `*total*` | `currency` | `number` | `numeric` | Ask user for currency code |
|
|
43
|
+
| `numeric` with CHECK `>= 0 AND <= 100` | `percent` | `number` | `numeric` | |
|
|
44
|
+
| `boolean`, `bool`, `bit` | `checkbox` | `bool` | `bool` | |
|
|
45
|
+
| `date` | `date` | `date` | `date` | |
|
|
46
|
+
| `timestamp`, `timestamptz`, `datetime` | `datetime` | `timestamp` | `timestamptz` | |
|
|
47
|
+
| `time` | `time` | `time` | `time` | |
|
|
48
|
+
| `json`, `jsonb` | `json` | `json` | `jsonb` | |
|
|
49
|
+
| `uuid` (non-PK, non-FK) | `text` | `string` | `varchar` | |
|
|
50
|
+
| `bytea`, `blob` | `file` | `binary` | — | Best guess, confirm with user |
|
|
51
|
+
| DBML `enum` | `dropdown` | `string` | `varchar` | Extract values to `params.options` |
|
|
52
|
+
| `varchar` with `CHECK IN (…)` | `dropdown` | `string` | `varchar` | Extract to `params.options` |
|
|
53
|
+
|
|
54
|
+
### Step 3: Detect audit columns → traits
|
|
55
|
+
|
|
56
|
+
Look for common audit column patterns and replace them with traits:
|
|
57
|
+
|
|
58
|
+
| Source columns | Replace with |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `created_at` + `updated_at` (timestamps only) | `traits: ["audit"]` — remove those columns from `fields` |
|
|
61
|
+
| `created_at` + `created_by` + `updated_at` + `updated_by` | `traits: ["audit-full"]` — remove all four |
|
|
62
|
+
| `created_date` + `last_updated` | `traits: ["audit"]` — remove both |
|
|
63
|
+
| `deleted_at` or `is_active` / `active` (soft delete) | Add `"soft-delete"` to traits |
|
|
64
|
+
| `sort_order` / `order_num` / `display_order` | Add `"sorting"` to traits |
|
|
65
|
+
|
|
66
|
+
If the source uses non-standard audit column names (e.g. `date_created`, `modified_by_user`), still convert them to traits and note the mapping.
|
|
67
|
+
|
|
68
|
+
### Step 4: Detect PKs → identity trait
|
|
69
|
+
|
|
70
|
+
| Source PK | Action |
|
|
71
|
+
|---|---|
|
|
72
|
+
| Single column `id`, `{table}_id` (integer or uuid) | Use `traits: ["identity"]` — don't declare the PK column manually |
|
|
73
|
+
| Composite PK (two+ columns) | Declare each column with `"isPk": true`, don't use `identity` trait |
|
|
74
|
+
| `uuid` PK with default `gen_random_uuid()` | Use `traits: ["identity"]` (dForge uses cuid, similar concept) |
|
|
75
|
+
|
|
76
|
+
### Step 5: Map relationships → FK+Reference pattern
|
|
77
|
+
|
|
78
|
+
**Explicit foreign keys** (DBML `Ref:` or SQL `FOREIGN KEY`):
|
|
79
|
+
|
|
80
|
+
For each FK, create **two columns** following the FK+Reference pattern:
|
|
81
|
+
|
|
82
|
+
1. Hidden FK column: `flags: "EM"`, matching `dbDatatype`
|
|
83
|
+
2. Visible Reference column: `columnType: "R"`, `fieldTypeCd: "lookup"`, `flags: "VEM"`, with `link`
|
|
84
|
+
|
|
85
|
+
Plus declare the constraint in the entity's `references` block.
|
|
86
|
+
|
|
87
|
+
**Implicit foreign keys** (column named `*_id` with no explicit FK):
|
|
88
|
+
|
|
89
|
+
Look for tables that match the prefix. If `customer_id` exists and a `customer` table exists → probably a FK. Create the reference pair but **ask the user to confirm** ambiguous cases.
|
|
90
|
+
|
|
91
|
+
### Step 6: Detect small lookup tables → dropdown options
|
|
92
|
+
|
|
93
|
+
Tables with 2-3 columns, < 20 rows, and a simple `id + name` shape are likely enums, not real entities. Examples: `statuses`, `categories`, `priorities`, `types`.
|
|
94
|
+
|
|
95
|
+
When detected:
|
|
96
|
+
|
|
97
|
+
- **Option A**: Convert to a `dropdown` column with `params.options` on the referencing entity. Remove the lookup table entirely.
|
|
98
|
+
- **Option B**: Keep as a real entity with a reference column.
|
|
99
|
+
|
|
100
|
+
**Ask the user** which approach they prefer. Default to Option B (keep the entity) for safety.
|
|
101
|
+
|
|
102
|
+
### Step 7: Generate the module
|
|
103
|
+
|
|
104
|
+
Using the converted entities, generate the full module package:
|
|
105
|
+
|
|
106
|
+
1. **`manifest.json`** — with `version: "0.1.0"`, `dbSchemaVersion: "0.0.1"`, entity list, dependencies (`admin`)
|
|
107
|
+
2. **`entities/*.json`** — one per entity, with traits, fields, references
|
|
108
|
+
3. **`ui/data_views.json`** — one grid view per entity with the first ~8 visible columns
|
|
109
|
+
4. **`ui/menus.json`** — grouped logically (group related entities under section nodes)
|
|
110
|
+
5. **`ui/folders.json`** — root folder with all entities listed
|
|
111
|
+
6. **`security/roles.json`** — at least `module.admin` (SIUDC on everything) and `module.user` (SIU on main entities)
|
|
112
|
+
7. **`translations/en-US.json`** — labels for all entities, fields, views, menus, actions, roles, folders
|
|
113
|
+
8. **`seed-data/`** — if the source had enum/lookup tables converted to entities, generate seed data for them
|
|
114
|
+
|
|
115
|
+
### Step 8: Generate IMPORT_NOTES.md
|
|
116
|
+
|
|
117
|
+
List everything you couldn't convert automatically:
|
|
118
|
+
|
|
119
|
+
```markdown
|
|
120
|
+
# Import Notes
|
|
121
|
+
|
|
122
|
+
Imported from: [source file name]
|
|
123
|
+
Date: [date]
|
|
124
|
+
Source: [N] tables, [N] columns, [N] foreign keys
|
|
125
|
+
|
|
126
|
+
## Converted
|
|
127
|
+
- [list of entities created]
|
|
128
|
+
|
|
129
|
+
## Decisions made
|
|
130
|
+
- `statuses` table converted to dropdown options on `order.status` column
|
|
131
|
+
- `created_at`/`updated_at` replaced with `audit` trait
|
|
132
|
+
- [etc.]
|
|
133
|
+
|
|
134
|
+
## Not converted — manual work needed
|
|
135
|
+
- View `v_customer_summary` — consider as a dForge report or formula column
|
|
136
|
+
- Stored procedure `sp_calculate_totals` — convert to a DSL action
|
|
137
|
+
- Trigger `trg_update_stock` — convert to a DSL action
|
|
138
|
+
- Index `idx_customer_email` — dForge generates indexes from column flags
|
|
139
|
+
|
|
140
|
+
## Questions for review
|
|
141
|
+
- Is `credit_limit` a currency field? (mapped as `number` — change to `currency` if monetary)
|
|
142
|
+
- Column `type` on `order` table — should this be a dropdown with fixed options or a free-text field?
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Example: DBML to dForge
|
|
146
|
+
|
|
147
|
+
### Input (DBML)
|
|
148
|
+
|
|
149
|
+
```dbml
|
|
150
|
+
Table customers {
|
|
151
|
+
id integer [pk, increment]
|
|
152
|
+
name varchar(200) [not null]
|
|
153
|
+
email varchar(250)
|
|
154
|
+
phone varchar(50)
|
|
155
|
+
created_at timestamp [default: `now()`]
|
|
156
|
+
updated_at timestamp [default: `now()`]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
Table orders {
|
|
160
|
+
id integer [pk, increment]
|
|
161
|
+
customer_id integer [not null, ref: > customers.id]
|
|
162
|
+
order_date date [not null]
|
|
163
|
+
status varchar(20) [not null, note: 'new, processing, shipped, delivered, cancelled']
|
|
164
|
+
total_amount decimal(10,2)
|
|
165
|
+
notes text
|
|
166
|
+
created_at timestamp [default: `now()`]
|
|
167
|
+
updated_at timestamp [default: `now()`]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Table order_lines {
|
|
171
|
+
id integer [pk, increment]
|
|
172
|
+
order_id integer [not null, ref: > orders.id]
|
|
173
|
+
product_name varchar(200) [not null]
|
|
174
|
+
quantity integer [not null]
|
|
175
|
+
unit_price decimal(10,2) [not null]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Output: `entities/customer.json`
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"description": "Customers",
|
|
184
|
+
"dbObject": "customer",
|
|
185
|
+
"toString": "{name}",
|
|
186
|
+
"traits": ["identity", "audit"],
|
|
187
|
+
"fields": {
|
|
188
|
+
"name": {
|
|
189
|
+
"dbDatatype": "varchar",
|
|
190
|
+
"fieldTypeCd": "text",
|
|
191
|
+
"flags": "VEM",
|
|
192
|
+
"maxLen": 200,
|
|
193
|
+
"orderNum": 10,
|
|
194
|
+
"description": "Customer Name"
|
|
195
|
+
},
|
|
196
|
+
"email": {
|
|
197
|
+
"dbDatatype": "varchar",
|
|
198
|
+
"fieldTypeCd": "email",
|
|
199
|
+
"flags": "VE",
|
|
200
|
+
"maxLen": 250,
|
|
201
|
+
"orderNum": 20,
|
|
202
|
+
"description": "Email"
|
|
203
|
+
},
|
|
204
|
+
"phone": {
|
|
205
|
+
"dbDatatype": "varchar",
|
|
206
|
+
"fieldTypeCd": "phone",
|
|
207
|
+
"flags": "VE",
|
|
208
|
+
"maxLen": 50,
|
|
209
|
+
"orderNum": 30,
|
|
210
|
+
"description": "Phone"
|
|
211
|
+
},
|
|
212
|
+
"orders": {
|
|
213
|
+
"columnType": "S",
|
|
214
|
+
"fieldTypeCd": "grid",
|
|
215
|
+
"flags": "VE",
|
|
216
|
+
"orderNum": 100,
|
|
217
|
+
"description": "Orders",
|
|
218
|
+
"link": {
|
|
219
|
+
"entity": "order",
|
|
220
|
+
"thisKey": "customer_id",
|
|
221
|
+
"otherKey": "customer_id"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Output: `entities/order.json`
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"description": "Orders",
|
|
233
|
+
"dbObject": "order",
|
|
234
|
+
"toString": "Order #{order_id}",
|
|
235
|
+
"traits": ["identity", "audit"],
|
|
236
|
+
"fields": {
|
|
237
|
+
"customer_id": {
|
|
238
|
+
"dbDatatype": "cuid",
|
|
239
|
+
"flags": "EM",
|
|
240
|
+
"orderNum": 10,
|
|
241
|
+
"description": "Customer ID"
|
|
242
|
+
},
|
|
243
|
+
"customer": {
|
|
244
|
+
"columnType": "R",
|
|
245
|
+
"fieldTypeCd": "lookup",
|
|
246
|
+
"flags": "VEM",
|
|
247
|
+
"orderNum": 15,
|
|
248
|
+
"description": "Customer",
|
|
249
|
+
"link": {
|
|
250
|
+
"entity": "customer",
|
|
251
|
+
"thisKey": "customer_id",
|
|
252
|
+
"otherKey": "customer_id"
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
"order_date": {
|
|
256
|
+
"dbDatatype": "date",
|
|
257
|
+
"fieldTypeCd": "date",
|
|
258
|
+
"flags": "VEM",
|
|
259
|
+
"orderNum": 20,
|
|
260
|
+
"description": "Order Date"
|
|
261
|
+
},
|
|
262
|
+
"status": {
|
|
263
|
+
"dbDatatype": "varchar",
|
|
264
|
+
"fieldTypeCd": "dropdown",
|
|
265
|
+
"flags": "VEM",
|
|
266
|
+
"maxLen": 20,
|
|
267
|
+
"orderNum": 30,
|
|
268
|
+
"description": "Status",
|
|
269
|
+
"params": {
|
|
270
|
+
"options": [
|
|
271
|
+
{ "value": "new", "label": "New" },
|
|
272
|
+
{ "value": "processing", "label": "Processing" },
|
|
273
|
+
{ "value": "shipped", "label": "Shipped" },
|
|
274
|
+
{ "value": "delivered", "label": "Delivered" },
|
|
275
|
+
{ "value": "cancelled", "label": "Cancelled" }
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
"total_amount": {
|
|
280
|
+
"dbDatatype": "numeric",
|
|
281
|
+
"fieldTypeCd": "currency",
|
|
282
|
+
"flags": "VE",
|
|
283
|
+
"orderNum": 40,
|
|
284
|
+
"description": "Total Amount"
|
|
285
|
+
},
|
|
286
|
+
"notes": {
|
|
287
|
+
"dbDatatype": "text",
|
|
288
|
+
"fieldTypeCd": "textarea",
|
|
289
|
+
"flags": "VE",
|
|
290
|
+
"orderNum": 50,
|
|
291
|
+
"description": "Notes"
|
|
292
|
+
},
|
|
293
|
+
"lines": {
|
|
294
|
+
"columnType": "S",
|
|
295
|
+
"fieldTypeCd": "grid",
|
|
296
|
+
"flags": "VE",
|
|
297
|
+
"orderNum": 100,
|
|
298
|
+
"description": "Order Lines",
|
|
299
|
+
"link": {
|
|
300
|
+
"entity": "order_line",
|
|
301
|
+
"thisKey": "order_id",
|
|
302
|
+
"otherKey": "order_id"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
"references": {
|
|
307
|
+
"FK_Order_Customer": {
|
|
308
|
+
"from": { "field": "customer_id" },
|
|
309
|
+
"to": { "entity": "customer", "field": "customer_id" }
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Note how:
|
|
316
|
+
- Source `id` columns → replaced by `identity` trait
|
|
317
|
+
- Source `created_at`/`updated_at` → replaced by `audit` trait
|
|
318
|
+
- Source `customer_id` FK → FK+Reference pair (hidden `customer_id` + visible `customer` lookup)
|
|
319
|
+
- Source `status` with note listing values → `dropdown` with `params.options`
|
|
320
|
+
- Source `total_amount decimal(10,2)` → `currency` (inferred from name)
|
|
321
|
+
- Parent `customers` gets a `orders` set column (backwards reference)
|
|
322
|
+
- PK type changes from `integer` to `cuid` (dForge's native PK type via `identity` trait)
|
|
323
|
+
|
|
324
|
+
## Tips
|
|
325
|
+
|
|
326
|
+
- **Always ask about ambiguous columns** rather than guessing. "Is `credit_limit` currency? Is `type` a dropdown or free text?"
|
|
327
|
+
- **Generate `IMPORT_NOTES.md`** with every schema import — the user needs to know what wasn't converted.
|
|
328
|
+
- **Start with the most referenced entities** (parents) and work down to detail/line entities.
|
|
329
|
+
- **Source PKs change type.** dForge uses `cuid` for PKs (via `identity` trait). The old integer PKs don't carry over. If the user later needs to migrate data, the migration script handles PK mapping (see `references/data-migration.md`).
|
|
330
|
+
- **Preserve source column names** as `dbObject` / column codes where possible — this makes data migration easier later.
|
|
331
|
+
- **Views, stored procedures, and triggers** cannot be converted automatically. List them in `IMPORT_NOTES.md` and offer to convert them one by one to dForge constructs (reports, actions, formulas).
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Security Reference
|
|
2
|
+
|
|
3
|
+
dForge has a **3-layer additive security model**:
|
|
4
|
+
|
|
5
|
+
1. **Row-level** — folders define SQL WHERE filters
|
|
6
|
+
2. **Column-level** — entity views control field visibility/editability
|
|
7
|
+
3. **Operation-level** — roles grant entity rights (S/I/U/D/C) and object access (E)
|
|
8
|
+
|
|
9
|
+
Roles are **additive** — a user with multiple roles gets the union of all their rights. Rights are never revoked by another role, only added.
|
|
10
|
+
|
|
11
|
+
Roles are **folder-scoped** — the same role can have different effective rights in different folders, because folders define the row filter.
|
|
12
|
+
|
|
13
|
+
Lives in: `security/roles.json`
|
|
14
|
+
|
|
15
|
+
## Role definition
|
|
16
|
+
|
|
17
|
+
Role codes use a `module.role` naming pattern (e.g. `crm.admin`, `crm.sales-rep`):
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"crm.admin": {
|
|
22
|
+
"description": "CRM Administrator",
|
|
23
|
+
"rights": {
|
|
24
|
+
"account": "SIUDC",
|
|
25
|
+
"contact": "SIUDC",
|
|
26
|
+
"lead": "SIUDC",
|
|
27
|
+
"opportunity": "SIUDC",
|
|
28
|
+
"opportunity_line": "SIUDC",
|
|
29
|
+
"product": "SIUDC",
|
|
30
|
+
"quote": "SIUDC",
|
|
31
|
+
"activity": "SIUDC"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"crm.sales-rep": {
|
|
35
|
+
"description": "Sales Representative",
|
|
36
|
+
"rights": {
|
|
37
|
+
"account": "SIUC",
|
|
38
|
+
"contact": "SIUC",
|
|
39
|
+
"lead": "SIUDC",
|
|
40
|
+
"opportunity": "SIUC",
|
|
41
|
+
"opportunity_line": "SIUC",
|
|
42
|
+
"product": "S",
|
|
43
|
+
"quote": "SIUC",
|
|
44
|
+
"activity": "SIUDC"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"crm.viewer": {
|
|
48
|
+
"description": "CRM Viewer (Read-Only)",
|
|
49
|
+
"rights": {
|
|
50
|
+
"account": "S",
|
|
51
|
+
"contact": "S",
|
|
52
|
+
"lead": "S",
|
|
53
|
+
"opportunity": "S",
|
|
54
|
+
"product": "S",
|
|
55
|
+
"quote": "S",
|
|
56
|
+
"activity": "S"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Rights letters
|
|
63
|
+
|
|
64
|
+
### Entity rights (on entity codes)
|
|
65
|
+
|
|
66
|
+
| Letter | Permission |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `S` | Select (read rows) |
|
|
69
|
+
| `I` | Insert (create rows) |
|
|
70
|
+
| `U` | Update (modify rows) |
|
|
71
|
+
| `D` | Delete (remove rows) |
|
|
72
|
+
| `C` | Clone (duplicate a row as a new record) |
|
|
73
|
+
|
|
74
|
+
### Action/Report/Folder rights
|
|
75
|
+
|
|
76
|
+
| Letter | Permission |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `E` | Execute (run the action/report, access the folder) |
|
|
79
|
+
|
|
80
|
+
### Common combinations
|
|
81
|
+
|
|
82
|
+
| Rights | Meaning |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `"S"` | Read only |
|
|
85
|
+
| `"SI"` | Read + create, no modify/delete |
|
|
86
|
+
| `"SIU"` | Read/write, no delete |
|
|
87
|
+
| `"SIUD"` | Full CRUD (most common for primary roles) |
|
|
88
|
+
| `"SIUDC"` | Full CRUD + clone |
|
|
89
|
+
| `"E"` | Execute (actions/reports/folders) |
|
|
90
|
+
|
|
91
|
+
## Object references
|
|
92
|
+
|
|
93
|
+
Role rights map object codes (entities, actions, reports) to rights strings. Object codes are:
|
|
94
|
+
|
|
95
|
+
- `contact` — the entity named "contact" in this module (bare, same-module)
|
|
96
|
+
- `fin.invoice` — the entity "invoice" in the `fin` module (cross-module entity — **dot**)
|
|
97
|
+
- `action:send_welcome` — an action named "send_welcome" (**colon**)
|
|
98
|
+
- `report:sales_pipeline` — a report named "sales_pipeline" (**colon**)
|
|
99
|
+
- `folder:customers` — the folder with code "customers" (**colon**)
|
|
100
|
+
|
|
101
|
+
> **Separator matters — colon for non-entity objects, dot only for cross-module entities.**
|
|
102
|
+
> Actions, reports and folders are prefixed with a **colon**: `action:<code>`, `report:<code>`,
|
|
103
|
+
> `folder:<code>`. A **dot** is reserved for a cross-module *entity* (`module.entity`, e.g.
|
|
104
|
+
> `parties.party`). Writing `action.send_welcome` (dot) is **wrong** — the installer treats it as
|
|
105
|
+
> entity `send_welcome` in a module named `action`, finds nothing, and rejects the grant as an
|
|
106
|
+
> unknown object. Same-module entities are bare (`contact`). This matches every dForge-core
|
|
107
|
+
> module (`"action:submit_po": "E"`).
|
|
108
|
+
|
|
109
|
+
## Rules
|
|
110
|
+
|
|
111
|
+
1. **Use `"rights"`**, never `"entityRights"` (the wrong name is the most common mistake).
|
|
112
|
+
2. **Every module should define at least one role.** Without roles, tenant admins can't grant access to anyone.
|
|
113
|
+
3. **Create multiple roles for different user personas** — e.g. `sales_rep`, `sales_admin`, `sales_manager`.
|
|
114
|
+
4. **Never grant `D` (Delete) to a rep-level role** unless deletion is a normal part of their job. Audit trails prefer soft-delete or update.
|
|
115
|
+
5. **Always grant the admin role `SIUDC` on everything the module owns.**
|
|
116
|
+
6. **Action and report access** — grant `E` on specific action/report codes, or omit them entirely (defaulting to no access).
|
|
117
|
+
7. **Folder access** is granted separately — roles can reference folder codes, but folder definitions themselves live in `ui/folders.json`.
|
|
118
|
+
|
|
119
|
+
## Folders (`ui/folders.json`)
|
|
120
|
+
|
|
121
|
+
Folders define the **folder tree** created when the module is installed. They declare which entities are available, default views, quick-add, and optionally **row-level filtering** and **subfolders**.
|
|
122
|
+
|
|
123
|
+
Lives in: `ui/folders.json`
|
|
124
|
+
|
|
125
|
+
### Simple example (CRM — flat, no subfolders)
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"label": "Sales CRM",
|
|
130
|
+
"description": "Customer relationship management and sales pipeline",
|
|
131
|
+
"color": "#2196F3",
|
|
132
|
+
"entities": {
|
|
133
|
+
"account": { "viewName": "default", "quickAdd": true },
|
|
134
|
+
"contact": { "viewName": "default", "quickAdd": true },
|
|
135
|
+
"opportunity": { "viewName": "default", "quickAdd": true },
|
|
136
|
+
"opportunity_line": { "viewName": "default", "quickAdd": false },
|
|
137
|
+
"product": { "viewName": "default", "quickAdd": true }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Advanced example (WMS — subfolders with row filters)
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"label": "Warehouse Management",
|
|
147
|
+
"description": "Inventory, purchasing, and warehouse operations",
|
|
148
|
+
"color": "#FF9800",
|
|
149
|
+
"entities": {
|
|
150
|
+
"warehouse": { "viewName": "default", "quickAdd": true },
|
|
151
|
+
"product": { "viewName": "default", "quickAdd": true },
|
|
152
|
+
"stock": { "viewName": "default", "quickAdd": true },
|
|
153
|
+
"stock_movement": { "viewName": "default", "quickAdd": true },
|
|
154
|
+
"purchase_order": { "viewName": "default", "quickAdd": true },
|
|
155
|
+
"purchase_order_line": { "viewName": "default", "quickAdd": false }
|
|
156
|
+
},
|
|
157
|
+
"children": {
|
|
158
|
+
"central": {
|
|
159
|
+
"label": "Central Warehouse",
|
|
160
|
+
"description": "Main distribution center",
|
|
161
|
+
"color": "#2196F3",
|
|
162
|
+
"inheritSecurity": true,
|
|
163
|
+
"entities": {
|
|
164
|
+
"stock": {
|
|
165
|
+
"viewName": "default",
|
|
166
|
+
"quickAdd": true,
|
|
167
|
+
"rowFilter": { "c": "warehouse_id", "o": "eq", "v": 2001 }
|
|
168
|
+
},
|
|
169
|
+
"stock_movement": {
|
|
170
|
+
"viewName": "default",
|
|
171
|
+
"quickAdd": true,
|
|
172
|
+
"rowFilter": { "c": "warehouse_id", "o": "eq", "v": 2001 }
|
|
173
|
+
},
|
|
174
|
+
"purchase_order": {
|
|
175
|
+
"viewName": "default",
|
|
176
|
+
"quickAdd": true,
|
|
177
|
+
"rowFilter": { "c": "warehouse_id", "o": "eq", "v": 2001 }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"east": {
|
|
182
|
+
"label": "East Warehouse",
|
|
183
|
+
"color": "#4CAF50",
|
|
184
|
+
"inheritSecurity": true,
|
|
185
|
+
"entities": {
|
|
186
|
+
"stock": {
|
|
187
|
+
"viewName": "default",
|
|
188
|
+
"quickAdd": true,
|
|
189
|
+
"rowFilter": { "c": "warehouse_id", "o": "eq", "v": 2002 }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Folder properties
|
|
198
|
+
|
|
199
|
+
| Property | Type | Description |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| `label` | string | Display name in sidebar |
|
|
202
|
+
| `description` | string | Shown in folder header |
|
|
203
|
+
| `color` | string | Hex accent color |
|
|
204
|
+
| `entities` | object | Entity membership (see below) |
|
|
205
|
+
| `children` | object | Subfolder definitions (same shape recursively) |
|
|
206
|
+
| `inheritSecurity` | boolean | Child inherits parent folder's role assignments |
|
|
207
|
+
|
|
208
|
+
### Entity membership properties
|
|
209
|
+
|
|
210
|
+
| Property | Type | Description |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `viewName` | string | Default data view (usually `"default"`) |
|
|
213
|
+
| `quickAdd` | boolean | Show "+" quick-add button (`true` for main entities, `false` for detail/line items) |
|
|
214
|
+
| `rowFilter` | object | Row-level filter scoping which records are visible in this folder |
|
|
215
|
+
|
|
216
|
+
### Row filters (`rowFilter`)
|
|
217
|
+
|
|
218
|
+
Row filters use a compact filter object with:
|
|
219
|
+
- `c` — column code to filter on
|
|
220
|
+
- `o` — operator (`eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `not_in`, etc.)
|
|
221
|
+
- `v` — value to compare against
|
|
222
|
+
|
|
223
|
+
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`.
|
|
224
|
+
|
|
225
|
+
### When to use subfolders
|
|
226
|
+
|
|
227
|
+
- **Multi-location modules** (warehouses, offices, branches) — one subfolder per location, filtered by location FK
|
|
228
|
+
- **Multi-department modules** — one subfolder per department
|
|
229
|
+
- **Regional access** — filter by region/country column
|
|
230
|
+
|
|
231
|
+
For simple modules, a flat root folder (no `children`) is fine — just `label`, `color`, and `entities`.
|
|
232
|
+
|
|
233
|
+
## Common mistakes
|
|
234
|
+
|
|
235
|
+
- Using `"entityRights"` instead of `"rights"` — **wrong**.
|
|
236
|
+
- Using a **dot** for an action/report/folder (`action.send_welcome`) — **wrong**. Use a **colon**: `action:send_welcome`, `report:...`, `folder:...`. A dot is only for cross-module *entities* (`fin.invoice`).
|
|
237
|
+
- Granting a bare action code (`send_welcome` with no prefix) — **wrong**, it's read as an entity and rejected as unknown. Prefix it: `action:send_welcome`.
|
|
238
|
+
- Mapping an object to an empty rights string (`"action:x": ""`) — **wrong**. To deny, **omit** the key entirely; to grant, use `"E"`.
|
|
239
|
+
- Using long names like `"read,write"` instead of `"SIU"` — **wrong**.
|
|
240
|
+
- Forgetting to define any role — module installs but no user can access anything.
|
|
241
|
+
- Granting `"D"` casually — delete should be rare.
|
|
242
|
+
- Trying to **revoke** rights with a second role — impossible. Rights are additive only.
|
|
243
|
+
- Inventing rights letters like `"R"` for read or `"W"` for write — **wrong**. Use `SIUD`.
|
|
244
|
+
- Putting folders in `security/` — **wrong**. It's `ui/folders.json`.
|