@dforge-core/dforge-mcp 0.2.24 → 0.2.26

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 CHANGED
@@ -4,6 +4,67 @@ 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.26
8
+
9
+ `logic/stored_procedures.json` is now something this server can describe. It had no
10
+ schema, no reference and no resource — yet a `datasetType: "S"` dataset is unusable
11
+ without it, because that file is what registers the function as the `spCd` a dataset
12
+ binds and the `sec_object` an `sp:` right is granted on. An author who reached the
13
+ install error demanding the file had nothing to read and guessed at its shape
14
+ (dForge-core#1191). `dforge://schema/stored-procedures` serves it now, and
15
+ `dforge://reference/reports` gained the declaration block, the `functionName` rule —
16
+ it names the FUNCTION, not the `logic/reports/*.sql` script that creates it, and
17
+ nothing in the file ever names a script — and the missing-declaration mistake.
18
+
19
+ **`reports.schema.json` was actively wrong about the field an SP dataset binds
20
+ through.** It declared `procedureName` under `additionalProperties: false`, so the
21
+ schema this server serves *rejected* the real `spCd` and accepted a name the platform
22
+ has never read. Re-vendored from `@dforge-core/metadata` 0.0.29, which fixes it.
23
+
24
+ The same bump corrects `entity.schema.json`, which had drifted two releases behind:
25
+ a column's `pattern` / `patternFlags` (format rules are metadata now, enforced in the
26
+ browser and again on `data.insert` / `data.update`) and `lockedFields` on an
27
+ accumulation register.
28
+
29
+ The references also stop contradicting themselves on parameter codes. A report
30
+ parameter must be spelled exactly as the procedure declares its `paramCd` —
31
+ `report.run` resolves each argument by that code alone and neither adds nor strips a
32
+ prefix — but the worked example named report parameters `customer_id` against a
33
+ procedure declaring `p_customer_id`, which binds NULL and makes the filter silently
34
+ disappear. Aligned, with the rule stated beside it.
35
+
36
+ **Still missing:** no tool here writes `logic/stored_procedures.json`, and
37
+ `dforge_report_add` takes its `datasets` map unvalidated — so an agent building an SP
38
+ report still emits the `.sql` file and the dataset and no declaration, which is the
39
+ shape that produced dForge-core#1191 in the first place.
40
+
41
+ ## 0.2.25
42
+
43
+ `sp:` is now a legal role-rights key. It always was, at install — `SecurityRegistrar`
44
+ resolves `sp:<cd>` and `sp:<module>.<cd>` against a stored procedure's sec_object, and
45
+ `report.run` enforces `E` on it for every `datasetType: "S"` dataset, on top of the
46
+ report's own right. This server's key validator did not know the prefix, so
47
+ `dforge_role_add` and `dforge_role_right_set` rejected the one grant such a report needs,
48
+ and nothing in the references mentioned it. A module built here with an SP-backed report
49
+ therefore shipped a report that answered `PERMISSION_DENIED` for everyone, unfixably: the
50
+ role-rights admin UI lists entities, actions and reports only, so no tenant admin can add
51
+ an SP grant afterwards (dForge-core#1146).
52
+
53
+ `report:` and `sp:` also accept the qualified `module.code` form now, which the installer
54
+ has always resolved cross-module. `action:` and `folder:` still do not — the installer
55
+ resolves those within the module, so a dotted one grants nothing.
56
+
57
+ `references/security.md` gains the two-grant rule; `references/reports.md` loses two
58
+ claims that were never true: that the platform injects `p_folder_uid` / `p_user_id` as an
59
+ SP function's first two params (it passes exactly the declared params and nothing else,
60
+ so such a function is uncallable and the install now says so), and that multi-result-set
61
+ refcursor datasets work (they install and return nothing). Both files also carry the SP param types the
62
+ runtime binds, now that dForge-core binds every scalar one (a `numeric` param used to go
63
+ over as text and fail the call) and rejects the rest at install.
64
+
65
+ The matching pack-time check lives in dForge-core (`ReportSpRightsValidator`), so
66
+ `dforge_module_pack` picks it up through the CLI it shells out to.
67
+
7
68
  ## 0.2.23
8
69
 
9
70
  The DSL checker was written twice. This server had one — regexes over
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.19/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
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.27/node_modules/@dforge-core/metadata/dist/index.js
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",
@@ -31633,8 +31633,9 @@ function isReadyToScaffold(moduleDir) {
31633
31633
  return fs.existsSync(v) && fs.readFileSync(v, "utf8").includes("readyToScaffold: true");
31634
31634
  }
