@dforge-core/dforge-mcp 0.2.22 → 0.2.24
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 +117 -0
- package/dist/server.js +1638 -292
- package/package.json +3 -3
- package/resources/docs/dsl-reference.md +42 -39
- package/resources/schemas/card-layouts.schema.json +141 -0
- package/resources/schemas/data-views.schema.json +2 -14
- package/resources/schemas/entity.schema.json +45 -1
- package/resources/schemas/jobs.schema.json +2 -2
- package/resources/schemas/manifest.schema.json +4 -0
- package/skills/dforge-mcp-author/examples/matrix-budget/manifest.json +1 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforge-core/dforge-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
4
4
|
"description": "MCP server for dForge module authoring. Exposes 34 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/iash44/dForge-core",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"install-skills": "node scripts/install-skills.mjs"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@dforge-core/dforge-cli": "^0.2.
|
|
39
|
-
"@dforge-core/metadata": "^0.0.
|
|
38
|
+
"@dforge-core/dforge-cli": "^0.2.19",
|
|
39
|
+
"@dforge-core/metadata": "^0.0.27",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
41
41
|
"zod": "^4.4.3"
|
|
42
42
|
},
|
|
@@ -10,22 +10,25 @@ A `.dsl` file consists of four ordered, optional blocks (only `execute:` is requ
|
|
|
10
10
|
|
|
11
11
|
```dsl
|
|
12
12
|
params:
|
|
13
|
-
|
|
13
|
+
// Parameter declarations (one per line)
|
|
14
14
|
|
|
15
15
|
canExecute:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// Single-expression formula returning boolean
|
|
17
|
+
// Enables/disables the button AND is re-checked server-side on execute
|
|
18
|
+
// (see "canExecute is enforced, with a fail-open subset")
|
|
19
19
|
|
|
20
20
|
onBeforeStart:
|
|
21
|
-
|
|
21
|
+
// Pre-queue initialization (async / background actions only)
|
|
22
22
|
|
|
23
23
|
execute:
|
|
24
|
-
|
|
24
|
+
// Main logic — required
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Block markers are case-insensitive but conventionally lowercase. Whitespace-indented bodies follow each marker. Blocks must appear in the order above when present.
|
|
28
28
|
|
|
29
|
+
Comments are `//` and `/* … */`, as in JavaScript. **`#` is not a comment** — the
|
|
30
|
+
compiler passes the line through and Jint rejects it as a syntax error at install.
|
|
31
|
+
|
|
29
32
|
---
|
|
30
33
|
|
|
31
34
|
## Execution modes
|
|
@@ -141,11 +144,11 @@ execute:
|
|
|
141
144
|
- `old[fieldName]` — the field's value *before* the change, in an action invoked by an event trigger. Read-only; reads `null` on `insert` events and outside triggers
|
|
142
145
|
|
|
143
146
|
```dsl
|
|
144
|
-
|
|
147
|
+
// Read
|
|
145
148
|
if ([quantity] > [available_qty]) { error("Not enough stock") }
|
|
146
149
|
if ([supplier].[is_active] == false) { error("Supplier disabled") }
|
|
147
150
|
|
|
148
|
-
|
|
151
|
+
// Write
|
|
149
152
|
[status] = 'closed'
|
|
150
153
|
[closed_date] = now()
|
|
151
154
|
```
|
|
@@ -381,19 +384,19 @@ so ids stay exact through reads, writes and comparisons. Smaller `int8` values
|
|
|
381
384
|
(counters, quantities) stay ordinary numbers, so arithmetic on them is unchanged.
|
|
382
385
|
|
|
383
386
|
```dsl
|
|
384
|
-
[copy] = [owner_id]
|
|
385
|
-
[is_mine] = currentUserId() == [last_updated_by]
|
|
386
|
-
[label] = '' + [owner_id]
|
|
387
|
+
[copy] = [owner_id] // exact round-trip
|
|
388
|
+
[is_mine] = currentUserId() == [last_updated_by] // exact — both sides BigInt
|
|
389
|
+
[label] = '' + [owner_id] // "200650220910936067"
|
|
387
390
|
```
|
|
388
391
|
|
|
389
392
|
Two operations throw instead of rounding silently:
|
|
390
393
|
|
|
391
394
|
```dsl
|
|
392
|
-
[x] = [owner_id] + 1
|
|
393
|
-
[x] = Number([owner_id]) + 1
|
|
395
|
+
[x] = [owner_id] + 1 // Cannot mix BigInt and other types
|
|
396
|
+
[x] = Number([owner_id]) + 1 // explicit — and accepts the rounding
|
|
394
397
|
|
|
395
|
-
[x] = JSON.stringify([owner_id])
|
|
396
|
-
[payload] = { id: [owner_id] }
|
|
398
|
+
[x] = JSON.stringify([owner_id]) // Do not know how to serialize a BigInt
|
|
399
|
+
[payload] = { id: [owner_id] } // assign the object; jsonb writes the number
|
|
397
400
|
```
|
|
398
401
|
|
|
399
402
|
Never write a large id as a bare numeric literal — the literal itself rounds
|
|
@@ -492,10 +495,10 @@ notify([approver_id], 'New purchase request from ' + [requester_name] + ' awaiti
|
|
|
492
495
|
Two modes:
|
|
493
496
|
|
|
494
497
|
```dsl
|
|
495
|
-
|
|
498
|
+
// Raw mode: (to, subject, body)
|
|
496
499
|
sendEmail('ap@acme.com', 'Bill ' + [bill_number] + ' approved', 'Amount: $' + [amount])
|
|
497
500
|
|
|
498
|
-
|
|
501
|
+
// Template mode: (to, templateCd, dataObject) — template defined in module's email_templates/
|
|
499
502
|
sendEmail([customer_id].[email], 'order_confirmation', {
|
|
500
503
|
orderNumber: [order_number],
|
|
501
504
|
total: [total],
|
|
@@ -564,8 +567,8 @@ execute:
|
|
|
564
567
|
Folder-scoped module setting — walks the folder inheritance chain up to the module default.
|
|
565
568
|
|
|
566
569
|
```dsl
|
|
567
|
-
var prefix = getSetting('invoice_number_prefix')
|
|
568
|
-
var maxDays = getSetting('payment_due_days')
|
|
570
|
+
var prefix = getSetting('invoice_number_prefix') // e.g. "INV-"
|
|
571
|
+
var maxDays = getSetting('payment_due_days') // e.g. 30
|
|
569
572
|
[due_date] = addDays(now(), maxDays)
|
|
570
573
|
```
|
|
571
574
|
|
|
@@ -666,7 +669,7 @@ An explicit `description` still wins.
|
|
|
666
669
|
|
|
667
670
|
```dsl
|
|
668
671
|
var task = insert('workspace.task', { title: 'Follow up' })
|
|
669
|
-
|
|
672
|
+
// Captioned by workspace.task's toString — no third argument needed.
|
|
670
673
|
insert('workspace.task_note', { link: entityLink('workspace.task', task) })
|
|
671
674
|
```
|
|
672
675
|
|
|
@@ -674,14 +677,14 @@ insert('workspace.task_note', { link: entityLink('workspace.task', task) })
|
|
|
674
677
|
Commit pending changes mid-script AND broadcast SSE updates to connected clients. **Async / background actions only** — in sync actions, everything commits at the end automatically.
|
|
675
678
|
|
|
676
679
|
```dsl
|
|
677
|
-
|
|
680
|
+
// Long-running background job that wants to surface progress
|
|
678
681
|
execute:
|
|
679
682
|
var total = records.count()
|
|
680
683
|
var done = 0
|
|
681
684
|
for rec in records {
|
|
682
685
|
rec[processed_at] = now()
|
|
683
686
|
done = done + 1
|
|
684
|
-
if (done % 100 == 0) { flush() }
|
|
687
|
+
if (done % 100 == 0) { flush() } // progress update every 100 records
|
|
685
688
|
}
|
|
686
689
|
```
|
|
687
690
|
|
|
@@ -788,7 +791,7 @@ execute:
|
|
|
788
791
|
|
|
789
792
|
```dsl
|
|
790
793
|
execute:
|
|
791
|
-
var threshold = getSetting('high_value_threshold')
|
|
794
|
+
var threshold = getSetting('high_value_threshold') // e.g. 10000
|
|
792
795
|
var ccApprover = getSetting('cc_approver_user_id')
|
|
793
796
|
if ([amount] >= threshold && ccApprover != null) {
|
|
794
797
|
notify(ccApprover, 'Large order requires your CC approval: $' + [amount])
|
|
@@ -928,15 +931,15 @@ execute:
|
|
|
928
931
|
### ❌ Using `[field]` in batch mode
|
|
929
932
|
|
|
930
933
|
```dsl
|
|
931
|
-
|
|
934
|
+
// WRONG — batch mode has no "current record"
|
|
932
935
|
execute:
|
|
933
936
|
for rec in records {
|
|
934
|
-
[status] = 'closed'
|
|
937
|
+
[status] = 'closed' // ← this references the dispatch context, not rec
|
|
935
938
|
}
|
|
936
939
|
```
|
|
937
940
|
|
|
938
941
|
```dsl
|
|
939
|
-
|
|
942
|
+
// RIGHT
|
|
940
943
|
execute:
|
|
941
944
|
for rec in records {
|
|
942
945
|
rec[status] = 'closed'
|
|
@@ -948,13 +951,13 @@ execute:
|
|
|
948
951
|
Jobs run as system user with no record context. The action must be entity-agnostic.
|
|
949
952
|
|
|
950
953
|
```dsl
|
|
951
|
-
|
|
954
|
+
// WRONG — fails at runtime with "no record context"
|
|
952
955
|
execute:
|
|
953
956
|
if ([overdue_count] > 0) { notify([owner_id], 'Overdue') }
|
|
954
957
|
```
|
|
955
958
|
|
|
956
959
|
```dsl
|
|
957
|
-
|
|
960
|
+
// RIGHT — fetch what you need via query
|
|
958
961
|
execute:
|
|
959
962
|
var overdue = query('SELECT owner_id, COUNT(*) AS n FROM invoice WHERE due_date < CURRENT_DATE GROUP BY owner_id', {})
|
|
960
963
|
for (var i = 0; i < overdue.length; i++) {
|
|
@@ -967,14 +970,14 @@ execute:
|
|
|
967
970
|
The DSL body is not wrapped in an IIFE — `return` at top level is a parse error.
|
|
968
971
|
|
|
969
972
|
```dsl
|
|
970
|
-
|
|
973
|
+
// WRONG
|
|
971
974
|
execute:
|
|
972
975
|
if ([status] == 'closed') { return }
|
|
973
976
|
[last_touched] = now()
|
|
974
977
|
```
|
|
975
978
|
|
|
976
979
|
```dsl
|
|
977
|
-
|
|
980
|
+
// RIGHT — use exit() for early success, error() for early failure
|
|
978
981
|
execute:
|
|
979
982
|
if ([status] == 'closed') { exit('Already closed', 'info') }
|
|
980
983
|
[last_touched] = now()
|
|
@@ -988,9 +991,9 @@ execution path. The same script keeps the row on one path and discards it on
|
|
|
988
991
|
another, and the difference isn't visible in the action.
|
|
989
992
|
|
|
990
993
|
```dsl
|
|
991
|
-
|
|
994
|
+
// WRONG — is this insert kept or discarded? Depends on the path.
|
|
992
995
|
execute:
|
|
993
|
-
insert('audit_log', { event: 'attempted_close', record_id: [id] })
|
|
996
|
+
insert('audit_log', { event: 'attempted_close', record_id: [id] }) // unreliable!
|
|
994
997
|
if ([balance_due] > 0) { error('Cannot close — balance owing') }
|
|
995
998
|
```
|
|
996
999
|
|
|
@@ -1003,7 +1006,7 @@ to be atomic — so don't design around it either way. Write the script so the
|
|
|
1003
1006
|
question never arises: validate first.
|
|
1004
1007
|
|
|
1005
1008
|
```dsl
|
|
1006
|
-
|
|
1009
|
+
// RIGHT — validate first, then perform side-effects
|
|
1007
1010
|
execute:
|
|
1008
1011
|
if ([balance_due] > 0) { error('Cannot close — balance owing') }
|
|
1009
1012
|
insert('audit_log', { event: 'closed', record_id: [id] })
|
|
@@ -1015,17 +1018,17 @@ execute:
|
|
|
1015
1018
|
The platform auto-generates the number on INSERT when the target column is empty. Manual `nextNumber()` + manual field-set risks double-allocating sequence values if you forget to leave the field blank.
|
|
1016
1019
|
|
|
1017
1020
|
```dsl
|
|
1018
|
-
|
|
1021
|
+
// WRONG — wastes a sequence value
|
|
1019
1022
|
execute:
|
|
1020
1023
|
var n = nextNumber('purchase_order')
|
|
1021
|
-
var po = insert('purchase_order', { po_number: n, ... })
|
|
1024
|
+
var po = insert('purchase_order', { po_number: n, ... }) // gets a DIFFERENT auto-number, n is wasted
|
|
1022
1025
|
```
|
|
1023
1026
|
|
|
1024
1027
|
```dsl
|
|
1025
|
-
|
|
1028
|
+
// RIGHT — let INSERT generate it
|
|
1026
1029
|
execute:
|
|
1027
1030
|
var po = insert('purchase_order', { supplier_id: params[supplier].supplier_id })
|
|
1028
|
-
info('Created PO ' + po.po_number)
|
|
1031
|
+
info('Created PO ' + po.po_number) // po_number was auto-populated
|
|
1029
1032
|
```
|
|
1030
1033
|
|
|
1031
1034
|
Only use `nextNumber()` explicitly when you need the number BEFORE insert (e.g. to mention in a notification or message).
|
|
@@ -1039,12 +1042,12 @@ Sync actions block the UI until they finish. A 30-second action freezes the user
|
|
|
1039
1042
|
### ❌ String-concatenating into SQL
|
|
1040
1043
|
|
|
1041
1044
|
```dsl
|
|
1042
|
-
|
|
1045
|
+
// WRONG — injection-vulnerable + breaks on quotes
|
|
1043
1046
|
var q = query('SELECT * FROM customer WHERE name = ' + "'" + params[name] + "'", {})
|
|
1044
1047
|
```
|
|
1045
1048
|
|
|
1046
1049
|
```dsl
|
|
1047
|
-
|
|
1050
|
+
// RIGHT — use named placeholders
|
|
1048
1051
|
var q = query('SELECT * FROM customer WHERE name = @n', { n: params[name] })
|
|
1049
1052
|
```
|
|
1050
1053
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "dForge Module Card Layouts",
|
|
4
|
+
"description": "Module-shipped card (form) layouts for a dForge module package (ui/card_layouts.json). Each key is the layout_name stored in dForge.entity_view_layout. Rows written from here carry module_id, so they are recreated on every install and removed on uninstall; a layout the tenant drew in the card editor has module_id NULL and is never touched.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"entity": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Entity code the layout belongs to. Unqualified means this module owns the entity; the cross-module form 'module.entity' targets another module's entity (a bridge module shipping a layout for its host)."
|
|
12
|
+
},
|
|
13
|
+
"view": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"default": "default",
|
|
16
|
+
"description": "Entity view the layout hangs off, matching an entity_view row's view_name. Defaults to 'default' — the same fallback entity.getLayouts and entity.saveLayout use for an entity with no folder binding of its own. Matched against the entity's declared views case-insensitively, and filed under their spelling; the installer creates the view row only when nothing matches."
|
|
17
|
+
},
|
|
18
|
+
"isDefault": {
|
|
19
|
+
"type": "boolean",
|
|
20
|
+
"default": false,
|
|
21
|
+
"description": "Whether this layout opens by default. Honoured on FIRST install only, and only when the view has no default layout yet — once a tenant has chosen their own default, an upgrade does not take it back."
|
|
22
|
+
},
|
|
23
|
+
"description": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Human-readable note for the package author. Not stored."
|
|
26
|
+
},
|
|
27
|
+
"layout": {
|
|
28
|
+
"$ref": "#/$defs/cardLayout",
|
|
29
|
+
"description": "The layout body, stored verbatim into entity_view_layout.layout. Same shape the card editor saves."
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"required": ["entity", "layout"],
|
|
33
|
+
"additionalProperties": false
|
|
34
|
+
},
|
|
35
|
+
"$defs": {
|
|
36
|
+
"cardLayout": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"sections": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"description": "Ordered sections rendered down the card.",
|
|
42
|
+
"items": { "$ref": "#/$defs/section" }
|
|
43
|
+
},
|
|
44
|
+
"sets": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"description": "Per-set-field view config, keyed by the set column code. Chooses the renderer a detail grid uses ('grid' or 'list') and its persisted options.",
|
|
47
|
+
"additionalProperties": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"properties": {
|
|
50
|
+
"viewType": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"enum": ["grid", "list"],
|
|
53
|
+
"description": "Set renderer. Defaults to the column's own metadata."
|
|
54
|
+
},
|
|
55
|
+
"options": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"description": "Opaque per-renderer options, passed verbatim to the set registration."
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"additionalProperties": false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"required": ["sections"],
|
|
65
|
+
"additionalProperties": false
|
|
66
|
+
},
|
|
67
|
+
"section": {
|
|
68
|
+
"oneOf": [
|
|
69
|
+
{ "$ref": "#/$defs/columnGroupSection" },
|
|
70
|
+
{ "$ref": "#/$defs/setSection" },
|
|
71
|
+
{ "$ref": "#/$defs/tabGroupSection" }
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"columnGroupSection": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"description": "A group of fields laid out in 1, 2 or 3 columns.",
|
|
77
|
+
"properties": {
|
|
78
|
+
"type": { "const": "columnGroup" },
|
|
79
|
+
"code": { "type": "string", "description": "Section code, unique within the layout." },
|
|
80
|
+
"label": { "type": "string", "description": "Section heading, in the module's authoring language. The installer gives it an entity_column_group row keyed on this section's code, so translations/<locale>.json localizes it under entities.<entity>.columnGroups.<code>.label and the card shows the localized heading." },
|
|
81
|
+
"cols": { "type": "integer", "minimum": 1, "maximum": 3, "description": "Field columns across the group. Defaults to the card's own setting." },
|
|
82
|
+
"columns": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"description": "Fields in the group, in render order. A bare string is the column code; the object form adds pixel sizing.",
|
|
85
|
+
"items": {
|
|
86
|
+
"oneOf": [
|
|
87
|
+
{ "type": "string" },
|
|
88
|
+
{
|
|
89
|
+
"type": "object",
|
|
90
|
+
"properties": {
|
|
91
|
+
"column_cd": { "type": "string", "description": "Column code on this entity." },
|
|
92
|
+
"width": { "type": "integer", "minimum": 1, "description": "Field width in px." },
|
|
93
|
+
"height": { "type": "integer", "minimum": 1, "description": "Field height in px — for multiline/textarea fields." }
|
|
94
|
+
},
|
|
95
|
+
"required": ["column_cd"],
|
|
96
|
+
"additionalProperties": false
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"required": ["type", "code", "columns"],
|
|
103
|
+
"additionalProperties": false
|
|
104
|
+
},
|
|
105
|
+
"setSection": {
|
|
106
|
+
"type": "object",
|
|
107
|
+
"description": "An embedded detail grid for a 1:N set column — the master/detail building block. The label comes from the column's own metadata.",
|
|
108
|
+
"properties": {
|
|
109
|
+
"type": { "const": "set" },
|
|
110
|
+
"code": { "type": "string", "description": "Section code, unique within the layout." },
|
|
111
|
+
"setField": { "type": "string", "description": "Set column code on this entity (columnType 'S')." }
|
|
112
|
+
},
|
|
113
|
+
"required": ["type", "code", "setField"],
|
|
114
|
+
"additionalProperties": false
|
|
115
|
+
},
|
|
116
|
+
"tabGroupSection": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"description": "A tab bar whose tabs are other sections. This is how a record shows several detail grids without stacking them down one long form.",
|
|
119
|
+
"properties": {
|
|
120
|
+
"type": { "const": "tabGroup" },
|
|
121
|
+
"code": { "type": "string", "description": "Section code, unique within the layout." },
|
|
122
|
+
"tabs": {
|
|
123
|
+
"type": "array",
|
|
124
|
+
"minItems": 1,
|
|
125
|
+
"description": "Tabs in order. Each references a columnGroup section by its code, or a set column by its column code.",
|
|
126
|
+
"items": {
|
|
127
|
+
"type": "object",
|
|
128
|
+
"properties": {
|
|
129
|
+
"type": { "type": "string", "enum": ["section", "set"] },
|
|
130
|
+
"code": { "type": "string", "description": "The referenced section's code, or the set column's code." }
|
|
131
|
+
},
|
|
132
|
+
"required": ["type", "code"],
|
|
133
|
+
"additionalProperties": false
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"required": ["type", "code", "tabs"],
|
|
138
|
+
"additionalProperties": false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"properties": {
|
|
15
15
|
"viewType": {
|
|
16
16
|
"type": "string",
|
|
17
|
-
"enum": ["grid", "list", "kanban", "calendar", "gallery", "tree-grid", "diagram", "
|
|
17
|
+
"enum": ["grid", "list", "kanban", "calendar", "gallery", "tree-grid", "diagram", "library", "matrix"],
|
|
18
18
|
"default": "grid",
|
|
19
19
|
"description": "View display type. Optional — omitted or unknown values fall back to 'grid' at runtime."
|
|
20
20
|
},
|
|
@@ -57,7 +57,6 @@
|
|
|
57
57
|
{ "$ref": "#/$defs/rules/tree-grid" },
|
|
58
58
|
{ "$ref": "#/$defs/rules/list" },
|
|
59
59
|
{ "$ref": "#/$defs/rules/diagram" },
|
|
60
|
-
{ "$ref": "#/$defs/rules/master-detail" },
|
|
61
60
|
{ "$ref": "#/$defs/rules/matrix" }
|
|
62
61
|
]
|
|
63
62
|
},
|
|
@@ -118,17 +117,6 @@
|
|
|
118
117
|
}
|
|
119
118
|
}
|
|
120
119
|
},
|
|
121
|
-
"master-detail": {
|
|
122
|
-
"if": {
|
|
123
|
-
"properties": { "viewType": { "const": "master-detail" } },
|
|
124
|
-
"required": ["viewType"]
|
|
125
|
-
},
|
|
126
|
-
"then": {
|
|
127
|
-
"properties": {
|
|
128
|
-
"dataSources": { "minItems": 2 }
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
120
|
"matrix": {
|
|
133
121
|
"if": {
|
|
134
122
|
"properties": { "viewType": { "const": "matrix" } },
|
|
@@ -493,7 +481,7 @@
|
|
|
493
481
|
},
|
|
494
482
|
"label": {
|
|
495
483
|
"type": "string",
|
|
496
|
-
"description": "Human-readable label for this data source
|
|
484
|
+
"description": "Human-readable label for this data source"
|
|
497
485
|
},
|
|
498
486
|
"columns": {
|
|
499
487
|
"type": "array",
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"toString": {
|
|
39
39
|
"description": "Display pattern using column placeholders, e.g. \"{first_name} {last_name}\". Optional in extension entities (with `extends`). No `type` constraint here because `toString` collides with Object.prototype.toString in JSON validators — they read the inherited function and reject it, even when the property is absent from the JSON."
|
|
40
40
|
},
|
|
41
|
+
"comments": {
|
|
42
|
+
"type": "boolean",
|
|
43
|
+
"description": "Records of this entity accept comments \u2014 the composer and thread appear on the record card, beside the audit history. Opt-in per entity; `comments` in the manifest sets a module-wide default that this overrides in both directions."
|
|
44
|
+
},
|
|
41
45
|
"traits": {
|
|
42
46
|
"type": "array",
|
|
43
47
|
"description": "Entity traits expanded during module install (authoring shortcut). Trait definitions loaded from DB (seeded by metadata module).",
|
|
@@ -221,9 +225,48 @@
|
|
|
221
225
|
},
|
|
222
226
|
"additionalProperties": false
|
|
223
227
|
},
|
|
228
|
+
"registryColumnParams": {
|
|
229
|
+
"type": "object",
|
|
230
|
+
"description": "params of an accumulation (A) or ledger (L) registry column. The lock lists are what BOTH generated guards freeze — the posted-document guard while the document is posted, the period guard while its period is closed — and they are required: install refuses a registry column that omits or empties them. The header's periodDateField and the state column are watched in addition, whatever the list says.",
|
|
231
|
+
"required": ["lockedFields"],
|
|
232
|
+
"properties": {
|
|
233
|
+
"lockedFields": {
|
|
234
|
+
"type": "array",
|
|
235
|
+
"minItems": 1,
|
|
236
|
+
"items": { "type": "string", "minLength": 1 },
|
|
237
|
+
"description": "Header fields frozen while the document is posted and while its period is closed. Required. [\"*\"] freezes the whole row."
|
|
238
|
+
},
|
|
239
|
+
"lockedLineFields": {
|
|
240
|
+
"type": "array",
|
|
241
|
+
"minItems": 1,
|
|
242
|
+
"items": { "type": "string", "minLength": 1 },
|
|
243
|
+
"description": "Line fields frozen under a posted or period-closed header. Required when lineEntity is declared, rejected when it is not. [\"*\"] freezes the whole row; the line's foreign key to the header is watched in addition."
|
|
244
|
+
},
|
|
245
|
+
"lineEntity": { "type": "string", "description": "The line entity, defined in this module. Declare together with lineField and lockedLineFields, or none of the three for a document without lines." },
|
|
246
|
+
"lineField": { "type": "string", "description": "The line's foreign-key column(s) to the header, comma-separated in the header's key order for a composite key." },
|
|
247
|
+
"stateField": { "type": "string", "description": "The bool column that records the posted state, when it is not the registry column itself. Written only by the register engine." },
|
|
248
|
+
"periodEntity": { "type": "string", "description": "Period entity this registry closes against — one of this module's, or 'module.entity' from a dependency." },
|
|
249
|
+
"periodDateField": { "type": "string", "description": "Header date column that resolves the period." },
|
|
250
|
+
"periodKeyField": { "type": "string", "default": "period_key" },
|
|
251
|
+
"periodClosedField": { "type": "string", "default": "closed" }
|
|
252
|
+
},
|
|
253
|
+
"dependencies": {
|
|
254
|
+
"lineEntity": ["lineField", "lockedLineFields"],
|
|
255
|
+
"lineField": ["lineEntity"],
|
|
256
|
+
"lockedLineFields": ["lineEntity"],
|
|
257
|
+
"periodEntity": ["periodDateField"]
|
|
258
|
+
},
|
|
259
|
+
"additionalProperties": true
|
|
260
|
+
},
|
|
224
261
|
"field": {
|
|
225
262
|
"type": "object",
|
|
226
263
|
"description": "Entity column/field definition",
|
|
264
|
+
"allOf": [
|
|
265
|
+
{
|
|
266
|
+
"if": { "required": ["columnType"], "properties": { "columnType": { "enum": ["A", "L"] } } },
|
|
267
|
+
"then": { "required": ["params"], "properties": { "params": { "$ref": "#/$defs/registryColumnParams" } } }
|
|
268
|
+
}
|
|
269
|
+
],
|
|
227
270
|
"properties": {
|
|
228
271
|
"dbDatatype": {
|
|
229
272
|
"type": "string",
|
|
@@ -551,8 +594,9 @@
|
|
|
551
594
|
"sign": { "$ref": "#/$defs/signConfig", "description": "Dynamic sign based on a field value (alternative to direction)" },
|
|
552
595
|
"autoCreateBalance": { "type": "boolean", "default": true },
|
|
553
596
|
"allowNegative": { "type": "boolean", "default": true },
|
|
554
|
-
"lockedFields": { "type": "array", "items": { "type": "string" }, "description": "Document fields locked when posted" }
|
|
597
|
+
"lockedFields": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "description": "Document fields locked when posted. Required; [\"*\"] freezes the whole row." }
|
|
555
598
|
},
|
|
599
|
+
"required": ["lockedFields"],
|
|
556
600
|
"additionalProperties": false
|
|
557
601
|
},
|
|
558
602
|
"signConfig": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"params": {
|
|
36
36
|
"type": "object",
|
|
37
|
-
"description": "Static parameters passed to the action as __params.
|
|
37
|
+
"description": "Static parameters passed to the action as __params. Manifest-fixed — an admin cannot change them. For tenant-configurable values use module settings, which resolve from the job's bound folder (scheduled_job.folder_id, defaulting to the module root).",
|
|
38
38
|
"additionalProperties": true
|
|
39
39
|
},
|
|
40
40
|
"timeout": {
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"paused": {
|
|
73
73
|
"type": "boolean",
|
|
74
74
|
"default": false,
|
|
75
|
-
"description": "Soft pause. Scheduler skips firing, but admin 'trigger now' still works."
|
|
75
|
+
"description": "Soft pause. Scheduler skips firing, but admin 'trigger now' still works. Seeds the initial value only — an upgrade never overwrites it, because an admin owns it at runtime."
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
"additionalProperties": false
|
|
@@ -107,6 +107,10 @@
|
|
|
107
107
|
"enum": ["none", "minimal", "full"],
|
|
108
108
|
"description": "Default audit history mode for entities in this module ('full' captures every field change; 'minimal' captures only inserts/deletes; 'none' disables). Individual entities can override."
|
|
109
109
|
},
|
|
110
|
+
"comments": {
|
|
111
|
+
"type": "boolean",
|
|
112
|
+
"description": "Default for `comments` on every entity in this module. Individual entities override it, including overriding true with false."
|
|
113
|
+
},
|
|
110
114
|
"entities": {
|
|
111
115
|
"type": "object",
|
|
112
116
|
"description": "Entity definitions and extensions: entity code → relative path to entity JSON file. Dotted keys (e.g. 'fin.invoice') indicate extensions of other modules' entities (the entity file must have an 'extends' property).",
|
|
@@ -19,12 +19,5 @@
|
|
|
19
19
|
"entities": {
|
|
20
20
|
"budget_category": "./entities/budget_category.json",
|
|
21
21
|
"budget_line": "./entities/budget_line.json"
|
|
22
|
-
}
|
|
23
|
-
"dataViews": "./ui/data_views.json",
|
|
24
|
-
"security": {
|
|
25
|
-
"roles": "./security/roles.json"
|
|
26
|
-
},
|
|
27
|
-
"seedData": [
|
|
28
|
-
"./seed-data/01-categories.json"
|
|
29
|
-
]
|
|
22
|
+
}
|
|
30
23
|
}
|