@xml-auditfile/auditfile 0.0.0-stage → 0.0.2-alpha.3

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.
Files changed (83) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +183 -2
  3. package/package.json +62 -4
  4. package/src/auditfile.js +186 -0
  5. package/src/definitions/dialect.js +40 -0
  6. package/src/definitions/element-definition.js +27 -0
  7. package/src/definitions/localized-text.js +41 -0
  8. package/src/definitions/manifest.js +160 -0
  9. package/src/definitions/names.js +42 -0
  10. package/src/definitions/resources.js +29 -0
  11. package/src/definitions/view-definition.js +160 -0
  12. package/src/diagnostics/collector.js +139 -0
  13. package/src/diagnostics/diagnostic.js +53 -0
  14. package/src/diagnostics/report.js +56 -0
  15. package/src/generated/spec.js +74 -0
  16. package/src/index.js +20 -0
  17. package/src/internal/composite-key.js +2 -0
  18. package/src/internal/decimal.js +193 -0
  19. package/src/internal/encoding.js +118 -0
  20. package/src/internal/json.js +106 -0
  21. package/src/internal/sha256.js +147 -0
  22. package/src/internal/streams.js +232 -0
  23. package/src/internal/xml-reader.js +247 -0
  24. package/src/memory/memory-sink.js +68 -0
  25. package/src/reading/mapping-plan.js +91 -0
  26. package/src/reading/read-options.js +136 -0
  27. package/src/reading/read-session.js +446 -0
  28. package/src/reading/row-loader.js +362 -0
  29. package/src/reading/row.js +123 -0
  30. package/src/reading/source.js +146 -0
  31. package/src/reading/value-converter.js +136 -0
  32. package/src/schema/schema.js +256 -0
  33. package/src/sinks/sink.js +53 -0
  34. package/src/validation/reference-checker.js +146 -0
  35. package/src/validation/xaf-rules.js +167 -0
  36. package/src/versioning/version-resolver.js +119 -0
  37. package/src/versioning/version.js +217 -0
  38. package/src/views/view-result.js +20 -0
  39. package/src/views/view-runner.js +449 -0
  40. package/src/xsd/datatypes.js +141 -0
  41. package/src/xsd/regex.js +121 -0
  42. package/src/xsd/schema-set.js +555 -0
  43. package/src/xsd/validator.js +484 -0
  44. package/types/auditfile.d.ts +57 -0
  45. package/types/definitions/dialect.d.ts +27 -0
  46. package/types/definitions/element-definition.d.ts +16 -0
  47. package/types/definitions/localized-text.d.ts +25 -0
  48. package/types/definitions/manifest.d.ts +118 -0
  49. package/types/definitions/names.d.ts +25 -0
  50. package/types/definitions/resources.d.ts +9 -0
  51. package/types/definitions/view-definition.d.ts +85 -0
  52. package/types/diagnostics/collector.d.ts +38 -0
  53. package/types/diagnostics/diagnostic.d.ts +37 -0
  54. package/types/diagnostics/report.d.ts +32 -0
  55. package/types/generated/spec.d.ts +6 -0
  56. package/types/index.d.ts +18 -0
  57. package/types/internal/composite-key.d.ts +2 -0
  58. package/types/internal/decimal.d.ts +55 -0
  59. package/types/internal/encoding.d.ts +43 -0
  60. package/types/internal/json.d.ts +17 -0
  61. package/types/internal/sha256.d.ts +17 -0
  62. package/types/internal/streams.d.ts +35 -0
  63. package/types/internal/xml-reader.d.ts +50 -0
  64. package/types/memory/memory-sink.d.ts +29 -0
  65. package/types/reading/mapping-plan.d.ts +45 -0
  66. package/types/reading/read-options.d.ts +69 -0
  67. package/types/reading/read-session.d.ts +28 -0
  68. package/types/reading/row-loader.d.ts +18 -0
  69. package/types/reading/row.d.ts +53 -0
  70. package/types/reading/source.d.ts +73 -0
  71. package/types/reading/value-converter.d.ts +15 -0
  72. package/types/schema/schema.d.ts +116 -0
  73. package/types/sinks/sink.d.ts +59 -0
  74. package/types/validation/reference-checker.d.ts +11 -0
  75. package/types/validation/xaf-rules.d.ts +13 -0
  76. package/types/versioning/version-resolver.d.ts +25 -0
  77. package/types/versioning/version.d.ts +71 -0
  78. package/types/views/view-result.d.ts +13 -0
  79. package/types/views/view-runner.d.ts +1 -0
  80. package/types/xsd/datatypes.d.ts +40 -0
  81. package/types/xsd/regex.d.ts +6 -0
  82. package/types/xsd/schema-set.d.ts +20 -0
  83. package/types/xsd/validator.d.ts +48 -0
