@dforge-core/dforge-mcp 0.1.0-rc.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/README.md +119 -0
- package/dist/server.js +21626 -0
- package/package.json +39 -0
- package/resources/docs/conventions.md +939 -0
- package/resources/schemas/data-view.schema.json +299 -0
- package/resources/schemas/entity.schema.json +444 -0
- package/resources/schemas/manifest.schema.json +100 -0
- package/skills/dforge-mcp-author/SKILL.md +107 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
# Module Development Conventions
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document defines the conventions and standards for developing modules in the dForge platform.
|
|
6
|
+
Use the CRM sample module (`modules/crm/`) as a reference implementation.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## File Formatting
|
|
11
|
+
|
|
12
|
+
### Indentation
|
|
13
|
+
|
|
14
|
+
**All project files use TABS for indentation:**
|
|
15
|
+
|
|
16
|
+
- **C# files** (`.cs`): Tabs, size 4
|
|
17
|
+
- **JSON files** (`.json`): Tabs, size 2
|
|
18
|
+
- **TypeScript/JavaScript** (`.ts`, `.js`): Tabs, size 4
|
|
19
|
+
- **Svelte files** (`.svelte`): Tabs, size 4
|
|
20
|
+
- **SQL files** (`.sql`): Tabs, size 4
|
|
21
|
+
- **Markdown files** (`.md`): Tabs
|
|
22
|
+
|
|
23
|
+
**Exceptions** (spaces only):
|
|
24
|
+
- `package.json`
|
|
25
|
+
- `tsconfig.json`
|
|
26
|
+
- `*.config.js` files
|
|
27
|
+
|
|
28
|
+
### Line Endings
|
|
29
|
+
|
|
30
|
+
- Use LF (Unix-style) line endings for all files
|
|
31
|
+
- Ensure final newline at end of file
|
|
32
|
+
- Trim trailing whitespace
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Module Structure
|
|
37
|
+
|
|
38
|
+
### Standard Module Layout
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
/your-module/
|
|
42
|
+
├── manifest.json # Module metadata + entity registry
|
|
43
|
+
├── settings.json # Module settings (optional)
|
|
44
|
+
├── /entities/ # Entity definitions (one per file)
|
|
45
|
+
│ ├── entity1.json
|
|
46
|
+
│ ├── entity2.json
|
|
47
|
+
│ └── entity3.json
|
|
48
|
+
├── /ui/ # UI definitions
|
|
49
|
+
│ ├── data_views.json # Data view definitions
|
|
50
|
+
│ ├── menus.json # Menu definitions
|
|
51
|
+
│ ├── folders.json # Folder + entity access definitions
|
|
52
|
+
│ ├── actions.json # Action metadata (optional)
|
|
53
|
+
│ ├── reports.json # Report definitions (optional)
|
|
54
|
+
│ └── print_templates.json # Print template definitions (optional)
|
|
55
|
+
├── /logic/ # Business logic
|
|
56
|
+
│ ├── /actions/ # DSL action scripts
|
|
57
|
+
│ │ ├── approve.dsl
|
|
58
|
+
│ │ └── send_invoice.dsl
|
|
59
|
+
│ ├── triggers.json # Event triggers (optional) — "when X happens, run action Y"
|
|
60
|
+
│ ├── webhooks.json # Webhook subscriptions (optional) — external HTTP notifications
|
|
61
|
+
│ └── jobs.json # Scheduled jobs (optional) — cron-driven action fires
|
|
62
|
+
├── /seed-data/ # Seed data files (numbered for FK order)
|
|
63
|
+
│ ├── 01-skills.json
|
|
64
|
+
│ ├── 02-departments.json
|
|
65
|
+
│ └── 03-employees.json
|
|
66
|
+
├── /security/ # Security definitions
|
|
67
|
+
│ └── roles.json
|
|
68
|
+
├── /translations/ # Translations (optional)
|
|
69
|
+
│ ├── en-US.json
|
|
70
|
+
│ └── de-DE.json
|
|
71
|
+
├── /print_templates/ # HTML/CSS print templates (optional)
|
|
72
|
+
│ ├── invoice.html
|
|
73
|
+
│ └── invoice.css
|
|
74
|
+
└── /files/ # Module static files (optional) — README, docs
|
|
75
|
+
└── README.md
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## JSON File Conventions
|
|
81
|
+
|
|
82
|
+
### 1. Manifest (`manifest.json`)
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"packageFormat": 1,
|
|
87
|
+
"moduleId": "10000000-0000-0000-0000-000000000002",
|
|
88
|
+
"code": "hr",
|
|
89
|
+
"version": "0.0.1",
|
|
90
|
+
"dbSchemaVersion": "0.0.1",
|
|
91
|
+
"displayName": "Human Resources",
|
|
92
|
+
"description": "HR management module",
|
|
93
|
+
"author": { "name": "dForge" },
|
|
94
|
+
"license": "MIT",
|
|
95
|
+
"entities": {
|
|
96
|
+
"department": "./entities/department.json",
|
|
97
|
+
"employee": "./entities/employee.json"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Rules:**
|
|
103
|
+
- `code` becomes the DB schema name and folder path
|
|
104
|
+
- `entities` maps entity codes to file paths (relative to module root)
|
|
105
|
+
- Omit `"system": true` for regular modules (defaults to false)
|
|
106
|
+
- Omit `schemaName` to use `code` as the schema name
|
|
107
|
+
|
|
108
|
+
### 1b. Module Extensions
|
|
109
|
+
|
|
110
|
+
Modules can add columns to entities owned by other modules using **extension entity files** — entity JSON files with an `extends` property. Extensions support both virtual columns (Reference, Set, Formula — metadata only) and physical columns (actual DB columns stored in a 1:1 extension table). This eliminates circular dependencies: the extending module declares the cross-module columns instead of the target entity.
|
|
111
|
+
|
|
112
|
+
Extension entity files are referenced from the manifest's `entities` map using dotted keys as a naming convention:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"dependencies": {
|
|
117
|
+
"crm": ">=0.0.1",
|
|
118
|
+
"fin": ">=0.0.1"
|
|
119
|
+
},
|
|
120
|
+
"entities": {
|
|
121
|
+
"fin.invoice": "entities/fin.invoice.json",
|
|
122
|
+
"fin.invoice_line": "entities/fin.invoice_line.json",
|
|
123
|
+
"crm.quote": "entities/crm.quote.json"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The entity file uses the same `fields` schema as regular entities, plus an `extends` property:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"extends": "fin.invoice",
|
|
133
|
+
"dbObject": "invoice",
|
|
134
|
+
"description": "CRM extensions for Invoice",
|
|
135
|
+
"fields": {
|
|
136
|
+
"customer_id": {
|
|
137
|
+
"dbDatatype": "int8",
|
|
138
|
+
"isNullable": true,
|
|
139
|
+
"flags": "EM",
|
|
140
|
+
"orderNum": 34,
|
|
141
|
+
"description": "Customer FK"
|
|
142
|
+
},
|
|
143
|
+
"customer": {
|
|
144
|
+
"columnType": "R",
|
|
145
|
+
"fieldTypeCd": "lookup",
|
|
146
|
+
"flags": "VEM",
|
|
147
|
+
"orderNum": 35,
|
|
148
|
+
"description": "Customer",
|
|
149
|
+
"link": {
|
|
150
|
+
"entity": "crm.account",
|
|
151
|
+
"thisKey": "customer_id",
|
|
152
|
+
"otherKey": "account_id"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
In this example, `customer_id` is a physical column (has `dbDatatype`) stored in `"crm_fin"."invoice_ext"`, while `customer` is a virtual Reference column (metadata only). The FK+Reference two-column pattern applies to extensions the same way as regular entities.
|
|
160
|
+
|
|
161
|
+
**Physical extension columns** are stored in a dedicated 1:1 extension table created in the bridge module's schema (e.g., `"crm_fin"."invoice_ext"`), not in the target entity's table. The platform handles this transparently — queries automatically LEFT JOIN the ext table, and writes split between the main table and ext table.
|
|
162
|
+
|
|
163
|
+
**Rules:**
|
|
164
|
+
- Entity files with `extends` are treated as extensions, not new entities
|
|
165
|
+
- The `extends` value must be a qualified entity code: `"module.entity"` (e.g., `"fin.invoice"`)
|
|
166
|
+
- The target module must be declared as a dependency
|
|
167
|
+
- Extension columns **must** use the `link` object (no fallback to `references` section)
|
|
168
|
+
- `dbObject` is optional — used for documentation and validation
|
|
169
|
+
- Physical columns (those with `dbDatatype`) are created in a 1:1 extension table: `{bridge_schema}.{entity_cd}_ext`
|
|
170
|
+
- The ext table PK is an FK to the target entity's PK — enforcing 1:1 relationship
|
|
171
|
+
- Virtual columns (`columnType: "R"`, `"S"`, or `"F"`) are metadata only — no ext table needed
|
|
172
|
+
- The `storage_table` metadata column tracks which ext table stores each physical extension column (set automatically by the installer)
|
|
173
|
+
- Extensions are processed after the module's own entities are registered
|
|
174
|
+
- Install order matters: the target module must be installed before the extending module
|
|
175
|
+
|
|
176
|
+
**File naming convention:** Use dotted names matching the target entity (`entities/fin.invoice.json`). This makes it immediately obvious in the file system which files are extensions:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
entities/
|
|
180
|
+
├── fin.invoice.json # extends fin.invoice
|
|
181
|
+
├── fin.invoice_line.json # extends fin.invoice_line
|
|
182
|
+
└── crm.quote.json # extends crm.quote
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**How ext tables work at runtime:**
|
|
186
|
+
|
|
187
|
+
```sql
|
|
188
|
+
-- Query builder automatically adds LEFT JOIN for ext table columns:
|
|
189
|
+
SELECT t0."invoice_id", t0."invoice_no", ext0."customer_id"
|
|
190
|
+
FROM "fin"."invoice" t0
|
|
191
|
+
LEFT OUTER JOIN "crm_fin"."invoice_ext" ext0
|
|
192
|
+
ON ext0."invoice_id" = t0."invoice_id"
|
|
193
|
+
|
|
194
|
+
-- Writes split automatically:
|
|
195
|
+
-- 1. Main table INSERT/UPDATE (columns without storage_table)
|
|
196
|
+
-- 2. Ext table UPSERT (INSERT ON CONFLICT DO UPDATE) for ext columns
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Uninstall** is clean: `DROP SCHEMA CASCADE` removes the ext tables, and extension column metadata rows are cleaned by matching `storage_table LIKE '{schema}%'`.
|
|
200
|
+
|
|
201
|
+
**Bridge module pattern (recommended):**
|
|
202
|
+
Rather than having core modules extend each other directly, create small bridge modules that own the integration. For example, instead of CRM extending FIN entities, create a `crm-fin` module that depends on both CRM and FIN and declares all the cross-module extensions, actions, and views.
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
CRM (standalone) ──┐
|
|
206
|
+
├── crm-fin (bridge: extensions + actions + views)
|
|
207
|
+
FIN (standalone) ──┘
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Benefits:
|
|
211
|
+
- Core modules remain fully independent (CRM works without FIN and vice versa)
|
|
212
|
+
- Integration is opt-in: only install the bridge when both modules are present
|
|
213
|
+
- Third-party developers can create bridges between any two modules
|
|
214
|
+
- Clean uninstall — DROP SCHEMA removes ext tables, no ALTER TABLE on other modules
|
|
215
|
+
|
|
216
|
+
Bridge modules typically have no entities of their own — just extensions, `actions`, and optionally `views`.
|
|
217
|
+
|
|
218
|
+
Multiple bridge modules can extend the same entity (e.g., `crm-fin` and `hr-fin` both extend `fin.invoice`). Each creates its own ext table; the query builder adds multiple LEFT JOINs on PK columns.
|
|
219
|
+
|
|
220
|
+
Reference implementations: `modules/crm-fin/`, `wms-fin/`
|
|
221
|
+
|
|
222
|
+
**Bridge menu merge constraints.** When a bridge module declares a `menus.json` with a menu name that already exists (owned by a core module), its items are merged into the existing menu under matching sections. Two constraints apply:
|
|
223
|
+
|
|
224
|
+
1. **Bridge menu items must be leaves** (`itemType` of `V`, `R`, or `D`). Do not insert sub-folders (`itemType: F`) from a bridge. On reinstall the cleaner identifies bridge-owned items via `data_view_id` / `report_id` FKs back to the bridge's module_id — folder rows have neither FK, so they cannot be traced back and would accumulate on every reinstall.
|
|
225
|
+
|
|
226
|
+
2. **Bridge menu items must reference the bridge's own views/reports**, not the host module's. The platform populates the report/view lookups from all installed modules so technically a bridge `menus.json` can reference any code, but reinstall cleanup only matches by the bridge's own module_id. Referencing a host-module report means the stale menu_item survives reinstall and the new INSERT then fails against the `ux_menu_item_report` / `ux_menu_item_view` partial unique indexes. If a bridge wants to expose a host-owned resource under its own menu slot, duplicate the definition into the bridge's `reports.json` / `data_views.json` so ownership stays within the bridge.
|
|
227
|
+
|
|
228
|
+
**When to use extensions vs. regular references:**
|
|
229
|
+
- Use extensions (via a bridge module) when two independent modules need cross-module lookups or actions
|
|
230
|
+
- Use regular `link` references for one-way dependencies where the target entity already exists
|
|
231
|
+
|
|
232
|
+
### 2. Data View Definitions (`ui/data_views.json`)
|
|
233
|
+
|
|
234
|
+
**CRITICAL: Every view MUST use a `dataSources` array. The client will not render views without it.**
|
|
235
|
+
|
|
236
|
+
**Simple grid view:**
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"positions": {
|
|
240
|
+
"viewType": "grid",
|
|
241
|
+
"description": "Positions",
|
|
242
|
+
"dataSources": [
|
|
243
|
+
{
|
|
244
|
+
"entityCode": "position",
|
|
245
|
+
"level": 0,
|
|
246
|
+
"columns": [
|
|
247
|
+
"position_code",
|
|
248
|
+
{ "column_cd": "position_title", "width": 200 },
|
|
249
|
+
"department",
|
|
250
|
+
"is_active"
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
]
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Master-detail view (parent + child tabs):**
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"employee_detail": {
|
|
262
|
+
"viewType": "master-detail",
|
|
263
|
+
"description": "Employee Detail",
|
|
264
|
+
"dataSources": [
|
|
265
|
+
{
|
|
266
|
+
"entityCode": "employee",
|
|
267
|
+
"level": 0,
|
|
268
|
+
"columns": ["employee_code", "first_name", "last_name", "department"]
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"entityCode": "leave_request",
|
|
272
|
+
"level": 1,
|
|
273
|
+
"parentSetField": "leave_requests",
|
|
274
|
+
"label": "Leave Requests"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"entityCode": "document",
|
|
278
|
+
"level": 1,
|
|
279
|
+
"parentSetField": "documents",
|
|
280
|
+
"label": "Documents"
|
|
281
|
+
}
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Rules:**
|
|
288
|
+
- Each view is a dictionary entry keyed by view code
|
|
289
|
+
- `dataSources` is a **required array** — never put `entityCode`/`columns` at the root level
|
|
290
|
+
- Level 0 = primary data source (one per view)
|
|
291
|
+
- Level 1 = child data sources (for master-detail views)
|
|
292
|
+
- `parentSetField` on level 1 sources must match a Set column field name on the parent entity
|
|
293
|
+
- Columns can be strings (field code) or objects (`{ "column_cd": "name", "width": 200 }`)
|
|
294
|
+
|
|
295
|
+
**Common Mistakes:**
|
|
296
|
+
- ❌ `"entityCode": "employee"` at view root level → ✅ Put inside `dataSources[0]`
|
|
297
|
+
- ❌ `"columns": [...]` at view root level → ✅ Put inside `dataSources[0]`
|
|
298
|
+
- ❌ `"isDefault": true` at view root level → ✅ Not a recognized property; remove it
|
|
299
|
+
|
|
300
|
+
### 3. Menu Definitions (`ui/menus.json`)
|
|
301
|
+
|
|
302
|
+
**Menus use nested dictionaries (not arrays):**
|
|
303
|
+
|
|
304
|
+
```json
|
|
305
|
+
{
|
|
306
|
+
"hr_menu": {
|
|
307
|
+
"description": "Human Resources",
|
|
308
|
+
"items": {
|
|
309
|
+
"organization": {
|
|
310
|
+
"orderNum": 1,
|
|
311
|
+
"description": "Organization",
|
|
312
|
+
"children": {
|
|
313
|
+
"departments": {
|
|
314
|
+
"itemType": "V",
|
|
315
|
+
"dataViewCode": "departments",
|
|
316
|
+
"orderNum": 1,
|
|
317
|
+
"description": "Departments"
|
|
318
|
+
},
|
|
319
|
+
"positions": {
|
|
320
|
+
"itemType": "V",
|
|
321
|
+
"dataViewCode": "positions",
|
|
322
|
+
"orderNum": 2,
|
|
323
|
+
"description": "Positions"
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Rules:**
|
|
333
|
+
- Top-level key is the menu code (e.g. `hr_menu`)
|
|
334
|
+
- `items` is a **dictionary** of sections (not an array)
|
|
335
|
+
- Section children are also a **dictionary** under `children`
|
|
336
|
+
- Omit `itemType` for folder/section items (parent nodes)
|
|
337
|
+
- Only leaf items have `"itemType": "V"` with `dataViewCode`
|
|
338
|
+
- Use `dataViewCode` (not `viewCode`) to reference data views
|
|
339
|
+
|
|
340
|
+
**Common Mistakes:**
|
|
341
|
+
- ❌ `"items": [{ "code": "...", "label": "..." }]` (array format) → ✅ Use dictionary format
|
|
342
|
+
- ❌ `"viewCode": "departments"` → ✅ Use `"dataViewCode": "departments"`
|
|
343
|
+
- ❌ `"itemType": null` on sections → ✅ Omit `itemType` entirely
|
|
344
|
+
|
|
345
|
+
### 4. Folder Definitions (`ui/folders.json`)
|
|
346
|
+
|
|
347
|
+
**The whole `folders.json` file is the module's root folder.** A module gets
|
|
348
|
+
exactly one root folder (named after the module code automatically), and any
|
|
349
|
+
subfolders nest under a `children` object. Folder paths and parent/child
|
|
350
|
+
relationships are derived from the JSON tree structure — there is no
|
|
351
|
+
`folderPath` or `parentCode` field to write.
|
|
352
|
+
|
|
353
|
+
**Minimal example** (the `crm` module):
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
{
|
|
357
|
+
"label": "Sales CRM",
|
|
358
|
+
"description": "Customer relationship management and sales pipeline",
|
|
359
|
+
"color": "#2196F3",
|
|
360
|
+
"entities": {
|
|
361
|
+
"account": { "viewName": "default", "quickAdd": true },
|
|
362
|
+
"contact": { "viewName": "default", "quickAdd": true },
|
|
363
|
+
"lead": { "viewName": "default", "quickAdd": true },
|
|
364
|
+
"opportunity": { "viewName": "default", "quickAdd": true }
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
That's the entire file. Modules with no subfolders (the 80% case) end here.
|
|
370
|
+
|
|
371
|
+
**Nested example** (the `wms` module — root + three regional warehouses):
|
|
372
|
+
|
|
373
|
+
```json
|
|
374
|
+
{
|
|
375
|
+
"label": "Warehouse Management",
|
|
376
|
+
"description": "Inventory, purchasing, and warehouse operations",
|
|
377
|
+
"color": "#FF9800",
|
|
378
|
+
"entities": {
|
|
379
|
+
"warehouse": { "viewName": "default", "quickAdd": true },
|
|
380
|
+
"product": { "viewName": "default", "quickAdd": true },
|
|
381
|
+
"stock": { "viewName": "default", "quickAdd": true }
|
|
382
|
+
},
|
|
383
|
+
"children": {
|
|
384
|
+
"central": {
|
|
385
|
+
"label": "Central Warehouse",
|
|
386
|
+
"description": "Main distribution center",
|
|
387
|
+
"color": "#2196F3",
|
|
388
|
+
"inheritSecurity": true,
|
|
389
|
+
"entities": {
|
|
390
|
+
"stock": {
|
|
391
|
+
"viewName": "default",
|
|
392
|
+
"rowFilter": { "c": "warehouse_id", "o": "eq", "v": 2001 }
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
"east": {
|
|
397
|
+
"label": "East Warehouse",
|
|
398
|
+
"color": "#4CAF50",
|
|
399
|
+
"inheritSecurity": true,
|
|
400
|
+
"entities": { /* with rowFilter for east */ }
|
|
401
|
+
},
|
|
402
|
+
"west": {
|
|
403
|
+
"label": "West Warehouse",
|
|
404
|
+
"color": "#9C27B0",
|
|
405
|
+
"inheritSecurity": true,
|
|
406
|
+
"entities": { /* with rowFilter for west */ }
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The install pipeline derives:
|
|
413
|
+
|
|
414
|
+
| Source | `folder_path` in DB | `folder_code` (synthesized) |
|
|
415
|
+
|---|---|---|
|
|
416
|
+
| Root (the file itself) | `wms` | `wms` |
|
|
417
|
+
| `children.central` | `wms/central` | `wms_central` |
|
|
418
|
+
| `children.east` | `wms/east` | `wms_east` |
|
|
419
|
+
| `children.west` | `wms/west` | `wms_west` |
|
|
420
|
+
|
|
421
|
+
The synthesized flat code is the module code joined with each child key by
|
|
422
|
+
underscores; the DB path is the module code joined with each child key by
|
|
423
|
+
slashes.
|
|
424
|
+
|
|
425
|
+
**Rules:**
|
|
426
|
+
|
|
427
|
+
- **The whole file is the root folder.** Top-level fields (`label`, `description`, `color`, `entities`) belong to the root.
|
|
428
|
+
- **Subfolders nest under `children`** (a dictionary keyed by path segment).
|
|
429
|
+
- **Each child key is a single path segment** — lowercase letters, digits, dashes, and underscores only, no slashes. Pattern: `^[a-z0-9][a-z0-9_-]*$`. Slashes are forbidden in keys because deeper trees use `children` recursively, not slashes in keys.
|
|
430
|
+
- **`entities` is a dictionary** mapping entity codes to settings objects. Each entity has `viewName` (string or null), `quickAdd` (boolean), and an optional `rowFilter`.
|
|
431
|
+
- **`inheritSecurity`** is allowed only on child folders. The root has no parent to inherit from, so the field is rejected at the root level with a clear error.
|
|
432
|
+
- **Children can have their own children**, recursively, for arbitrary tree depth.
|
|
433
|
+
|
|
434
|
+
**Forbidden fields (migration traps):**
|
|
435
|
+
|
|
436
|
+
- ❌ `folderPath` anywhere in the file. The path is derived; the field is rejected with an error pointing at `modules/<code>/ui/folders.json`.
|
|
437
|
+
- ❌ `parentCode` anywhere in the file. Parent/child relationships are expressed by nesting under `children`; the field is rejected with the same kind of error.
|
|
438
|
+
- ❌ `inheritSecurity` on the root.
|
|
439
|
+
|
|
440
|
+
**Common Mistakes:**
|
|
441
|
+
|
|
442
|
+
- ❌ `"entities": ["department", "employee"]` (array format) → ✅ Use dictionary with objects.
|
|
443
|
+
- ❌ `"quickAdd": ["employee"]` (separate array) → ✅ Set `quickAdd: true` per entity.
|
|
444
|
+
- ❌ Mixing the old flat-dictionary format with `parentCode` and `folderPath` fields → ✅ Migrate to the tree shape; the install pipeline will tell you exactly which field to remove.
|
|
445
|
+
- ❌ Using slashes in child keys (`"regions/east"`) → ✅ Nest with `children: { "regions": { "children": { "east": {} } } }`.
|
|
446
|
+
|
|
447
|
+
#### Why the format looks like this
|
|
448
|
+
|
|
449
|
+
The frontend routes folder URLs as `/{folderPath}/v/{viewSlug}` with no
|
|
450
|
+
module segment. If two modules' folders shared a `folder_path`, the sidebar
|
|
451
|
+
would render them indistinguishably even though the database stores them as
|
|
452
|
+
separate rows scoped per module. The previous file format encoded paths and
|
|
453
|
+
parent/child relationships as explicit fields (`folderPath`, `parentCode`)
|
|
454
|
+
that module authors could easily get wrong — and a scaffold-from-template
|
|
455
|
+
flow could silently produce a violating module by copying those fields
|
|
456
|
+
verbatim from the source.
|
|
457
|
+
|
|
458
|
+
The current tree-shape format eliminates the entire class of bug:
|
|
459
|
+
|
|
460
|
+
- The root path is **always** the module code (it's not even a field).
|
|
461
|
+
- Subfolder paths are **derived from nesting**, so they can't disagree with the parent/child relationship.
|
|
462
|
+
- The whole file is module-scoped by construction — paths under one module physically cannot collide with another module's paths because they all start with their own module's code.
|
|
463
|
+
|
|
464
|
+
The DB still has a `UNIQUE INDEX` on `(module_id, folder_path)` as
|
|
465
|
+
defense-in-depth, and `FolderTreeFlattener` validates the file format
|
|
466
|
+
before the install pipeline touches the database.
|
|
467
|
+
|
|
468
|
+
### 5. Security Roles (`security/roles.json`)
|
|
469
|
+
|
|
470
|
+
```json
|
|
471
|
+
{
|
|
472
|
+
"hr.admin": {
|
|
473
|
+
"description": "HR Administrator — full access",
|
|
474
|
+
"rights": {
|
|
475
|
+
"department": "SIUDC",
|
|
476
|
+
"employee": "SIUDC",
|
|
477
|
+
"leave_request": "SIUDC"
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
"hr.viewer": {
|
|
481
|
+
"description": "HR Viewer — read-only",
|
|
482
|
+
"rights": {
|
|
483
|
+
"department": "S",
|
|
484
|
+
"employee": "S",
|
|
485
|
+
"leave_request": "S"
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
**Rules:**
|
|
492
|
+
- Key is the role code (convention: `module.role-name`)
|
|
493
|
+
- **Property name is `"rights"` (NOT `"entityRights"`)** — this maps to `RoleDef.Rights`
|
|
494
|
+
- Each entry in `rights` maps entity code → rights string
|
|
495
|
+
- Rights characters: `S` (Select), `I` (Insert), `U` (Update), `D` (Delete), `C` (Clone)
|
|
496
|
+
- Every entity should appear in every role (even if only `"S"` for read-only)
|
|
497
|
+
|
|
498
|
+
**Common Mistakes:**
|
|
499
|
+
- ❌ `"entityRights": { ... }` → ✅ Use `"rights": { ... }`
|
|
500
|
+
- ❌ Omitting entities from a role → Entity will be inaccessible to that role
|
|
501
|
+
|
|
502
|
+
### 6. Seed Data (`seed-data/*.json`)
|
|
503
|
+
|
|
504
|
+
**Number files to ensure FK dependency order:**
|
|
505
|
+
|
|
506
|
+
```
|
|
507
|
+
seed-data/
|
|
508
|
+
├── 01-skills.json # No FKs
|
|
509
|
+
├── 02-departments.json # No FKs
|
|
510
|
+
├── 03-positions.json # FK → department
|
|
511
|
+
├── 04-employees.json # FK → department, position
|
|
512
|
+
└── 05-leave_requests.json # FK → employee
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
**File format:**
|
|
516
|
+
```json
|
|
517
|
+
{
|
|
518
|
+
"entityCode": "department",
|
|
519
|
+
"records": [
|
|
520
|
+
{
|
|
521
|
+
"department_id": "a0030000-0000-0000-0000-000000000001",
|
|
522
|
+
"department_code": "EXEC",
|
|
523
|
+
"department_name": "Executive / Leadership",
|
|
524
|
+
"is_active": true
|
|
525
|
+
}
|
|
526
|
+
]
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
**Rules:**
|
|
531
|
+
- Files are loaded alphabetically — use numbered prefixes (01-, 02-, etc.)
|
|
532
|
+
- Parent tables must come before child tables (FK dependency order)
|
|
533
|
+
- Include explicit PK values (UUIDs) so child records can reference them
|
|
534
|
+
- Use a stable UUID scheme: `a00X0000-0000-0000-0000-00000000000Y` where X=entity type, Y=record
|
|
535
|
+
- Inserts use `ON CONFLICT DO NOTHING` (idempotent)
|
|
536
|
+
- UUIDs and dates in string format are auto-converted by the seed runner
|
|
537
|
+
- Omit auto-generated fields (`created_date`, `last_updated`) — they use DB defaults
|
|
538
|
+
- Omit cross-module FK fields (e.g., `owner_id` referencing `user` table)
|
|
539
|
+
|
|
540
|
+
### 7. Scheduled Jobs (`logic/jobs.json`)
|
|
541
|
+
|
|
542
|
+
Cron-driven action fires. Each entry pairs an existing action (declared in `ui/actions.json`) with a 5-field cron expression; the `dForge.Scheduler` worker fires it on schedule. Full reference: [module-package-format.md → jobs.json](./module-package-format.md#jobsjson).
|
|
543
|
+
|
|
544
|
+
```json
|
|
545
|
+
{
|
|
546
|
+
"jobs": [
|
|
547
|
+
{
|
|
548
|
+
"code": "tick",
|
|
549
|
+
"description": "Fires log_tick every minute",
|
|
550
|
+
"action": "log_tick",
|
|
551
|
+
"schedule": "* * * * *",
|
|
552
|
+
"timeout": 30,
|
|
553
|
+
"class": "standard"
|
|
554
|
+
}
|
|
555
|
+
]
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**Rules:**
|
|
560
|
+
- `code` — unique within the module, `[a-z][a-z0-9_]*`
|
|
561
|
+
- `action` — must be an action declared in this module's `ui/actions.json`; cross-module references are rejected
|
|
562
|
+
- `schedule` — five fields, minute granularity (sub-minute schedules are rejected at install)
|
|
563
|
+
- `timeout` — required, in seconds, range `(0, 3600]`
|
|
564
|
+
- `timeout > 300` requires `"class": "long_running"` (the standard pool is capped at 300s)
|
|
565
|
+
- Referenced action must NOT use `[field]` or `for x in records` — scheduled fires have no record context. Install fails fast if it does. Use `query()` / `insert()` for table-level work.
|
|
566
|
+
- Max 50 jobs per module
|
|
567
|
+
- Optional `timeZone` (IANA name) overrides `auth.tenant.time_zone` for that job
|
|
568
|
+
- Use `paused: true` to freeze a job during incidents — admin "Run now" still works while the cron path is silent
|
|
569
|
+
|
|
570
|
+
**Reference:** `modules/chore/` — single-job smoke-test module.
|
|
571
|
+
|
|
572
|
+
### 8. Translations (`translations/en-US.json`)
|
|
573
|
+
|
|
574
|
+
```json
|
|
575
|
+
{
|
|
576
|
+
"entities": {
|
|
577
|
+
"department": {
|
|
578
|
+
"label": "Department",
|
|
579
|
+
"desc": "Company departments",
|
|
580
|
+
"fields": {
|
|
581
|
+
"department_id": { "label": "Department ID" },
|
|
582
|
+
"department_name": { "label": "Department Name" }
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
"folders": {
|
|
587
|
+
"hr": { "label": "Human Resources" }
|
|
588
|
+
},
|
|
589
|
+
"views": {
|
|
590
|
+
"departments": { "label": "Departments" },
|
|
591
|
+
"employees": { "label": "All Employees" }
|
|
592
|
+
},
|
|
593
|
+
"menus": {
|
|
594
|
+
"hr_menu": { "label": "Human Resources" }
|
|
595
|
+
},
|
|
596
|
+
"roles": {
|
|
597
|
+
"hr.admin": { "label": "HR Administrator" },
|
|
598
|
+
"hr.viewer": { "label": "HR Viewer" }
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
## Entity Definitions (`entities/*.json`)
|
|
606
|
+
|
|
607
|
+
### Complete Entity Example
|
|
608
|
+
|
|
609
|
+
```json
|
|
610
|
+
{
|
|
611
|
+
"description": "Employees",
|
|
612
|
+
"dbObject": "employee",
|
|
613
|
+
"toString": "{first_name} {last_name}",
|
|
614
|
+
"fields": {
|
|
615
|
+
"employee_id": {
|
|
616
|
+
"dbDatatype": "uuid",
|
|
617
|
+
"isPk": true,
|
|
618
|
+
"isIdentity": true,
|
|
619
|
+
"isNullable": false,
|
|
620
|
+
"orderNum": 10,
|
|
621
|
+
"description": "Employee ID"
|
|
622
|
+
},
|
|
623
|
+
"employee_code": {
|
|
624
|
+
"dbDatatype": "varchar",
|
|
625
|
+
"fieldTypeCd": "text",
|
|
626
|
+
"flags": "VEM",
|
|
627
|
+
"isNullable": false,
|
|
628
|
+
"maxLen": 20,
|
|
629
|
+
"orderNum": 20,
|
|
630
|
+
"description": "Employee Code"
|
|
631
|
+
},
|
|
632
|
+
"department_id": {
|
|
633
|
+
"dbDatatype": "uuid",
|
|
634
|
+
"flags": "EM",
|
|
635
|
+
"orderNum": 30,
|
|
636
|
+
"description": "Department ID"
|
|
637
|
+
},
|
|
638
|
+
"department": {
|
|
639
|
+
"columnType": "R",
|
|
640
|
+
"fieldTypeCd": "lookup",
|
|
641
|
+
"flags": "VEM",
|
|
642
|
+
"orderNum": 35,
|
|
643
|
+
"description": "Department",
|
|
644
|
+
"link": {
|
|
645
|
+
"entity": "department",
|
|
646
|
+
"thisKey": "department_id",
|
|
647
|
+
"otherKey": "department_id"
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
"employment_status": {
|
|
651
|
+
"dbDatatype": "varchar",
|
|
652
|
+
"fieldTypeCd": "dropdown",
|
|
653
|
+
"flags": "VEM",
|
|
654
|
+
"isNullable": false,
|
|
655
|
+
"maxLen": 50,
|
|
656
|
+
"formula": "'Active'",
|
|
657
|
+
"orderNum": 40,
|
|
658
|
+
"description": "Status",
|
|
659
|
+
"params": {
|
|
660
|
+
"options": ["Active", "On Leave", "Suspended", "Terminated"]
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
"leave_requests": {
|
|
664
|
+
"columnType": "S",
|
|
665
|
+
"fieldTypeCd": "grid",
|
|
666
|
+
"flags": "VEM",
|
|
667
|
+
"orderNum": 100,
|
|
668
|
+
"description": "Leave Requests",
|
|
669
|
+
"link": {
|
|
670
|
+
"entity": "leave_request",
|
|
671
|
+
"thisKey": "employee_id",
|
|
672
|
+
"otherKey": "employee_id"
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
"constraints": {
|
|
677
|
+
"UQ_employee_code": {
|
|
678
|
+
"type": "unique",
|
|
679
|
+
"fields": ["employee_code"],
|
|
680
|
+
"message": "Employee code must be unique"
|
|
681
|
+
},
|
|
682
|
+
"chk_salary_positive": {
|
|
683
|
+
"type": "check",
|
|
684
|
+
"expression": "salary > 0",
|
|
685
|
+
"message": "Salary must be positive"
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
"references": {
|
|
689
|
+
"FK_Employee_Department": {
|
|
690
|
+
"from": { "field": "department_id" },
|
|
691
|
+
"to": { "entity": "department", "field": "department_id" }
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## FK+Reference Two-Column Pattern
|
|
700
|
+
|
|
701
|
+
For every foreign key relationship, create TWO columns:
|
|
702
|
+
|
|
703
|
+
### 1. Hidden FK Column (Database)
|
|
704
|
+
```json
|
|
705
|
+
"department_id": {
|
|
706
|
+
"dbDatatype": "uuid",
|
|
707
|
+
"flags": "EM",
|
|
708
|
+
"orderNum": 30,
|
|
709
|
+
"description": "Department ID"
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### 2. Visible Reference Column (UI)
|
|
714
|
+
```json
|
|
715
|
+
"department": {
|
|
716
|
+
"columnType": "R",
|
|
717
|
+
"fieldTypeCd": "lookup",
|
|
718
|
+
"flags": "VEM",
|
|
719
|
+
"orderNum": 35,
|
|
720
|
+
"description": "Department",
|
|
721
|
+
"link": {
|
|
722
|
+
"entity": "department",
|
|
723
|
+
"thisKey": "department_id",
|
|
724
|
+
"otherKey": "department_id"
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
### 3. Declare Foreign Key
|
|
730
|
+
```json
|
|
731
|
+
"references": {
|
|
732
|
+
"FK_Employee_Department": {
|
|
733
|
+
"from": { "field": "department_id" },
|
|
734
|
+
"to": { "entity": "department", "field": "department_id" }
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
**Rules:**
|
|
740
|
+
- Reference columns use `"link"` (not `"params"`) for entity binding
|
|
741
|
+
- `link.entity` = target entity code, `link.thisKey` = local FK field, `link.otherKey` = remote PK field
|
|
742
|
+
- `params` is used for other purposes (e.g., dropdown `options`)
|
|
743
|
+
- Hidden FK column: `flags: "EM"` (no `V` = hidden from UI)
|
|
744
|
+
- Visible reference column: `flags: "VEM"`, `columnType: "R"`
|
|
745
|
+
|
|
746
|
+
---
|
|
747
|
+
|
|
748
|
+
## Set Columns (1:N Relationships)
|
|
749
|
+
|
|
750
|
+
Declare set columns on parent entities for child collections:
|
|
751
|
+
|
|
752
|
+
```json
|
|
753
|
+
"leave_requests": {
|
|
754
|
+
"columnType": "S",
|
|
755
|
+
"fieldTypeCd": "grid",
|
|
756
|
+
"flags": "VEM",
|
|
757
|
+
"orderNum": 100,
|
|
758
|
+
"description": "Leave Requests",
|
|
759
|
+
"link": {
|
|
760
|
+
"entity": "leave_request",
|
|
761
|
+
"thisKey": "employee_id",
|
|
762
|
+
"otherKey": "employee_id"
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
**Rules:**
|
|
768
|
+
- Set columns use `"link"` (same as reference columns)
|
|
769
|
+
- `link.entity` = child entity, `link.thisKey` = parent PK, `link.otherKey` = child FK
|
|
770
|
+
- The set field name is used as `parentSetField` in master-detail data views
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
## Field Type Reference
|
|
775
|
+
|
|
776
|
+
### Valid Field Types
|
|
777
|
+
|
|
778
|
+
| Code | Description | Column Type |
|
|
779
|
+
|------|-------------|-------------|
|
|
780
|
+
| `text` | Single-line text | D (Data) |
|
|
781
|
+
| `email` | Email address | D |
|
|
782
|
+
| `phone` | Phone number | D |
|
|
783
|
+
| `url` | URL / web address | D |
|
|
784
|
+
| `textarea` | Multi-line text | D |
|
|
785
|
+
| `code` | Code editor (monospace) | D |
|
|
786
|
+
| `richtext` | Rich text editor | D |
|
|
787
|
+
| `number` | Numeric input | D |
|
|
788
|
+
| `currency` | Currency with precision | D |
|
|
789
|
+
| `percent` | Percentage value | D |
|
|
790
|
+
| `checkbox` | Boolean checkbox | D |
|
|
791
|
+
| `date` | Date | D |
|
|
792
|
+
| `datetime` | Date and time | D |
|
|
793
|
+
| `time` | Time | D |
|
|
794
|
+
| `dropdown` | Dropdown select | D |
|
|
795
|
+
| `flags` | Multi-select checkboxes (bitwise flags) | D |
|
|
796
|
+
| `tags` | Tag / chip input | D |
|
|
797
|
+
| `user` | User selector | D |
|
|
798
|
+
| `entitylink` | Entity record link | D |
|
|
799
|
+
| `json` | JSON editor | D |
|
|
800
|
+
| `hidden` | Hidden field | D |
|
|
801
|
+
| `color` | Color picker | D |
|
|
802
|
+
| `image` | Image upload | D |
|
|
803
|
+
| `file` | File upload | D |
|
|
804
|
+
| `lookup` | Reference lookup | R (Reference) |
|
|
805
|
+
| `grid` | Detail grid | S (Set) |
|
|
806
|
+
|
|
807
|
+
**Common Mistakes:**
|
|
808
|
+
- ❌ `"integer"` → ✅ Use `"number"`
|
|
809
|
+
- ❌ `"datePicker"` → ✅ Use `"date"`
|
|
810
|
+
- ❌ `"autocomplete"` → ✅ Use `"lookup"`
|
|
811
|
+
|
|
812
|
+
### Dropdown Options (`params.options`)
|
|
813
|
+
|
|
814
|
+
For `dropdown` fields, define available options in `params.options`:
|
|
815
|
+
|
|
816
|
+
**Simple format** — value equals label:
|
|
817
|
+
```json
|
|
818
|
+
"status": {
|
|
819
|
+
"dbDatatype": "varchar",
|
|
820
|
+
"fieldTypeCd": "dropdown",
|
|
821
|
+
"flags": "VEM",
|
|
822
|
+
"maxLen": 50,
|
|
823
|
+
"params": {
|
|
824
|
+
"options": ["New", "In Progress", "Done"]
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
**Rich format** — separate value/label with optional icon and color:
|
|
830
|
+
```json
|
|
831
|
+
"priority": {
|
|
832
|
+
"dbDatatype": "varchar",
|
|
833
|
+
"fieldTypeCd": "dropdown",
|
|
834
|
+
"flags": "VEM",
|
|
835
|
+
"maxLen": 20,
|
|
836
|
+
"params": {
|
|
837
|
+
"options": [
|
|
838
|
+
{ "value": "low", "label": "Low", "icon": "🟢", "color": "#e8f5e9" },
|
|
839
|
+
{ "value": "medium", "label": "Medium", "icon": "🟡", "color": "#fff3e0" },
|
|
840
|
+
{ "value": "high", "label": "High", "icon": "🔴", "color": "#ffebee" }
|
|
841
|
+
]
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
**Rich option properties:**
|
|
847
|
+
| Property | Required | Description |
|
|
848
|
+
|----------|----------|-------------|
|
|
849
|
+
| `value` | Yes | Stored value (must fit `maxLen`) |
|
|
850
|
+
| `label` | Yes | Display label |
|
|
851
|
+
| `icon` | No | Emoji or icon string shown before label |
|
|
852
|
+
| `color` | No | Background color for badge display (hex, e.g. `"#e8f5e9"`) |
|
|
853
|
+
|
|
854
|
+
Both formats can be mixed in the same array, but this is not recommended.
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
## Version Management
|
|
859
|
+
|
|
860
|
+
### Semantic Versioning
|
|
861
|
+
|
|
862
|
+
Use semantic versioning: `MAJOR.MINOR.PATCH`
|
|
863
|
+
|
|
864
|
+
- **MAJOR**: Breaking changes to schema or entity definitions
|
|
865
|
+
- **MINOR**: New entities or backward-compatible features
|
|
866
|
+
- **PATCH**: Bug fixes, documentation updates
|
|
867
|
+
|
|
868
|
+
### Two Version Numbers
|
|
869
|
+
|
|
870
|
+
1. **`version`**: Overall module version (includes all changes)
|
|
871
|
+
2. **`dbSchemaVersion`**: Database schema version (only bumped for schema changes)
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## Installation & Reinstallation
|
|
876
|
+
|
|
877
|
+
### CLI Commands
|
|
878
|
+
|
|
879
|
+
```bash
|
|
880
|
+
# Via Docker (from /docker directory)
|
|
881
|
+
docker/cli.sh module install --code <tenant> --path /modules/<module>
|
|
882
|
+
docker/setup-tenant.sh <code> <name> [email] [--erp]
|
|
883
|
+
docker/install-modules.sh <code> --erp
|
|
884
|
+
|
|
885
|
+
# Local CLI (from /server directory)
|
|
886
|
+
dotnet run --project src/dForge.Cli -- module install --code <tenant> --path /path/to/module
|
|
887
|
+
|
|
888
|
+
# Force reinstall (cleans metadata and re-registers)
|
|
889
|
+
dotnet run --project src/dForge.Cli -- module install --code <tenant> --path /path/to/module --force
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
### What Happens During Install
|
|
893
|
+
|
|
894
|
+
1. **DDL**: Schema + tables created (`CREATE TABLE IF NOT EXISTS`)
|
|
895
|
+
2. **Seed data**: Records inserted (`INSERT ... ON CONFLICT DO NOTHING`)
|
|
896
|
+
3. **Entities**: Registered in `dForge.entity` + `dForge.entity_column`
|
|
897
|
+
4. **Security**: `sec_object` + `module_role` + `module_role_rights` + `user_role`
|
|
898
|
+
5. **Data views**: Registered in `dForge.data_view`
|
|
899
|
+
6. **Menus**: `dForge.menu` + `dForge.menu_item` + `dForge.folder` + `dForge.folder_entity` + `dForge.folder_menu`
|
|
900
|
+
|
|
901
|
+
### Force Reinstall
|
|
902
|
+
|
|
903
|
+
With `--force`, the metadata cleaner runs first:
|
|
904
|
+
- Deletes: `folder_entity` → `folder_menu` → `folder` → `menu_item` → `menu` → `data_view` → `module_role_rights` → `user_role` → `module_role` → `sec_object` → `entity_column` → `entity`
|
|
905
|
+
- Then re-registers everything fresh
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
## Summary Checklist
|
|
910
|
+
|
|
911
|
+
When creating a module, ensure:
|
|
912
|
+
|
|
913
|
+
- [ ] Uses TABS for indentation (JSON: 2, Code: 4)
|
|
914
|
+
- [ ] `manifest.json` includes `entities` registry mapping codes to file paths
|
|
915
|
+
- [ ] Entity files use `"link"` (not `"params"`) for reference/set column bindings
|
|
916
|
+
- [ ] All `fieldTypeCd` values are valid — see [Field Types](../data-model/field-types.md) (use `number`, not `integer`)
|
|
917
|
+
- [ ] FK+Reference two-column pattern used for all foreign keys
|
|
918
|
+
- [ ] `data_views.json` uses `dataSources` array (not root-level `entityCode`)
|
|
919
|
+
- [ ] `menus.json` uses nested dictionaries with `children` (not arrays)
|
|
920
|
+
- [ ] `menus.json` leaf items use `dataViewCode` (not `viewCode`)
|
|
921
|
+
- [ ] `folders.json` uses entity dictionary with `{ viewName, quickAdd }` objects
|
|
922
|
+
- [ ] `roles.json` uses `"rights"` property (not `"entityRights"`)
|
|
923
|
+
- [ ] Seed data files are numbered for FK dependency order (01-, 02-, etc.)
|
|
924
|
+
- [ ] Seed data includes explicit PK UUIDs for cross-entity references
|
|
925
|
+
- [ ] `translations/en-US.json` covers entities, fields, views, menus, folders, roles
|
|
926
|
+
- [ ] Constraints have clear user-facing `message` values
|
|
927
|
+
- [ ] Check constraint `expression` uses standard SQL subset (test in PostgreSQL first)
|
|
928
|
+
- [ ] Number sequences declared as `numberSequence` on entity definitions (auto-fills on INSERT, never manual counting)
|
|
929
|
+
- [ ] Print templates defined in `ui/print_templates.json` with HTML files in `print_templates/`
|
|
930
|
+
- [ ] Scheduled jobs (if any) declared in `logic/jobs.json` with 5-field cron, explicit `timeout`, and `class: "long_running"` for any `timeout > 300`
|
|
931
|
+
|
|
932
|
+
---
|
|
933
|
+
|
|
934
|
+
## Reference Implementations
|
|
935
|
+
|
|
936
|
+
- **CRM module**: `modules/crm/` — complete example with 9 entities
|
|
937
|
+
- **HR module**: `modules/hr/` — complete example with 10 entities
|
|
938
|
+
- **Chore module**: `modules/chore/` — minimal `logic/jobs.json` reference (one cron-fired action)
|
|
939
|
+
- **System modules**: `server/database/system-modules/admin/` and `metadata/`
|