@dforge-core/dforge-mcp 0.2.43 → 0.2.44
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 +16 -0
- package/dist/server.js +25 -6
- package/package.json +3 -3
- package/resources/docs/dsl-reference.md +22 -4
- package/resources/schemas/entity.schema.json +13 -3
- package/resources/schemas/reports.schema.json +6 -2
- package/resources/schemas/translations.schema.json +21 -0
- package/skills/dforge-mcp-author/references/action-dsl.md +19 -2
- package/skills/dforge-mcp-author/references/filters.md +2 -0
- package/skills/dforge-mcp-author/references/print-templates.md +9 -1
- package/skills/dforge-mcp-author/references/reports.md +4 -1
- package/skills/dforge-mcp-author/references/translations.md +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to `@dforge-core/dforge-mcp`. This project uses semver-ish
|
|
|
4
4
|
`0.1.0-rc.N` pre-release tags; the published version is set at publish time via
|
|
5
5
|
the release workflow, so committed `package.json` versions are placeholders.
|
|
6
6
|
|
|
7
|
+
## 0.2.44 — 2026-10-21
|
|
8
|
+
|
|
9
|
+
**Schemas follow metadata 0.0.44.** `dforge://schema/translations` describes `storedProcedures`
|
|
10
|
+
(result-column headers of a stored procedure, shared by every report bound to it) and a report's
|
|
11
|
+
`desc`. `dforge://schema/reports` accepts `fieldTypeCd`, `format` and `baseDatatypeCd` on a dataset's
|
|
12
|
+
`columnsDef`. `dforge://schema/entity` accepts a trait declared in the module's own `traits.json`.
|
|
13
|
+
|
|
14
|
+
**DSL builtins.** `printPdf(entityCd, key, templateCd)` renders a print template to a PDF as the current
|
|
15
|
+
user; `sendEmail()` takes a fourth `attachments` argument (a `printPdf()` result or an array of them);
|
|
16
|
+
`downloadText()` / `downloadBase64()` hand the user a file the script built, in any code page.
|
|
17
|
+
|
|
18
|
+
**Needs `@dforge-core/dforge-cli` 0.2.35**, which compiles the above, requires a translation per
|
|
19
|
+
stored-procedure column on `dforge_module_pack` / `dforge_module_validate`, refuses a request filter
|
|
20
|
+
with an unknown operator instead of dropping it, and reads and generates every module file with LF
|
|
21
|
+
line endings on every platform.
|
|
22
|
+
|
|
7
23
|
## 0.2.43 — 2026-10-19
|
|
8
24
|
|
|
9
25
|
**Schemas follow metadata 0.0.43.** `dforge://schema/registers` describes `analyticTotals` (an
|
package/dist/server.js
CHANGED
|
@@ -31002,7 +31002,7 @@ var import_node_crypto = require("crypto");
|
|
|
31002
31002
|
var fs2 = __toESM(require("fs"));
|
|
31003
31003
|
var path2 = __toESM(require("path"));
|
|
31004
31004
|
|
|
31005
|
-
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.
|
|
31005
|
+
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.35/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
|
|
31006
31006
|
function buildManifest(opts, moduleId) {
|
|
31007
31007
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
31008
31008
|
const manifest = {
|
|
@@ -31290,7 +31290,7 @@ function buildZedTasks() {
|
|
|
31290
31290
|
var fs = __toESM(require("fs"));
|
|
31291
31291
|
var path = __toESM(require("path"));
|
|
31292
31292
|
|
|
31293
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
31293
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.44/node_modules/@dforge-core/metadata/dist/index.js
|
|
31294
31294
|
var AggType = {
|
|
31295
31295
|
Sum: "sum",
|
|
31296
31296
|
Count: "count",
|
|
@@ -34622,7 +34622,7 @@ var path10 = __toESM(require("path"));
|
|
|
34622
34622
|
var fs7 = __toESM(require("fs"));
|
|
34623
34623
|
var path9 = __toESM(require("path"));
|
|
34624
34624
|
|
|
34625
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
34625
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.44/node_modules/@dforge-core/metadata/dist/dsl/index.js
|
|
34626
34626
|
var PUNCT3 = ["===", "!=="];
|
|
34627
34627
|
var PUNCT2 = [
|
|
34628
34628
|
"==",
|
|
@@ -35474,9 +35474,9 @@ var BUILTINS = [
|
|
|
35474
35474
|
},
|
|
35475
35475
|
{
|
|
35476
35476
|
name: "sendEmail",
|
|
35477
|
-
signature: "sendEmail(to, subjectOrTemplate, dataOrBody?)",
|
|
35477
|
+
signature: "sendEmail(to, subjectOrTemplate, dataOrBody?, attachments?)",
|
|
35478
35478
|
returns: "void",
|
|
35479
|
-
doc: "Queue an outbound email. Three arguments, and the **second** picks the mode: `sendEmail(to, 'order.shipped', { ref: [code] })` sends a template, `sendEmail(to, 'Your order shipped', '<p>\u2026</p>')` sends raw HTML. A second argument with no spaces that contains `.` or `_` is read as a template code \u2014 so a raw subject like `'Shipped.'` silently becomes one; give a raw subject spaces.
|
|
35479
|
+
doc: "Queue an outbound email. Three arguments, and the **second** picks the mode: `sendEmail(to, 'order.shipped', { ref: [code] })` sends a template, `sendEmail(to, 'Your order shipped', '<p>\u2026</p>')` sends raw HTML. A second argument with no spaces that contains `.` or `_` is read as a template code \u2014 so a raw subject like `'Shipped.'` silently becomes one; give a raw subject spaces. The optional **fourth** argument attaches files: a `printPdf()` result, or an array of them (`[a, b]`; pass a single file as itself, because a one-name `[a]` reads as a field reference in the DSL). Only a `printPdf()` result can be attached, and only by the user who generated it (or a workspace admin); at most 5 files and 10 MB in all per email (`Email:MaxAttachments`, `Email:MaxAttachmentBytes`). A file the sender may not attach fails the action. Not available in a scheduled job, whose emails with attachments are not queued."
|
|
35480
35480
|
},
|
|
35481
35481
|
// External calls
|
|
35482
35482
|
{
|
|
@@ -35515,7 +35515,19 @@ var BUILTINS = [
|
|
|
35515
35515
|
name: "download",
|
|
35516
35516
|
signature: "download(url, fileName?)",
|
|
35517
35517
|
returns: "void",
|
|
35518
|
-
doc: "Set a download URL on the action result; the browser opens it when the action returns. It fetches nothing and stores nothing \u2014 the URL must already serve the bytes, e.g. the `/api/temp-file/{token}` a service handed back. To put a remote file INTO storage, fetch it with `callApi()` and write it to a file column."
|
|
35518
|
+
doc: "Set a download URL on the action result; the browser opens it when the action returns. It fetches nothing and stores nothing \u2014 the URL must already serve the bytes, e.g. the `/api/temp-file/{token}` a service handed back. To hand over a file the script builds, use `downloadText()` / `downloadBase64()`. To put a remote file INTO storage, fetch it with `callApi()` and write it to a file column."
|
|
35519
|
+
},
|
|
35520
|
+
{
|
|
35521
|
+
name: "downloadText",
|
|
35522
|
+
signature: "downloadText(content, fileName, encoding?, mimeType?)",
|
|
35523
|
+
returns: "string",
|
|
35524
|
+
doc: "Hand the user a text file the script built: encodes `content` in `encoding` (`utf-8` by default; `windows-1251`, `cp866`, `koi8-u`\u2026 also work), stores it in temp storage and sets it as the action's download. Line endings are written as given (join with `'\\r\\n'` for CR LF), no BOM. Returns the signed `/api/temp-file/{token}` URL, valid for an hour (`FileStorage:TempTokenExpirationMinutes`). An action has one download: a later call replaces the earlier. In a background or scheduled run nobody receives the download, so pass the URL on in `notify()`. Max 10 MB per file, 10 files per run."
|
|
35525
|
+
},
|
|
35526
|
+
{
|
|
35527
|
+
name: "downloadBase64",
|
|
35528
|
+
signature: "downloadBase64(base64, fileName, mimeType?)",
|
|
35529
|
+
returns: "string",
|
|
35530
|
+
doc: "`downloadText()` for bytes: decodes `base64` and hands it to the user as `fileName` (`application/octet-stream` unless `mimeType` says otherwise). Returns the signed download URL."
|
|
35519
35531
|
},
|
|
35520
35532
|
{
|
|
35521
35533
|
name: "getSetting",
|
|
@@ -35568,6 +35580,13 @@ var BUILTINS = [
|
|
|
35568
35580
|
returns: "link",
|
|
35569
35581
|
entityArg: 0,
|
|
35570
35582
|
doc: "Build a clickable link to a record, for an `entitylink` column. Always pass a **qualified** code \u2014 `entity_cd` is only unique per module."
|
|
35583
|
+
},
|
|
35584
|
+
{
|
|
35585
|
+
name: "printPdf",
|
|
35586
|
+
signature: "printPdf(entityCd, key, templateCd)",
|
|
35587
|
+
returns: "object",
|
|
35588
|
+
entityArg: 0,
|
|
35589
|
+
doc: "Render a print template to a PDF **as the current user** and return the stored file as `{ fileId, fileName, mimeType, size, checksum }` \u2014 a descriptor, never the bytes. `key` is the record's `{ column: value }` key (or a scalar for a single-column key); `templateCd` is the template's code on that entity. The record is read under the user's rights and the folder's row filter, as the Generate PDF button reads it. Always pass a **qualified** entity code. Not available in a scheduled job or a background action (no signed-in user), refused in a computing-only run, and limited to 10 per execution; each render holds the action's transaction open while it works."
|
|
35571
35590
|
}
|
|
35572
35591
|
];
|
|
35573
35592
|
var BUILTIN_BY_NAME = new Map(BUILTINS.map((b) => [b.name, b]));
|
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.44",
|
|
4
4
|
"description": "MCP server for dForge module authoring. Exposes 35 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",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"node": ">=18"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@dforge-core/dforge-cli": "^0.2.
|
|
30
|
-
"@dforge-core/metadata": "^0.0.
|
|
29
|
+
"@dforge-core/dforge-cli": "^0.2.35",
|
|
30
|
+
"@dforge-core/metadata": "^0.0.44",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
32
|
"zod": "^4.4.3"
|
|
33
33
|
},
|
|
@@ -503,8 +503,8 @@ Send a notification to a specific user's inbox.
|
|
|
503
503
|
notify([approver_id], 'New purchase request from ' + [requester_name] + ' awaiting approval')
|
|
504
504
|
```
|
|
505
505
|
|
|
506
|
-
#### `sendEmail(to, subjectOrTemplateCd, bodyOrData?)`
|
|
507
|
-
Two modes:
|
|
506
|
+
#### `sendEmail(to, subjectOrTemplateCd, bodyOrData?, attachments?)`
|
|
507
|
+
Two modes (a second argument with no spaces that contains `.` or `_` is read as a template code, so give a raw subject spaces). The optional fourth argument attaches files: a `printPdf()` result, or an array of them (a single file as itself, not `[a]`). Max 5 files and 10 MB; only the user who generated a PDF, or a workspace admin, can attach it; not in a scheduled job.
|
|
508
508
|
|
|
509
509
|
```dsl
|
|
510
510
|
// Raw mode: (to, subject, body)
|
|
@@ -567,8 +567,25 @@ if (info.fileSize > 5 * 1024 * 1024) { error('Receipt larger than 5 MB') }
|
|
|
567
567
|
if (info.mimeType != 'application/pdf') { error('Receipt must be PDF') }
|
|
568
568
|
```
|
|
569
569
|
|
|
570
|
+
#### `printPdf(entityCd, key, templateCd)`
|
|
571
|
+
Render a print template to a PDF as the current user and return the archived file as `{ fileId, fileName, mimeType, size, checksum }`; attach it with `sendEmail()`. Pass a qualified entity code. Not in a scheduled job or a background action; max 10 per run.
|
|
572
|
+
|
|
573
|
+
```dsl
|
|
574
|
+
execute:
|
|
575
|
+
var pdf = printPdf('fin.invoice', { invoice_id: [invoice_id] }, 'invoice')
|
|
576
|
+
sendEmail([customer_id].[email], 'Your invoice ' + [number], '<p>Attached.</p>', pdf)
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
#### `downloadText(content, fileName, encoding?, mimeType?)` / `downloadBase64(base64, fileName, mimeType?)`
|
|
580
|
+
Hand the user a file the script built. `encoding` defaults to `utf-8`; code pages such as `windows-1251` and `cp866` work; line endings are written as given, no BOM. Both return the signed download URL, which a background or scheduled run can pass on in `notify()`. One download per action (a later call replaces the earlier); max 10 MB per file, 10 files per run.
|
|
581
|
+
|
|
582
|
+
```dsl
|
|
583
|
+
execute:
|
|
584
|
+
downloadText(csv, 'payments.txt', 'windows-1251')
|
|
585
|
+
```
|
|
586
|
+
|
|
570
587
|
#### `download(url, fileName?)`
|
|
571
|
-
Set HTTP download response. Only valid in UI-triggered actions; the user's browser receives the file.
|
|
588
|
+
Set HTTP download response. Only valid in UI-triggered actions; the user's browser receives the file. It fetches nothing: the URL must already serve the bytes.
|
|
572
589
|
|
|
573
590
|
```dsl
|
|
574
591
|
execute:
|
|
@@ -1180,7 +1197,8 @@ The `audit` trait auto-maintains `last_updated` on INSERT/UPDATE if the column e
|
|
|
1180
1197
|
| Run SQL | `query('SELECT ... WHERE c = @v', { v: value })` |
|
|
1181
1198
|
| Get setting | `getSetting('setting_cd')` |
|
|
1182
1199
|
| Send toast | `info('msg')`, `warn('msg')` |
|
|
1183
|
-
| Send email | `sendEmail(to, subj, body)` or `sendEmail(to, templateCd, dataObj)` |
|
|
1200
|
+
| Send email | `sendEmail(to, subj, body)` or `sendEmail(to, templateCd, dataObj)`; add a `printPdf()` result as the 4th argument to attach |
|
|
1201
|
+
| Give the user a built file | `downloadText(content, 'f.txt', 'windows-1251')` |
|
|
1184
1202
|
| Send inbox notification | `notify(userId, 'msg')` |
|
|
1185
1203
|
| Call a shared helper | `round2([amount])` from `logic/functions/*.dsl` |
|
|
1186
1204
|
| Call another module's helper | `cdy_accounting.roleOf(...)`, imported in `deps/<module>.json` |
|
|
@@ -108,10 +108,20 @@
|
|
|
108
108
|
},
|
|
109
109
|
"traits": {
|
|
110
110
|
"type": "array",
|
|
111
|
-
"description": "Entity traits expanded during module install (authoring shortcut).
|
|
111
|
+
"description": "Entity traits expanded during module install (authoring shortcut): a platform trait, or one the module declares in its own traits.json. Pack and install refuse a name that is neither; the schema cannot see traits.json, so it accepts any trait-shaped name and lists only the platform traits.",
|
|
112
112
|
"items": {
|
|
113
|
-
"
|
|
114
|
-
|
|
113
|
+
"anyOf": [
|
|
114
|
+
{
|
|
115
|
+
"type": "string",
|
|
116
|
+
"enum": ["identity", "audit", "audit-full", "soft-delete", "sorting", "postable", "accumulation", "ledger", "period", "file-library"],
|
|
117
|
+
"description": "Platform trait."
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"type": "string",
|
|
121
|
+
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
|
|
122
|
+
"description": "A trait declared in the module's own traits.json."
|
|
123
|
+
}
|
|
124
|
+
]
|
|
115
125
|
},
|
|
116
126
|
"uniqueItems": true
|
|
117
127
|
},
|
|
@@ -401,9 +401,13 @@
|
|
|
401
401
|
|
|
402
402
|
"columnDef": {
|
|
403
403
|
"type": "object",
|
|
404
|
+
"description": "Override of one column the dataset already projects (from the entity, the stored procedure's result columns, or the register). Name only what differs: each key replaces the projected value. A label here is a literal and wins over a translated header.",
|
|
404
405
|
"properties": {
|
|
405
|
-
"label": { "type": "string" },
|
|
406
|
-
"
|
|
406
|
+
"label": { "type": "string", "description": "Column header. Not translated: leave it out to keep the entity's or the procedure's translated header." },
|
|
407
|
+
"fieldTypeCd": { "type": "string", "description": "Control the value renders with, replacing the projected one." },
|
|
408
|
+
"format": { "type": "string", "description": "Display format, replacing the column's display format." },
|
|
409
|
+
"baseDatatypeCd": { "type": "string", "description": "Read only where the dataset projects no columns of its own (a stored procedure with no result columns), when columnsDef is the column list. Ignored as an override: the projected column's datatype stands." },
|
|
410
|
+
"visible": { "type": "boolean", "description": "Accepted, but neither report.run nor the client reads it: it has no effect." },
|
|
407
411
|
"width": { "type": "integer", "minimum": 1 }
|
|
408
412
|
},
|
|
409
413
|
"additionalProperties": false
|
|
@@ -84,6 +84,23 @@
|
|
|
84
84
|
"$ref": "#/$defs/report"
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
|
+
"storedProcedures": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"description": "Stored-procedure code (logic/stored_procedures.json) → result-column headers. A header belongs to the procedure, not to a report: every dataset bound to the procedure shows the same translation. A dataset's columnsDef label overrides it with a literal, so leave the label out of columnsDef to keep the column translated. Required, per declared column, by `module pack` / `module validate` for each non-English supported locale; install only warns.",
|
|
90
|
+
"additionalProperties": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"additionalProperties": false,
|
|
93
|
+
"properties": {
|
|
94
|
+
"columns": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"description": "Result column code → header.",
|
|
97
|
+
"additionalProperties": {
|
|
98
|
+
"$ref": "#/$defs/labelOnly"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
87
104
|
"actions": {
|
|
88
105
|
"type": "object",
|
|
89
106
|
"description": "Action code (ui/actions.json) → label, tooltip and parameter labels.",
|
|
@@ -248,6 +265,10 @@
|
|
|
248
265
|
"label": {
|
|
249
266
|
"$ref": "#/$defs/label"
|
|
250
267
|
},
|
|
268
|
+
"desc": {
|
|
269
|
+
"type": "string",
|
|
270
|
+
"description": "Translated report description."
|
|
271
|
+
},
|
|
251
272
|
"datasets": {
|
|
252
273
|
"type": "object",
|
|
253
274
|
"description": "Dataset code → caption.",
|
|
@@ -249,6 +249,14 @@ var note = params[note] // no quotes around param name in this DSL
|
|
|
249
249
|
> `null` key **throws** (a `col = NULL` predicate can never match), and array keys are rejected —
|
|
250
250
|
> loop and mutate per id instead.
|
|
251
251
|
|
|
252
|
+
> **Posting state.** `update()` setting a register posting state (the `A` / `L` column) posts or unposts
|
|
253
|
+
> the document through the register engine, in a trigger action and in a background run alike; a refused
|
|
254
|
+
> posting rolls the caller's write back. A background run holds no transaction, so such a call opens its
|
|
255
|
+
> own: atomic per call, not per run. A scheduled run cannot post with `update()`.
|
|
256
|
+
|
|
257
|
+
> **Keys of any type.** A field saved with `[field] = value` binds the record key by the key column's own
|
|
258
|
+
> type, so a `uuid` or a digits-only `varchar` key works.
|
|
259
|
+
|
|
252
260
|
> **These writes raise no events.** `insert()`, `update()`, `delete()` and `query()` are the raw
|
|
253
261
|
> write layer: by design they skip folder row-filters, closed-period checks and registry state, and
|
|
254
262
|
> they raise no trigger or webhook events either. Only the **acted-on record's own**
|
|
@@ -339,7 +347,16 @@ error('Insufficient stock. Available: {available}, requested: {requested}',
|
|
|
339
347
|
| Function | Description |
|
|
340
348
|
|---|---|
|
|
341
349
|
| `sendEmail(to, subject, htmlBody)` | Send a raw email. `to` is an email address string. |
|
|
342
|
-
| `sendEmail(to,
|
|
350
|
+
| `sendEmail(to, templateCode, dataObj)` | Send a templated email. The **second** argument picks the mode: one with no spaces that contains `.` or `_` is read as a template code, so give a raw subject spaces. |
|
|
351
|
+
| `sendEmail(to, subjectOrTemplate, dataOrBody, attachments)` | The optional fourth argument attaches files: a `printPdf()` result, or an array of them (`[a, b]`; pass a single file as itself, because a one-name `[a]` reads as a field reference). Only a `printPdf()` result can be attached, and only by the user who generated it or a workspace admin. At most 5 files and 10 MB per email. A file the sender may not attach fails the action. A scheduled job's email with attachments fails and rolls back. |
|
|
352
|
+
|
|
353
|
+
### PDF and file output
|
|
354
|
+
|
|
355
|
+
| Function | Returns | Description |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| `printPdf('module.entity', key, templateCd)` | `{ fileId, fileName, mimeType, size, checksum }` | Render a print template to a PDF **as the current user** (the record is read under their rights and the folder's row filter) and archive it. A descriptor, never the bytes: attach it with `sendEmail()`. Always pass a qualified entity code. Not available in a scheduled job or a background action, refused in a computing-only run, max 10 per run; each render holds the action's transaction open. |
|
|
358
|
+
| `downloadText(content, fileName, encoding?, mimeType?)` | URL string | Hand the user a text file the script built. `encoding` is `utf-8` by default; `windows-1251`, `cp866`, `koi8-u` and other code pages work, and a character the encoding cannot represent is an error. Line endings are written as given (join with `'\r\n'` for CR LF), no BOM. Returns the signed download URL (valid an hour). An action has one download: a later call replaces the earlier. In a background or scheduled run nobody receives it, so pass the URL on in `notify()`. Max 10 MB per file, 10 files per run. |
|
|
359
|
+
| `downloadBase64(base64, fileName, mimeType?)` | URL string | `downloadText()` for bytes; `application/octet-stream` unless `mimeType` says otherwise. |
|
|
343
360
|
|
|
344
361
|
### External API Calls
|
|
345
362
|
|
|
@@ -350,7 +367,7 @@ error('Insufficient stock. Available: {available}, requested: {requested}',
|
|
|
350
367
|
| `getFileUrl(fileFieldValue)` | String | Generate a temporary signed download URL (relative path) for a file field. Use in email templates or notifications — not for `callApi()`. |
|
|
351
368
|
| `getSecret('secret_cd')` | String | Retrieve a decrypted secret value (API keys, tokens). Secrets are managed in the admin UI. |
|
|
352
369
|
| `getFileInfo(fileFieldValue)` | Object \| null | File metadata for a file field. |
|
|
353
|
-
| `download(url, fileName?)` | — |
|
|
370
|
+
| `download(url, fileName?)` | — | Set a download URL on the action result; the browser opens it when the action returns. It fetches and stores nothing: the URL must already serve the bytes. To hand over a file the script builds, use `downloadText()` / `downloadBase64()`. |
|
|
354
371
|
| `callService('name', { args })` | Any | Invoke a registered host service. |
|
|
355
372
|
|
|
356
373
|
### Files, settings, document extraction
|
|
@@ -311,6 +311,7 @@ The user never sees a record unless it passes **all** layers. You don't need to
|
|
|
311
311
|
|
|
312
312
|
- **`null` filter** (or omitted) means no filtering — show everything the user has access to.
|
|
313
313
|
- **Single condition** is valid without a group wrapper.
|
|
314
|
+
- **An unknown operator is refused**, not dropped: a request filter (`data.get`, export, reports, a view's query, DSL `select()`) with an unknown operator, an unknown group combinator or an item that is neither condition nor group fails with `INVALID_REQUEST` naming the operator, its column and the valid set. `like` is not an operator: `mask` is SQL `LIKE`, `start` a case-insensitive prefix. A condition on a column the entity does not have is still dropped.
|
|
314
315
|
- Use **short operators** (`=`, `!=`, `>`) or **enum keys** (`eq`, `notEq`, `gr`) — both work.
|
|
315
316
|
- For folder `rowFilter` in module packages, prefer the short compact form: `{ "c": "...", "o": "eq", "v": ... }`.
|
|
316
317
|
- Dates are ISO strings: `"2026-01-01"`.
|
|
@@ -320,6 +321,7 @@ The user never sees a record unless it passes **all** layers. You don't need to
|
|
|
320
321
|
|
|
321
322
|
- Using the JSON filter format inside `query()` in DSL actions — **wrong**. `query()` uses raw SQL.
|
|
322
323
|
- Wrapping a single condition in a group unnecessarily — **valid but noisy**. A bare condition is fine.
|
|
324
|
+
- Using `"o": "like"` — **wrong**, refused. Use `"o": "mask"` with `%`.
|
|
323
325
|
- Using `"operator": "equals"` — **wrong**. Use `"o": "="` or `"o": "eq"`.
|
|
324
326
|
- Using `"column": "status"` — **wrong**. Use `"c": "status"`.
|
|
325
327
|
- Forgetting that `null` means "no filter" (not "filter by null") — to filter for null values, use `{ "c": "field", "o": "null" }`.
|
|
@@ -8,7 +8,7 @@ Lives in: `ui/print_templates.json` + HTML/CSS files under `print_templates/`. S
|
|
|
8
8
|
|
|
9
9
|
| Path | Engine | Used by |
|
|
10
10
|
| ----------- | --------------- | -------------------------------------------------------- |
|
|
11
|
-
| **Server** | Scriban (Liquid) | `print.render` RPC
|
|
11
|
+
| **Server** | Scriban (Liquid) | `print.render` RPC; `print.renderPdf` (Generate PDF, headless Chromium); `printPdf()` in a DSL action |
|
|
12
12
|
| **Client** | LiquidJS | Editor preview — fast iteration |
|
|
13
13
|
|
|
14
14
|
Filters are mirrored on both sides so the preview matches the print.
|
|
@@ -67,6 +67,12 @@ Every column on the entity is a top-level variable. Display formatting is pre-ap
|
|
|
67
67
|
|
|
68
68
|
Inside a child-set loop, the same shape applies per row: `line._fmt.<col>`, `line._raw.<col>`, `line._color.<col>`.
|
|
69
69
|
|
|
70
|
+
A loop over a document's set can read a set of the line's own entity off each line (`{% for l in lines %}{% for a in l.attributes %}`): detected by the template reading it, loaded for every line in one query under the same rights and row filter, one level deep. The editor preview does not fill it.
|
|
71
|
+
|
|
72
|
+
A printed set, nested or not, needs **Select** on its entity in the folder, as `data.get` does; one the user cannot read prints empty. `print.render` and `print.renderPdf` check Select on the printed entity.
|
|
73
|
+
|
|
74
|
+
A currency column prints its glyph for the common codes (`EUR` → `€`, `USD` → `$`, `GBP` → `£`, `UAH` → `₴`, `PLN` → `zł`); other codes print as the code. `format_currency` follows the same mapping.
|
|
75
|
+
|
|
70
76
|
Formula columns (`F`) are read in the same SQL as the record, as `data.get` does, so a formula that navigates references or calls a SQL function prints what the grid shows, and `TODAY()` / `NOW()` work. A formula the SQL translator cannot express prints empty and the response carries a warning naming the column.
|
|
71
77
|
|
|
72
78
|
**Sets: loading and order.** A set is loaded when the template's Liquid code (`{% … %}` or `{{ … }}`) mentions its name anywhere: a `for`, a filter, a condition, `.size`, `.first`. Plain text, `{% comment %}` / `{% raw %}` blocks, string literals and the member of a path (`rec.lines`) do not count, and a snippet's code is not scanned (mention the set in the host template). Rows come in primary-key order; sort in the template:
|
|
@@ -170,6 +176,8 @@ my_module/
|
|
|
170
176
|
- **Forgetting `_raw` for codes.** Direct `{{ status }}` is the label (`"Partially Paid"`). For CSS classes / wire-format payloads / predicates against codes, use `{{ _raw.status }}` (`"P"`).
|
|
171
177
|
- **Manually formatting dates/currency.** Don't reach for `format_currency` when the column is already `fieldTypeCd: "currency"` — `_fmt.<col>` already matches the form. Use the filter only for derived values.
|
|
172
178
|
- **Expecting a set to load only from a `for`.** It loads wherever the template names it (`sort`, `.size`, `.first`). A set read only inside a snippet is not seen: mention it in the host template.
|
|
179
|
+
- **Testing emptiness with `blank` / `empty`.** LiquidJS (preview) knows them, Scriban (print) does not, so the comparison is always false on the server for an unset field. Use `{% unless x %}`, `{% if x and x != '' %}` or `x != nil`.
|
|
180
|
+
- **Dashed comment tags.** The server cuts every `{%- comment -%}` block before parsing, but keep snippets to plain `{% comment %}` so both engines agree.
|
|
173
181
|
- **Snippet `template_cd` with a dot.** `{% include 'my_module.foo.bar' %}` — the resolver splits on the last dot only, so `my_module.foo` becomes the module name and breaks. No dots in snippet codes.
|
|
174
182
|
- **Dropping CSS in `<style>` blocks inline.** The renderer wires `<style>` for you; declare CSS in the separate `.css` file (or inline string via the `css` field).
|
|
175
183
|
- **Printing an unsorted set.** Rows come in primary-key order; use `| sort: 'col'` (and `| reverse`) in the template.
|
|
@@ -78,7 +78,7 @@ A report has a `label`, a `description`, a `datasets` map, a `layout` object (`{
|
|
|
78
78
|
- `query.columns` — column codes; supports dot navigation (`customer.name`).
|
|
79
79
|
- `query.filter` — canonical filter (`{c,o,v}` or `{g,i:[...]}`); use `@param_code` to reference a parameter.
|
|
80
80
|
- `query.sort` — `[{ c, d }]` where `d` ∈ `asc | desc`.
|
|
81
|
-
- `columnsDef` — optional per-column display overrides (`{ label,
|
|
81
|
+
- `columnsDef` — optional per-column display overrides (`{ label, fieldTypeCd, format, width }`; each key replaces the projected value). `label` is a literal and wins over a translated header. `visible` is accepted but read by nothing. `baseDatatypeCd` is read only where the dataset projects no columns of its own.
|
|
82
82
|
|
|
83
83
|
### Stored procedure dataset (developer path)
|
|
84
84
|
|
|
@@ -110,6 +110,9 @@ Key differences from entity datasets:
|
|
|
110
110
|
**override** — `report.run` projects `dForge.sp_column`, and `columnsDef` only restyles it.
|
|
111
111
|
A function returning an unnamed `SETOF <composite>` or a bare scalar declares no column
|
|
112
112
|
names, so that one needs an explicit `columns` block in `logic/stored_procedures.json`.
|
|
113
|
+
Leave `label` out of `columnsDef` to keep the column's translated header: result-column headers
|
|
114
|
+
translate under the procedure, `storedProcedures.<sp_cd>.columns.<column_cd>.label`
|
|
115
|
+
(see `references/translations.md`), and every dataset bound to the procedure shows it.
|
|
113
116
|
- 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.
|
|
114
117
|
- **The parameter code must match the procedure's declared `paramCd` exactly.** `report.run`
|
|
115
118
|
resolves each argument by that code alone — no prefix is added or stripped — so a report
|
|
@@ -83,7 +83,8 @@ Translation files mirror the module's content structure. Each translatable objec
|
|
|
83
83
|
| `views` | Data view labels and descriptions (`label`, `desc`) | View codes (keys from `ui/data_views.json`) → `{ label, desc }` |
|
|
84
84
|
| `menus` | Menu node labels (root + nested items) | Menu root key → `{ label, items: { item_code: { label } } }` matching `ui/menus.json` structure |
|
|
85
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: … } } } }` |
|
|
86
|
+
| `reports` | Report `label`, `desc`, dataset captions, param labels **and param dropdown option labels** | Report codes → `{ datasets: { ds_cd: { caption } }, params: { param_cd: { label, options: { value: … } } } }` |
|
|
87
|
+
| `storedProcedures` | Result-column headers of a stored procedure (`logic/stored_procedures.json`), shared by every report bound to it | Procedure codes → `{ columns: { column_cd: { label } } }` |
|
|
87
88
|
| `entities.<e>.constraints` | Check/unique constraint violation messages (opt-in — warned, not enforced) | Constraint codes → `{ message }` under the owning entity |
|
|
88
89
|
| `messages` | DSL `info` / `warn` / `error` / `exit` texts (opt-in — warned, not enforced) | Exact English text → translated text |
|
|
89
90
|
| `entities.<e>.fields.<f>.options` | Dropdown/radio/flags **option labels** (opt-in) | Option `value` → localized label (string) or `{ label, icon, color }` |
|
|
@@ -269,6 +270,10 @@ Non-English locales are declared in `supportedLocales` (an array of `ll-CC` tags
|
|
|
269
270
|
|
|
270
271
|
Every listed locale MUST have a matching `translations/<locale>.json` with a label for each translatable resource, or install fails completeness validation.
|
|
271
272
|
|
|
273
|
+
### Stored-procedure column headers
|
|
274
|
+
|
|
275
|
+
A result column's header belongs to the procedure, not to a report. `module pack` / `module validate` require one `storedProcedures.<sp_cd>.columns.<column_cd>.label` per declared column for each non-English supported locale; install only warns (also when a translation names a column the module lacks). A dataset's `columnsDef` label is a literal and overrides it, so leave the label out to keep the column translated.
|
|
276
|
+
|
|
272
277
|
## Rules
|
|
273
278
|
|
|
274
279
|
1. **Always provide `en-US`** as the base language. Other languages fall back to it for missing keys.
|