@@ -0,0 +1,40 @@
1
+ import { object, objects, optionalString, string, strings } from '../internal/json.js';
2
+ import { LocalizedText } from './localized-text.js';
3
+ import { DialectAction, oneOf } from './names.js';
4
+
5
+ /** Corrections for a package that deviates from the standard, from dialects.json. */
6
+ export class Dialect {
7
+ /** @internal */
8
+ constructor(json) {
9
+ const match = object(json, 'match');
10
+ /** Dialect name. */
11
+ this.name = string(json, 'name');
12
+ /** What the dialect is for. */
13
+ this.description = LocalizedText.from(object(json, 'description'));
14
+ /** Namespaces that select this dialect. */
15
+ this.namespaces = Object.freeze(match === null ? [] : strings(match, 'namespaces'));
16
+ /** Value of header/softwareDesc that selects this dialect. */
17
+ this.softwareDescription = match === null ? null : optionalString(match, 'softwareDesc');
18
+ /** Corrections to apply. */
19
+ this.corrections = Object.freeze(objects(json, 'corrections').map((c) => new DialectCorrection(c)));
20
+ Object.freeze(this);
21
+ }
22
+ }
23
+
24
+ /** One correction of a dialect. */
25
+ export class DialectCorrection {
26
+ /** @internal */
27
+ constructor(json) {
28
+ /** Diagnostic code reported when the correction is applied. */
29
+ this.code = string(json, 'code');
30
+ /** What the correction does, one of {@link DialectAction}. */
31
+ this.action = oneOf(string(json, 'action'), DialectAction, 'dialect action');
32
+ /** XML paths the correction applies to. */
33
+ this.paths = Object.freeze(strings(json, 'paths'));
34
+ /** Original value (replaceNamespace). */
35
+ this.from = optionalString(json, 'from');
36
+ /** Replacement value (replaceNamespace). */
37
+ this.to = optionalString(json, 'to');
38
+ Object.freeze(this);
39
+ }
40
+ }
@@ -0,0 +1,27 @@
1
+ import { isObject, object, string } from '../internal/json.js';
2
+ import { LocalizedText } from './localized-text.js';
3
+
4
+ /** Documentation of a table, column or flattened group, from definition.json. */
5
+ export class ElementDefinition {
6
+ /** @internal */
7
+ constructor(json) {
8
+ /** "tr_line" (table), "tr_line.amnt" (column) or "tr_line.currency" (group). */
9
+ this.key = string(json, 'key');
10
+ /** "table", "column" or "group". */
11
+ this.kind = string(json, 'kind');
12
+ /** Official name, usually English only. */
13
+ this.label = LocalizedText.from(object(json, 'label'));
14
+ /** Meaning according to the official documentation. */
15
+ this.definition = LocalizedText.from(object(json, 'definition'));
16
+ /** Practical explanation. */
17
+ this.description = LocalizedText.from(object(json, 'description'));
18
+
19
+ /** @type {Map<string, LocalizedText>} Meaning of code values, e.g. "D" = Debit. */
20
+ this.codes = new Map();
21
+ for (const [code, text] of Object.entries(object(json, 'codes') ?? {})) {
22
+ this.codes.set(code, LocalizedText.from(isObject(text) ? text : null) ?? new LocalizedText(null, null));
23
+ }
24
+
25
+ Object.freeze(this);
26
+ }
27
+ }
@@ -0,0 +1,41 @@
1
+ import { optionalString } from '../internal/json.js';
2
+
3
+ /** Language for definitions and diagnostic messages. */
4
+ export const TextLanguage = Object.freeze({
5
+ /** Dutch: the official language of the Belastingdienst publications. */
6
+ Dutch: 'nl',
7
+ /** English; falls back to Dutch where no translation exists. */
8
+ English: 'en',
9
+ });
10
+
11
+ /** A text in Dutch and English. English falls back to Dutch when empty. */
12
+ export class LocalizedText {
13
+ /**
14
+ * @param {string | null} dutch
15
+ * @param {string | null} english
16
+ */
17
+ constructor(dutch, english) {
18
+ /** Dutch text, or null. */
19
+ this.dutch = dutch ? dutch : null;
20
+ /** English text, or null. */
21
+ this.english = english ? english : null;
22
+ Object.freeze(this);
23
+ }
24
+
25
+ /**
26
+ * The text in the requested language, falling back to the other language.
27
+ * @param {'nl' | 'en'} [language]
28
+ */
29
+ get(language = TextLanguage.Dutch) {
30
+ return language === TextLanguage.English ? this.english ?? this.dutch : this.dutch ?? this.english;
31
+ }
32
+
33
+ toString() {
34
+ return this.get(TextLanguage.Dutch) ?? '';
35
+ }
36
+
37
+ /** @internal */
38
+ static from(json) {
39
+ return json === null || json === undefined ? null : new LocalizedText(optionalString(json, 'nl'), optionalString(json, 'en'));
40
+ }
41
+ }
@@ -0,0 +1,160 @@
1
+ import { int, object, objects, optionalInt, optionalString, string, strings } from '../internal/json.js';
2
+
3
+ /** Logical type of a column; the values are the type names of auditfile-spec. */
4
+ export const ColumnType = Object.freeze({
5
+ /** Text (`string`). */
6
+ String: 'string',
7
+ /** Exact decimal number (`Decimal`). */
8
+ Decimal: 'decimal',
9
+ /** Whole number (`number`, always a safe integer). */
10
+ Integer: 'integer',
11
+ /** Date without time (`Date` at midnight UTC). */
12
+ Date: 'date',
13
+ /** Date and time (`Date`; a value without time zone is taken as UTC). */
14
+ DateTime: 'datetime',
15
+ /** Time of day (`string` "HH:MM:SS"). */
16
+ Time: 'time',
17
+ /** True or false (`boolean`). */
18
+ Boolean: 'boolean',
19
+ });
20
+
21
+ const COLUMN_TYPES = new Set(Object.values(ColumnType));
22
+
23
+ /** @param {string} type */
24
+ export function parseColumnType(type) {
25
+ if (!COLUMN_TYPES.has(type)) {
26
+ throw new SyntaxError(`Unknown column type '${type}'.`);
27
+ }
28
+
29
+ return type;
30
+ }
31
+
32
+ /** The structure of one audit file version, from manifest.json. */
33
+ export class AuditfileManifest {
34
+ #byName;
35
+
36
+ /** @internal */
37
+ constructor(json) {
38
+ /** Format version of manifest.json. */
39
+ this.formatVersion = int(json, 'formatVersion');
40
+ if (this.formatVersion !== 1) {
41
+ throw new Error(`Manifest format version ${this.formatVersion} is not supported by this reader.`);
42
+ }
43
+
44
+ /** Audit file type, e.g. "xaf". */
45
+ this.standard = string(json, 'standard');
46
+ /** Version, e.g. "3.2". */
47
+ this.version = string(json, 'version');
48
+ /** Official namespace. */
49
+ this.namespace = string(json, 'namespace');
50
+ /** Name of the root element. */
51
+ this.rootElement = string(json, 'rootElement');
52
+ /** SHA-256 of the official XSD. */
53
+ this.xsdSha256 = optionalString(object(json, 'xsd') ?? {}, 'sha256') ?? '';
54
+ /** Tables required by the XSD. */
55
+ this.requiredTables = Object.freeze(strings(json, 'requiredTables'));
56
+ /** Tables optional in the XSD but expected in practice. */
57
+ this.expectedTables = Object.freeze(strings(json, 'expectedTables'));
58
+ /** All tables in document order. */
59
+ this.tables = Object.freeze(objects(json, 'tables').map((t, i) => new ManifestTable(t, i)));
60
+ this.#byName = new Map(this.tables.map((t) => [t.name, t]));
61
+ Object.freeze(this);
62
+ }
63
+
64
+ /**
65
+ * Finds a table by name, or returns null.
66
+ * @param {string} name
67
+ */
68
+ findTable(name) {
69
+ return this.#byName.get(name) ?? null;
70
+ }
71
+ }
72
+
73
+ /** A table of the manifest. */
74
+ export class ManifestTable {
75
+ /** @internal */
76
+ constructor(json, index) {
77
+ /** Position in the manifest. */
78
+ this.index = index;
79
+ /** Table name, e.g. "tr_line". */
80
+ this.name = string(json, 'name');
81
+ /** Element name, e.g. "trLine". */
82
+ this.xmlName = string(json, 'xmlName');
83
+ /** Full XML path, e.g. "auditfile/company/transactions/journal/transaction/trLine". */
84
+ this.path = string(json, 'path');
85
+ /** Name of the parent table, or null for the root. */
86
+ this.parent = optionalString(json, 'parent');
87
+ /** Minimum occurrences relative to the XML parent element. */
88
+ this.minOccurs = int(json, 'minOccurs');
89
+ /** Maximum occurrences relative to the XML parent element; null means unbounded. */
90
+ this.maxOccurs = optionalInt(json, 'maxOccurs');
91
+ /** Natural keys. */
92
+ this.keys = Object.freeze(objects(json, 'keys').map((k) => new ManifestKey(string(k, 'name'), strings(k, 'columns'))));
93
+ /** References to keys of other tables. */
94
+ this.references = Object.freeze(objects(json, 'references').map((r) =>
95
+ new ManifestReference(string(r, 'name'), strings(r, 'columns'), string(r, 'table'), string(r, 'key'))));
96
+ /** Columns in XSD order. */
97
+ this.columns = Object.freeze(objects(json, 'columns').map((c, i) => new ManifestColumn(c, i)));
98
+ Object.freeze(this);
99
+ }
100
+ }
101
+
102
+ /** A column of a manifest table. */
103
+ export class ManifestColumn {
104
+ /** @internal */
105
+ constructor(json, index) {
106
+ /** Position within the table. */
107
+ this.index = index;
108
+ /** Column name, e.g. "amnt". */
109
+ this.name = string(json, 'name');
110
+ /** Element or attribute name. */
111
+ this.xmlName = string(json, 'xmlName');
112
+ /** Path relative to the table element: "amnt", "currency/curCode", "@attr" or ".". */
113
+ this.path = string(json, 'path');
114
+ /** "element" or "attribute". */
115
+ this.kind = string(json, 'kind');
116
+ /** Logical type, one of {@link ColumnType}. */
117
+ this.type = parseColumnType(string(json, 'type'));
118
+ /** Name of the XSD type. */
119
+ this.xsdType = string(json, 'xsdType');
120
+ /** Minimum occurrences relative to the XML parent element. */
121
+ this.minOccurs = int(json, 'minOccurs');
122
+ Object.freeze(this);
123
+ }
124
+ }
125
+
126
+ /** A natural key: the column values are unique within an import. */
127
+ export class ManifestKey {
128
+ /**
129
+ * @param {string} name
130
+ * @param {string[]} columns
131
+ */
132
+ constructor(name, columns) {
133
+ /** Key name from the XSD. */
134
+ this.name = name;
135
+ /** Key columns. */
136
+ this.columns = Object.freeze([...columns]);
137
+ Object.freeze(this);
138
+ }
139
+ }
140
+
141
+ /** A reference from columns of one table to a key of another table. */
142
+ export class ManifestReference {
143
+ /**
144
+ * @param {string} name
145
+ * @param {string[]} columns
146
+ * @param {string} table
147
+ * @param {string} key
148
+ */
149
+ constructor(name, columns, table, key) {
150
+ /** Reference name from the XSD. */
151
+ this.name = name;
152
+ /** Referencing columns. */
153
+ this.columns = Object.freeze([...columns]);
154
+ /** Referenced table. */
155
+ this.table = table;
156
+ /** Referenced key. */
157
+ this.key = key;
158
+ Object.freeze(this);
159
+ }
160
+ }
@@ -0,0 +1,42 @@
1
+ /** What a dialect correction does. Unknown actions are refused when the definitions are loaded. */
2
+ export const DialectAction = Object.freeze({
3
+ /** Read a file in `from` as the official namespace. */
4
+ ReplaceNamespace: 'replaceNamespace',
5
+ /** A decimal comma becomes a point. */
6
+ DecimalComma: 'decimalComma',
7
+ /** The time is removed from a date. */
8
+ DateFromDateTime: 'dateFromDateTime',
9
+ });
10
+
11
+ /** How a view column aggregates its values. Unknown aggregates are refused when the definitions are loaded. */
12
+ export const ViewAggregate = Object.freeze({
13
+ Sum: 'sum',
14
+ Count: 'count',
15
+ Min: 'min',
16
+ Max: 'max',
17
+ });
18
+
19
+ /** How a view column gets its value from an input row. Unknown operators are refused when the definitions are loaded. */
20
+ export const ViewOperator = Object.freeze({
21
+ /** The value of the column. */
22
+ Column: 'column',
23
+ /** The value when `by` is one of the codes, otherwise null. */
24
+ Only: 'only',
25
+ /** The value, negated when `by` is one of the codes. */
26
+ Signed: 'signed',
27
+ });
28
+
29
+ /**
30
+ * A name from the JSON definitions, checked against the known names.
31
+ * @internal
32
+ * @param {string} value
33
+ * @param {Readonly<Record<string, string>>} allowed
34
+ * @param {string} what
35
+ */
36
+ export function oneOf(value, allowed, what) {
37
+ if (!Object.values(allowed).includes(value)) {
38
+ throw new SyntaxError(`Embedded JSON: unknown ${what} '${value}'.`);
39
+ }
40
+
41
+ return value;
42
+ }
@@ -0,0 +1,29 @@
1
+ import { files } from '../generated/spec.js';
2
+ import { parseObject } from '../internal/json.js';
3
+
4
+ /** The files from auditfile-spec/generated/ that are embedded in this package (tools/embed-spec.mjs). */
5
+
6
+ /** @param {string} name */
7
+ export function exists(name) {
8
+ return files.has(name);
9
+ }
10
+
11
+ /** @param {string} name */
12
+ export function readText(name) {
13
+ const text = files.get(name);
14
+ if (text === undefined) {
15
+ throw new Error(`Embedded definition '${name}' is missing. Was auditfile-spec/generated/ present when the package was built?`);
16
+ }
17
+
18
+ return text;
19
+ }
20
+
21
+ /** @param {string} name */
22
+ export function readJson(name) {
23
+ return parseObject(readText(name), name);
24
+ }
25
+
26
+ /** The bytes of an embedded file as published. */
27
+ export function readBytes(name) {
28
+ return new TextEncoder().encode(readText(name));
29
+ }
@@ -0,0 +1,160 @@
1
+ import { array, isObject, object, objects, optionalString, string, strings } from '../internal/json.js';
2
+ import { runView } from '../views/view-runner.js';
3
+ import { exists, readJson } from './resources.js';
4
+ import { LocalizedText, TextLanguage } from './localized-text.js';
5
+ import { oneOf, ViewAggregate, ViewOperator } from './names.js';
6
+
7
+ /**
8
+ * A view from views/<type>.json: a report over the rows of an import, such as a trial balance.
9
+ * Run it with {@link ViewDefinition#run} on any store that can hand out the rows of a table.
10
+ */
11
+ export class ViewDefinition {
12
+ /** @internal */
13
+ constructor(json) {
14
+ /** View name, e.g. "trial_balance". */
15
+ this.name = string(json, 'name');
16
+ /** Display name. */
17
+ this.label = LocalizedText.from(object(json, 'label'));
18
+ /** What the view shows. */
19
+ this.description = LocalizedText.from(object(json, 'description'));
20
+ /** The table with one input row per row of the view (before grouping). */
21
+ this.source = string(json, 'source');
22
+ /** Tables joined to the source: an ancestor of the source, or a lookup on key columns. */
23
+ this.joins = Object.freeze(objects(json, 'joins').map((j) => new ViewJoin(j)));
24
+ /** Optional filters. */
25
+ this.parameters = Object.freeze(objects(json, 'parameters').map((p) => new ViewParameter(p)));
26
+ /** Output columns. When any column aggregates, the other columns group. */
27
+ this.columns = Object.freeze(objects(json, 'columns').map((c) => new ViewColumn(c)));
28
+ /** Output column names to sort by, ascending. */
29
+ this.orderBy = Object.freeze(strings(json, 'orderBy'));
30
+ Object.freeze(this);
31
+ }
32
+
33
+ /** True when at least one column aggregates. */
34
+ get isAggregated() {
35
+ return this.columns.some((c) => c.aggregate !== null);
36
+ }
37
+
38
+ /**
39
+ * Runs the view. `rows` returns the rows of a table by name.
40
+ * `parameters` holds filter values by parameter name; null and undefined values are ignored.
41
+ * @param {import('../schema/schema.js').AuditfileSchema} schema
42
+ * @param {(table: string) => Iterable<import('../reading/row.js').AuditfileRow>} rows
43
+ * @param {Record<string, unknown> | Map<string, unknown> | null} [parameters]
44
+ * @param {'nl' | 'en'} [language]
45
+ * @returns {import('../views/view-result.js').ViewResult}
46
+ * @throws {TypeError} A parameter is unknown or its value cannot be converted.
47
+ */
48
+ run(schema, rows, parameters = null, language = TextLanguage.Dutch) {
49
+ if (schema === null || schema === undefined) {
50
+ throw new TypeError('schema is required.');
51
+ }
52
+
53
+ if (typeof rows !== 'function') {
54
+ throw new TypeError('rows must be a function that returns the rows of a table.');
55
+ }
56
+
57
+ return runView(this, schema, rows, parameters, language);
58
+ }
59
+
60
+ toString() {
61
+ return this.name;
62
+ }
63
+
64
+ /** @internal */
65
+ static load(type) {
66
+ const name = `views/${type}.json`;
67
+ return exists(name) ? Object.freeze(objects(readJson(name), 'views').map((v) => new ViewDefinition(v))) : Object.freeze([]);
68
+ }
69
+ }
70
+
71
+ /** A table joined to the source of a view. */
72
+ export class ViewJoin {
73
+ /** @internal */
74
+ constructor(json) {
75
+ /** Table name. */
76
+ this.table = string(json, 'table');
77
+ /** Equality conditions; empty means {@link ViewJoin#table} is an ancestor of the source. */
78
+ this.on = Object.freeze(array(json, 'on').map((pair) => {
79
+ if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== 'string' || typeof pair[1] !== 'string') {
80
+ throw new SyntaxError('Embedded JSON: a join condition must be a pair of column references.');
81
+ }
82
+
83
+ return new ViewJoinCondition(pair[0], pair[1]);
84
+ }));
85
+ Object.freeze(this);
86
+ }
87
+ }
88
+
89
+ /** One equality condition of a join, as "table.column" references. */
90
+ export class ViewJoinCondition {
91
+ constructor(left, right) {
92
+ /** Column of the source or an earlier join. */
93
+ this.left = left;
94
+ /** Column of the joined table. */
95
+ this.right = right;
96
+ Object.freeze(this);
97
+ }
98
+ }
99
+
100
+ /** An optional filter of a view. */
101
+ export class ViewParameter {
102
+ /** @internal */
103
+ constructor(json) {
104
+ /** Parameter name, e.g. "period_from". */
105
+ this.name = string(json, 'name');
106
+ /** Display name. */
107
+ this.label = LocalizedText.from(object(json, 'label'));
108
+ /** The filtered column, as "table.column". */
109
+ this.column = string(json, 'column');
110
+ /** "=", "<", "<=", ">" or ">=". */
111
+ this.operator = string(json, 'operator');
112
+ Object.freeze(this);
113
+ }
114
+ }
115
+
116
+ /** An output column of a view. */
117
+ export class ViewColumn {
118
+ /** @internal */
119
+ constructor(json) {
120
+ /** Column name. */
121
+ this.name = string(json, 'name');
122
+ /** Display name. */
123
+ this.label = LocalizedText.from(object(json, 'label'));
124
+ /** The aggregate, one of {@link ViewAggregate}, or null for a grouping column. */
125
+ const aggregate = optionalString(json, 'aggregate');
126
+ this.aggregate = aggregate === null ? null : oneOf(aggregate, ViewAggregate, 'view aggregate');
127
+ /** The value per input row; null for a row count. */
128
+ this.value = Object.prototype.hasOwnProperty.call(json, 'value') && json.value !== null ? ViewValue.from(json.value) : null;
129
+ Object.freeze(this);
130
+ }
131
+ }
132
+
133
+ /** How a view column gets its value from an input row. */
134
+ export class ViewValue {
135
+ constructor(operator, column, by, codes) {
136
+ /** How the value is taken from `column`, one of {@link ViewOperator}. */
137
+ this.operator = operator;
138
+ /** The value column, as "table.column". */
139
+ this.column = column;
140
+ /** The column holding the code, as "table.column". */
141
+ this.by = by;
142
+ /** Codes for "only" (in) or "signed" (negative). */
143
+ this.codes = Object.freeze([...codes]);
144
+ Object.freeze(this);
145
+ }
146
+
147
+ /** @internal */
148
+ static from(json) {
149
+ if (typeof json === 'string') {
150
+ return new ViewValue(ViewOperator.Column, json, null, []);
151
+ }
152
+
153
+ if (isObject(json)) {
154
+ return new ViewValue(oneOf(string(json, 'op'), ViewOperator, 'view value operator'), string(json, 'value'), optionalString(json, 'by'),
155
+ [...strings(json, 'in'), ...strings(json, 'negative')]);
156
+ }
157
+
158
+ throw new SyntaxError('Embedded JSON: invalid view column value.');
159
+ }
160
+ }
@@ -0,0 +1,139 @@
1
+ import { LocalizedText } from '../definitions/localized-text.js';
2
+ import { readJson } from '../definitions/resources.js';
3
+ import { Decimal } from '../internal/decimal.js';
4
+ import { object, objects, string } from '../internal/json.js';
5
+ import { toText } from '../reading/value-converter.js';
6
+ import { Diagnostic, DiagnosticSeverity } from './diagnostic.js';
7
+
8
+ /** Where a diagnostic applies. */
9
+ export class Location {
10
+ constructor(part = null, line = null, position = null, path = null) {
11
+ this.part = part;
12
+ this.line = line;
13
+ this.position = position;
14
+ this.path = path;
15
+ Object.freeze(this);
16
+ }
17
+
18
+ static NONE = new Location();
19
+ }
20
+
21
+ const SEVERITIES = new Set(Object.values(DiagnosticSeverity));
22
+
23
+ /** @type {Map<string, object> | null} */
24
+ let catalog = null;
25
+
26
+ function entry(code) {
27
+ catalog ??= new Map(objects(readJson('diagnostics.json'), 'codes').map((c) => [string(c, 'code'), c]));
28
+ const found = catalog.get(code);
29
+ if (found === undefined) {
30
+ throw new Error(`Diagnostic code ${code} is not defined in diagnostics.json.`);
31
+ }
32
+
33
+ return found;
34
+ }
35
+
36
+ function severityOf(json) {
37
+ const severity = string(json, 'severity');
38
+ return SEVERITIES.has(severity) ? severity : DiagnosticSeverity.Fatal;
39
+ }
40
+
41
+ function format(value) {
42
+ return value instanceof Decimal ? value.toString() : toText(value);
43
+ }
44
+ /**
45
+ * Collects diagnostics, formats messages from diagnostics.json and caps the list.
46
+ * The counts per severity cover every diagnostic, also the ones left out of a full list,
47
+ * and a full list makes room for an error by dropping an info or warning, so errors are never hidden behind them.
48
+ */
49
+ export class DiagnosticCollector {
50
+ #language;
51
+ #maximum;
52
+ /** @type {Diagnostic | null} */
53
+ #capped = null;
54
+ /** @type {Diagnostic[]} */
55
+ #items = [];
56
+ /** Number of diagnostics per severity, including the ones left out. */
57
+ counts = { info: 0, warning: 0, error: 0, fatal: 0 };
58
+
59
+ constructor(language, maximum) {
60
+ this.#language = language;
61
+ this.#maximum = maximum;
62
+ }
63
+
64
+ /** @type {Diagnostic[]} The kept diagnostics, followed by XAF-X900 when some were left out. */
65
+ get items() {
66
+ return this.#capped === null ? this.#items : [...this.#items, this.#capped];
67
+ }
68
+
69
+ get hasFatal() {
70
+ return this.counts.fatal > 0;
71
+ }
72
+
73
+ get hasErrors() {
74
+ return this.hasFatal || this.counts.error > 0;
75
+ }
76
+
77
+ /** True when diagnostics were left out of {@link DiagnosticCollector#items}. */
78
+ get isTruncated() {
79
+ return this.#capped !== null;
80
+ }
81
+
82
+ /**
83
+ * @param {string} code
84
+ * @param {Location} location
85
+ * @param {Record<string, unknown>} [args]
86
+ */
87
+ add(code, location, args = {}) {
88
+ const json = entry(code);
89
+ const severity = severityOf(json);
90
+ this.counts[severity]++;
91
+
92
+ if (this.#items.length >= this.#maximum && !this.#makeRoomFor(severity)) {
93
+ this.#capped ??= this.#create('XAF-X900', Location.NONE, { max: this.#maximum });
94
+ return;
95
+ }
96
+
97
+ this.#items.push(this.#create(code, location, args));
98
+ }
99
+
100
+ /**
101
+ * A fatal diagnostic is always kept. An error replaces the last info or warning in the list;
102
+ * a dropped diagnostic still counts, and the list is marked as truncated.
103
+ */
104
+ #makeRoomFor(severity) {
105
+ if (severity === DiagnosticSeverity.Fatal) {
106
+ return true;
107
+ }
108
+
109
+ if (severity !== DiagnosticSeverity.Error) {
110
+ return false;
111
+ }
112
+
113
+ let index = this.#items.findLastIndex((d) => d.severity === DiagnosticSeverity.Info);
114
+ if (index < 0) {
115
+ index = this.#items.findLastIndex((d) => d.severity === DiagnosticSeverity.Warning);
116
+ }
117
+
118
+ if (index < 0) {
119
+ return false;
120
+ }
121
+
122
+ this.#items.splice(index, 1);
123
+ this.#capped ??= this.#create('XAF-X900', Location.NONE, { max: this.#maximum });
124
+ return true;
125
+ }
126
+
127
+ #create(code, location, args) {
128
+ const json = entry(code);
129
+ let message = LocalizedText.from(object(json, 'message'))?.get(this.#language) ?? code;
130
+ for (const [name, value] of Object.entries(args)) {
131
+ // A replacer function: values come from the file and must never be read as replacement patterns ($&, $1).
132
+ const text = format(value);
133
+ message = message.replaceAll(`{${name}}`, () => text);
134
+ }
135
+
136
+ return new Diagnostic(code, string(json, 'layer'), severityOf(json), message,
137
+ location.part, location.line, location.position, location.path);
138
+ }
139
+ }