31635
31635
  var RIGHTS_ENTITY = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/;
31636
- var RIGHTS_OBJECT = /^(action|report|folder):[a-z][a-z0-9_]*$/;
31637
- var RIGHTS_OBJECT_DOT = /^(action|report|folder)\.[a-z]/;
31636
+ var RIGHTS_OBJECT = /^(action|folder):[a-z][a-z0-9_]*$/;
31637
+ var RIGHTS_OBJECT_QUALIFIABLE = /^(report|sp):[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/;
31638
+ var RIGHTS_OBJECT_DOT = /^(action|report|sp|folder)\.[a-z]/;
31638
31639
  function assertValidRightKey(key) {
31639
31640
  if (RIGHTS_OBJECT_DOT.test(key)) {
31640
31641
  const fixed = key.replace(".", ":");
@@ -31642,9 +31643,9 @@ function assertValidRightKey(key) {
31642
31643
  `Rights key '${key}' uses a dot for an action/report/folder \u2014 use a colon: '${fixed}'. (A dot is only for cross-module entities like 'fin.invoice'.)`
31643
31644
  );
31644
31645
  }
31645
- if (RIGHTS_OBJECT.test(key) || RIGHTS_ENTITY.test(key)) return;
31646
+ if (RIGHTS_OBJECT.test(key) || RIGHTS_OBJECT_QUALIFIABLE.test(key) || RIGHTS_ENTITY.test(key)) return;
31646
31647
  throw new Error(
31647
- `Invalid rights key '${key}'. Use a same-module entity ('product'), a cross-module entity ('fin.invoice'), or a colon-prefixed object ('action:approve', 'report:summary', 'folder:east').`
31648
+ `Invalid rights key '${key}'. Use a same-module entity ('product'), a cross-module entity ('fin.invoice'), or a colon-prefixed object ('action:approve', 'report:summary', 'sp:rpt_totals', 'folder:east'). Only report: and sp: may be qualified ('report:fin.ar_aging').`
31648
31649
  );
31649
31650
  }
31650
31651
  function assertValidRightValue(key, value, allowEmpty) {
@@ -31659,13 +31660,13 @@ function assertValidRightValue(key, value, allowEmpty) {
31659
31660
  `Invalid rights '${value}' on '${key}'. Use S/I/U/D/C for entities, or 'E' for actions/reports/folders.`
31660
31661
  );
31661
31662
  }
31662
- const isObject2 = /^(action|report|folder):/.test(key);
31663
+ const isObject2 = /^(action|report|sp|folder):/.test(key);
31663
31664
  if (isObject2 && value !== "E") {
31664
31665
  throw new Error(`Object '${key}' takes 'E' (Execute), got '${value}'.`);
31665
31666
  }
31666
31667
  if (!isObject2 && value.includes("E")) {
31667
31668
  throw new Error(
31668
- `'${key}' is granted 'E' but has no action:/report:/folder: prefix \u2014 entity rights are S/I/U/D/C. If '${key}' is an action or report, prefix it (e.g. 'action:${key}' or 'report:${key}').`
31669
+ `'${key}' is granted 'E' but has no action:/report:/sp:/folder: prefix \u2014 entity rights are S/I/U/D/C. If '${key}' is an action or report, prefix it (e.g. 'action:${key}' or 'report:${key}').`
31669
31670
  );
31670
31671
  }
31671
31672
  }
