@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,232 @@
1
+ import { OPEN, PartInfo, SourceUsageError } from '../reading/source.js';
2
+ import { PartEncoding, PartEncodingError } from './encoding.js';
3
+ import { Sha256 } from './sha256.js';
4
+
5
+ const PREFIX_SIZE = 1024;
6
+
7
+ /** A part could not be opened or read; reported as XAF-X002, never thrown to the caller. */
8
+ export class SourceError extends Error {
9
+ constructor(part, cause) {
10
+ super(cause?.message ?? String(cause), { cause });
11
+ this.name = 'SourceError';
12
+ this.part = part;
13
+ }
14
+ }
15
+
16
+ /** Runs a call into the caller's stream or factory; any failure becomes a SourceError for that part. */
17
+ async function guarded(part, call) {
18
+ try {
19
+ return await call();
20
+ } catch (error) {
21
+ throw error instanceof SourceError || error instanceof SourceUsageError ? error : new SourceError(part, error);
22
+ }
23
+ }
24
+
25
+ /** @param {unknown} chunk */
26
+ function toBytes(chunk) {
27
+ if (chunk instanceof Uint8Array) {
28
+ return chunk;
29
+ }
30
+
31
+ if (chunk instanceof ArrayBuffer) {
32
+ return new Uint8Array(chunk);
33
+ }
34
+
35
+ if (ArrayBuffer.isView(chunk)) {
36
+ return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
37
+ }
38
+
39
+ throw new TypeError('A stream must produce bytes (Uint8Array); text is refused because its encoding is unknown.');
40
+ }
41
+
42
+ async function* single(bytes) {
43
+ yield bytes;
44
+ }
45
+
46
+ async function* fromReader(stream, keepOpen) {
47
+ const reader = stream.getReader();
48
+ let done = false;
49
+ try {
50
+ for (;;) {
51
+ const result = await reader.read();
52
+ if (result.done) {
53
+ done = true;
54
+ return;
55
+ }
56
+
57
+ yield result.value;
58
+ }
59
+ } finally {
60
+ if (!done && !keepOpen) {
61
+ await reader.cancel().catch(() => {});
62
+ }
63
+
64
+ reader.releaseLock();
65
+ }
66
+ }
67
+
68
+ /**
69
+ * An async iterator over the chunks of any supported stream.
70
+ * With keepOpen the caller's stream is not destroyed when reading stops early.
71
+ * @returns {AsyncIterator<unknown>}
72
+ */
73
+ export function iterate(stream, keepOpen) {
74
+ if (stream instanceof Uint8Array || stream instanceof ArrayBuffer || ArrayBuffer.isView(stream)) {
75
+ return single(toBytes(stream));
76
+ }
77
+
78
+ if (typeof Blob !== 'undefined' && stream instanceof Blob) {
79
+ return iterate(stream.stream(), false);
80
+ }
81
+
82
+ if (stream !== null && typeof stream === 'object') {
83
+ // Node Readable: do not destroy a caller-owned stream.
84
+ if (typeof stream.iterator === 'function' && typeof stream.pipe === 'function') {
85
+ return stream.iterator({ destroyOnReturn: !keepOpen });
86
+ }
87
+
88
+ // Web ReadableStream.
89
+ if (typeof stream.getReader === 'function') {
90
+ return fromReader(stream, keepOpen);
91
+ }
92
+
93
+ if (typeof stream[Symbol.asyncIterator] === 'function') {
94
+ return stream[Symbol.asyncIterator]();
95
+ }
96
+
97
+ if (typeof stream[Symbol.iterator] === 'function') {
98
+ return (async function* () {
99
+ yield* stream;
100
+ })();
101
+ }
102
+ }
103
+
104
+ throw new TypeError('Unsupported stream: pass a Readable, ReadableStream, Blob, Uint8Array or an iterable of Uint8Array.');
105
+ }
106
+
107
+ /**
108
+ * Reads the parts of a source one after another as one document.
109
+ * For continuation parts a leading byte order mark and XML declaration are skipped; a part in another encoding
110
+ * than the first is refused (PartEncodingError), because it would be decoded with the encoding of the first part.
111
+ * Collects size, SHA-256 and line count per part when collectStatistics is set.
112
+ */
113
+ export class ConcatenatedStream {
114
+ #parts;
115
+ /** @type {PartInfo[]} Statistics of the parts read completely so far. */
116
+ completed = [];
117
+ /** Number (1-based) of the part the most recent chunk came from. */
118
+ part = 0;
119
+ /** Bytes handed out so far, over all parts. */
120
+ position = 0;
121
+ /** @type {PartEncoding | null} */
122
+ #encoding = null;
123
+
124
+ /**
125
+ * @param {readonly import('../reading/source.js').SourcePart[]} parts
126
+ * @param {boolean} collectStatistics
127
+ */
128
+ constructor(parts, collectStatistics) {
129
+ this.#parts = parts;
130
+ this.collectStatistics = collectStatistics;
131
+ }
132
+
133
+ /** @returns {AsyncGenerator<Uint8Array>} */
134
+ async *chunks() {
135
+ for (const part of this.#parts) {
136
+ this.part = part.number;
137
+ const { stream, keepOpen } = await guarded(part.name, () => part[OPEN]());
138
+ const iterator = iterate(stream, keepOpen);
139
+ const next = () => guarded(part.name, () => iterator.next());
140
+ const statistics = this.collectStatistics ? { hash: Sha256.create(), size: 0, lines: 1, lastWasCarriageReturn: false } : null;
141
+ const track = (bytes) => {
142
+ if (statistics === null) {
143
+ return;
144
+ }
145
+
146
+ statistics.size += bytes.length;
147
+ statistics.hash.update(bytes);
148
+ for (const b of bytes) {
149
+ if (b === 0x0a) {
150
+ if (!statistics.lastWasCarriageReturn) {
151
+ statistics.lines++;
152
+ }
153
+
154
+ statistics.lastWasCarriageReturn = false;
155
+ } else if (b === 0x0d) {
156
+ statistics.lines++;
157
+ statistics.lastWasCarriageReturn = true;
158
+ } else {
159
+ statistics.lastWasCarriageReturn = false;
160
+ }
161
+ }
162
+ };
163
+
164
+ try {
165
+ const prefix = [];
166
+ let prefixLength = 0;
167
+ let done = false;
168
+ while (prefixLength < PREFIX_SIZE) {
169
+ const result = await next();
170
+ if (result.done) {
171
+ done = true;
172
+ break;
173
+ }
174
+
175
+ const bytes = toBytes(result.value);
176
+ prefix.push(bytes);
177
+ prefixLength += bytes.length;
178
+ }
179
+
180
+ const first = concat(prefix, prefixLength);
181
+ track(first);
182
+ const encoding = PartEncoding.detect(first);
183
+ if (part.number === 1) {
184
+ this.#encoding = encoding;
185
+ } else if (!encoding.sameAs(this.#encoding)) {
186
+ throw new PartEncodingError(part.number, encoding, this.#encoding);
187
+ }
188
+
189
+ const rest = part.number === 1 ? first : first.subarray(encoding.skipDeclaration(first, Math.min(first.length, PREFIX_SIZE)));
190
+ if (rest.length > 0) {
191
+ this.position += rest.length;
192
+ yield rest;
193
+ }
194
+
195
+ while (!done) {
196
+ const result = await next();
197
+ if (result.done) {
198
+ break;
199
+ }
200
+
201
+ const bytes = toBytes(result.value);
202
+ track(bytes);
203
+ if (bytes.length > 0) {
204
+ this.position += bytes.length;
205
+ yield bytes;
206
+ }
207
+ }
208
+
209
+ if (statistics !== null) {
210
+ this.completed.push(new PartInfo(part.number, part.name, statistics.size, statistics.hash.digestHex(), statistics.lines));
211
+ }
212
+ } finally {
213
+ await iterator.return?.();
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ function concat(chunks, length) {
220
+ if (chunks.length === 1) {
221
+ return chunks[0];
222
+ }
223
+
224
+ const bytes = new Uint8Array(length);
225
+ let offset = 0;
226
+ for (const chunk of chunks) {
227
+ bytes.set(chunk, offset);
228
+ offset += chunk.length;
229
+ }
230
+
231
+ return bytes;
232
+ }
@@ -0,0 +1,247 @@
1
+ import { SaxesParser } from 'saxes';
2
+ import { PartEncoding, XmlEncodingError } from './encoding.js';
3
+
4
+ /** Event kinds. */
5
+ export const START = 1;
6
+ export const END = 2;
7
+ export const TEXT = 3;
8
+
9
+ const SLICE_SIZE = 64 * 1024;
10
+
11
+ /** The document is not well-formed XML, or its bytes cannot be decoded. */
12
+ export class XmlSyntaxError extends Error {
13
+ constructor(message, line, column, part) {
14
+ super(message);
15
+ this.name = 'XmlSyntaxError';
16
+ this.line = line;
17
+ this.column = column;
18
+ this.part = part;
19
+ }
20
+ }
21
+
22
+ export { XmlEncodingError };
23
+
24
+ function createDecoder(encoding) {
25
+ try {
26
+ return new TextDecoder(encoding, { fatal: true });
27
+ } catch {
28
+ throw new XmlEncodingError(encoding);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Pull reader over a byte stream: start, text and end events with line, column, depth and source part.
34
+ * DTDs are refused, entities other than the five predefined ones are errors, and namespaces are resolved.
35
+ */
36
+ export class XmlReader {
37
+ #stream;
38
+ #chunks;
39
+ #parser;
40
+ #decoder = null;
41
+ #queue = [];
42
+ #head = 0;
43
+ #depth = 0;
44
+ #ended = false;
45
+ #pendingText = null;
46
+ #tagLine = 1;
47
+ #tagColumn = 1;
48
+ #mapFrom = null;
49
+ #mapTo = null;
50
+ #fed = 0;
51
+ #characters = 0;
52
+ #maxCharacters = 0;
53
+
54
+ /** @param {import('./streams.js').ConcatenatedStream} stream */
55
+ /**
56
+ * @param {import('./streams.js').ConcatenatedStream} stream
57
+ * @param {{ maxCharacters?: number }} [limits] maxCharacters: characters in the whole document; 0 is no limit.
58
+ */
59
+ constructor(stream, { maxCharacters = 0 } = {}) {
60
+ this.#maxCharacters = maxCharacters;
61
+ this.#stream = stream;
62
+ this.#chunks = stream.chunks();
63
+ const parser = new SaxesParser({ xmlns: true, position: true, forceXMLVersion: true, defaultXMLVersion: '1.0' });
64
+ this.#parser = parser;
65
+
66
+ parser.on('error', (error) => {
67
+ throw new XmlSyntaxError(error.message.replace(/^\d+:\d+: /, ''), parser.line, parser.column + 1, this.#stream.part);
68
+ });
69
+ parser.on('doctype', () => {
70
+ throw new XmlSyntaxError('For security reasons DTD is prohibited in this XML document.', parser.line, parser.column + 1, this.#stream.part);
71
+ });
72
+ parser.on('opentagstart', (tag) => {
73
+ this.#flushText();
74
+ // The parser stands after the name; report the position of the name itself (1-based), as .NET does.
75
+ this.#tagLine = parser.line;
76
+ this.#tagColumn = Math.max(1, parser.column - tag.name.length + 1);
77
+ });
78
+ parser.on('opentag', (tag) => {
79
+ const attributes = [];
80
+ for (const attribute of Object.values(tag.attributes)) {
81
+ attributes.push({ ns: attribute.uri, local: attribute.local, prefix: attribute.prefix, value: attribute.value });
82
+ }
83
+
84
+ this.#queue.push({
85
+ kind: START, ns: tag.uri, local: tag.local, prefix: tag.prefix, attributes,
86
+ line: this.#tagLine, column: this.#tagColumn, depth: this.#depth, part: this.#stream.part,
87
+ });
88
+ if (this.#depth === 0) {
89
+ this.rootOffset = this.#fed;
90
+ }
91
+
92
+ this.#depth++;
93
+ });
94
+ parser.on('closetag', (tag) => {
95
+ this.#flushText();
96
+ this.#depth--;
97
+ this.#queue.push({ kind: END, ns: tag.uri, local: tag.local, line: parser.line, column: parser.column, depth: this.#depth, part: this.#stream.part });
98
+ });
99
+ const text = (value) => {
100
+ if (this.#pendingText === null) {
101
+ this.#pendingText = { kind: TEXT, text: value, line: parser.line, column: parser.column, part: this.#stream.part };
102
+ } else {
103
+ this.#pendingText.text += value;
104
+ }
105
+ };
106
+ parser.on('text', text);
107
+ parser.on('cdata', text);
108
+ }
109
+
110
+ /** Bytes before the slice in which the root element starts; precise to one slice (64 KB). */
111
+ rootOffset = null;
112
+
113
+ /** Bytes handed to the parser so far. */
114
+ get position() {
115
+ return this.#stream.position;
116
+ }
117
+
118
+ /** The source part being parsed. */
119
+ get part() {
120
+ return this.#stream.part;
121
+ }
122
+
123
+ /** Presents one namespace as another, e.g. an alias as the official namespace. */
124
+ mapNamespace(from, to) {
125
+ this.#mapFrom = from;
126
+ this.#mapTo = to;
127
+ }
128
+
129
+ /**
130
+ * The next event when one is already parsed; undefined when the caller must await {@link XmlReader#next}.
131
+ * Saves a promise per event, which is most of the cost of a large file.
132
+ */
133
+ poll() {
134
+ if (this.#head >= this.#queue.length) {
135
+ return undefined;
136
+ }
137
+
138
+ const event = this.#queue[this.#head];
139
+ this.#queue[this.#head++] = undefined;
140
+ return this.#map(event);
141
+ }
142
+
143
+ /**
144
+ * The next event, or null at the end of the document.
145
+ * @throws {XmlSyntaxError | XmlEncodingError}
146
+ */
147
+ async next() {
148
+ while (this.#head >= this.#queue.length) {
149
+ this.#queue.length = 0;
150
+ this.#head = 0;
151
+ if (this.#ended) {
152
+ return null;
153
+ }
154
+
155
+ await this.#feed();
156
+ }
157
+
158
+ const event = this.#queue[this.#head];
159
+ this.#queue[this.#head++] = undefined;
160
+ return this.#map(event);
161
+ }
162
+
163
+ /** Puts an event back so the next call returns it again. */
164
+ pushBack(event) {
165
+ if (this.#head > 0) {
166
+ this.#queue[--this.#head] = event;
167
+ } else {
168
+ this.#queue.unshift(event);
169
+ }
170
+ }
171
+
172
+ /** Stops reading and closes the streams it opened. */
173
+ async close() {
174
+ this.#ended = true;
175
+ await this.#chunks.return();
176
+ }
177
+
178
+ async #feed() {
179
+ let result;
180
+ try {
181
+ result = await this.#chunks.next();
182
+ } catch (error) {
183
+ this.#ended = true;
184
+ throw error;
185
+ }
186
+
187
+ try {
188
+ if (result.done) {
189
+ this.#ended = true;
190
+ if (this.#decoder !== null) {
191
+ this.#write(this.#decoder.decode());
192
+ }
193
+
194
+ this.#parser.close();
195
+ this.#flushText();
196
+ return;
197
+ }
198
+
199
+ this.#decoder ??= createDecoder(PartEncoding.detect(result.value).name);
200
+ // Slices keep the byte position of events meaningful when a caller hands over the whole file at once.
201
+ for (let offset = 0; offset < result.value.length; offset += SLICE_SIZE) {
202
+ const slice = result.value.subarray(offset, offset + SLICE_SIZE);
203
+ this.#write(this.#decoder.decode(slice, { stream: true }));
204
+ this.#fed += slice.length;
205
+ }
206
+ } catch (error) {
207
+ this.#ended = true;
208
+ if (error instanceof TypeError && /decode|encoded data/i.test(error.message)) {
209
+ throw new XmlSyntaxError(`Invalid byte sequence for the encoding: ${error.message}`, this.#parser.line, this.#parser.column + 1, this.#stream.part);
210
+ }
211
+
212
+ throw error;
213
+ }
214
+ }
215
+
216
+ #write(text) {
217
+ if (text.length > 0) {
218
+ this.#characters += text.length;
219
+ if (this.#maxCharacters > 0 && this.#characters > this.#maxCharacters) {
220
+ throw new XmlSyntaxError(`The document has more than ${this.#maxCharacters} characters.`, this.#parser.line, this.#parser.column + 1, this.#stream.part);
221
+ }
222
+
223
+ this.#parser.write(text);
224
+ // Text is handed out per slice, so a huge text node is never joined into one string here.
225
+ this.#flushText();
226
+ }
227
+ }
228
+
229
+ #flushText() {
230
+ if (this.#pendingText !== null) {
231
+ this.#queue.push(this.#pendingText);
232
+ this.#pendingText = null;
233
+ }
234
+ }
235
+
236
+ #map(event) {
237
+ if (this.#mapFrom === null || event.kind === TEXT) {
238
+ return event;
239
+ }
240
+
241
+ const ns = event.ns === this.#mapFrom ? this.#mapTo : event.ns;
242
+ const attributes = event.kind === START && event.attributes.some((a) => a.ns === this.#mapFrom)
243
+ ? event.attributes.map((a) => (a.ns === this.#mapFrom ? { ...a, ns: this.#mapTo } : a))
244
+ : event.attributes;
245
+ return ns === event.ns && attributes === event.attributes ? event : { ...event, ns, attributes };
246
+ }
247
+ }
@@ -0,0 +1,68 @@
1
+ import { TextLanguage } from '../definitions/localized-text.js';
2
+ import { SinkCapabilities } from '../sinks/sink.js';
3
+
4
+ /** Keeps all rows in memory, per table. Useful for tests, small files and the browser. */
5
+ export class MemorySink {
6
+ /** @type {Map<string, import('../reading/row.js').AuditfileRow[]>} */
7
+ #tables = new Map();
8
+
9
+ capabilities = SinkCapabilities.None;
10
+ /** @type {import('../schema/schema.js').AuditfileSchema | null} The import schema, after begin. */
11
+ schema = null;
12
+ /** @type {import('../diagnostics/report.js').AuditfileReport | null} The report, after completion or abort. */
13
+ report = null;
14
+ /** True when the import was aborted; no rows are kept then. */
15
+ isAborted = false;
16
+
17
+ /**
18
+ * Rows of a table; empty when the table has no rows.
19
+ * @param {string} table
20
+ * @returns {readonly import('../reading/row.js').AuditfileRow[]}
21
+ */
22
+ rows(table) {
23
+ return this.#tables.get(table) ?? [];
24
+ }
25
+
26
+ /**
27
+ * Runs a view, e.g. `sink.run(sink.schema.version.findView('trial_balance'))`, over the rows in memory.
28
+ * @param {import('../definitions/view-definition.js').ViewDefinition} view
29
+ * @param {Record<string, unknown> | Map<string, unknown> | null} [parameters]
30
+ * @param {'nl' | 'en'} [language]
31
+ * @throws {Error} Nothing has been read yet.
32
+ */
33
+ run(view, parameters = null, language = TextLanguage.Dutch) {
34
+ if (this.schema === null) {
35
+ throw new Error('Read an audit file into this sink first.');
36
+ }
37
+
38
+ return view.run(this.schema, (table) => this.rows(table), parameters, language);
39
+ }
40
+
41
+ begin(context) {
42
+ this.#tables.clear();
43
+ this.schema = context.schema;
44
+ this.isAborted = false;
45
+ }
46
+
47
+ write(table, rows) {
48
+ let list = this.#tables.get(table.name);
49
+ if (list === undefined) {
50
+ list = [];
51
+ this.#tables.set(table.name, list);
52
+ }
53
+
54
+ for (const row of rows) {
55
+ list.push(row);
56
+ }
57
+ }
58
+
59
+ complete(report) {
60
+ this.report = report;
61
+ }
62
+
63
+ abort(report) {
64
+ this.#tables.clear();
65
+ this.report = report;
66
+ this.isAborted = true;
67
+ }
68
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The paths of an import as a tree, built once per import: every node knows its table, column or group.
3
+ * Reading an element is then one lookup of its local name under the current node, instead of building the
4
+ * full path as a string and looking that up. Only known paths have a node, so a file cannot make the tree grow.
5
+ */
6
+ export class MappingPlan {
7
+ /** @type {Map<string, PathNode>} */
8
+ #nodes = new Map();
9
+ /** Above the root element: its only child is the root element of the version. */
10
+ root = new PathNode('');
11
+
12
+ /** @param {import('../schema/schema.js').AuditfileSchema} schema */
13
+ constructor(schema) {
14
+ const plans = [];
15
+ for (const table of schema.tables) {
16
+ const plan = new TablePlan(table);
17
+ plans.push(plan);
18
+ this.#node(table.path).table = plan;
19
+
20
+ for (const column of table.columns) {
21
+ if (column.path !== '.') {
22
+ this.#node(`${table.path}/${column.path}`).column = { table: plan, column };
23
+ }
24
+ }
25
+ }
26
+
27
+ for (const node of this.#nodes.values()) {
28
+ node.isGroup = node.table === null && node.column === null;
29
+ }
30
+
31
+ for (const plan of plans) {
32
+ plan.required = plan.table.columns
33
+ .filter((c) => c.minOccurs >= 1 && c.path !== '.')
34
+ .map((c) => {
35
+ const slash = c.path.lastIndexOf('/');
36
+ return { column: c, group: slash > 0 ? this.#nodes.get(`${plan.table.path}/${c.path.slice(0, slash)}`) : null };
37
+ });
38
+ }
39
+ }
40
+
41
+ #node(path) {
42
+ let node = this.#nodes.get(path);
43
+ if (node !== undefined) {
44
+ return node;
45
+ }
46
+
47
+ const slash = path.lastIndexOf('/');
48
+ const parent = slash < 0 ? this.root : this.#node(path.slice(0, slash));
49
+ const name = slash < 0 ? path : path.slice(slash + 1);
50
+ node = new PathNode(path);
51
+ (name.startsWith('@') ? parent.attributes : parent.children).set(name.replace(/^@/, ''), node);
52
+ this.#nodes.set(path, node);
53
+ return node;
54
+ }
55
+ }
56
+
57
+ /** A known element or attribute path. */
58
+ export class PathNode {
59
+ /** @type {TablePlan | null} */
60
+ table = null;
61
+ /** @type {{ table: TablePlan, column: import('../schema/schema.js').SchemaColumn } | null} */
62
+ column = null;
63
+ /** A flattened element or pure container: no table and no column. */
64
+ isGroup = false;
65
+ /** @type {Map<string, PathNode>} Child elements by local name. */
66
+ children = new Map();
67
+ /** @type {Map<string, PathNode>} Attributes by local name. */
68
+ attributes = new Map();
69
+
70
+ /** @param {string} path The full path, e.g. "auditfile/company/transactions"; attributes as ".../@name". */
71
+ constructor(path) {
72
+ this.path = path;
73
+ }
74
+
75
+ /** The path of an unknown child, for a diagnostic. */
76
+ childPath(localName) {
77
+ return this.path === '' ? localName : `${this.path}/${localName}`;
78
+ }
79
+ }
80
+
81
+ export class TablePlan {
82
+ /** @type {{ column: import('../schema/schema.js').SchemaColumn, group: PathNode | null }[]} Columns with minOccurs ≥ 1, with the group they are in. */
83
+ required = [];
84
+
85
+ /** @param {import('../schema/schema.js').SchemaTable} table */
86
+ constructor(table) {
87
+ this.table = table;
88
+ /** A repeating simple element: one "value" column read from the element text. */
89
+ this.isValueTable = table.columns.length === 1 && table.columns[0].path === '.';
90
+ }
91
+ }