@dforge-core/metadata 0.0.46 → 0.0.48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/metadata",
3
- "version": "0.0.46",
3
+ "version": "0.0.48",
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",
@@ -52,6 +52,7 @@
52
52
  },
53
53
  "additionalProperties": false,
54
54
  "allOf": [
55
+ { "$ref": "#/$defs/rules/grid" },
55
56
  { "$ref": "#/$defs/rules/kanban" },
56
57
  { "$ref": "#/$defs/rules/calendar" },
57
58
  { "$ref": "#/$defs/rules/tree-grid" },
@@ -62,6 +63,17 @@
62
63
  },
63
64
 
64
65
  "rules": {
66
+ "grid": {
67
+ "if": {
68
+ "properties": { "viewType": { "const": "grid" } },
69
+ "required": ["viewType"]
70
+ },
71
+ "then": {
72
+ "properties": {
73
+ "viewConfig": { "$ref": "#/$defs/gridViewConfig" }
74
+ }
75
+ }
76
+ },
65
77
  "kanban": {
66
78
  "if": {
67
79
  "properties": { "viewType": { "const": "kanban" } },
@@ -131,6 +143,26 @@
131
143
  }
132
144
  },
133
145
 
146
+ "gridViewConfig": {
147
+ "type": "object",
148
+ "properties": {
149
+ "groupByField": {
150
+ "type": "string",
151
+ "description": "Dropdown, reference or user column the rows are grouped by under header rows"
152
+ },
153
+ "groupByOptions": {
154
+ "type": "array",
155
+ "items": { "type": "string" },
156
+ "description": "Columns the Group by menu offers; must include groupByField"
157
+ },
158
+ "collapsedGroups": {
159
+ "type": "array",
160
+ "items": { "type": "string" },
161
+ "description": "Option values of groupByField whose groups start collapsed"
162
+ }
163
+ },
164
+ "additionalProperties": false
165
+ },
134
166
  "kanbanViewConfig": {
135
167
  "type": "object",
136
168
  "required": ["groupByField"],
@@ -195,6 +227,24 @@
195
227
  "type": "array",
196
228
  "items": { "$ref": "#/$defs/listLevelConfig" },
197
229
  "description": "Hierarchical child levels for expandable list rows"
230
+ },
231
+ "groupByField": {
232
+ "type": "string",
233
+ "description": "Dropdown, reference or user column the rows are grouped by"
234
+ },
235
+ "groupByOptions": {
236
+ "type": "array",
237
+ "items": { "type": "string" },
238
+ "description": "Columns the user may regroup by; must include groupByField"
239
+ },
240
+ "collapsedGroups": {
241
+ "type": "array",
242
+ "items": { "type": "string" },
243
+ "description": "Option values of groupByField whose groups start collapsed"
244
+ },
245
+ "inlineAdd": {
246
+ "type": "boolean",
247
+ "description": "Show an add row in each group (at the top when ungrouped)"
198
248
  }
199
249
  },
200
250
  "additionalProperties": false
@@ -345,11 +345,90 @@
345
345
  },
346
346
  "additionalProperties": false
347
347
  },
