@dforge-core/dforge-mcp 0.2.4 → 0.2.14
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 +183 -0
- package/dist/server.js +411 -287
- package/package.json +47 -46
- package/resources/docs/conventions.md +4 -0
- package/resources/docs/dsl-reference.md +48 -2
- package/resources/schemas/data-views.schema.json +40 -0
- package/resources/schemas/manifest.schema.json +6 -0
- package/resources/schemas/reports.schema.json +48 -19
- package/skills/dforge-mcp-author/references/action-dsl.md +40 -2
- package/skills/dforge-mcp-author/references/column-domains.md +21 -2
- package/skills/dforge-mcp-author/references/manifest.md +29 -0
- package/skills/dforge-mcp-author/references/reports.md +107 -15
- package/skills/dforge-mcp-author/references/translations.md +29 -3
- package/skills/dforge-mcp-author/references/validation-checklist.md +13 -0
- package/skills/dforge-module-build/SKILL.md +29 -5
|
@@ -7,7 +7,7 @@ SP files: `logic/reports/rpt_*.sql`
|
|
|
7
7
|
|
|
8
8
|
## Structure
|
|
9
9
|
|
|
10
|
-
A report has `description`, a `datasets` map, a `layout` object (`{ panels: [...] }`), and optional `
|
|
10
|
+
A report has `description`, a `datasets` map, a `layout` object (`{ panels: [...] }`), optional per-dataset `params`, and an optional `entities` array (record-report attachments). Panels reference datasets by code via `datasetCd`.
|
|
11
11
|
|
|
12
12
|
```json
|
|
13
13
|
{
|
|
@@ -17,12 +17,18 @@ A report has `description`, a `datasets` map, a `layout` object (`{ panels: [...
|
|
|
17
17
|
"pipeline": {
|
|
18
18
|
"caption": "Pipeline Data",
|
|
19
19
|
"datasetType": "Q",
|
|
20
|
+
"params": {
|
|
21
|
+
"min_amount": { "fieldTypeCd": "number", "label": "Min Amount", "required": false, "default": 0 }
|
|
22
|
+
},
|
|
20
23
|
"query": {
|
|
21
24
|
"entityCd": "opportunity",
|
|
22
25
|
"columns": ["stage", "amount", "lead_source", "customer.name"],
|
|
23
26
|
"filter": {
|
|
24
27
|
"g": "and",
|
|
25
|
-
"i": [
|
|
28
|
+
"i": [
|
|
29
|
+
{ "c": "stage", "o": "nIn", "v": ["Closed Won", "Closed Lost"] },
|
|
30
|
+
{ "c": "amount", "o": "grEq", "v": "@min_amount" }
|
|
31
|
+
]
|
|
26
32
|
},
|
|
27
33
|
"sort": [ { "c": "amount", "d": "desc" } ]
|
|
28
34
|
}
|
|
@@ -38,9 +44,6 @@ A report has `description`, a `datasets` map, a `layout` object (`{ panels: [...
|
|
|
38
44
|
},
|
|
39
45
|
{ "vizType": "table", "datasetCd": "pipeline", "title": "Pipeline Detail" }
|
|
40
46
|
]
|
|
41
|
-
},
|
|
42
|
-
"parameters": {
|
|
43
|
-
"min_amount": { "fieldTypeCd": "number", "label": "Min Amount", "required": false, "default": 0 }
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
}
|
|
@@ -85,7 +88,7 @@ For SQL the query builder can't express — window functions, CTEs, cross-schema
|
|
|
85
88
|
"spCd": "rpt_ar_aging",
|
|
86
89
|
"params": {
|
|
87
90
|
"as_of_date": { "fieldTypeCd": "date", "label": "As of Date", "required": true, "default": "=NOW()" },
|
|
88
|
-
"customer_id": { "fieldTypeCd": "lookup", "label": "Customer", "required": false, "params": { "
|
|
91
|
+
"customer_id": { "fieldTypeCd": "lookup", "label": "Customer", "required": false, "params": { "link": { "entity": "account" } } }
|
|
89
92
|
},
|
|
90
93
|
"columnsDef": {
|
|
91
94
|
"customer_name": { "label": "Customer", "fieldTypeCd": "text", "baseDatatypeCd": "string", "width": 200 },
|
|
@@ -99,7 +102,7 @@ Key differences from entity datasets:
|
|
|
99
102
|
- `datasetType: "S"`.
|
|
100
103
|
- `spCd` — the stored-procedure code (the function name **without** the schema prefix; resolved to `sp_id` at install). (**Not** `sp` or `procedureName`.)
|
|
101
104
|
- `columnsDef` — **required** — the platform can't infer columns from a function.
|
|
102
|
-
- Params
|
|
105
|
+
- Params work exactly as they do for an entity dataset, and `datasetType` makes no difference: declare them at report level (`parameters`) or on the dataset (`params`). Report level wins on a code collision.
|
|
103
106
|
|
|
104
107
|
**Multi-result-set SPs** map extra datasets to the same function via `parentDatasetCd` (the dataset that owns the SP call) + `parentRef` (the named refcursor):
|
|
105
108
|
|
|
@@ -139,25 +142,50 @@ $$;
|
|
|
139
142
|
|
|
140
143
|
## Parameters
|
|
141
144
|
|
|
142
|
-
Parameters prompt the user before running the report.
|
|
145
|
+
Parameters prompt the user before running the report.
|
|
146
|
+
|
|
147
|
+
**Parameters are report-scoped**, so there are two legal declaration sites and they mean the same thing:
|
|
148
|
+
|
|
149
|
+
- **`parameters`** at report level — the canonical home, and the only one that fits a param several datasets use.
|
|
150
|
+
- **`datasets.<cd>.params`** — shorthand, fine when exactly one dataset uses it. Every module written before this existed uses it, and it stays fully supported.
|
|
151
|
+
|
|
152
|
+
The installer merges both into the report's single param set (**report level wins** on a code collision), so either way the param serves the whole report: the param bar, saved param sets, `@param` filters in *any* dataset, and record-report mappings.
|
|
153
|
+
|
|
154
|
+
> Why report-scoped: the declaration is stored once per report (`report.param_set_id` → `param_set`), the authoring API is `report.params.save`, and `report.get` flattens per-dataset defaults report-wide before use. There is no dataset-scoped parameter behaviour to choose between.
|
|
143
155
|
|
|
144
156
|
```json
|
|
145
157
|
"parameters": {
|
|
146
|
-
"
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
"customer": { "fieldTypeCd": "lookup", "label": "Customer", "params": { "link": { "entity": "account" } } }
|
|
159
|
+
},
|
|
160
|
+
"datasets": {
|
|
161
|
+
"sales": {
|
|
162
|
+
"datasetType": "Q",
|
|
163
|
+
"params": {
|
|
164
|
+
"start_date": { "fieldTypeCd": "date", "label": "Start Date", "required": true, "default": "=STARTMONTH()" },
|
|
165
|
+
"status": { "domain": "fin.doc_status", "label": "Status" },
|
|
166
|
+
"region": { "fieldTypeCd": "dropdown", "label": "Region", "params": { "options": [
|
|
167
|
+
{ "value": "north", "label": "North" }, { "value": "south", "label": "South" }
|
|
168
|
+
] } }
|
|
169
|
+
},
|
|
170
|
+
"query": { "entityCd": "invoice", "columns": ["invoice_number", "total"] }
|
|
171
|
+
}
|
|
149
172
|
}
|
|
150
173
|
```
|
|
151
174
|
|
|
152
175
|
| Property | Description |
|
|
153
176
|
|---|---|
|
|
154
|
-
| `fieldTypeCd` | Control type: `date`, `datetime`, `text`, `number`, `dropdown`, `lookup`, `user`, `checkbox` |
|
|
177
|
+
| `fieldTypeCd` | Control type: `date`, `datetime`, `text`, `number`, `dropdown`, `lookup`, `user`, `checkbox`. Omit when `domain` is set. |
|
|
178
|
+
| `domain` | Column domain (`domain_cd` or `module_cd.domain_cd`) supplying the control **and** the option list. Mutually exclusive with `fieldTypeCd` — declaring both is rejected at install. |
|
|
155
179
|
| `label` | Display label in the parameter dialog |
|
|
156
180
|
| `required` | Whether the user must fill this before running |
|
|
157
181
|
| `default` | Plain value or `=`-prefixed formula (`"=NOW()"`, `"=STARTMONTH()"`, `"=TODAY()"`) |
|
|
158
|
-
| `params` | Extra config — `options` for dropdowns, `
|
|
182
|
+
| `params` | Extra config — `options` for dropdowns, `link: { entity, otherKey? }` for lookups |
|
|
159
183
|
| `orderNum` | Display order in the parameter form (falls back to declaration order) |
|
|
160
184
|
|
|
185
|
+
Two spellings that look right and are silently ignored: **`isRequired`** (the key is `required`, so the param installs as optional) and a **top-level `link`** (it must be nested under `params`, so the lookup installs with no autocomplete). Both are validator errors.
|
|
186
|
+
|
|
187
|
+
**Dropdown params: prefer `domain`, and never ship bare codes.** A list shared with a column belongs on a `column_domain` — bind the param to it and the options plus their translations are authored once (`column-domains.md`). For a list that exists only for this report, write the rich option form (`{ "value": "north", "label": "North" }`); a bare `"options": ["north", "south"]` renders the raw codes in **every** locale, English included. Per-locale labels go under `reports.<cd>.params.<param_cd>.options` (`translations.md`).
|
|
188
|
+
|
|
161
189
|
Reference a parameter with `@param_code` inside a query filter value:
|
|
162
190
|
|
|
163
191
|
```json
|
|
@@ -254,14 +282,72 @@ Dashboard KPI/chart *tiles* get the same support via a `sources` map in the tile
|
|
|
254
282
|
|
|
255
283
|
`config.chartSize` ∈ `sm | m | l | xl` controls the default chart height (`m` when omitted).
|
|
256
284
|
|
|
285
|
+
## Record reports (`entities`)
|
|
286
|
+
|
|
287
|
+
A report can be **attached to an entity** so it opens from a record — the way a print template does — with the record's values feeding its parameters. Add an `entities` array to the report:
|
|
288
|
+
|
|
289
|
+
```json
|
|
290
|
+
"credit_check": {
|
|
291
|
+
"description": "Customer Credit Check — limit against outstanding AR and open quotes",
|
|
292
|
+
"entities": [
|
|
293
|
+
{ "entityCd": "parties.party", "params": { "customer_id": "party_id" }, "orderNum": 45 },
|
|
294
|
+
{ "entityCd": "crm.quote", "params": { "customer_id": "customer_id" }, "orderNum": 45 }
|
|
295
|
+
],
|
|
296
|
+
"parameters": {
|
|
297
|
+
"customer_id": {
|
|
298
|
+
"label": "Customer",
|
|
299
|
+
"fieldTypeCd": "lookup",
|
|
300
|
+
"params": { "link": { "entity": "parties.party" } },
|
|
301
|
+
"required": true
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
"layout": { "panels": [ { "vizType": "table", "datasetCd": "open_invoices" } ] },
|
|
305
|
+
"datasets": {
|
|
306
|
+
"open_invoices": {
|
|
307
|
+
"caption": "Open Invoices",
|
|
308
|
+
"datasetType": "Q",
|
|
309
|
+
"query": {
|
|
310
|
+
"entityCd": "invoice",
|
|
311
|
+
"columns": ["invoice_number", "due_date", "amount_due"],
|
|
312
|
+
"filter": { "g": "and", "i": [ { "c": "customer_id", "o": "eq", "v": "@customer_id" } ] }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Each entry becomes one attachment row, and the report gains a toolbar entry on that entity's record. Note the two different `params` keys:
|
|
320
|
+
|
|
321
|
+
- `entities[].params` — the **mapping**: report param code → **source column on the attached entity**.
|
|
322
|
+
- `parameters` / `datasets.<cd>.params` — the param **declaration**. The mapping can only name codes declared in one of those.
|
|
323
|
+
|
|
324
|
+
| Rule | Detail |
|
|
325
|
+
|---|---|
|
|
326
|
+
| `entityCd` | Qualified `module.entity` for anything outside this module, and that module must be a **declared dependency**. |
|
|
327
|
+
| Source columns | The entity PK, a reference (`R`) column, or a bounded scalar (number, date/datetime, bool, dropdown/radio/flags code). `text`, `json`, `file`/`image` are rejected. |
|
|
328
|
+
| Types | Source and target must be compatible; only `lookup`↔`number` and `date`↔`datetime` widen. A names-only match like `{ "customer_id": "created_date" }` is an error, not a runtime surprise. |
|
|
329
|
+
| One per pair | One attachment per (entity, report). Two entries for the same entity overwrite each other. |
|
|
330
|
+
| Unmapped params | Keep their normal behaviour — default value, or prompt the user. |
|
|
331
|
+
| Mapped params | Are **hidden** on the record-report page. The URL is about this record; pivoting means going back to a record. |
|
|
332
|
+
| Dependency | Declare `"metadata": ">=1.5.0"` — the attachment table ships with the metadata system module, so this is the gate that fails loudly on an older platform instead of the attachment quietly doing nothing. This is metadata's **`version`**; dependency ranges are never checked against `dbSchemaVersion`, so do not use the `1.4.0` of the migration that creates the table. |
|
|
333
|
+
| Rights | Report `E` is still required. A bridge role must grant `E` on the report **and** `S` on the dataset entities. |
|
|
334
|
+
|
|
335
|
+
There is no `canExecute` on an attachment: if the record exists the report is meaningful for it, and an empty report is a legitimate answer.
|
|
336
|
+
|
|
337
|
+
> **Prefer a record report over an action that calculates.** See *When an action is the wrong tool* in `references/action-dsl.md`. If the `execute:` block only reads, computes and `info()`s a number, it is not an action — a printed number is never stored, can't be re-read, and shows the verdict without the working. Attach a report instead: it gets you KPI tiles, charts, the underlying rows, drill-through, saved layouts and row caps for free, and the DSL you don't write is DSL that can't drift from the data.
|
|
338
|
+
>
|
|
339
|
+
> Keep it an action when it **changes** something — writing the result to a column, creating a record, or *blocking* a transition. "Explain the number" is a report; "refuse the over-limit quote" is an action or trigger.
|
|
340
|
+
|
|
257
341
|
## Grants
|
|
258
342
|
|
|
259
|
-
Grant report access with the `E` right:
|
|
343
|
+
Grant report access with the `E` right — a **colon** prefix, never a dot:
|
|
260
344
|
|
|
261
345
|
```json
|
|
262
|
-
"rights": { "report
|
|
346
|
+
"rights": { "report:sales_pipeline": "E" }
|
|
263
347
|
```
|
|
264
348
|
|
|
349
|
+
A dependency's report is granted with the qualified form, `"report:fin.ar_aging": "E"` — bridge modules need this to grant on a report they attach but do not own.
|
|
350
|
+
|
|
265
351
|
## Common mistakes
|
|
266
352
|
|
|
267
353
|
- Using a bare `"layout": [ ... ]` array — `layout` is an **object**: `"layout": { "panels": [ ... ] }`.
|
|
@@ -270,5 +356,11 @@ Grant report access with the `E` right:
|
|
|
270
356
|
- Using `sp` / `procedureName` for an SP dataset — the field is `spCd`.
|
|
271
357
|
- Forgetting `columnsDef` on an SP dataset — **required**.
|
|
272
358
|
- Forgetting to grant `E` on the report in at least one role — it becomes invisible.
|
|
359
|
+
- Writing the rights key as `report.<code>` — actions/reports/folders take a **colon**: `report:<code>`.
|
|
360
|
+
- Writing `isRequired` instead of `required`, or a top-level `link` instead of `params.link` — both are ignored at install.
|
|
361
|
+
- Declaring both `fieldTypeCd` and `domain` on one param — install rejects the pair rather than picking a winner.
|
|
362
|
+
- Mapping a record-report attachment onto a param no dataset declares, or from a `text`/`json` column — both are rejected.
|
|
363
|
+
- Attaching a report to another module's entity without declaring that module as a dependency.
|
|
273
364
|
- Forgetting `p_folder_uid` / `p_user_id` as the first two SP function params — the call fails.
|
|
274
365
|
- Referencing a parameter as `$param` — use `@param_code` in filters.
|
|
366
|
+
- Writing an action that computes a number and `info()`s it, where a record report would show the working.
|
|
@@ -82,8 +82,8 @@ Translation files mirror the module's content structure. Each translatable objec
|
|
|
82
82
|
| `roles` | Role display labels | Role codes (keys from `security/roles.json`, already module-qualified — e.g. `crm.admin`) → `{ label }` |
|
|
83
83
|
| `views` | Data view labels | View codes (keys from `ui/data_views.json`) |
|
|
84
84
|
| `menus` | Menu node labels (root + nested items) | Menu root key → `{ label, items: { item_code: { label } } }` matching `ui/menus.json` structure |
|
|
85
|
-
| `actions` | Action labels, descriptions, and param labels | Action codes (keys from `ui/actions.json`) → `{ label, desc, params: { param_cd: { label } } }` |
|
|
86
|
-
| `reports` | Report dataset captions and param labels | Report codes → `{ datasets: { ds_cd: { caption } }, params: { param_cd: { label } } }` |
|
|
85
|
+
| `actions` | Action labels, descriptions, param labels **and param dropdown option labels** | Action codes (keys from `ui/actions.json`) → `{ label, desc, params: { param_cd: { label, options: { value: … } } } }` |
|
|
86
|
+
| `reports` | Report dataset captions, param labels **and param dropdown option labels** | Report codes → `{ datasets: { ds_cd: { caption } }, params: { param_cd: { label, options: { value: … } } } }` |
|
|
87
87
|
| `entities.<e>.constraints` | Check/unique constraint violation messages (opt-in — warned, not enforced) | Constraint codes → `{ message }` under the owning entity |
|
|
88
88
|
| `entities.<e>.fields.<f>.options` | Dropdown/radio/flags **option labels** (opt-in) | Option `value` → localized label (string) or `{ label, icon, color }` |
|
|
89
89
|
| `domains` | Column domain labels **and** their shared option labels (opt-in) | Domain codes → `{ label, options: { value: … } }` |
|
|
@@ -146,7 +146,33 @@ If a column uses a **domain** (see `column-domains.md`), translate the domain **
|
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
- Keys are domain codes from this module's `domains.json`. The `options` map uses the same value-keyed, partial-override shape as field options above.
|
|
149
|
-
- This is the whole point of domains for localization: the shared option list is authored and translated in one place. Don't also translate the consuming columns' options — they inherit from the domain.
|
|
149
|
+
- This is the whole point of domains for localization: the shared option list is authored and translated in one place. Don't also translate the consuming columns' options — they inherit from the domain. **Nor the params bound to it** — an action or report parameter declared as `domain <domainCd>` inherits from the same place.
|
|
150
|
+
|
|
151
|
+
### Action / report PARAM option labels ARE translatable (opt-in)
|
|
152
|
+
|
|
153
|
+
A `dropdown` **parameter** carries per-value labels exactly like a column, and translates them under an `options` map next to the param's `label`:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
"actions": {
|
|
157
|
+
"distribute_payment": {
|
|
158
|
+
"params": {
|
|
159
|
+
"payment_method": {
|
|
160
|
+
"label": "Zahlungsart",
|
|
161
|
+
"options": {
|
|
162
|
+
"bank_transfer": { "label": "Überweisung" },
|
|
163
|
+
"cash": "Bar"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- Same shape and rules as field options: value-keyed, partial override merged over the base, bare string = `{ "label": … }`, only `label`/`icon`/`color` translatable, `value` never.
|
|
172
|
+
- The same section works for `reports.<report_cd>.params.<param_cd>.options`.
|
|
173
|
+
- A param may translate **only** its options and keep the authored caption — omit `label`.
|
|
174
|
+
- **This requires base labels in the DSL.** `options=bank_transfer,cash` stores bare codes, and the dialog shows `bank_transfer` in *every* locale including English until you write `options=[bank_transfer:Bank transfer, cash:Cash]` (or the JSON object form). See `action-dsl.md`.
|
|
175
|
+
- **Prefer a domain.** If the param's choices are the same list a column uses — which they usually are, since the value normally ends up written to that column — declare the param as `domain <domainCd>` and skip this section entirely: it inherits the domain's translated options. Reach for per-param `options` only when the list belongs to that one parameter and matches no column.
|
|
150
176
|
|
|
151
177
|
### Roles ARE translated — and completeness is enforced
|
|
152
178
|
|
|
@@ -73,6 +73,9 @@ For each entity:
|
|
|
73
73
|
- [ ] Each source has `entityCode` and `columns`
|
|
74
74
|
- [ ] `entityCode` points to an entity that exists
|
|
75
75
|
- [ ] Column codes in `columns` all exist on the entity
|
|
76
|
+
- [ ] No `columns` entry is a **set** (`columnType: "S"`) — a 1:N collection has no per-row value to put in a column; show it on the record card instead. **Rejected at validate/pack**
|
|
77
|
+
- [ ] No `columns` entry is a **dotted navigation path** (`owner.email`, `account.company_name`) — a view's column list is matched against the entity's own columns, so a dotted entry resolves to nothing. Add a Reference (`R`) or Formula (`F`) column to the entity and list that. **Rejected at validate/pack**
|
|
78
|
+
- [ ] A listed column does **not** need `V` in its flags — the renderer promotes anything the list names (that is how trait-contributed `created_by_user`, flagged `I`, appears). Listing a non-`V` column is valid intent, not a mistake
|
|
76
79
|
- [ ] The rendered entity has **at least one visible scalar column** (a field with `V` in `flags`, `columnType` not `S`) — a grid/list/kanban/etc. view over an entity whose fields are all hidden (or only sets) is **rejected** at validate/pack and would render "No visible columns configured for this entity." `diagram`/`matrix`/`library` are exempt
|
|
77
80
|
- [ ] If set, `viewType` is from the supported list (`grid`, `list`, `kanban`, `calendar`, `gallery`, `tree-grid`, `diagram`, `master-detail`, `library`, `matrix`)
|
|
78
81
|
- [ ] A `matrix` view has a `viewConfig` with `rowAxis`, `colAxis`, and `cell` (cell `entity` matches the primary `dataSources` entity; `rowKey`/`colKey` are real cell columns)
|
|
@@ -89,6 +92,16 @@ For each entity:
|
|
|
89
92
|
- [ ] Every `reportCode` points to a report that exists
|
|
90
93
|
- [ ] `orderNum` is set on each node for deterministic ordering
|
|
91
94
|
|
|
95
|
+
## Reports (`ui/reports.json`)
|
|
96
|
+
|
|
97
|
+
- [ ] Params are declared in the report-level `parameters` block, or under `datasets.<cd>.params` — a param several datasets use belongs at report level (the installer merges both; report level wins on a collision)
|
|
98
|
+
- [ ] Params use `required` (not `isRequired`), and a lookup nests its binding as `params: { link: { entity } }` (not a top-level `link`)
|
|
99
|
+
- [ ] No param declares both `fieldTypeCd` and `domain`
|
|
100
|
+
- [ ] `layout` is an object (`{ "panels": [...] }`), and the chart kind is `config.chartType` with `vizType: "chart"`
|
|
101
|
+
- [ ] Record-report attachments (`entities`): every mapped param code is declared at report level or on a dataset; every source column is the PK, a reference (`R`) column, or a bounded scalar; cross-module `entityCd` is qualified and its module is a declared dependency
|
|
102
|
+
- [ ] A module with attachments declares `"metadata": ">=1.5.0"`
|
|
103
|
+
- [ ] No `check_…` / `calculate_…` action exists that a record report should be
|
|
104
|
+
|
|
92
105
|
## Security
|
|
93
106
|
|
|
94
107
|
- [ ] `security/roles.json` exists (at least one role)
|
|
@@ -172,9 +172,12 @@ to fill a sub-step** — a pure CRUD module skips Phase 2 entirely (record it wi
|
|
|
172
172
|
|
|
173
173
|
**2a Actions.** First check the action is an action at all: it must **change**
|
|
174
174
|
something. If the `execute:` block would only read, compute and `info()` the
|
|
175
|
-
answer, build a report
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
answer, build a **record report** (a report with an `entities` attachment — same
|
|
176
|
+
toolbar entry point, but it shows the working and re-reads live data), a formula
|
|
177
|
+
column, or an action that writes a result record instead — a printed number is
|
|
178
|
+
never stored and can't be re-read (`dforge://reference/action-dsl`, *When an
|
|
179
|
+
action is the wrong tool*). Do not compute in DSL what a report dataset already
|
|
180
|
+
aggregates. Then load
|
|
178
181
|
all four resources in the action row — the wrong-field-access /
|
|
179
182
|
wrong-batch-flag / wrong-`ui/actions.json`-property mistakes only surface when
|
|
180
183
|
you cross-check them. Draft the DSL, run
|
|
@@ -219,10 +222,31 @@ pointing at the views.
|
|
|
219
222
|
|
|
220
223
|
If none fire, skip specialized views for that entity.
|
|
221
224
|
|
|
222
|
-
**3c. Reports** only when aggregation/grouping isn't covered by views
|
|
225
|
+
**3c. Reports** only when aggregation/grouping isn't covered by views — or when
|
|
226
|
+
Phase 2a sent you here because the "action" only wanted to show a number.
|
|
227
|
+
|
|
228
|
+
Params are **report-scoped**: declare them in the report-level `parameters`
|
|
229
|
+
block, or as shorthand under `datasets.<cd>.params` when exactly one dataset uses
|
|
230
|
+
the param. The installer merges both into the report's one param set, report level
|
|
231
|
+
winning on a collision — so a param several datasets share belongs at report level.
|
|
232
|
+
Grant with a **colon** key: `"report:<code>": "E"`.
|
|
233
|
+
|
|
234
|
+
A report that answers a question *about one record* takes an `entities`
|
|
235
|
+
attachment, which puts it on that record's toolbar with the record's values
|
|
236
|
+
feeding its params:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
"entities": [ { "entityCd": "parties.party", "params": { "customer_id": "party_id" }, "orderNum": 45 } ]
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`entities[].params` is the **mapping** (report param → source column), not a
|
|
243
|
+
param declaration. Qualify a cross-module `entityCd` and declare that module as a
|
|
244
|
+
dependency; add `"metadata": ">=1.5.0"` too. Source columns are limited to the
|
|
245
|
+
PK, a reference column, or a bounded scalar. Full rules:
|
|
246
|
+
`dforge://reference/reports`.
|
|
223
247
|
|
|
224
248
|
**Exit:** every entity has a grid and a menu entry; every specialized view has a
|
|
225
|
-
stated trigger.
|
|
249
|
+
stated trigger; every report's params sit on a dataset.
|
|
226
250
|
|
|
227
251
|
## Phase 4 — Polish (mostly optional)
|
|
228
252
|
|