@@ -34448,7 +34449,7 @@ function entityDelete(args) {
34448
34449
  // src/tools/action-add.ts
34449
34450
  var path9 = __toESM(require("path"));
34450
34451
 
34451
- // node_modules/.pnpm/@dforge-core+metadata@0.0.27/node_modules/@dforge-core/metadata/dist/dsl/index.js
34452
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.29/node_modules/@dforge-core/metadata/dist/dsl/index.js
34452
34453
  var PUNCT3 = ["===", "!=="];
34453
34454
  var PUNCT2 = [
34454
34455
  "==",
@@ -36654,7 +36655,7 @@ var roleAddSchema = {
36654
36655
  ),
36655
36656
  description: external_exports.string(),
36656
36657
  rights: external_exports.record(external_exports.string(), external_exports.string()).describe(
36657
- "Map: object code \u2192 rights string. Keys: same-module entity bare ('product'), cross-module entity dotted ('fin.invoice'), action/report/folder with a COLON prefix ('action:approve', 'report:summary', 'folder:east') \u2014 never a dot. Values: entities use 'S'/'I'/'U'/'D'/'C' (e.g. 'SIUDC'); actions/reports/folders use 'E'. To deny, omit the key (never map to '')."
36658
+ "Map: object code \u2192 rights string. Keys: same-module entity bare ('product'), cross-module entity dotted ('fin.invoice'), action/report/SP/folder with a COLON prefix ('action:approve', 'report:summary', 'sp:rpt_totals', 'folder:east') \u2014 never a dot. Values: entities use 'S'/'I'/'U'/'D'/'C' (e.g. 'SIUDC'); actions/reports/SPs/folders use 'E'. A report with a datasetType 'S' dataset needs BOTH 'report:<cd>' and 'sp:<spCd>'. To deny, omit the key (never map to '')."
36658
36659
  )
36659
36660
  };
36660
36661
  function roleAdd(args) {
@@ -36807,10 +36808,10 @@ var roleRightSetSchema = {
36807
36808
  moduleDir: external_exports.string(),
36808
36809
  roleCode: external_exports.string().describe("Role code as it appears in security/roles.json (e.g. 'crm.admin')."),
36809
36810
  object: external_exports.string().describe(
36810
- "Object to grant rights on. Same-module entity: bare ('product'). Cross-module entity: dotted ('fin.invoice'). Action/report/folder: COLON prefix ('action:approve', 'report:summary', 'folder:east') \u2014 never a dot."
36811
+ "Object to grant rights on. Same-module entity: bare ('product'). Cross-module entity: dotted ('fin.invoice'). Action/report/SP/folder: COLON prefix ('action:approve', 'report:summary', 'sp:rpt_totals', 'folder:east') \u2014 never a dot. A report whose dataset is datasetType 'S' ALSO needs 'sp:<spCd>' \u2014 report.run enforces both, and the admin UI cannot grant SP rights later."
36811
36812
  ),
36812
36813
  rights: external_exports.string().describe(
36813
- "Rights string. Entities: any combination of S/I/U/D/C (use '' to revoke all). Actions/reports/folders: 'E' or '' to revoke."
36814
+ "Rights string. Entities: any combination of S/I/U/D/C (use '' to revoke all). Actions/reports/SPs/folders: 'E' or '' to revoke."
36814
36815
  )
36815
36816
  };
36816
36817
  function roleRightSet(args) {
@@ -38042,6 +38043,11 @@ var resources = [
38042
38043
  "Reports JSON schema",
38043
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."
38044
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
+ ),
38045
38051
  {
38046
38052
  uri: "dforge://docs/conventions",
38047
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.24",
3
+ "version": "0.2.26",
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.19",
39
- "@dforge-core/metadata": "^0.0.27",
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
- "procedureName": {
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
+ }
@@ -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
- "as_of_date": { "fieldTypeCd": "date", "label": "As of Date", "required": true, "default": "=NOW()" },
91
- "customer_id": { "fieldTypeCd": "lookup", "label": "Customer", "required": false, "params": { "link": { "entity": "account" } } }
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,11 +100,22 @@ 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 (the function name **without** the schema prefix; resolved to `sp_id` at install). (**Not** `sp` or `procedureName`.)
104
- - `columnsDef` — **required** — the platform can't infer columns from a function.
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
+ - `columnsDef` — labels, widths and formats for the columns, keyed by column code. Optional
105
+ since the registrar derives a procedure's columns from its `RETURNS TABLE (…)` signature;
106
+ ship it when you want a label or a width rather than the bare column code. It is an
107
+ **override** — `report.run` projects `dForge.sp_column`, and `columnsDef` only restyles it.
108
+ A function returning an unnamed `SETOF <composite>` or a bare scalar declares no column
109
+ names, so that one needs an explicit `columns` block in `logic/stored_procedures.json`.
105
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.
106
117
 
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):
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.
108
119
 
109
120
  ```json
110
121
  "datasets": {
@@ -119,10 +130,8 @@ SP files live in `logic/reports/` and follow this convention:
119
130
 
120
131
  ```sql
121
132
  CREATE OR REPLACE FUNCTION crm.rpt_ar_aging(
122
- p_folder_uid uuid, -- REQUIRED: injected by platform (folder context)
123
- p_user_id bigint, -- REQUIRED: injected by platform (current user)
124
- p_as_of_date date DEFAULT NULL, -- User parameter
125
- p_customer_id bigint DEFAULT NULL -- User parameter (optional)
133
+ p_as_of_date date DEFAULT NULL, -- one per declared param, in declaration order
134
+ p_customer_id bigint DEFAULT NULL
126
135
  )
127
136
  RETURNS TABLE ( customer_name text, current_amount numeric, total numeric )
128
137
  LANGUAGE sql STABLE
@@ -135,10 +144,54 @@ $$;
135
144
  ```
136
145
 
137
146
  **Rules for SP functions:**
138
- - First two params are **always** `p_folder_uid uuid` and `p_user_id bigint` — injected by the platform.
139
- - User params come after, `DEFAULT NULL` for optional ones (order matches `params` declaration order).
140
- - `RETURNS TABLE (...)` for a single set; `RETURNS SETOF refcursor` for multi-set (mapped via `parentRef`).
141
- - Use the module's schema prefix (`crm.rpt_*`), `STABLE` volatility, and filter by `p_folder_uid` / `p_user_id` where needed — **security is your responsibility**.
147
+ - The signature is **exactly** the params declared in `logic/stored_procedures.json`, in that
148
+ order. `report.run` passes one positional argument per declared param and **nothing else** —
149
+ there is no folder or user argument, and a function taking one can never be called. Install
150
+ rejects the mismatch (`no callable PostgreSQL function … with N argument(s)`).
151
+ - `DEFAULT NULL` for optional params — an empty one arrives as a typed NULL.
152
+ - `pgType` must be a scalar type the runtime binds: `bigint`, `integer`, `smallint`,
153
+ `numeric`, `real`, `double precision`, `text`, `boolean`, `uuid`, `date`, `timestamp`,
154
+ `timestamptz`, `time`, or an alias of one. A type modifier is ignored, so
155
+ `numeric(18,2)` is fine. `jsonb`, arrays, enums and `citext` are rejected at install.
156
+ - `paramCd` is capped at 50 characters, and a declared `fieldTypeCd` must be a real one —
157
+ both are checked at install, because binding the SP in the report editor copies them
158
+ into report params.
159
+ - `RETURNS TABLE (...)`.
160
+ - Use the module's schema prefix (`crm.rpt_*`) and `STABLE` volatility.
161
+ - **Security is your responsibility, and the function has no context to work from**: it is not
162
+ told the folder or the user, so an SP dataset cannot be row-level scoped the way a `"Q"`
163
+ dataset is. Use `"Q"` for anything that must be filtered per user, and keep `"S"` for
164
+ aggregates that are safe for everyone holding the report's grant.
165
+ - Grant **both** `report:<code>` and `sp:<spCd>` — see `references/security.md`.
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`.
142
195
 
143
196
  ## Parameters
144
197
 
@@ -354,13 +407,22 @@ A dependency's report is granted with the qualified form, `"report:fin.ar_aging"
354
407
  - Setting the chart kind as `vizType` (`"vizType": "bar"`) — the panel `vizType` is `"chart"`; the kind goes in `config.chartType`.
355
408
  - Using `entityCode` / `groupBy` / `aggregations` on a dataset — use `query.entityCd` + `query.columns`, and aggregate in the viz (`agg`, `metrics`).
356
409
  - Using `sp` / `procedureName` for an SP dataset — the field is `spCd`.
357
- - Forgetting `columnsDef` on an SP dataset — **required**.
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.
416
+ - Assuming an SP dataset must carry `columnsDef` — it is an override over the columns the
417
+ registrar derives from the function signature, not the source of them.
358
418
  - Forgetting to grant `E` on the report in at least one role — it becomes invisible.
359
419
  - Writing the rights key as `report.<code>` — actions/reports/folders take a **colon**: `report:<code>`.
360
420
  - Writing `isRequired` instead of `required`, or a top-level `link` instead of `params.link` — both are ignored at install.
361
421
  - Declaring both `fieldTypeCd` and `domain` on one param — install rejects the pair rather than picking a winner.
362
422
  - Mapping a record-report attachment onto a param no dataset declares, or from a `text`/`json` column — both are rejected.
363
423
  - Attaching a report to another module's entity without declaring that module as a dependency.
364
- - Forgetting `p_folder_uid` / `p_user_id` as the first two SP function params — the call fails.
424
+ - Giving an SP function `p_folder_uid` / `p_user_id` params — the platform passes neither, so install rejects the function as uncallable. The signature is exactly the declared params.
425
+ - Granting only `report:<code>` on a report with a `datasetType: "S"` dataset — it also needs `sp:<spCd>`, or every user gets PERMISSION_DENIED on open.
426
+ - Declaring an SP param as `jsonb`, an array, an enum or `citext` — the runtime binds scalar types only (`bigint`, `integer`, `smallint`, `numeric`, `real`, `double precision`, `text`, `boolean`, `uuid`, `date`, `timestamp`, `timestamptz`, `time` and their aliases), and install rejects anything else. `citext` looks like `text` but is a distinct type PostgreSQL will not resolve a text argument against.
365
427
  - Referencing a parameter as `$param` — use `@param_code` in filters.
366
428
  - Writing an action that computes a number and `info()`s it, where a record report would show the working.
@@ -75,11 +75,11 @@ Example:
75
75
  | `D` | Delete (remove rows) |
76
76
  | `C` | Clone (duplicate a row as a new record) |
77
77
 
78
- ### Action/Report/Folder rights
78
+ ### Action/Report/SP/Folder rights
79
79
 
80
80
  | Letter | Permission |
81
81
  |---|---|
82
- | `E` | Execute (run the action/report, access the folder) |
82
+ | `E` | Execute (run the action/report/stored procedure, access the folder) |
83
83
 
84
84
  ### Common combinations
85
85
 
@@ -100,8 +100,13 @@ Role rights map object codes (entities, actions, reports) to rights strings. Obj
100
100
  - `fin.invoice` — the entity "invoice" in the `fin` module (cross-module entity — **dot**)
101
101
  - `action:send_welcome` — an action named "send_welcome" (**colon**)
102
102
  - `report:sales_pipeline` — a report named "sales_pipeline" (**colon**)
103
+ - `sp:rpt_sales_totals` — a stored procedure from `logic/stored_procedures.json` (**colon**)
103
104
  - `folder:customers` — the folder with code "customers" (**colon**)
104
105
 
106
+ `report:` and `sp:` also take a qualified form — `report:fin.ar_aging`, `sp:fin.rpt_ar_aging` — to
107
+ grant on a **dependency's** report or stored procedure. `action:` and `folder:` do not: the
108
+ installer resolves those within this module only, so a dotted one silently grants nothing.
109
+
105
110
  > **Separator matters — colon for non-entity objects, dot only for cross-module entities.**
106
111
  > Actions, reports and folders are prefixed with a **colon**: `action:<code>`, `report:<code>`,
107
112
  > `folder:<code>`. A **dot** is reserved for a cross-module *entity* (`module.entity`, e.g.
@@ -118,7 +123,28 @@ Role rights map object codes (entities, actions, reports) to rights strings. Obj
118
123
  4. **Never grant `D` (Delete) to a rep-level role** unless deletion is a normal part of their job. Audit trails prefer soft-delete or update.
119
124
  5. **Always grant the admin role `SIUDC` on everything the module owns.**
120
125
  6. **Action and report access** — grant `E` on specific action/report codes, or omit them entirely (defaulting to no access).
121
- 7. **Folder access** is granted separately — roles can reference folder codes, but folder definitions themselves live in `ui/folders.json`.
126
+ 7. **A report with a `datasetType: "S"` dataset needs TWO grants** — `report:<code>` **and**
127
+ `sp:<spCd>` for the stored procedure the dataset binds. `report.run` enforces the SP's own
128
+ sec_object on top of the report's right, so granting only the report yields
129
+ `PERMISSION_DENIED` on open. There is no recovery after install: the role-rights admin UI
130
+ lists entities, actions and reports only, so a tenant admin cannot add an SP grant by hand.
131
+ `dforge_module_pack` fails the build when a role grants such a report and no role grants
132
+ the SP — for the module's own SP. A dependency's SP is left to install: its grant may
133
+ ship with the dependency's own role, and rights are additive across the roles a user holds.
134
+
135
+ ```json
136
+ {
137
+ "sales.analyst": {
138
+ "description": "Sales Analyst",
139
+ "rights": {
140
+ "opportunity": "S",
141
+ "report:sales_totals": "E",
142
+ "sp:rpt_sales_totals": "E"
143
+ }
144
+ }
145
+ }
146
+ ```
147
+ 8. **Folder access** is granted separately — roles can reference folder codes, but folder definitions themselves live in `ui/folders.json`.
122
148
 
123
149
  ## Folders (`ui/folders.json`)
124
150