@dforge-core/dforge-mcp 0.2.25 → 0.2.27
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 +52 -0
- package/dist/server.js +8 -3
- package/package.json +3 -3
- package/resources/schemas/entity.schema.json +10 -2
- package/resources/schemas/reports.schema.json +2 -2
- package/resources/schemas/stored-procedures.schema.json +125 -0
- package/skills/dforge-mcp-author/references/formulas.md +38 -3
- package/skills/dforge-mcp-author/references/reports.md +44 -3
- package/skills/dforge-module-build/SKILL.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,58 @@ 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.27
|
|
8
|
+
|
|
9
|
+
**`dforge://reference/formulas` told authors that one-hop navigation is synchronous and
|
|
10
|
+
"resolved instantly".** It is not: on the record card and in grids every `[ref].[field]`
|
|
11
|
+
resolves after the initial load. Read at face value, it made a self-referencing
|
|
12
|
+
`toString` — `[parent].[name]` behind a `{display_path}` placeholder — look safe, and in
|
|
13
|
+
platform builds before dForge-core#1247 that pattern froze the browser. The reference now
|
|
14
|
+
states the real rule, and the sync/async section says which is which.
|
|
15
|
+
|
|
16
|
+
- **Text `+` with a NULL side gives NULL**, on the card and in reports alike — documented,
|
|
17
|
+
with `COALESCE` / `IF` for a part that may be missing. The platform's client used to join
|
|
18
|
+
the NULL as empty text while SQL returned NULL; dForge-core#1247 made them agree.
|
|
19
|
+
- **New "Computed toString" example.** A `toString` placeholder may name a formula column,
|
|
20
|
+
and it now renders everywhere the display does. Evaluated for the `toString`, text `+`
|
|
21
|
+
joins a NULL side as empty text so a caption never goes blank — which is why the
|
|
22
|
+
hierarchy-path example tests the parent with `IF`, not `COALESCE(a + b, c)`.
|
|
23
|
+
- `dforge-module-build` points from its `toString` rule to that example.
|
|
24
|
+
|
|
25
|
+
## 0.2.26
|
|
26
|
+
|
|
27
|
+
`logic/stored_procedures.json` is now something this server can describe. It had no
|
|
28
|
+
schema, no reference and no resource — yet a `datasetType: "S"` dataset is unusable
|
|
29
|
+
without it, because that file is what registers the function as the `spCd` a dataset
|
|
30
|
+
binds and the `sec_object` an `sp:` right is granted on. An author who reached the
|
|
31
|
+
install error demanding the file had nothing to read and guessed at its shape
|
|
32
|
+
(dForge-core#1191). `dforge://schema/stored-procedures` serves it now, and
|
|
33
|
+
`dforge://reference/reports` gained the declaration block, the `functionName` rule —
|
|
34
|
+
it names the FUNCTION, not the `logic/reports/*.sql` script that creates it, and
|
|
35
|
+
nothing in the file ever names a script — and the missing-declaration mistake.
|
|
36
|
+
|
|
37
|
+
**`reports.schema.json` was actively wrong about the field an SP dataset binds
|
|
38
|
+
through.** It declared `procedureName` under `additionalProperties: false`, so the
|
|
39
|
+
schema this server serves *rejected* the real `spCd` and accepted a name the platform
|
|
40
|
+
has never read. Re-vendored from `@dforge-core/metadata` 0.0.29, which fixes it.
|
|
41
|
+
|
|
42
|
+
The same bump corrects `entity.schema.json`, which had drifted two releases behind:
|
|
43
|
+
a column's `pattern` / `patternFlags` (format rules are metadata now, enforced in the
|
|
44
|
+
browser and again on `data.insert` / `data.update`) and `lockedFields` on an
|
|
45
|
+
accumulation register.
|
|
46
|
+
|
|
47
|
+
The references also stop contradicting themselves on parameter codes. A report
|
|
48
|
+
parameter must be spelled exactly as the procedure declares its `paramCd` —
|
|
49
|
+
`report.run` resolves each argument by that code alone and neither adds nor strips a
|
|
50
|
+
prefix — but the worked example named report parameters `customer_id` against a
|
|
51
|
+
procedure declaring `p_customer_id`, which binds NULL and makes the filter silently
|
|
52
|
+
disappear. Aligned, with the rule stated beside it.
|
|
53
|
+
|
|
54
|
+
**Still missing:** no tool here writes `logic/stored_procedures.json`, and
|
|
55
|
+
`dforge_report_add` takes its `datasets` map unvalidated — so an agent building an SP
|
|
56
|
+
report still emits the `.sql` file and the dataset and no declaration, which is the
|
|
57
|
+
shape that produced dForge-core#1191 in the first place.
|
|
58
|
+
|
|
7
59
|
## 0.2.25
|
|
8
60
|
|
|
9
61
|
`sp:` is now a legal role-rights key. It always was, at install — `SecurityRegistrar`
|
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.20/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 = {
|
|
@@ -31283,7 +31283,7 @@ function buildZedTasks() {
|
|
|
31283
31283
|
var fs = __toESM(require("fs"));
|
|
31284
31284
|
var path = __toESM(require("path"));
|
|
31285
31285
|
|
|
31286
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
31286
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.29/node_modules/@dforge-core/metadata/dist/index.js
|
|
31287
31287
|
var AggType = {
|
|
31288
31288
|
Sum: "sum",
|
|
31289
31289
|
Count: "count",
|
|
@@ -34449,7 +34449,7 @@ function entityDelete(args) {
|
|
|
34449
34449
|
// src/tools/action-add.ts
|
|
34450
34450
|
var path9 = __toESM(require("path"));
|
|
34451
34451
|
|
|
34452
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
34452
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.29/node_modules/@dforge-core/metadata/dist/dsl/index.js
|
|
34453
34453
|
var PUNCT3 = ["===", "!=="];
|
|
34454
34454
|
var PUNCT2 = [
|
|
34455
34455
|
"==",
|
|
@@ -38043,6 +38043,11 @@ var resources = [
|
|
|
38043
38043
|
"Reports JSON schema",
|
|
38044
38044
|
"JSON Schema for ui/reports.json \u2014 map of report code \u2192 { description, layout.panels[], datasets, parameters? }. Datasets and panel queries reuse the filter shape from data_views."
|
|
38045
38045
|
),
|
|
38046
|
+
schema(
|
|
38047
|
+
"stored-procedures",
|
|
38048
|
+
"Stored procedures JSON schema",
|
|
38049
|
+
"JSON Schema for logic/stored_procedures.json \u2014 map of stored-procedure code \u2192 { functionName, schemaName?, params?, columns? }. Required for any report dataset with datasetType 'S': the .sql file under logic/reports/ creates the function, this file registers it as the spCd a dataset binds and the sec_object an sp: right is granted on. functionName names the FUNCTION, never the script."
|
|
38050
|
+
),
|
|
38046
38051
|
{
|
|
38047
38052
|
uri: "dforge://docs/conventions",
|
|
38048
38053
|
name: "dForge module conventions",
|
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.27",
|
|
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.20",
|
|
39
|
+
"@dforge-core/metadata": "^0.0.29",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
41
41
|
"zod": "^4.4.3"
|
|
42
42
|
},
|
|
@@ -363,6 +363,15 @@
|
|
|
363
363
|
},
|
|
364
364
|
"serverDefault": {
|
|
365
365
|
"$ref": "#/$defs/serverDefault"
|
|
366
|
+
},
|
|
367
|
+
"pattern": {
|
|
368
|
+
"type": "string",
|
|
369
|
+
"description": "Format rule for this column, as an ECMAScript regex source. Overrides the field type's own pattern (field_type.def_params.pattern, which is what makes email/phone/url reject a malformed value). Enforced in the browser and again on data.insert / data.update, so keep to constructs both engines read alike: [0-9] and \\d are the same there, \\s is normalized to JavaScript's set, and a pattern that will not compile is skipped by both rather than enforced by one."
|
|
370
|
+
},
|
|
371
|
+
"patternFlags": {
|
|
372
|
+
"type": "string",
|
|
373
|
+
"description": "Flags for 'pattern', as a JavaScript regex literal would carry them; 'i' and 'm' are the two honoured, each at most once — the RegExp constructor rejects a repeated flag, and a rule the browser cannot compile is skipped there and would then be enforced by the API alone. JavaScript has no inline (?i), so case-insensitivity has to be stated here.",
|
|
374
|
+
"pattern": "^(i|m|im|mi)?$"
|
|
366
375
|
}
|
|
367
376
|
}
|
|
368
377
|
},
|
|
@@ -569,7 +578,7 @@
|
|
|
569
578
|
"accumulationConfig": {
|
|
570
579
|
"type": "object",
|
|
571
580
|
"description": "Configuration for simple accumulation (document without lines, e.g. receipt → stock).",
|
|
572
|
-
"required": ["balanceEntity", "dimensions", "resources"],
|
|
581
|
+
"required": ["balanceEntity", "dimensions", "resources", "lockedFields"],
|
|
573
582
|
"properties": {
|
|
574
583
|
"balanceEntity": { "type": "string", "description": "Balance entity code (e.g. 'stock')" },
|
|
575
584
|
"dimensions": {
|
|
@@ -596,7 +605,6 @@
|
|
|
596
605
|
"allowNegative": { "type": "boolean", "default": true },
|
|
597
606
|
"lockedFields": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "description": "Document fields locked when posted. Required; [\"*\"] freezes the whole row." }
|
|
598
607
|
},
|
|
599
|
-
"required": ["lockedFields"],
|
|
600
608
|
"additionalProperties": false
|
|
601
609
|
},
|
|
602
610
|
"signConfig": {
|
|
@@ -249,9 +249,9 @@
|
|
|
249
249
|
"$ref": "#/$defs/query",
|
|
250
250
|
"description": "Used when datasetType is 'Q'"
|
|
251
251
|
},
|
|
252
|
-
"
|
|
252
|
+
"spCd": {
|
|
253
253
|
"type": "string",
|
|
254
|
-
"description": "Used when datasetType is 'S'"
|
|
254
|
+
"description": "Used when datasetType is 'S': the stored-procedure code, as `logic/stored_procedures.json` keys it — NOT the PostgreSQL function name and never schema-qualified. `module.spCd` binds a dependency's procedure. Resolved to `report_ds.sp_id` at install. A role granting `report:<code>` must also grant `sp:<spCd>`, which no admin UI can add afterwards."
|
|
255
255
|
},
|
|
256
256
|
"columnsDef": {
|
|
257
257
|
"type": "object",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "dForge Module Stored Procedures",
|
|
4
|
+
"description": "Stored-procedure declarations for a dForge module package (logic/stored_procedures.json). Each entry registers one PostgreSQL function as a procedure a report dataset can bind with `datasetType: \"S\"` and `spCd`. The SQL that creates the function ships separately, as a CREATE OR REPLACE FUNCTION script under logic/reports/; this file does not name the script, it names the function the script creates. See docs/business-logic/stored-procedures.md.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"required": ["functionName"],
|
|
9
|
+
"description": "Keyed by stored-procedure code (sp_cd) — module-internal, referenced from a dataset's `spCd` and a role's `sp:` right. It may not contain a dot: a dot is the module qualifier in both of those key spaces.",
|
|
10
|
+
"properties": {
|
|
11
|
+
"description": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "Label for the procedure, shown in the report editor's procedure picker. Defaults to the code."
|
|
14
|
+
},
|
|
15
|
+
"schemaName": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"pattern": "^[a-zA-Z_][a-zA-Z0-9_\\-]*$",
|
|
18
|
+
"description": "PostgreSQL schema holding the function. Defaults to the module's own schema, which is what a module-shipped function normally uses."
|
|
19
|
+
},
|
|
20
|
+
"functionName": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"pattern": "^[a-zA-Z_][a-zA-Z0-9_\\-]*$",
|
|
23
|
+
"description": "The PostgreSQL function's own name, unqualified — `report.run` calls SELECT * FROM \"schemaName\".\"functionName\"(...). Required: it is the one fact this file exists to record, and install fails naming it when absent."
|
|
24
|
+
},
|
|
25
|
+
"returnsTable": {
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"default": true,
|
|
28
|
+
"description": "The function is set-returning (RETURNS TABLE / SETOF). The only shape report.run projects."
|
|
29
|
+
},
|
|
30
|
+
"params": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"description": "The call signature, in order: report.run passes exactly one positional argument per entry and NOTHING else — no folder, no user. A function taking p_folder_uid / p_user_id can never be called, and install rejects the arity mismatch. Omit the block for a function taking no arguments.",
|
|
33
|
+
"items": { "$ref": "#/$defs/param" }
|
|
34
|
+
},
|
|
35
|
+
"columns": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"description": "Result columns, stored in dForge.sp_column and used to project the dataset. Omit the block and the registrar derives them from the function's own RETURNS TABLE / OUT column names. Declare it to set labels, widths, a currency/percent control or a refEntityCd link — a declared list is used as written and never merged with the signature. A function returning an unnamed SETOF composite or a bare scalar declares no column names, so it must list them here.",
|
|
38
|
+
"items": { "$ref": "#/$defs/column" }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"additionalProperties": false
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
"$defs": {
|
|
45
|
+
"param": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"required": ["paramCd"],
|
|
48
|
+
"properties": {
|
|
49
|
+
"paramCd": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"maxLength": 50,
|
|
52
|
+
"description": "Parameter code. Conventionally the function's own argument name (p_customer_id), though the call is positional."
|
|
53
|
+
},
|
|
54
|
+
"pgType": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"default": "bigint",
|
|
57
|
+
"description": "PostgreSQL type of the argument — this is what binds the value, and it is carried even when the value is null so an omitted optional argument arrives as a TYPED null. Defaults to `bigint`, so declare it for every argument that is not one. One of: bigint, integer, smallint, numeric, real, double precision, text, boolean, uuid, date, timestamp, timestamptz, time — or any alias PostgreSQL treats as the same type (int8, int4, int, int2, decimal, float8, float4, varchar, character varying, char, bpchar, bool, timestamp without time zone, timestamp with time zone, time without time zone). Case does not matter and a type modifier is ignored, so `numeric(18,2)` may be pasted straight from the signature. jsonb, arrays, enums and citext cannot be bound and are rejected at install. Enforced by SpParamTypes, which is where this list lives — left out of an enum here so the spellings install accepts are not flagged by an editor.",
|
|
58
|
+
"examples": ["bigint", "text", "date", "numeric(18,2)", "timestamptz", "uuid", "boolean"]
|
|
59
|
+
},
|
|
60
|
+
"fieldTypeCd": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "Control the report editor copies into the report param when the procedure is bound there. Optional — derived from pgType when omitted."
|
|
63
|
+
},
|
|
64
|
+
"label": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Parameter label for that same copy."
|
|
67
|
+
},
|
|
68
|
+
"required": {
|
|
69
|
+
"type": "boolean",
|
|
70
|
+
"default": true,
|
|
71
|
+
"description": "Whether the parameter must have a value. Give every optional one DEFAULT NULL in the function signature."
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"additionalProperties": false
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
"column": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": ["columnCd"],
|
|
80
|
+
"properties": {
|
|
81
|
+
"columnCd": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"maxLength": 100,
|
|
84
|
+
"description": "Result column name as the function returns it. Unique within the procedure."
|
|
85
|
+
},
|
|
86
|
+
"label": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"description": "Column header. Defaults to the code."
|
|
89
|
+
},
|
|
90
|
+
"fieldTypeCd": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"default": "text",
|
|
93
|
+
"description": "Control that renders the value (number, currency, date, dropdown, flags, ...)."
|
|
94
|
+
},
|
|
95
|
+
"baseDatatypeCd": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"default": "string",
|
|
98
|
+
"description": "Underlying datatype the value is read as."
|
|
99
|
+
},
|
|
100
|
+
"orderNum": {
|
|
101
|
+
"type": "integer",
|
|
102
|
+
"description": "Display order. Defaults to declaration order in steps of 10."
|
|
103
|
+
},
|
|
104
|
+
"refEntityCd": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"description": "Entity this column links to, making the value clickable. Resolved within this module and its dependencies."
|
|
107
|
+
},
|
|
108
|
+
"refColumnCd": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"description": "Column in the result holding the FK value for that link, when it is not this column."
|
|
111
|
+
},
|
|
112
|
+
"width": {
|
|
113
|
+
"type": "integer",
|
|
114
|
+
"description": "Column width in pixels."
|
|
115
|
+
},
|
|
116
|
+
"params": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"description": "Control parameters, the same shape an entity column's `params` takes — `options` for a dropdown or flags, `currency`, `align`, and so on.",
|
|
119
|
+
"additionalProperties": true
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"additionalProperties": false
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -44,6 +44,13 @@ so `IF([completed_date] != null, 100, 0)` gives the same result on the record ca
|
|
|
44
44
|
a report. At SQL time `=` / `!=` translate to `IS NOT DISTINCT FROM` / `IS DISTINCT FROM`
|
|
45
45
|
to preserve that.
|
|
46
46
|
|
|
47
|
+
**Text `+` with a NULL side gives NULL**, on the record card and in a report alike:
|
|
48
|
+
`[first_name] + ' ' + [last_name]` is NULL when either name is missing. Wrap a part that may
|
|
49
|
+
be missing — `[first_name] + ' ' + COALESCE([last_name], '')` — or test for it with `IF`.
|
|
50
|
+
The one exception is a formula evaluated for an entity's `toString` (see
|
|
51
|
+
[Computed toString](#computed-tostring)), where a NULL side joins as empty text so a caption
|
|
52
|
+
never goes blank.
|
|
53
|
+
|
|
47
54
|
## Examples
|
|
48
55
|
|
|
49
56
|
```
|
|
@@ -205,7 +212,7 @@ and the JSON shape: `column-types.md` → "Roll-up totals over child rows — us
|
|
|
205
212
|
[account].[primary_contact].[phone] -- chained
|
|
206
213
|
```
|
|
207
214
|
|
|
208
|
-
Navigation works through `columnType: "R"` columns (reference columns) — every step except the last must be a Reference, and the last reads a field on the final referenced record.
|
|
215
|
+
Navigation works through `columnType: "R"` columns (reference columns) — every step except the last must be a Reference, and the last reads a field on the final referenced record. On the record card and in grids, any navigation — one hop or several — is **asynchronous**: the engine resolves it after the initial data load, from the referenced rows it loads alongside. A formula may navigate back into its own entity (`[parent].[name]` on a self-reference); a formula that reads itself round a loop, directly or through other records, comes back NULL.
|
|
209
216
|
|
|
210
217
|
**SQL-time (reports, filters, sorts).** Multi-hop navigation works here too: QueryBuilder pre-scans the formula and registers one `LEFT JOIN` per hop, then the translator resolves the path against the final alias. A nav path may also land on a *formula* column of the referenced entity — that formula is translated against the referenced entity's alias, and its own hops are joined off that alias. The same nav map drives formula columns in filters and ORDER BY, so filtering and sorting a report by a nav-formula column works.
|
|
211
218
|
|
|
@@ -222,8 +229,10 @@ Warning wording follows the clause, because the same unresolvable path costs a d
|
|
|
222
229
|
|
|
223
230
|
## Sync vs async formulas
|
|
224
231
|
|
|
225
|
-
- **Sync formula**:
|
|
226
|
-
- **Async formula**:
|
|
232
|
+
- **Sync formula**: no navigation — fields of the same record only. Evaluated on load and on every edit to a dependency.
|
|
233
|
+
- **Async formula**: any navigation (`[ref].[field]`). Evaluated after the initial data load, re-evaluated when a dependency changes — as is every formula that reads it.
|
|
234
|
+
|
|
235
|
+
At SQL time (reports, filters, sorts, reference display) both kinds are translated to SQL, navigation as `LEFT JOIN`s.
|
|
227
236
|
|
|
228
237
|
You don't declare which is which — the engine detects it from the formula's AST.
|
|
229
238
|
|
|
@@ -263,6 +272,32 @@ The constraint's `message` (the violation text shown to users) is **localizable*
|
|
|
263
272
|
}
|
|
264
273
|
```
|
|
265
274
|
|
|
275
|
+
### Computed toString
|
|
276
|
+
|
|
277
|
+
A `toString` placeholder may name a formula column — the way to build a display from more
|
|
278
|
+
than plain fields, e.g. a hierarchy path:
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
"toString": "{display_path}",
|
|
282
|
+
"fields": {
|
|
283
|
+
"display_path": {
|
|
284
|
+
"columnType": "F",
|
|
285
|
+
"fieldTypeCd": "text",
|
|
286
|
+
"baseDatatypeCd": "string",
|
|
287
|
+
"flags": "V",
|
|
288
|
+
"formula": "IF([parent_id] = null, [name], [parent].[name] + ' / ' + [name])",
|
|
289
|
+
"description": "Display Path"
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The formula renders wherever the `toString` does — form caption, reference cells, pickers —
|
|
295
|
+
on the client and in SQL alike. Evaluated for the `toString`, text `+` joins a NULL side as
|
|
296
|
+
empty text, so the caption never goes blank; the same column in a grid or report keeps
|
|
297
|
+
NULL. Test for a missing parent with `IF`, not `COALESCE(a + b, c)`: in the caption `a + b`
|
|
298
|
+
is not NULL when one part is missing, so the fallback never applies. The path is one level
|
|
299
|
+
deep — `[parent].[name]` reads the parent's plain name.
|
|
300
|
+
|
|
266
301
|
### Line total (quantity × price)
|
|
267
302
|
|
|
268
303
|
```json
|
|
@@ -87,8 +87,8 @@ For SQL the query builder can't express — window functions, CTEs, cross-schema
|
|
|
87
87
|
"datasetType": "S",
|
|
88
88
|
"spCd": "rpt_ar_aging",
|
|
89
89
|
"params": {
|
|
90
|
-
"
|
|
91
|
-
"
|
|
90
|
+
"p_as_of_date": { "fieldTypeCd": "date", "label": "As of Date", "required": true, "default": "=NOW()" },
|
|
91
|
+
"p_customer_id": { "fieldTypeCd": "lookup", "label": "Customer", "required": false, "params": { "link": { "entity": "account" } } }
|
|
92
92
|
},
|
|
93
93
|
"columnsDef": {
|
|
94
94
|
"customer_name": { "label": "Customer", "fieldTypeCd": "text", "baseDatatypeCd": "string", "width": 200 },
|
|
@@ -100,7 +100,7 @@ For SQL the query builder can't express — window functions, CTEs, cross-schema
|
|
|
100
100
|
|
|
101
101
|
Key differences from entity datasets:
|
|
102
102
|
- `datasetType: "S"`.
|
|
103
|
-
- `spCd` — the stored-procedure code
|
|
103
|
+
- `spCd` — the stored-procedure **code**: the key of an entry in `logic/stored_procedures.json`, never schema-qualified, resolved to `sp_id` at install. By convention it is spelled the same as the function it declares, but it is the code that is matched, not the function name. (**Not** `sp` or `procedureName`.)
|
|
104
104
|
- `columnsDef` — labels, widths and formats for the columns, keyed by column code. Optional
|
|
105
105
|
since the registrar derives a procedure's columns from its `RETURNS TABLE (…)` signature;
|
|
106
106
|
ship it when you want a label or a width rather than the bare column code. It is an
|
|
@@ -108,6 +108,12 @@ Key differences from entity datasets:
|
|
|
108
108
|
A function returning an unnamed `SETOF <composite>` or a bare scalar declares no column
|
|
109
109
|
names, so that one needs an explicit `columns` block in `logic/stored_procedures.json`.
|
|
110
110
|
- 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.
|
|
111
|
+
- **The parameter code must match the procedure's declared `paramCd` exactly.** `report.run`
|
|
112
|
+
resolves each argument by that code alone — no prefix is added or stripped — so a report
|
|
113
|
+
offering `customer_id` against a procedure declaring `p_customer_id` passes NULL and the
|
|
114
|
+
filter silently does nothing. Prefix the report parameter too (as above), or drop the
|
|
115
|
+
prefix from `paramCd`; the SQL argument name is unconstrained either way, the call being
|
|
116
|
+
positional.
|
|
111
117
|
|
|
112
118
|
**Multi-result-set SPs** map extra datasets to the same function via `parentDatasetCd` (the dataset that owns the SP call) + `parentRef` (the named refcursor). **Not wired at runtime**: both columns install, but `report.run` executes only datasets holding their own `sp_id`, so a child dataset returns nothing. Give each dataset its own function until that changes.
|
|
113
119
|
|
|
@@ -158,6 +164,35 @@ $$;
|
|
|
158
164
|
aggregates that are safe for everyone holding the report's grant.
|
|
159
165
|
- Grant **both** `report:<code>` and `sp:<spCd>` — see `references/security.md`.
|
|
160
166
|
|
|
167
|
+
### The declaration — `logic/stored_procedures.json`
|
|
168
|
+
|
|
169
|
+
The function existing in the database is not enough: this file is what creates the
|
|
170
|
+
`stored_procedure` row an `spCd` resolves to and the `sec_object` an `sp:` right is
|
|
171
|
+
granted on. Keyed by stored-procedure code, which may not contain a dot.
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"rpt_ar_aging": {
|
|
176
|
+
"description": "AR Aging",
|
|
177
|
+
"functionName": "rpt_ar_aging",
|
|
178
|
+
"params": [
|
|
179
|
+
{ "paramCd": "p_as_of_date", "pgType": "date", "fieldTypeCd": "date", "label": "As of Date", "required": true },
|
|
180
|
+
{ "paramCd": "p_customer_id", "pgType": "bigint", "fieldTypeCd": "lookup", "label": "Customer", "required": false }
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- **`functionName` is required** — the function's own name, unqualified. It is **not** the
|
|
187
|
+
path of the `.sql` file: nothing here names a script, because every `logic/reports/*.sql`
|
|
188
|
+
runs at install regardless. Omitting it fails the package at load, on `pack`/`validate`
|
|
189
|
+
and every install path alike.
|
|
190
|
+
- **`schemaName`** is optional, defaulting to the module's own schema.
|
|
191
|
+
- **`params`** is the call signature, in order — omit it for a zero-argument function.
|
|
192
|
+
- **`columns`** is optional; without it install derives the result columns from the
|
|
193
|
+
function's `RETURNS TABLE (…)`.
|
|
194
|
+
- Schema: `stored_procedures.schema.json`.
|
|
195
|
+
|
|
161
196
|
## Parameters
|
|
162
197
|
|
|
163
198
|
Parameters prompt the user before running the report.
|
|
@@ -372,6 +407,12 @@ A dependency's report is granted with the qualified form, `"report:fin.ar_aging"
|
|
|
372
407
|
- Setting the chart kind as `vizType` (`"vizType": "bar"`) — the panel `vizType` is `"chart"`; the kind goes in `config.chartType`.
|
|
373
408
|
- Using `entityCode` / `groupBy` / `aggregations` on a dataset — use `query.entityCd` + `query.columns`, and aggregate in the viz (`agg`, `metrics`).
|
|
374
409
|
- Using `sp` / `procedureName` for an SP dataset — the field is `spCd`.
|
|
410
|
+
- Shipping an SP's `.sql` file and its dataset but no `logic/stored_procedures.json` entry —
|
|
411
|
+
the function exists, the dataset names something, and nothing connects the two.
|
|
412
|
+
- Writing `file` / `function` in `logic/stored_procedures.json` — the one required field is
|
|
413
|
+
`functionName`, and it names the function, not the script that creates it.
|
|
414
|
+
- Naming a report parameter `customer_id` for a procedure param declared `p_customer_id` —
|
|
415
|
+
the lookup is by exact code, so the argument binds NULL and the filter quietly disappears.
|
|
375
416
|
- Assuming an SP dataset must carry `columnsDef` — it is an override over the columns the
|
|
376
417
|
registrar derives from the function signature, not the source of them.
|
|
377
418
|
- Forgetting to grant `E` on the report in at least one role — it becomes invisible.
|
|
@@ -80,6 +80,8 @@ Enough to author inline; load the linked reference for the full detail.
|
|
|
80
80
|
`created_by`/`last_updated_by` with no default — a seeded `audit-full` entity
|
|
81
81
|
must set both to `0` in every record, or use plain `audit`. → `traits`
|
|
82
82
|
- **`toString`** on every entity, `{column}` braces: `"{first_name} {last_name}"`.
|
|
83
|
+
A computed display (a hierarchy path) goes in a formula column named as the
|
|
84
|
+
placeholder. → `formulas` (Computed toString)
|
|
83
85
|
- **Data views:** `dataSources` array at root — never a root-level `entityCode` +
|
|
84
86
|
`columns`. Sort is `"order": ["-col", "col"]`. → `data-views`
|
|
85
87
|
- **Menus:** leaf items use `dataViewCode` (not `viewCode`); section nodes omit
|