348
+ "queryColumnParams": {
349
+ "type": "object",
350
+ "description": "Binding of a query column (columnType Q): a read-only list of rows for the record, with no physical column. It reads either a balance register as of the record's date (register, asOf, dimensions) or a query written in the column and filled from the record (entity, query, params). Shown on the card and read by print forms, with the reader's rights.",
351
+ "oneOf": [
352
+ {
353
+ "required": ["register", "asOf"],
354
+ "properties": {
355
+ "register": {
356
+ "type": "string",
357
+ "description": "A balance register of this module or of one it depends on, as 'module_cd.register_cd'. It needs a journal that stamps the date and the direction of each movement.",
358
+ "examples": ["wms-registry.stock"]
359
+ },
360
+ "asOf": {
361
+ "type": "string",
362
+ "description": "Date (or timestamp) column of this entity the balance is taken on. The day itself is included."
363
+ },
364
+ "viz": {
365
+ "type": "object",
366
+ "description": "How the card shows the rows: a report visualization by its type (table, a chart, pivot...) and its config as that visualization takes it. Unset: a table. A print form reads the rows as a table whatever this says.",
367
+ "required": ["type"],
368
+ "properties": {
369
+ "type": { "type": "string" },
370
+ "config": { "type": "object" }
371
+ },
372
+ "additionalProperties": false
373
+ },
374
+ "dimensions": {
375
+ "type": "object",
376
+ "description": "Register dimension → column of this entity that narrows it. A dimension left out is not narrowed, so its tuples all come back. A narrowing column that is empty on the record leaves the list empty.",
377
+ "additionalProperties": { "type": "string" },
378
+ "examples": [{ "warehouse_id": "warehouse_id" }]
379
+ }
380
+ },
381
+ "additionalProperties": false
382
+ },
383
+ {
384
+ "required": ["entity", "query"],
385
+ "properties": {
386
+ "entity": {
387
+ "type": "string",
388
+ "description": "The entity the query reads, as 'module_cd.entity_cd' (a bare code is this module's own). Of this module or of one it depends on.",
389
+ "examples": ["retail.goods_line"]
390
+ },
391
+ "query": {
392
+ "type": "object",
393
+ "description": "The query itself, in the shape query.run takes: columns (a plain name, or { c, agg } with agg group / sum / count / avg / min / max — grouped if any column has one), filter and sort. A filter value written '@name' is filled from the record through 'params'. An aggregate is read under its column's code. Written here and not saved elsewhere, so no edit of a shared query can change the form.",
394
+ "properties": {
395
+ "columns": { "type": "array" },
396
+ "filter": {},
397
+ "sort": { "type": "array" }
398
+ },
399
+ "additionalProperties": false
400
+ },
401
+ "viz": {
402
+ "type": "object",
403
+ "description": "How the card shows the rows: a report visualization by its type (table, a chart, pivot...) and its config as that visualization takes it. Unset: a table. A print form reads the rows as a table whatever this says.",
404
+ "required": ["type"],
405
+ "properties": {
406
+ "type": { "type": "string" },
407
+ "config": { "type": "object" }
408
+ },
409
+ "additionalProperties": false
410
+ },
411
+ "params": {
412
+ "type": "object",
413
+ "description": "Query parameter (the name after '@' in a filter value) → stored column of this entity that fills it. Every @param of the query is listed, and only those. A column that is empty on the record leaves the list empty rather than widening the query.",
414
+ "additionalProperties": { "type": "string" },
415
+ "examples": [{ "doc": "document_id" }]
416
+ }
417
+ },
418
+ "additionalProperties": false
419
+ }
420
+ ]
421
+ },
348
422
  "registryColumnParams": {
349
423
  "type": "object",
350
424
  "description": "params of an accumulation (A) or ledger (L) registry column. The lock lists are what BOTH generated guards freeze — the posted-document guard while the document is posted, the period guard while its period is closed — and they are required: install refuses a registry column that omits or empties them. The header's periodDateField and the state column are watched in addition, whatever the list says.",
351
425
  "required": ["lockedFields"],
426
+ "if": { "required": ["editorMayPost"], "properties": { "editorMayPost": { "const": true } } },
427
+ "then": { "not": { "anyOf": [ { "required": ["postAction"] }, { "required": ["unpostAction"] } ] } },
352
428
  "properties": {
429
+ "postAction": { "type": "string", "minLength": 1, "description": "Code of the action (available for this entity) that posts the document. A direct data.insert / data.update that turns the state flag on needs 'E' on it, as the button does, so \"may edit\" and \"may post\" stay two rights through the API too. An action's own writes are not re-checked. Pack refuses a name that no action declared for this entity carries. Unset: the transition needs only Insert/Update." },
430
+ "unpostAction": { "type": "string", "minLength": 1, "description": "Code of the action that unposts the document; the same gate for a direct write that turns the state flag off." },
431
+ "editorMayPost": { "type": "boolean", "description": "Declares that Insert/Update on the document includes posting it: no separate right to post is intended, so no postAction / unpostAction is named. Changes nothing at run time; it silences the pack warning for a column that names no posting action. Not together with postAction / unpostAction." },
353
432
  "lockedFields": {
354
433
  "type": "array",
355
434
  "minItems": 1,
@@ -408,6 +487,10 @@
408
487
  {
409
488
  "if": { "required": ["columnType"], "properties": { "columnType": { "enum": ["A", "L"] } } },
410
489
  "then": { "required": ["params"], "properties": { "params": { "$ref": "#/$defs/registryColumnParams" } } }
490
+ },
491
+ {
492
+ "if": { "required": ["columnType"], "properties": { "columnType": { "const": "Q" } } },
493
+ "then": { "required": ["params"], "properties": { "params": { "$ref": "#/$defs/queryColumnParams" } } }
411
494
  }
412
495
  ],
413
496
  "properties": {
@@ -422,8 +505,8 @@
422
505
  },
423
506
  "columnType": {
424
507
  "type": "string",
425
- "enum": ["D", "R", "S", "F", "A", "L", "G"],
426
- "description": "Column type: D=Data (physical column), R=Reference (N:1 lookup), S=Set (1:N collection), F=Formula (virtual calculated), G=Generated/Computed (physical, auto-maintained), A=Accumulation registry, L=Ledger registry"
508
+ "enum": ["D", "R", "S", "F", "A", "L", "G", "Q"],
509
+ "description": "Column type: D=Data (physical column), R=Reference (N:1 lookup), S=Set (1:N collection), F=Formula (virtual calculated), G=Generated/Computed (physical, auto-maintained), A=Accumulation registry, L=Ledger registry, Q=Query (virtual list of rows read from a register, bound to this record)"
427
510
  },
428
511
  "fieldTypeCd": {
429
512
  "type": "string",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "idempotencyKey": {
54
54
  "type": "string",
55
- "description": "Template like '{job_cd}:{run.month}'. Resolved at fire time. A successful run with the same key blocks re-dispatch (Phase 3 enforcement)."
55
+ "description": "Reserved; not enforced yet. Stored but never read by the scheduler, so it does not block re-dispatch. Only the scheduled slot (job, scheduled_at) is deduplicated; guard business-level 'once per period' inside the action."
56
56
  },
57
57
  "class": {
58
58
  "type": "string",
@@ -55,6 +55,11 @@
55
55
  "left": { "type": "string" }
56
56
  },
57
57
  "additionalProperties": false
58
+ },
59
+ "culture": {
60
+ "type": "string",
61
+ "description": "The form's own culture, e.g. 'uk-UA': format_number, format_currency, format_date, _fmt and Yes/No print in it whoever prints the form. Omitted: the user's culture. An unknown culture is refused at pack and install.",
62
+ "examples": ["uk-UA", "de-CH"]
58
63
  }
59
64
  },
60
65
  "additionalProperties": false
@@ -1,4 +1,4 @@
1
- // Column-type registry — the seven column kinds a field can have. Letters match
1
+ // Column-type registry — the eight column kinds a field can have. Letters match
2
2
  // the `column.column_type` values and entity.schema.json. The `name` /
3
3
  // `description` here drive the friendly "what kind of field is this?" picker.
4
4
 
@@ -11,6 +11,7 @@ export const columnTypes: readonly ColumnTypeDef[] = [
11
11
  { cd: "F", name: "Formula", description: "A virtual value calculated from a formula expression." },
12
12
  { cd: "G", name: "Generated", description: "A physical column auto-maintained by the database (aggregate or expression)." },
13
13
  { cd: "A", name: "Accumulation register", description: "Posts this document into a balance entity (single direction)." },
14
+ { cd: "Q", name: "Query rows", description: "A read-only list of rows this record reads: a register's balance as of its date, or a query filled from its columns." },
14
15
  { cd: "L", name: "Ledger register", description: "Posts document lines into movements + balances (double entry)." },
15
16
  ] as const;
16
17
 
package/src/data-views.ts CHANGED
@@ -79,6 +79,15 @@ export interface CalendarViewConfig {
79
79
  colorField?: string;
80
80
  }
81
81
 
82
+ export interface GridViewConfig {
83
+ /** Dropdown, reference or user column the rows are grouped by under header rows. */
84
+ groupByField?: string;
85
+ /** Columns the Group by menu offers; must include `groupByField`. */
86
+ groupByOptions?: string[];
87
+ /** Option values of `groupByField` whose groups start collapsed. */
88
+ collapsedGroups?: string[];
89
+ }
90
+
82
91
  export interface TreeGridViewConfig {
83
92
  /** Self-referencing FK column (parent record). */
84
93
  parentField: string;
@@ -102,6 +111,14 @@ export interface ListViewConfig {
102
111
  subtitleField?: string;
103
112
  /** Hierarchical child levels for expandable list rows. */
104
113
  levels?: ListLevelConfig[];
114
+ /** Dropdown, reference or user column the rows are grouped by. */
115
+ groupByField?: string;
116
+ /** Columns the user may regroup by; includes `groupByField`. */
117
+ groupByOptions?: string[];
118
+ /** Option values of `groupByField` whose groups start collapsed. */
119
+ collapsedGroups?: string[];
120
+ /** Show an add row in each group (at the top when ungrouped). */
121
+ inlineAdd?: boolean;
105
122
  }
106
123
 
107
124
  export interface DiagramViewConfig {
@@ -127,6 +144,7 @@ export interface DataViewDef {
127
144
  order?: unknown;
128
145
  /** View-type-specific configuration. */
129
146
  viewConfig?:
147
+ | GridViewConfig
130
148
  | KanbanViewConfig
131
149
  | CalendarViewConfig
132
150
  | TreeGridViewConfig
package/src/index.ts CHANGED
@@ -75,6 +75,10 @@ export type {
75
75
  AuditHistoryLevel,
76
76
  } from "./entity";
77
77
 
78
+ // Query column (`columnType: "Q"`) params and where they name the record's columns.
79
+ export { QUERY_COLUMN_RECORD_SLOTS, isQueryColumnRecordSlot } from "./query-column";
80
+ export type { QueryColumnParams, RegisterQueryColumnParams, EntityQueryColumnParams, QueryColumnViz } from "./query-column";
81
+
78
82
  // Entity introspection helpers (primary keys, identity keys, reference links).
79
83
  export { getLinks, getIdentityKeys, getPrimaryKeys } from "./entity-meta";
80
84
 
@@ -85,6 +89,7 @@ export {
85
89
  type DataSource,
86
90
  type ViewColumn,
87
91
  type ViewType,
92
+ type GridViewConfig,
88
93
  type KanbanViewConfig,
89
94
  type CalendarViewConfig,
90
95
  type TreeGridViewConfig,
package/src/jobs.ts CHANGED
@@ -15,7 +15,7 @@ export interface JobDef {
15
15
  timeout: number;
16
16
  /** Max parallel runs (1–5; default 1). */
17
17
  concurrency?: number;
18
- /** Idempotency key template, e.g. '{job_cd}:{run.month}'. */
18
+ /** Reserved, not enforced: stored but never read by the scheduler. */
19
19
  idempotencyKey?: string;
20
20
  /** Routing class ('long_running' required for any timeout > 300s). */
21
21
  class?: "standard" | "long_running";
@@ -14,6 +14,8 @@ export interface PrintPageSettings {
14
14
  size?: "A4" | "letter" | "legal";
15
15
  orientation?: "portrait" | "landscape";
16
16
  margins?: PrintMargins;
17
+ /** The form's own culture ('uk-UA') for its numbers and dates, whoever prints it. */
18
+ culture?: string;
17
19
  }
18
20
 
19
21
  /**
@@ -0,0 +1,49 @@
1
+ // A query column (`columnType: "Q"`): a read-only list of rows a record reads, with no
2
+ // physical column. `params` binds it to one source — a balance register as of the
3
+ // record's date, or a query written in the column and filled from the record.
4
+
5
+ /** How the card shows the rows: a report visualization by its type and config. Unset: a table. */
6
+ export interface QueryColumnViz {
7
+ type: string;
8
+ config?: Record<string, unknown>;
9
+ }
10
+
11
+ /** The balance of a register as of the record's date, narrowed by the record's own columns. */
12
+ export interface RegisterQueryColumnParams {
13
+ /** `module_cd.register_cd` of a balance register. */
14
+ register: string;
15
+ /** Date column of the record the balance is taken on. */
16
+ asOf: string;
17
+ /** Register dimension → column of the record that narrows it. */
18
+ dimensions?: Record<string, string>;
19
+ viz?: QueryColumnViz;
20
+ }
21
+
22
+ /** A query written in the column, its `@name` filter values filled from the record. */
23
+ export interface EntityQueryColumnParams {
24
+ /** `module_cd.entity_cd` the query reads. */
25
+ entity: string;
26
+ /** The query, in the shape `query.run` takes. */
27
+ query: { columns?: unknown[]; filter?: unknown; sort?: unknown[] };
28
+ /** Query parameter name → column of the record that fills it. */
29
+ params?: Record<string, string>;
30
+ viz?: QueryColumnViz;
31
+ }
32
+
33
+ export type QueryColumnParams = RegisterQueryColumnParams | EntityQueryColumnParams;
34
+
35
+ /**
36
+ * The places in a Q column's `params` that name a column of the entity the column is on, so an
37
+ * editor can offer that entity's columns there. `*` matches any key.
38
+ */
39
+ export const QUERY_COLUMN_RECORD_SLOTS: readonly (readonly string[])[] = [
40
+ ["asOf"],
41
+ ["dimensions", "*"],
42
+ ["params", "*"],
43
+ ];
44
+
45
+ /** Whether a path inside a Q column's `params` names a column of the record's entity. */
46
+ export function isQueryColumnRecordSlot(path: readonly (string | number)[]): boolean {
47
+ return QUERY_COLUMN_RECORD_SLOTS.some(slot =>
48
+ slot.length === path.length && slot.every((segment, i) => segment === "*" || segment === path[i]));
49
+ }
package/src/types.ts CHANGED
@@ -14,7 +14,7 @@ export interface NamedKind {
14
14
  export type AlignType = "left" | "right" | "center";
15
15
 
16
16
  /** Column kind letter as stored in entity JSON / the `column` metadata table. */
17
- export type ColumnTypeCd = "D" | "R" | "S" | "F" | "A" | "L" | "G";
17
+ export type ColumnTypeCd = "D" | "R" | "S" | "F" | "A" | "L" | "G" | "Q";
18
18
 
19
19
  /** Abstract data type code from the `base_datatype` table. */
20
20
  export type BaseDatatypeCd =