@dforge-core/metadata 0.0.42 → 0.0.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 +39 -1
- package/dist/dsl/index.js +22 -3
- package/dist/dsl/index.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/entity.schema.json +15 -3
- package/schemas/registers.schema.json +60 -3
- package/schemas/reports.schema.json +6 -2
- package/schemas/translations.schema.json +21 -0
- package/src/dsl/builtins.ts +22 -3
- package/src/registers/check-binding.ts +19 -1
- package/src/registers/types.ts +2 -0
- package/src/reports.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforge-core/metadata",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.44",
|
|
4
4
|
"description": "Canonical dForge authoring registries — field types, base datatypes, column types, traits, data-view kinds and chart types — with derivation helpers. Runtime-dependency-free; the single source of truth shared by editors, the web app and the CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dForge",
|
|
@@ -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
|
},
|
|
@@ -359,6 +369,7 @@
|
|
|
359
369
|
"map": { "type": "object", "additionalProperties": { "oneOf": [ { "type": "string" }, { "type": "object", "required": ["debit", "credit"], "additionalProperties": false, "properties": { "debit": { "type": "string" }, "credit": { "type": "string" } } } ] }, "description": "Register dimension or resource name -> this document's column carrying it. On an accounting (L) column a resource or dimension the register splits by side maps to { \"debit\": \"<line column>\", \"credit\": \"<line column>\" } instead of one column. Every resource the register declares must be mapped (install refuses otherwise); a dimension left out binds to a column of the same name. Only meaningful with 'register'. On an accumulation (A) column a value may be a one-hop reference path, '<reference column>.<target column>', read through the reference at posting time (off the line when the binding posts from lines), or an expression: a string starting with '=' holding an F-column formula over the same row with one-hop reference reads — \"=COALESCE([transit_warehouse_id], [dest_warehouse_id])\"." },
|
|
360
370
|
"headerMap": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Register dimension or resource name -> this DOCUMENT's column carrying it, for a binding that posts from LINES. A line binding reads every other value off the line, and a dimension that belongs to the document (a dealer, an organisation, a date) cannot be put on the line: a G column is a same-row expression or an aggregate over a child set, so it cannot read a parent. Explicit rather than a fallback, because 'not on the line, try the header' turns a typo into a silent read of an unrelated header column with the same name. Install refuses: a name on neither table, a field also named in 'map', a column also read off the line by 'map', a column that is part of the line's key, a column 'lockedFields' does not freeze (the posting is keyed by it, so it has to hold still while the document is posted), and a RESOURCE unless the register is 'info' (a quantity from the document would be added once per line; an information register's records replace rather than add). A value may be a one-hop reference path off the document, whose reference key column 'lockedFields' must then freeze, or an expression over the document ('=...', see 'map')." },
|
|
361
371
|
"groupLinesBy": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true, "description": "Line columns whose distinct values each post ONE movement, instead of one per line — a production report posting 'this unit worked today' once per unit with a count of 1. 'map' is then read over a group row: the grouped line columns, every column of the document, and one-hop reads through a reference of either, so an expression may mix the document and the group key. A quoted constant is stamped once per group; a numeric line column mapped to a resource is summed over the group. Install refuses: a binding that posts from the document, an L column, a grouped column the line does not store or 'lockedLineFields' does not freeze, a line column read that is neither grouped nor a summed resource, a non-numeric summed resource, a column both grouped and summed, an ungrouped line resource on an 'info' register, a name both the line and the document carry, and a document column read that 'lockedFields' does not freeze. Accumulation (A) columns only." },
|
|
372
|
+
"lineDateField": { "type": "string", "description": "A line column dating that line's movement instead of 'dateField' — for lines that happen on days of their own under one document date (a limit card issuing through the month and dated at its end). A line that leaves it empty moves on 'dateField'. With 'periodEntity', each line's period must exist and be open, at posting and unposting and at every write to the line, judged by the date the movement is stamped with (the line's or the document's, after 'dateTrunc'). On an information register that records changes only, two lines may not write one key: post one line per key. Install refuses: a register that keeps no date, a binding that posts from the document, a column that is not a stored 'date' or 'timestamp' without time zone, one 'lockedLineFields' does not freeze, one 'groupLinesBy' omits when lines are grouped, and a binding with 'periodDimField'. Accumulation (A) columns only." },
|
|
362
373
|
"dateField": { "type": "string", "description": "This document's date column, supplying the date the register stamps on each journal row. Required once the register declares a 'journalDate' — one writer that supplied none would make every balance read as of a date quietly short, across all writers. On an accumulation (A) column it may be a one-hop reference path, '<reference column>.<target column>', or an expression over the document ('=...', see 'map')." },
|
|
363
374
|
"dateTrunc": { "type": "string", "enum": ["day", "month", "year"], "description": "Takes the date 'dateField' supplies back to the start of its day, month or year before the register stamps it — for a document that carries a time while the register's records sit on whole days. The calendar is the value's as stored; no time zone is applied. Needs 'dateField'. Accumulation (A) columns only." },
|
|
364
375
|
"skipPostingWhenEmpty": { "type": "array", "items": { "type": "string" }, "description": "THE EXCEPTION, not the normal way to model this — read the guide before declaring it. Register dimensions (by the register's names) whose empty value means THIS POSTING records nothing: the line, or the document when the binding posts from the document. One posting is skipped, never the register and never the document. When that is all the document would have recorded it is left unposted to this register, and the flag is per binding, so its other registers post as usual. An empty dimension NOT listed refuses the posting, naming the row and the dimension. It leaves a document fully posted while some of its lines produced no movement, which usually means the document is modelling two kinds of line in one table: since 'lineEntity' is declared per BINDING, prefer giving each register its own line table (goods lines post to stock, service lines do not) so that every line of each table posts. Declare this only when the distinction is not structural but lives in the data — a batch or serial number filled on some rows and not others — so there is no second table to put those rows in. Accumulation (A) columns only. Formerly skipWhenNull: a package using the old name is refused at install, while a tenant's already-stored params keep being read under it." },
|
|
@@ -381,6 +392,7 @@
|
|
|
381
392
|
"dependencies": {
|
|
382
393
|
"headerMap": ["lineEntity"],
|
|
383
394
|
"groupLinesBy": ["lineEntity"],
|
|
395
|
+
"lineDateField": ["lineEntity", "dateField"],
|
|
384
396
|
"dateTrunc": ["dateField"],
|
|
385
397
|
"lineEntity": ["lineField", "lockedLineFields"],
|
|
386
398
|
"lineField": ["lineEntity"],
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"account": false,
|
|
25
25
|
"debitAccount": false, "creditAccount": false,
|
|
26
26
|
"debitAnalyticFields": false, "creditAnalyticFields": false,
|
|
27
|
-
"sideDimensions": false
|
|
27
|
+
"sideDimensions": false, "analyticTotals": false
|
|
28
28
|
},
|
|
29
29
|
"description": "A balance register keeps state, so it needs the entity that holds it. It dates its movements on the journal row under 'journalDate'; 'period' belongs to an information register, whose records ARE the dated thing. Its writer is recorded on the journal row, which is what 'recorder' names for an information register that has no journal."
|
|
30
30
|
}
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"account": false,
|
|
42
42
|
"debitAccount": false, "creditAccount": false,
|
|
43
43
|
"debitAnalyticFields": false, "creditAnalyticFields": false,
|
|
44
|
-
"sideDimensions": false,
|
|
44
|
+
"sideDimensions": false, "analyticTotals": false,
|
|
45
45
|
"allowNegative": false,
|
|
46
46
|
"autoCreate": false
|
|
47
47
|
},
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"account": false,
|
|
61
61
|
"debitAccount": false, "creditAccount": false,
|
|
62
62
|
"debitAnalyticFields": false, "creditAnalyticFields": false,
|
|
63
|
-
"sideDimensions": false,
|
|
63
|
+
"sideDimensions": false, "analyticTotals": false,
|
|
64
64
|
"totals": false,
|
|
65
65
|
"allowNegative": false,
|
|
66
66
|
"autoCreate": false
|
|
@@ -241,6 +241,63 @@
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
},
|
|
244
|
+
"analyticTotals": {
|
|
245
|
+
"type": "object",
|
|
246
|
+
"description": "An 'accounting' register's per-period cache keyed by the analytics as well: a cached row is keyed by (account, cached dimensions, analytic slots, period), for the accounts the chart flags. It is what makes a balance READ BY AN ANALYTIC cheap - stock by item and warehouse, settlements by counterparty and contract - which 'totals' leaves to the postings because over a whole ledger the analytics are where the rows are. getAccountBalance / getAccountTurnover read whole periods from it and the part-period at the edge from the postings. Beside 'totals', never instead of it, and in an entity of its own. Like 'totals' it caches the turnover within each period, so a backdated posting touches one row; a write to the journal that was not a posting, or a change to an account's flag, marks it stale until it is rebuilt.",
|
|
247
|
+
"required": ["entity", "period", "periodField", "accountField", "accountFlag", "analyticFields", "resources"],
|
|
248
|
+
"additionalProperties": false,
|
|
249
|
+
"properties": {
|
|
250
|
+
"entity": {
|
|
251
|
+
"type": "string",
|
|
252
|
+
"description": "Entity holding the cached rows. Keyed by ONE surrogate column the platform mints (the 'identity' trait), because a slot or a dimension may be empty and a PRIMARY KEY holds no NULL; the platform keeps a unique index over the cache's own key, NULLS NOT DISTINCT, and merges on it. It carries the account column, a column per cached dimension, the jsonb slot columns, the period column and both columns of every resource."
|
|
253
|
+
},
|
|
254
|
+
"period": {
|
|
255
|
+
"type": "string",
|
|
256
|
+
"enum": ["day", "month", "quarter", "year"],
|
|
257
|
+
"description": "How much one cached row covers, as for 'totals'."
|
|
258
|
+
},
|
|
259
|
+
"periodField": {
|
|
260
|
+
"type": "string",
|
|
261
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
262
|
+
"description": "Column holding the FIRST date of the period a row covers."
|
|
263
|
+
},
|
|
264
|
+
"accountField": {
|
|
265
|
+
"type": "string",
|
|
266
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
267
|
+
"description": "Column holding the account a cached row belongs to. A compound posting folds into two rows, one per side, each with that side's analytics."
|
|
268
|
+
},
|
|
269
|
+
"accountFlag": {
|
|
270
|
+
"type": "string",
|
|
271
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
272
|
+
"description": "A bool column of the register's chart: the accounts where it is true are cached, and no other. Changing it on an account marks the cache stale until it is rebuilt (install, or dforge register rebuild-totals --cache analytic)."
|
|
273
|
+
},
|
|
274
|
+
"dimensions": {
|
|
275
|
+
"type": "array",
|
|
276
|
+
"items": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" },
|
|
277
|
+
"description": "Which of the register's dimensions the cache is grouped by; omitted, it is all of them. Unlike 'totals' a nullable one may be cached: an empty value is a key value of its own."
|
|
278
|
+
},
|
|
279
|
+
"analyticFields": {
|
|
280
|
+
"type": "array",
|
|
281
|
+
"minItems": 1,
|
|
282
|
+
"items": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" },
|
|
283
|
+
"description": "The cache's jsonb columns holding the analytic slots, slot N in the Nth, one per slot a posting side carries (the wider side's count on a compound register). Each holds the analytic's entity and key, which is what a read filters and groups by."
|
|
284
|
+
},
|
|
285
|
+
"resources": {
|
|
286
|
+
"type": "object",
|
|
287
|
+
"description": "Every resource the register declares, each with its 'debitField' and 'creditField' columns, as for an accounting register's 'totals'.",
|
|
288
|
+
"minProperties": 1,
|
|
289
|
+
"additionalProperties": {
|
|
290
|
+
"type": "object",
|
|
291
|
+
"required": ["debitField", "creditField"],
|
|
292
|
+
"additionalProperties": false,
|
|
293
|
+
"properties": {
|
|
294
|
+
"debitField": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" },
|
|
295
|
+
"creditField": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
},
|
|
244
301
|
"period": {
|
|
245
302
|
"type": "string",
|
|
246
303
|
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
@@ -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.",
|
package/src/dsl/builtins.ts
CHANGED
|
@@ -149,9 +149,9 @@ export const BUILTINS: Builtin[] = [
|
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
name: "sendEmail",
|
|
152
|
-
signature: "sendEmail(to, subjectOrTemplate, dataOrBody?)",
|
|
152
|
+
signature: "sendEmail(to, subjectOrTemplate, dataOrBody?, attachments?)",
|
|
153
153
|
returns: "void",
|
|
154
|
-
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>…</p>')` sends raw HTML. A second argument with no spaces that contains `.` or `_` is read as a template code — so a raw subject like `'Shipped.'` silently becomes one; give a raw subject spaces.
|
|
154
|
+
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>…</p>')` sends raw HTML. A second argument with no spaces that contains `.` or `_` is read as a template code — 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.",
|
|
155
155
|
},
|
|
156
156
|
|
|
157
157
|
// External calls
|
|
@@ -192,7 +192,19 @@ export const BUILTINS: Builtin[] = [
|
|
|
192
192
|
name: "download",
|
|
193
193
|
signature: "download(url, fileName?)",
|
|
194
194
|
returns: "void",
|
|
195
|
-
doc: "Set a download URL on the action result; the browser opens it when the action returns. It fetches nothing and stores nothing — 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.",
|
|
195
|
+
doc: "Set a download URL on the action result; the browser opens it when the action returns. It fetches nothing and stores nothing — 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.",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "downloadText",
|
|
199
|
+
signature: "downloadText(content, fileName, encoding?, mimeType?)",
|
|
200
|
+
returns: "string",
|
|
201
|
+
doc: "Hand the user a text file the script built: encodes `content` in `encoding` (`utf-8` by default; `windows-1251`, `cp866`, `koi8-u`… 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.",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "downloadBase64",
|
|
205
|
+
signature: "downloadBase64(base64, fileName, mimeType?)",
|
|
206
|
+
returns: "string",
|
|
207
|
+
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.",
|
|
196
208
|
},
|
|
197
209
|
{
|
|
198
210
|
name: "getSetting",
|
|
@@ -247,6 +259,13 @@ export const BUILTINS: Builtin[] = [
|
|
|
247
259
|
entityArg: 0,
|
|
248
260
|
doc: "Build a clickable link to a record, for an `entitylink` column. Always pass a **qualified** code — `entity_cd` is only unique per module.",
|
|
249
261
|
},
|
|
262
|
+
{
|
|
263
|
+
name: "printPdf",
|
|
264
|
+
signature: "printPdf(entityCd, key, templateCd)",
|
|
265
|
+
returns: "object",
|
|
266
|
+
entityArg: 0,
|
|
267
|
+
doc: "Render a print template to a PDF **as the current user** and return the stored file as `{ fileId, fileName, mimeType, size, checksum }` — 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.",
|
|
268
|
+
},
|
|
250
269
|
];
|
|
251
270
|
|
|
252
271
|
export const BUILTIN_BY_NAME = new Map(BUILTINS.map((b) => [b.name, b]));
|
|
@@ -48,18 +48,36 @@ function checkPosting(params: Raw, ledger: boolean, error: RegisterReporter): vo
|
|
|
48
48
|
if (hasKey(params, "groupLinesBy")) {
|
|
49
49
|
error(["groupLinesBy"], "register-group-lines", "'groupLinesBy' needs lines to group — name the 'lineEntity'.");
|
|
50
50
|
}
|
|
51
|
+
if (hasKey(params, "lineDateField")) {
|
|
52
|
+
error(["lineDateField"], "register-line-date", "'lineDateField' dates the lines a posting is made from — name the 'lineEntity', or date the document with 'dateField'.");
|
|
53
|
+
}
|
|
51
54
|
}
|
|
52
55
|
if (ledger) {
|
|
53
56
|
if (!lineEntity) {
|
|
54
57
|
error(["lineEntity"], "register-line", "An L column posts lines: 'lineEntity' and 'lineField' are required.");
|
|
55
58
|
}
|
|
56
|
-
for (const key of ["headerMap", "skipPostingWhenEmpty", "dateTrunc", "groupLinesBy"]) {
|
|
59
|
+
for (const key of ["headerMap", "skipPostingWhenEmpty", "dateTrunc", "groupLinesBy", "lineDateField"]) {
|
|
57
60
|
if (hasKey(params, key)) {
|
|
58
61
|
error([key], "register-ledger-only", `'${key}' is read by A columns only.`);
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
const dateField = safeTrim(params.dateField);
|
|
66
|
+
const lineDateField = safeTrim(params.lineDateField);
|
|
67
|
+
if (lineEntity && lineDateField && !ledger) {
|
|
68
|
+
if (!dateField) {
|
|
69
|
+
error(["dateField"], "register-line-date", "'lineDateField' falls back to the document's 'dateField' for a line with no date — name a 'dateField'.");
|
|
70
|
+
}
|
|
71
|
+
if (hasKey(params, "periodDimField")) {
|
|
72
|
+
error(["lineDateField"], "register-line-date", "'lineDateField' and 'periodDimField' cannot be combined: the period dimension is the document's one period, and a line dated in another month would post under it.");
|
|
73
|
+
}
|
|
74
|
+
if (!lockedLines.includes("*") && !lockedLines.includes(lineDateField)) {
|
|
75
|
+
error(["lockedLineFields"], "register-line-date", `'${lineDateField}' must be in 'lockedLineFields': a posted line re-dated would leave its movement under the old date.`);
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(params.groupLinesBy) && !stringList(params.groupLinesBy).includes(lineDateField)) {
|
|
78
|
+
error(["groupLinesBy"], "register-line-date", `'${lineDateField}' must be in 'groupLinesBy': a group posts one movement, which has one date only when its lines share it.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
63
81
|
if (hasKey(params, "dateTrunc") && !dateField) {
|
|
64
82
|
error(["dateTrunc"], "register-date", "'dateTrunc' needs a 'dateField'.");
|
|
65
83
|
}
|
package/src/registers/types.ts
CHANGED
|
@@ -119,6 +119,8 @@ export interface RegisterBindingParams {
|
|
|
119
119
|
lineEntity?: string;
|
|
120
120
|
lineField?: string;
|
|
121
121
|
groupLinesBy?: string[];
|
|
122
|
+
/** On a line posting: the line column dating that line's movement instead of `dateField`. */
|
|
123
|
+
lineDateField?: string;
|
|
122
124
|
/** Register dimension or resource name → column of the row read. */
|
|
123
125
|
map?: Record<string, RegisterMapValue>;
|
|
124
126
|
/** On a line posting: register name → DOCUMENT column. */
|
package/src/reports.ts
CHANGED
|
@@ -115,7 +115,11 @@ export interface ReportQuery {
|
|
|
115
115
|
|
|
116
116
|
/** Per-column metadata override on a dataset. */
|
|
117
117
|
export interface ColumnDef {
|
|
118
|
+
/** A literal header: wins over the entity's or the procedure's translated one. */
|
|
118
119
|
label?: string;
|
|
120
|
+
fieldTypeCd?: string;
|
|
121
|
+
format?: string;
|
|
122
|
+
/** Accepted, but read by nothing. */
|
|
119
123
|
visible?: boolean;
|
|
120
124
|
width?: number;
|
|
121
125
|
}
|