@noy-db/as-xml 0.1.0-pre.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.
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/index.cjs +286 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +95 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +248 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vLannaAi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @noy-db/as-xml
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@noy-db/as-xml)
|
|
4
|
+
|
|
5
|
+
> XML plaintext export for noy-db
|
|
6
|
+
|
|
7
|
+
Part of [**`@noy-db/hub`**](https://www.npmjs.com/package/@noy-db/hub) — the zero-knowledge, offline-first, encrypted document store.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @noy-db/hub @noy-db/as-xml
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What it is
|
|
16
|
+
|
|
17
|
+
XML plaintext export for noy-db — decrypts records and formats as XML with RFC-compliant entity escaping. For legacy systems, banking batch imports, SOAP endpoints, and accounting software that requires XML. Gated by RFC #249 canExportPlaintext.
|
|
18
|
+
|
|
19
|
+
## Status
|
|
20
|
+
|
|
21
|
+
**Pre-release** (`0.1.0-pre.1`). API may change before `1.0`.
|
|
22
|
+
|
|
23
|
+
## Documentation
|
|
24
|
+
|
|
25
|
+
See the [main repository](https://github.com/vLannaAi/noy-db#readme) for setup, examples, and the full subsystem catalog.
|
|
26
|
+
|
|
27
|
+
- Source — [`packages/as-xml`](https://github.com/vLannaAi/noy-db/tree/main/packages/as-xml)
|
|
28
|
+
- Issues — [github.com/vLannaAi/noy-db/issues](https://github.com/vLannaAi/noy-db/issues)
|
|
29
|
+
- Spec — [`SPEC.md`](https://github.com/vLannaAi/noy-db/blob/main/SPEC.md)
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
[MIT](./LICENSE) © vLannaAi
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
download: () => download,
|
|
34
|
+
fromString: () => fromString,
|
|
35
|
+
toString: () => toString,
|
|
36
|
+
write: () => write
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
var import_hub = require("@noy-db/hub");
|
|
40
|
+
var import_fast_xml_parser = require("fast-xml-parser");
|
|
41
|
+
async function toString(vault, options) {
|
|
42
|
+
vault.assertCanExport("plaintext", "xml");
|
|
43
|
+
const records = [];
|
|
44
|
+
for await (const chunk of vault.exportStream({ granularity: "collection" })) {
|
|
45
|
+
if (chunk.collection === options.collection) {
|
|
46
|
+
records.push(...chunk.records);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const rootName = options.rootElement ?? "Records";
|
|
51
|
+
const recordName = options.recordElement ?? pascalSingular(options.collection);
|
|
52
|
+
const fields = options.fields ?? inferFields(records);
|
|
53
|
+
const pretty = options.pretty !== false;
|
|
54
|
+
const declaration = options.xmlDeclaration !== false;
|
|
55
|
+
const indent = pretty ? " " : "";
|
|
56
|
+
const nl = pretty ? "\n" : "";
|
|
57
|
+
const parts = [];
|
|
58
|
+
if (declaration) parts.push('<?xml version="1.0" encoding="UTF-8"?>' + nl);
|
|
59
|
+
const nsAttr = options.namespace ? options.namespacePrefix ? ` xmlns:${options.namespacePrefix}="${escapeAttr(options.namespace)}"` : ` xmlns="${escapeAttr(options.namespace)}"` : "";
|
|
60
|
+
const rootTag = options.namespacePrefix ? `${options.namespacePrefix}:${rootName}` : rootName;
|
|
61
|
+
const recordTag = options.namespacePrefix ? `${options.namespacePrefix}:${recordName}` : recordName;
|
|
62
|
+
parts.push(`<${rootTag}${nsAttr}>`);
|
|
63
|
+
for (const record of records) {
|
|
64
|
+
parts.push(nl + indent + `<${recordTag}>`);
|
|
65
|
+
for (const field of fields) {
|
|
66
|
+
const value = record[field];
|
|
67
|
+
if (value === void 0) continue;
|
|
68
|
+
const tag = escapeElementName(field);
|
|
69
|
+
parts.push(nl + indent + indent + `<${tag}>${escapeText(serializeValue(value))}</${tag}>`);
|
|
70
|
+
}
|
|
71
|
+
parts.push(nl + indent + `</${recordTag}>`);
|
|
72
|
+
}
|
|
73
|
+
parts.push(nl + `</${rootTag}>`);
|
|
74
|
+
return parts.join("");
|
|
75
|
+
}
|
|
76
|
+
async function download(vault, options) {
|
|
77
|
+
const xml = await toString(vault, options);
|
|
78
|
+
const filename = options.filename ?? `${options.collection}.xml`;
|
|
79
|
+
const blob = new Blob([xml], { type: "application/xml;charset=utf-8" });
|
|
80
|
+
const url = URL.createObjectURL(blob);
|
|
81
|
+
const a = document.createElement("a");
|
|
82
|
+
a.href = url;
|
|
83
|
+
a.download = filename;
|
|
84
|
+
a.click();
|
|
85
|
+
URL.revokeObjectURL(url);
|
|
86
|
+
}
|
|
87
|
+
async function write(vault, path, options) {
|
|
88
|
+
if (options.acknowledgeRisks !== true) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
'as-xml.write: acknowledgeRisks: true is required for on-disk plaintext output. See docs/patterns/as-exports.md \xA7"The three tiers of \\"plaintext out\\""'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const xml = await toString(vault, options);
|
|
94
|
+
const { writeFile } = await import("fs/promises");
|
|
95
|
+
await writeFile(path, xml, "utf-8");
|
|
96
|
+
}
|
|
97
|
+
var XML_ENTITIES = {
|
|
98
|
+
"&": "&",
|
|
99
|
+
"<": "<",
|
|
100
|
+
">": ">",
|
|
101
|
+
'"': """,
|
|
102
|
+
"'": "'"
|
|
103
|
+
};
|
|
104
|
+
function escapeText(s) {
|
|
105
|
+
let out = s.replace(/[&<>]/g, (ch) => XML_ENTITIES[ch]);
|
|
106
|
+
let cleaned = "";
|
|
107
|
+
for (let i = 0; i < out.length; i++) {
|
|
108
|
+
const code = out.charCodeAt(i);
|
|
109
|
+
if (code >= 0 && code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31) continue;
|
|
110
|
+
cleaned += out[i];
|
|
111
|
+
}
|
|
112
|
+
out = cleaned;
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function escapeAttr(s) {
|
|
116
|
+
return s.replace(/[&<>"']/g, (ch) => XML_ENTITIES[ch]);
|
|
117
|
+
}
|
|
118
|
+
function escapeElementName(name) {
|
|
119
|
+
const safe = name.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
120
|
+
return /^[A-Za-z_]/.test(safe) ? safe : `_${safe}`;
|
|
121
|
+
}
|
|
122
|
+
function serializeValue(value) {
|
|
123
|
+
if (value === null) return "";
|
|
124
|
+
if (typeof value === "string") return value;
|
|
125
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
126
|
+
if (value instanceof Date) return value.toISOString();
|
|
127
|
+
return JSON.stringify(value);
|
|
128
|
+
}
|
|
129
|
+
function inferFields(records) {
|
|
130
|
+
const seen = /* @__PURE__ */ new Set();
|
|
131
|
+
const fields = [];
|
|
132
|
+
for (const r of records) {
|
|
133
|
+
if (r && typeof r === "object") {
|
|
134
|
+
for (const k of Object.keys(r)) {
|
|
135
|
+
if (k === "_v" || k === "_ts" || k === "_by" || k === "_iv" || k === "_data" || k === "_noydb") continue;
|
|
136
|
+
if (!seen.has(k)) {
|
|
137
|
+
seen.add(k);
|
|
138
|
+
fields.push(k);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return fields;
|
|
144
|
+
}
|
|
145
|
+
function pascalSingular(collection) {
|
|
146
|
+
const singular = collection.endsWith("s") ? collection.slice(0, -1) : collection;
|
|
147
|
+
return singular.charAt(0).toUpperCase() + singular.slice(1);
|
|
148
|
+
}
|
|
149
|
+
async function fromString(vault, xml, options) {
|
|
150
|
+
vault.assertCanImport("plaintext", "xml");
|
|
151
|
+
const policy = options.policy ?? "merge";
|
|
152
|
+
const idKey = options.idKey ?? "id";
|
|
153
|
+
const types = options.fieldTypes ?? {};
|
|
154
|
+
const parser = new import_fast_xml_parser.XMLParser({
|
|
155
|
+
ignoreAttributes: true,
|
|
156
|
+
parseTagValue: false,
|
|
157
|
+
// keep raw strings — we coerce ourselves
|
|
158
|
+
parseAttributeValue: false,
|
|
159
|
+
trimValues: true
|
|
160
|
+
// Only force-as-array the record element so a single-record XML still
|
|
161
|
+
// produces an array. We'll narrow this when we discover the element name.
|
|
162
|
+
});
|
|
163
|
+
const validation = import_fast_xml_parser.XMLValidator.validate(xml);
|
|
164
|
+
if (validation !== true) {
|
|
165
|
+
const err = validation.err;
|
|
166
|
+
throw new Error(
|
|
167
|
+
`as-xml.fromString: input is not valid XML (${err.code} at line ${err.line}: ${err.msg})`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
let parsed;
|
|
171
|
+
try {
|
|
172
|
+
parsed = parser.parse(xml);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
throw new Error(`as-xml.fromString: input is not valid XML (${err.message})`);
|
|
175
|
+
}
|
|
176
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
177
|
+
throw new Error("as-xml.fromString: parser produced a non-object root");
|
|
178
|
+
}
|
|
179
|
+
const root = stripNamespacePrefixes(parsed);
|
|
180
|
+
const rootKeys = Object.keys(root);
|
|
181
|
+
if (rootKeys.length === 0) {
|
|
182
|
+
return emptyPlan(vault, options.collection, policy, idKey);
|
|
183
|
+
}
|
|
184
|
+
const rootName = rootKeys.find((k) => !k.startsWith("?")) ?? rootKeys[0];
|
|
185
|
+
const rootBody = stripIfObject(root[rootName]);
|
|
186
|
+
if (rootBody === null) {
|
|
187
|
+
return emptyPlan(vault, options.collection, policy, idKey);
|
|
188
|
+
}
|
|
189
|
+
const recordElName = options.recordElement ? stripPrefix(options.recordElement) : pickRecordElementName(rootBody);
|
|
190
|
+
if (recordElName === null) {
|
|
191
|
+
return emptyPlan(vault, options.collection, policy, idKey);
|
|
192
|
+
}
|
|
193
|
+
const recordsRaw = rootBody[recordElName];
|
|
194
|
+
const recordsArr = recordsRaw === void 0 ? [] : Array.isArray(recordsRaw) ? recordsRaw : [recordsRaw];
|
|
195
|
+
const records = [];
|
|
196
|
+
for (const r of recordsArr) {
|
|
197
|
+
if (r === null || typeof r !== "object" || Array.isArray(r)) continue;
|
|
198
|
+
const flat = stripNamespacePrefixes(r);
|
|
199
|
+
const record = {};
|
|
200
|
+
for (const [field, raw] of Object.entries(flat)) {
|
|
201
|
+
const text = textValue(raw);
|
|
202
|
+
record[field] = coerce(text, types[field]);
|
|
203
|
+
}
|
|
204
|
+
records.push(record);
|
|
205
|
+
}
|
|
206
|
+
const plan = await (0, import_hub.diffVault)(vault, { [options.collection]: records }, {
|
|
207
|
+
collections: [options.collection],
|
|
208
|
+
idKey
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
plan,
|
|
212
|
+
policy,
|
|
213
|
+
async apply() {
|
|
214
|
+
await vault.noydb.transaction((tx) => {
|
|
215
|
+
const txVault = tx.vault(vault.name);
|
|
216
|
+
for (const entry of plan.added) {
|
|
217
|
+
txVault.collection(entry.collection).put(entry.id, entry.record);
|
|
218
|
+
}
|
|
219
|
+
if (policy !== "insert-only") {
|
|
220
|
+
for (const entry of plan.modified) {
|
|
221
|
+
txVault.collection(entry.collection).put(entry.id, entry.record);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (policy === "replace") {
|
|
225
|
+
for (const entry of plan.deleted) {
|
|
226
|
+
txVault.collection(entry.collection).delete(entry.id);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
async function emptyPlan(vault, collection, policy, idKey) {
|
|
234
|
+
const plan = await (0, import_hub.diffVault)(vault, { [collection]: [] }, { collections: [collection], idKey });
|
|
235
|
+
return { plan, policy, async apply() {
|
|
236
|
+
} };
|
|
237
|
+
}
|
|
238
|
+
function stripPrefix(name) {
|
|
239
|
+
const idx = name.indexOf(":");
|
|
240
|
+
return idx === -1 ? name : name.slice(idx + 1);
|
|
241
|
+
}
|
|
242
|
+
function stripNamespacePrefixes(obj) {
|
|
243
|
+
const out = {};
|
|
244
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
245
|
+
out[stripPrefix(k)] = v;
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
function stripIfObject(v) {
|
|
250
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) return null;
|
|
251
|
+
return stripNamespacePrefixes(v);
|
|
252
|
+
}
|
|
253
|
+
function pickRecordElementName(body) {
|
|
254
|
+
for (const k of Object.keys(body)) {
|
|
255
|
+
if (k.startsWith("@_") || k.startsWith("?")) continue;
|
|
256
|
+
return k;
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
function textValue(raw) {
|
|
261
|
+
if (raw === null || raw === void 0) return "";
|
|
262
|
+
if (typeof raw === "string") return raw;
|
|
263
|
+
if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
|
|
264
|
+
return "";
|
|
265
|
+
}
|
|
266
|
+
function coerce(cell, type) {
|
|
267
|
+
if (type === "number") {
|
|
268
|
+
if (cell === "") return void 0;
|
|
269
|
+
const n = Number(cell);
|
|
270
|
+
return Number.isFinite(n) ? n : cell;
|
|
271
|
+
}
|
|
272
|
+
if (type === "boolean") {
|
|
273
|
+
if (cell === "true") return true;
|
|
274
|
+
if (cell === "false") return false;
|
|
275
|
+
return cell;
|
|
276
|
+
}
|
|
277
|
+
return cell;
|
|
278
|
+
}
|
|
279
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
280
|
+
0 && (module.exports = {
|
|
281
|
+
download,
|
|
282
|
+
fromString,
|
|
283
|
+
toString,
|
|
284
|
+
write
|
|
285
|
+
});
|
|
286
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/as-xml** — XML plaintext export for noy-db.\n *\n * Hand-rolled XML emitter — zero dependencies, ~100 LoC. Escapes the\n * five predefined XML entities (`<`, `>`, `&`, `'`, `\"`) and numeric\n * control characters. Supports custom root/record element names,\n * namespaces, pretty-printing, and XML declarations.\n *\n * **Scope.** Single collection per call (mirroring as-csv's shape).\n * Multi-collection XML documents are best produced by the consumer\n * composing multiple `toString()` calls into a wrapping element.\n *\n * ### When to use\n *\n * - Legacy systems requiring XML input (accounting software, SOAP).\n * - Banking batch imports (ISO 20022, CAMT/PAIN files consumers wrap).\n * - Excel `.xml` SpreadsheetML 2003 legacy format (wrap in a\n * Workbook template).\n *\n * @packageDocumentation\n */\n\nimport type { Vault } from '@noy-db/hub'\n\nexport interface AsXMLOptions {\n /** Collection to export. */\n readonly collection: string\n /** Root element name. Default `'Records'`. */\n readonly rootElement?: string\n /** Per-record element name. Default pascal-cased singular of collection. */\n readonly recordElement?: string\n /** Explicit field list — defaults to inferred columns. */\n readonly fields?: readonly string[]\n /** Include `<?xml version=\"1.0\" encoding=\"UTF-8\"?>` declaration. Default `true`. */\n readonly xmlDeclaration?: boolean\n /** Pretty-print with 2-space indentation. Default `true`. */\n readonly pretty?: boolean\n /** Optional XML namespace on the root element. */\n readonly namespace?: string\n /** Optional namespace prefix. Used together with `namespace`. */\n readonly namespacePrefix?: string\n}\n\nexport interface AsXMLDownloadOptions extends AsXMLOptions {\n /** Filename offered to the browser. Default `'<collection>.xml'`. */\n readonly filename?: string\n}\n\nexport interface AsXMLWriteOptions extends AsXMLOptions {\n /** Required for Node file-write — Tier 3 risk gate. */\n readonly acknowledgeRisks: true\n}\n\nexport async function toString(vault: Vault, options: AsXMLOptions): Promise<string> {\n vault.assertCanExport('plaintext', 'xml')\n\n const records: unknown[] = []\n for await (const chunk of vault.exportStream({ granularity: 'collection' })) {\n if (chunk.collection === options.collection) {\n records.push(...chunk.records)\n break\n }\n }\n\n const rootName = options.rootElement ?? 'Records'\n const recordName = options.recordElement ?? pascalSingular(options.collection)\n const fields = options.fields ?? inferFields(records)\n const pretty = options.pretty !== false\n const declaration = options.xmlDeclaration !== false\n const indent = pretty ? ' ' : ''\n const nl = pretty ? '\\n' : ''\n\n const parts: string[] = []\n if (declaration) parts.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>' + nl)\n\n const nsAttr = options.namespace\n ? options.namespacePrefix\n ? ` xmlns:${options.namespacePrefix}=\"${escapeAttr(options.namespace)}\"`\n : ` xmlns=\"${escapeAttr(options.namespace)}\"`\n : ''\n const rootTag = options.namespacePrefix\n ? `${options.namespacePrefix}:${rootName}`\n : rootName\n const recordTag = options.namespacePrefix\n ? `${options.namespacePrefix}:${recordName}`\n : recordName\n\n parts.push(`<${rootTag}${nsAttr}>`)\n for (const record of records) {\n parts.push(nl + indent + `<${recordTag}>`)\n for (const field of fields) {\n const value = (record as Record<string, unknown>)[field]\n if (value === undefined) continue\n const tag = escapeElementName(field)\n parts.push(nl + indent + indent + `<${tag}>${escapeText(serializeValue(value))}</${tag}>`)\n }\n parts.push(nl + indent + `</${recordTag}>`)\n }\n parts.push(nl + `</${rootTag}>`)\n return parts.join('')\n}\n\nexport async function download(vault: Vault, options: AsXMLDownloadOptions): Promise<void> {\n const xml = await toString(vault, options)\n const filename = options.filename ?? `${options.collection}.xml`\n const blob = new Blob([xml], { type: 'application/xml;charset=utf-8' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n a.click()\n URL.revokeObjectURL(url)\n}\n\nexport async function write(vault: Vault, path: string, options: AsXMLWriteOptions): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-xml.write: acknowledgeRisks: true is required for on-disk plaintext output. ' +\n 'See docs/patterns/as-exports.md §\"The three tiers of \\\\\"plaintext out\\\\\"\"',\n )\n }\n const xml = await toString(vault, options)\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, xml, 'utf-8')\n}\n\n// ─── XML formatting internals ───────────────────────────────────────────\n\nconst XML_ENTITIES: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n}\n\nfunction escapeText(s: string): string {\n let out = s.replace(/[&<>]/g, ch => XML_ENTITIES[ch]!)\n // Strip XML-invalid control characters (U+0000–U+0008, U+000B, U+000C, U+000E–U+001F).\n // Done as a char-by-char scan to keep the regex free of control-character literals\n // (which eslint's no-control-regex flags).\n let cleaned = ''\n for (let i = 0; i < out.length; i++) {\n const code = out.charCodeAt(i)\n if (\n (code >= 0x00 && code <= 0x08) ||\n code === 0x0B ||\n code === 0x0C ||\n (code >= 0x0E && code <= 0x1F)\n ) continue\n cleaned += out[i]\n }\n out = cleaned\n return out\n}\n\nfunction escapeAttr(s: string): string {\n return s.replace(/[&<>\"']/g, ch => XML_ENTITIES[ch]!)\n}\n\nfunction escapeElementName(name: string): string {\n // XML element names must start with a letter/underscore and contain only\n // letters, digits, hyphens, underscores, dots. Replace everything else.\n const safe = name.replace(/[^A-Za-z0-9_.-]/g, '_')\n return /^[A-Za-z_]/.test(safe) ? safe : `_${safe}`\n}\n\nfunction serializeValue(value: unknown): string {\n if (value === null) return ''\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n return JSON.stringify(value)\n}\n\nfunction inferFields(records: readonly unknown[]): string[] {\n const seen = new Set<string>()\n const fields: string[] = []\n for (const r of records) {\n if (r && typeof r === 'object') {\n for (const k of Object.keys(r)) {\n if (k === '_v' || k === '_ts' || k === '_by' || k === '_iv' || k === '_data' || k === '_noydb') continue\n if (!seen.has(k)) {\n seen.add(k)\n fields.push(k)\n }\n }\n }\n }\n return fields\n}\n\nfunction pascalSingular(collection: string): string {\n const singular = collection.endsWith('s') ? collection.slice(0, -1) : collection\n return singular.charAt(0).toUpperCase() + singular.slice(1)\n}\n\n// ─── Reader ──────────────────────────────────────\n\nimport { diffVault, type VaultDiff } from '@noy-db/hub'\nimport { XMLParser, XMLValidator } from 'fast-xml-parser'\n\nexport type ImportPolicy = 'merge' | 'replace' | 'insert-only'\n\nexport interface AsXMLImportOptions {\n /** Target collection. Required — XML has no native collection grouping. */\n readonly collection: string\n /**\n * Optional explicit record-element name. When omitted, the reader\n * picks the first repeated child of the root. (Writer's default is\n * `pascalSingular(collection)` — pass that here for symmetric\n * round-tripping if you wrote with a custom `recordElement`.)\n */\n readonly recordElement?: string\n /**\n * Optional field type hints. Cells are returned as strings unless\n * overridden. Same shape as `as-csv.fromString`'s `columnTypes`.\n */\n readonly fieldTypes?: Record<string, 'string' | 'number' | 'boolean'>\n /** Field carrying the record id. Default `'id'`. */\n readonly idKey?: string\n /** Reconciliation policy. Default `'merge'`. */\n readonly policy?: ImportPolicy\n}\n\nexport interface AsXMLImportPlan {\n readonly plan: VaultDiff\n readonly policy: ImportPolicy\n apply(): Promise<void>\n}\n\n/**\n * Parse XML into records and build an import plan. Inverts what\n * `toString()` writes: root → recordElement[] → field elements with\n * text content. Field values default to strings; pass `fieldTypes` to\n * coerce numbers / booleans on read.\n *\n * Throws on malformed XML — fast-xml-parser is invoked in strict mode\n * so unbalanced tags or invalid character sequences fail fast.\n *\n * Capability: `assertCanImport('plaintext', 'xml')`.\n * Atomicity: `apply()` runs inside `vault.noydb.transaction()`.\n */\nexport async function fromString(\n vault: Vault,\n xml: string,\n options: AsXMLImportOptions,\n): Promise<AsXMLImportPlan> {\n vault.assertCanImport('plaintext', 'xml')\n\n const policy: ImportPolicy = options.policy ?? 'merge'\n const idKey = options.idKey ?? 'id'\n const types = options.fieldTypes ?? {}\n\n const parser = new XMLParser({\n ignoreAttributes: true,\n parseTagValue: false, // keep raw strings — we coerce ourselves\n parseAttributeValue: false,\n trimValues: true,\n // Only force-as-array the record element so a single-record XML still\n // produces an array. We'll narrow this when we discover the element name.\n })\n\n // Strict validation first — fast-xml-parser is permissive by default\n // (silently accepts unbalanced tags). XMLValidator returns true on\n // success or an error object describing the position of the failure.\n const validation = XMLValidator.validate(xml)\n if (validation !== true) {\n const err = validation.err\n throw new Error(\n `as-xml.fromString: input is not valid XML (${err.code} at line ${err.line}: ${err.msg})`,\n )\n }\n\n let parsed: unknown\n try {\n parsed = parser.parse(xml)\n } catch (err) {\n throw new Error(`as-xml.fromString: input is not valid XML (${(err as Error).message})`)\n }\n\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('as-xml.fromString: parser produced a non-object root')\n }\n\n // Drill into the root. fast-xml-parser surfaces `<Root><Item/>...</Root>` as\n // `{ Root: { Item: [...] | {...} } }`. Strip namespace prefixes from element\n // names by splitting on `:` so the writer's `<ns:Records>` round-trips.\n const root = stripNamespacePrefixes(parsed as Record<string, unknown>)\n const rootKeys = Object.keys(root)\n if (rootKeys.length === 0) {\n return emptyPlan(vault, options.collection, policy, idKey)\n }\n // Pick first non-`?xml` root child — fast-xml-parser ignores the\n // declaration by default but be defensive.\n const rootName = rootKeys.find((k) => !k.startsWith('?')) ?? rootKeys[0]!\n const rootBody = stripIfObject(root[rootName])\n\n if (rootBody === null) {\n return emptyPlan(vault, options.collection, policy, idKey)\n }\n\n const recordElName = options.recordElement\n ? stripPrefix(options.recordElement)\n : pickRecordElementName(rootBody)\n\n if (recordElName === null) {\n return emptyPlan(vault, options.collection, policy, idKey)\n }\n\n const recordsRaw = rootBody[recordElName]\n const recordsArr = recordsRaw === undefined ? []\n : Array.isArray(recordsRaw) ? recordsRaw : [recordsRaw]\n\n const records: Record<string, unknown>[] = []\n for (const r of recordsArr) {\n if (r === null || typeof r !== 'object' || Array.isArray(r)) continue\n const flat = stripNamespacePrefixes(r as Record<string, unknown>)\n const record: Record<string, unknown> = {}\n for (const [field, raw] of Object.entries(flat)) {\n // The XML parser exposes text-only elements as primitives or\n // empty objects (when the element is `<tag/>`). Normalize.\n const text = textValue(raw)\n record[field] = coerce(text, types[field])\n }\n records.push(record)\n }\n\n const plan = await diffVault(vault, { [options.collection]: records }, {\n collections: [options.collection],\n idKey,\n })\n\n return {\n plan,\n policy,\n async apply(): Promise<void> {\n // Routes through txStrategy seam — throws clearly when\n // withTransactions() isn't opted in. Atomicity rolls back any\n // partial writes if a put fails mid-batch.\n await vault.noydb.transaction((tx) => {\n const txVault = tx.vault(vault.name)\n for (const entry of plan.added) {\n txVault.collection(entry.collection).put(entry.id, entry.record)\n }\n if (policy !== 'insert-only') {\n for (const entry of plan.modified) {\n txVault.collection(entry.collection).put(entry.id, entry.record)\n }\n }\n if (policy === 'replace') {\n for (const entry of plan.deleted) {\n txVault.collection(entry.collection).delete(entry.id)\n }\n }\n })\n },\n }\n}\n\nasync function emptyPlan(\n vault: Vault,\n collection: string,\n policy: ImportPolicy,\n idKey: string,\n): Promise<AsXMLImportPlan> {\n const plan = await diffVault(vault, { [collection]: [] }, { collections: [collection], idKey })\n return { plan, policy, async apply() { /* nothing to do */ } }\n}\n\nfunction stripPrefix(name: string): string {\n const idx = name.indexOf(':')\n return idx === -1 ? name : name.slice(idx + 1)\n}\n\nfunction stripNamespacePrefixes(obj: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(obj)) {\n out[stripPrefix(k)] = v\n }\n return out\n}\n\nfunction stripIfObject(v: unknown): Record<string, unknown> | null {\n if (v === null || typeof v !== 'object' || Array.isArray(v)) return null\n return stripNamespacePrefixes(v as Record<string, unknown>)\n}\n\n/**\n * Pick the first non-attribute key in `body` — that's the per-record\n * element. Skips namespace-attribute keys like `@_xmlns:foo` (defensive\n * even though `ignoreAttributes: true` should drop them).\n */\nfunction pickRecordElementName(body: Record<string, unknown>): string | null {\n for (const k of Object.keys(body)) {\n if (k.startsWith('@_') || k.startsWith('?')) continue\n return k\n }\n return null\n}\n\n/**\n * Extract the text content from a parsed-element value. The parser\n * surfaces `<a>x</a>` as the string `\"x\"`, `<a/>` as the empty string,\n * and `<a><b>...</b></a>` as an object — we treat the last case as\n * \"not a leaf\" and stringify whatever's there.\n */\nfunction textValue(raw: unknown): string {\n if (raw === null || raw === undefined) return ''\n if (typeof raw === 'string') return raw\n if (typeof raw === 'number' || typeof raw === 'boolean') return String(raw)\n return ''\n}\n\nfunction coerce(cell: string, type?: 'string' | 'number' | 'boolean'): unknown {\n if (type === 'number') {\n if (cell === '') return undefined\n const n = Number(cell)\n return Number.isFinite(n) ? n : cell\n }\n if (type === 'boolean') {\n if (cell === 'true') return true\n if (cell === 'false') return false\n return cell\n }\n return cell\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { VaultDiff, Vault } from '@noy-db/hub';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* **@noy-db/as-xml** — XML plaintext export for noy-db.
|
|
5
|
+
*
|
|
6
|
+
* Hand-rolled XML emitter — zero dependencies, ~100 LoC. Escapes the
|
|
7
|
+
* five predefined XML entities (`<`, `>`, `&`, `'`, `"`) and numeric
|
|
8
|
+
* control characters. Supports custom root/record element names,
|
|
9
|
+
* namespaces, pretty-printing, and XML declarations.
|
|
10
|
+
*
|
|
11
|
+
* **Scope.** Single collection per call (mirroring as-csv's shape).
|
|
12
|
+
* Multi-collection XML documents are best produced by the consumer
|
|
13
|
+
* composing multiple `toString()` calls into a wrapping element.
|
|
14
|
+
*
|
|
15
|
+
* ### When to use
|
|
16
|
+
*
|
|
17
|
+
* - Legacy systems requiring XML input (accounting software, SOAP).
|
|
18
|
+
* - Banking batch imports (ISO 20022, CAMT/PAIN files consumers wrap).
|
|
19
|
+
* - Excel `.xml` SpreadsheetML 2003 legacy format (wrap in a
|
|
20
|
+
* Workbook template).
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface AsXMLOptions {
|
|
26
|
+
/** Collection to export. */
|
|
27
|
+
readonly collection: string;
|
|
28
|
+
/** Root element name. Default `'Records'`. */
|
|
29
|
+
readonly rootElement?: string;
|
|
30
|
+
/** Per-record element name. Default pascal-cased singular of collection. */
|
|
31
|
+
readonly recordElement?: string;
|
|
32
|
+
/** Explicit field list — defaults to inferred columns. */
|
|
33
|
+
readonly fields?: readonly string[];
|
|
34
|
+
/** Include `<?xml version="1.0" encoding="UTF-8"?>` declaration. Default `true`. */
|
|
35
|
+
readonly xmlDeclaration?: boolean;
|
|
36
|
+
/** Pretty-print with 2-space indentation. Default `true`. */
|
|
37
|
+
readonly pretty?: boolean;
|
|
38
|
+
/** Optional XML namespace on the root element. */
|
|
39
|
+
readonly namespace?: string;
|
|
40
|
+
/** Optional namespace prefix. Used together with `namespace`. */
|
|
41
|
+
readonly namespacePrefix?: string;
|
|
42
|
+
}
|
|
43
|
+
interface AsXMLDownloadOptions extends AsXMLOptions {
|
|
44
|
+
/** Filename offered to the browser. Default `'<collection>.xml'`. */
|
|
45
|
+
readonly filename?: string;
|
|
46
|
+
}
|
|
47
|
+
interface AsXMLWriteOptions extends AsXMLOptions {
|
|
48
|
+
/** Required for Node file-write — Tier 3 risk gate. */
|
|
49
|
+
readonly acknowledgeRisks: true;
|
|
50
|
+
}
|
|
51
|
+
declare function toString(vault: Vault, options: AsXMLOptions): Promise<string>;
|
|
52
|
+
declare function download(vault: Vault, options: AsXMLDownloadOptions): Promise<void>;
|
|
53
|
+
declare function write(vault: Vault, path: string, options: AsXMLWriteOptions): Promise<void>;
|
|
54
|
+
|
|
55
|
+
type ImportPolicy = 'merge' | 'replace' | 'insert-only';
|
|
56
|
+
interface AsXMLImportOptions {
|
|
57
|
+
/** Target collection. Required — XML has no native collection grouping. */
|
|
58
|
+
readonly collection: string;
|
|
59
|
+
/**
|
|
60
|
+
* Optional explicit record-element name. When omitted, the reader
|
|
61
|
+
* picks the first repeated child of the root. (Writer's default is
|
|
62
|
+
* `pascalSingular(collection)` — pass that here for symmetric
|
|
63
|
+
* round-tripping if you wrote with a custom `recordElement`.)
|
|
64
|
+
*/
|
|
65
|
+
readonly recordElement?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Optional field type hints. Cells are returned as strings unless
|
|
68
|
+
* overridden. Same shape as `as-csv.fromString`'s `columnTypes`.
|
|
69
|
+
*/
|
|
70
|
+
readonly fieldTypes?: Record<string, 'string' | 'number' | 'boolean'>;
|
|
71
|
+
/** Field carrying the record id. Default `'id'`. */
|
|
72
|
+
readonly idKey?: string;
|
|
73
|
+
/** Reconciliation policy. Default `'merge'`. */
|
|
74
|
+
readonly policy?: ImportPolicy;
|
|
75
|
+
}
|
|
76
|
+
interface AsXMLImportPlan {
|
|
77
|
+
readonly plan: VaultDiff;
|
|
78
|
+
readonly policy: ImportPolicy;
|
|
79
|
+
apply(): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Parse XML into records and build an import plan. Inverts what
|
|
83
|
+
* `toString()` writes: root → recordElement[] → field elements with
|
|
84
|
+
* text content. Field values default to strings; pass `fieldTypes` to
|
|
85
|
+
* coerce numbers / booleans on read.
|
|
86
|
+
*
|
|
87
|
+
* Throws on malformed XML — fast-xml-parser is invoked in strict mode
|
|
88
|
+
* so unbalanced tags or invalid character sequences fail fast.
|
|
89
|
+
*
|
|
90
|
+
* Capability: `assertCanImport('plaintext', 'xml')`.
|
|
91
|
+
* Atomicity: `apply()` runs inside `vault.noydb.transaction()`.
|
|
92
|
+
*/
|
|
93
|
+
declare function fromString(vault: Vault, xml: string, options: AsXMLImportOptions): Promise<AsXMLImportPlan>;
|
|
94
|
+
|
|
95
|
+
export { type AsXMLDownloadOptions, type AsXMLImportOptions, type AsXMLImportPlan, type AsXMLOptions, type AsXMLWriteOptions, type ImportPolicy, download, fromString, toString, write };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { VaultDiff, Vault } from '@noy-db/hub';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* **@noy-db/as-xml** — XML plaintext export for noy-db.
|
|
5
|
+
*
|
|
6
|
+
* Hand-rolled XML emitter — zero dependencies, ~100 LoC. Escapes the
|
|
7
|
+
* five predefined XML entities (`<`, `>`, `&`, `'`, `"`) and numeric
|
|
8
|
+
* control characters. Supports custom root/record element names,
|
|
9
|
+
* namespaces, pretty-printing, and XML declarations.
|
|
10
|
+
*
|
|
11
|
+
* **Scope.** Single collection per call (mirroring as-csv's shape).
|
|
12
|
+
* Multi-collection XML documents are best produced by the consumer
|
|
13
|
+
* composing multiple `toString()` calls into a wrapping element.
|
|
14
|
+
*
|
|
15
|
+
* ### When to use
|
|
16
|
+
*
|
|
17
|
+
* - Legacy systems requiring XML input (accounting software, SOAP).
|
|
18
|
+
* - Banking batch imports (ISO 20022, CAMT/PAIN files consumers wrap).
|
|
19
|
+
* - Excel `.xml` SpreadsheetML 2003 legacy format (wrap in a
|
|
20
|
+
* Workbook template).
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
interface AsXMLOptions {
|
|
26
|
+
/** Collection to export. */
|
|
27
|
+
readonly collection: string;
|
|
28
|
+
/** Root element name. Default `'Records'`. */
|
|
29
|
+
readonly rootElement?: string;
|
|
30
|
+
/** Per-record element name. Default pascal-cased singular of collection. */
|
|
31
|
+
readonly recordElement?: string;
|
|
32
|
+
/** Explicit field list — defaults to inferred columns. */
|
|
33
|
+
readonly fields?: readonly string[];
|
|
34
|
+
/** Include `<?xml version="1.0" encoding="UTF-8"?>` declaration. Default `true`. */
|
|
35
|
+
readonly xmlDeclaration?: boolean;
|
|
36
|
+
/** Pretty-print with 2-space indentation. Default `true`. */
|
|
37
|
+
readonly pretty?: boolean;
|
|
38
|
+
/** Optional XML namespace on the root element. */
|
|
39
|
+
readonly namespace?: string;
|
|
40
|
+
/** Optional namespace prefix. Used together with `namespace`. */
|
|
41
|
+
readonly namespacePrefix?: string;
|
|
42
|
+
}
|
|
43
|
+
interface AsXMLDownloadOptions extends AsXMLOptions {
|
|
44
|
+
/** Filename offered to the browser. Default `'<collection>.xml'`. */
|
|
45
|
+
readonly filename?: string;
|
|
46
|
+
}
|
|
47
|
+
interface AsXMLWriteOptions extends AsXMLOptions {
|
|
48
|
+
/** Required for Node file-write — Tier 3 risk gate. */
|
|
49
|
+
readonly acknowledgeRisks: true;
|
|
50
|
+
}
|
|
51
|
+
declare function toString(vault: Vault, options: AsXMLOptions): Promise<string>;
|
|
52
|
+
declare function download(vault: Vault, options: AsXMLDownloadOptions): Promise<void>;
|
|
53
|
+
declare function write(vault: Vault, path: string, options: AsXMLWriteOptions): Promise<void>;
|
|
54
|
+
|
|
55
|
+
type ImportPolicy = 'merge' | 'replace' | 'insert-only';
|
|
56
|
+
interface AsXMLImportOptions {
|
|
57
|
+
/** Target collection. Required — XML has no native collection grouping. */
|
|
58
|
+
readonly collection: string;
|
|
59
|
+
/**
|
|
60
|
+
* Optional explicit record-element name. When omitted, the reader
|
|
61
|
+
* picks the first repeated child of the root. (Writer's default is
|
|
62
|
+
* `pascalSingular(collection)` — pass that here for symmetric
|
|
63
|
+
* round-tripping if you wrote with a custom `recordElement`.)
|
|
64
|
+
*/
|
|
65
|
+
readonly recordElement?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Optional field type hints. Cells are returned as strings unless
|
|
68
|
+
* overridden. Same shape as `as-csv.fromString`'s `columnTypes`.
|
|
69
|
+
*/
|
|
70
|
+
readonly fieldTypes?: Record<string, 'string' | 'number' | 'boolean'>;
|
|
71
|
+
/** Field carrying the record id. Default `'id'`. */
|
|
72
|
+
readonly idKey?: string;
|
|
73
|
+
/** Reconciliation policy. Default `'merge'`. */
|
|
74
|
+
readonly policy?: ImportPolicy;
|
|
75
|
+
}
|
|
76
|
+
interface AsXMLImportPlan {
|
|
77
|
+
readonly plan: VaultDiff;
|
|
78
|
+
readonly policy: ImportPolicy;
|
|
79
|
+
apply(): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Parse XML into records and build an import plan. Inverts what
|
|
83
|
+
* `toString()` writes: root → recordElement[] → field elements with
|
|
84
|
+
* text content. Field values default to strings; pass `fieldTypes` to
|
|
85
|
+
* coerce numbers / booleans on read.
|
|
86
|
+
*
|
|
87
|
+
* Throws on malformed XML — fast-xml-parser is invoked in strict mode
|
|
88
|
+
* so unbalanced tags or invalid character sequences fail fast.
|
|
89
|
+
*
|
|
90
|
+
* Capability: `assertCanImport('plaintext', 'xml')`.
|
|
91
|
+
* Atomicity: `apply()` runs inside `vault.noydb.transaction()`.
|
|
92
|
+
*/
|
|
93
|
+
declare function fromString(vault: Vault, xml: string, options: AsXMLImportOptions): Promise<AsXMLImportPlan>;
|
|
94
|
+
|
|
95
|
+
export { type AsXMLDownloadOptions, type AsXMLImportOptions, type AsXMLImportPlan, type AsXMLOptions, type AsXMLWriteOptions, type ImportPolicy, download, fromString, toString, write };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { diffVault } from "@noy-db/hub";
|
|
3
|
+
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
4
|
+
async function toString(vault, options) {
|
|
5
|
+
vault.assertCanExport("plaintext", "xml");
|
|
6
|
+
const records = [];
|
|
7
|
+
for await (const chunk of vault.exportStream({ granularity: "collection" })) {
|
|
8
|
+
if (chunk.collection === options.collection) {
|
|
9
|
+
records.push(...chunk.records);
|
|
10
|
+
break;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const rootName = options.rootElement ?? "Records";
|
|
14
|
+
const recordName = options.recordElement ?? pascalSingular(options.collection);
|
|
15
|
+
const fields = options.fields ?? inferFields(records);
|
|
16
|
+
const pretty = options.pretty !== false;
|
|
17
|
+
const declaration = options.xmlDeclaration !== false;
|
|
18
|
+
const indent = pretty ? " " : "";
|
|
19
|
+
const nl = pretty ? "\n" : "";
|
|
20
|
+
const parts = [];
|
|
21
|
+
if (declaration) parts.push('<?xml version="1.0" encoding="UTF-8"?>' + nl);
|
|
22
|
+
const nsAttr = options.namespace ? options.namespacePrefix ? ` xmlns:${options.namespacePrefix}="${escapeAttr(options.namespace)}"` : ` xmlns="${escapeAttr(options.namespace)}"` : "";
|
|
23
|
+
const rootTag = options.namespacePrefix ? `${options.namespacePrefix}:${rootName}` : rootName;
|
|
24
|
+
const recordTag = options.namespacePrefix ? `${options.namespacePrefix}:${recordName}` : recordName;
|
|
25
|
+
parts.push(`<${rootTag}${nsAttr}>`);
|
|
26
|
+
for (const record of records) {
|
|
27
|
+
parts.push(nl + indent + `<${recordTag}>`);
|
|
28
|
+
for (const field of fields) {
|
|
29
|
+
const value = record[field];
|
|
30
|
+
if (value === void 0) continue;
|
|
31
|
+
const tag = escapeElementName(field);
|
|
32
|
+
parts.push(nl + indent + indent + `<${tag}>${escapeText(serializeValue(value))}</${tag}>`);
|
|
33
|
+
}
|
|
34
|
+
parts.push(nl + indent + `</${recordTag}>`);
|
|
35
|
+
}
|
|
36
|
+
parts.push(nl + `</${rootTag}>`);
|
|
37
|
+
return parts.join("");
|
|
38
|
+
}
|
|
39
|
+
async function download(vault, options) {
|
|
40
|
+
const xml = await toString(vault, options);
|
|
41
|
+
const filename = options.filename ?? `${options.collection}.xml`;
|
|
42
|
+
const blob = new Blob([xml], { type: "application/xml;charset=utf-8" });
|
|
43
|
+
const url = URL.createObjectURL(blob);
|
|
44
|
+
const a = document.createElement("a");
|
|
45
|
+
a.href = url;
|
|
46
|
+
a.download = filename;
|
|
47
|
+
a.click();
|
|
48
|
+
URL.revokeObjectURL(url);
|
|
49
|
+
}
|
|
50
|
+
async function write(vault, path, options) {
|
|
51
|
+
if (options.acknowledgeRisks !== true) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'as-xml.write: acknowledgeRisks: true is required for on-disk plaintext output. See docs/patterns/as-exports.md \xA7"The three tiers of \\"plaintext out\\""'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const xml = await toString(vault, options);
|
|
57
|
+
const { writeFile } = await import("fs/promises");
|
|
58
|
+
await writeFile(path, xml, "utf-8");
|
|
59
|
+
}
|
|
60
|
+
var XML_ENTITIES = {
|
|
61
|
+
"&": "&",
|
|
62
|
+
"<": "<",
|
|
63
|
+
">": ">",
|
|
64
|
+
'"': """,
|
|
65
|
+
"'": "'"
|
|
66
|
+
};
|
|
67
|
+
function escapeText(s) {
|
|
68
|
+
let out = s.replace(/[&<>]/g, (ch) => XML_ENTITIES[ch]);
|
|
69
|
+
let cleaned = "";
|
|
70
|
+
for (let i = 0; i < out.length; i++) {
|
|
71
|
+
const code = out.charCodeAt(i);
|
|
72
|
+
if (code >= 0 && code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31) continue;
|
|
73
|
+
cleaned += out[i];
|
|
74
|
+
}
|
|
75
|
+
out = cleaned;
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function escapeAttr(s) {
|
|
79
|
+
return s.replace(/[&<>"']/g, (ch) => XML_ENTITIES[ch]);
|
|
80
|
+
}
|
|
81
|
+
function escapeElementName(name) {
|
|
82
|
+
const safe = name.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
83
|
+
return /^[A-Za-z_]/.test(safe) ? safe : `_${safe}`;
|
|
84
|
+
}
|
|
85
|
+
function serializeValue(value) {
|
|
86
|
+
if (value === null) return "";
|
|
87
|
+
if (typeof value === "string") return value;
|
|
88
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
89
|
+
if (value instanceof Date) return value.toISOString();
|
|
90
|
+
return JSON.stringify(value);
|
|
91
|
+
}
|
|
92
|
+
function inferFields(records) {
|
|
93
|
+
const seen = /* @__PURE__ */ new Set();
|
|
94
|
+
const fields = [];
|
|
95
|
+
for (const r of records) {
|
|
96
|
+
if (r && typeof r === "object") {
|
|
97
|
+
for (const k of Object.keys(r)) {
|
|
98
|
+
if (k === "_v" || k === "_ts" || k === "_by" || k === "_iv" || k === "_data" || k === "_noydb") continue;
|
|
99
|
+
if (!seen.has(k)) {
|
|
100
|
+
seen.add(k);
|
|
101
|
+
fields.push(k);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return fields;
|
|
107
|
+
}
|
|
108
|
+
function pascalSingular(collection) {
|
|
109
|
+
const singular = collection.endsWith("s") ? collection.slice(0, -1) : collection;
|
|
110
|
+
return singular.charAt(0).toUpperCase() + singular.slice(1);
|
|
111
|
+
}
|
|
112
|
+
async function fromString(vault, xml, options) {
|
|
113
|
+
vault.assertCanImport("plaintext", "xml");
|
|
114
|
+
const policy = options.policy ?? "merge";
|
|
115
|
+
const idKey = options.idKey ?? "id";
|
|
116
|
+
const types = options.fieldTypes ?? {};
|
|
117
|
+
const parser = new XMLParser({
|
|
118
|
+
ignoreAttributes: true,
|
|
119
|
+
parseTagValue: false,
|
|
120
|
+
// keep raw strings — we coerce ourselves
|
|
121
|
+
parseAttributeValue: false,
|
|
122
|
+
trimValues: true
|
|
123
|
+
// Only force-as-array the record element so a single-record XML still
|
|
124
|
+
// produces an array. We'll narrow this when we discover the element name.
|
|
125
|
+
});
|
|
126
|
+
const validation = XMLValidator.validate(xml);
|
|
127
|
+
if (validation !== true) {
|
|
128
|
+
const err = validation.err;
|
|
129
|
+
throw new Error(
|
|
130
|
+
`as-xml.fromString: input is not valid XML (${err.code} at line ${err.line}: ${err.msg})`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
let parsed;
|
|
134
|
+
try {
|
|
135
|
+
parsed = parser.parse(xml);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
throw new Error(`as-xml.fromString: input is not valid XML (${err.message})`);
|
|
138
|
+
}
|
|
139
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
140
|
+
throw new Error("as-xml.fromString: parser produced a non-object root");
|
|
141
|
+
}
|
|
142
|
+
const root = stripNamespacePrefixes(parsed);
|
|
143
|
+
const rootKeys = Object.keys(root);
|
|
144
|
+
if (rootKeys.length === 0) {
|
|
145
|
+
return emptyPlan(vault, options.collection, policy, idKey);
|
|
146
|
+
}
|
|
147
|
+
const rootName = rootKeys.find((k) => !k.startsWith("?")) ?? rootKeys[0];
|
|
148
|
+
const rootBody = stripIfObject(root[rootName]);
|
|
149
|
+
if (rootBody === null) {
|
|
150
|
+
return emptyPlan(vault, options.collection, policy, idKey);
|
|
151
|
+
}
|
|
152
|
+
const recordElName = options.recordElement ? stripPrefix(options.recordElement) : pickRecordElementName(rootBody);
|
|
153
|
+
if (recordElName === null) {
|
|
154
|
+
return emptyPlan(vault, options.collection, policy, idKey);
|
|
155
|
+
}
|
|
156
|
+
const recordsRaw = rootBody[recordElName];
|
|
157
|
+
const recordsArr = recordsRaw === void 0 ? [] : Array.isArray(recordsRaw) ? recordsRaw : [recordsRaw];
|
|
158
|
+
const records = [];
|
|
159
|
+
for (const r of recordsArr) {
|
|
160
|
+
if (r === null || typeof r !== "object" || Array.isArray(r)) continue;
|
|
161
|
+
const flat = stripNamespacePrefixes(r);
|
|
162
|
+
const record = {};
|
|
163
|
+
for (const [field, raw] of Object.entries(flat)) {
|
|
164
|
+
const text = textValue(raw);
|
|
165
|
+
record[field] = coerce(text, types[field]);
|
|
166
|
+
}
|
|
167
|
+
records.push(record);
|
|
168
|
+
}
|
|
169
|
+
const plan = await diffVault(vault, { [options.collection]: records }, {
|
|
170
|
+
collections: [options.collection],
|
|
171
|
+
idKey
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
plan,
|
|
175
|
+
policy,
|
|
176
|
+
async apply() {
|
|
177
|
+
await vault.noydb.transaction((tx) => {
|
|
178
|
+
const txVault = tx.vault(vault.name);
|
|
179
|
+
for (const entry of plan.added) {
|
|
180
|
+
txVault.collection(entry.collection).put(entry.id, entry.record);
|
|
181
|
+
}
|
|
182
|
+
if (policy !== "insert-only") {
|
|
183
|
+
for (const entry of plan.modified) {
|
|
184
|
+
txVault.collection(entry.collection).put(entry.id, entry.record);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (policy === "replace") {
|
|
188
|
+
for (const entry of plan.deleted) {
|
|
189
|
+
txVault.collection(entry.collection).delete(entry.id);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function emptyPlan(vault, collection, policy, idKey) {
|
|
197
|
+
const plan = await diffVault(vault, { [collection]: [] }, { collections: [collection], idKey });
|
|
198
|
+
return { plan, policy, async apply() {
|
|
199
|
+
} };
|
|
200
|
+
}
|
|
201
|
+
function stripPrefix(name) {
|
|
202
|
+
const idx = name.indexOf(":");
|
|
203
|
+
return idx === -1 ? name : name.slice(idx + 1);
|
|
204
|
+
}
|
|
205
|
+
function stripNamespacePrefixes(obj) {
|
|
206
|
+
const out = {};
|
|
207
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
208
|
+
out[stripPrefix(k)] = v;
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
function stripIfObject(v) {
|
|
213
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) return null;
|
|
214
|
+
return stripNamespacePrefixes(v);
|
|
215
|
+
}
|
|
216
|
+
function pickRecordElementName(body) {
|
|
217
|
+
for (const k of Object.keys(body)) {
|
|
218
|
+
if (k.startsWith("@_") || k.startsWith("?")) continue;
|
|
219
|
+
return k;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
function textValue(raw) {
|
|
224
|
+
if (raw === null || raw === void 0) return "";
|
|
225
|
+
if (typeof raw === "string") return raw;
|
|
226
|
+
if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
|
|
227
|
+
return "";
|
|
228
|
+
}
|
|
229
|
+
function coerce(cell, type) {
|
|
230
|
+
if (type === "number") {
|
|
231
|
+
if (cell === "") return void 0;
|
|
232
|
+
const n = Number(cell);
|
|
233
|
+
return Number.isFinite(n) ? n : cell;
|
|
234
|
+
}
|
|
235
|
+
if (type === "boolean") {
|
|
236
|
+
if (cell === "true") return true;
|
|
237
|
+
if (cell === "false") return false;
|
|
238
|
+
return cell;
|
|
239
|
+
}
|
|
240
|
+
return cell;
|
|
241
|
+
}
|
|
242
|
+
export {
|
|
243
|
+
download,
|
|
244
|
+
fromString,
|
|
245
|
+
toString,
|
|
246
|
+
write
|
|
247
|
+
};
|
|
248
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/as-xml** — XML plaintext export for noy-db.\n *\n * Hand-rolled XML emitter — zero dependencies, ~100 LoC. Escapes the\n * five predefined XML entities (`<`, `>`, `&`, `'`, `\"`) and numeric\n * control characters. Supports custom root/record element names,\n * namespaces, pretty-printing, and XML declarations.\n *\n * **Scope.** Single collection per call (mirroring as-csv's shape).\n * Multi-collection XML documents are best produced by the consumer\n * composing multiple `toString()` calls into a wrapping element.\n *\n * ### When to use\n *\n * - Legacy systems requiring XML input (accounting software, SOAP).\n * - Banking batch imports (ISO 20022, CAMT/PAIN files consumers wrap).\n * - Excel `.xml` SpreadsheetML 2003 legacy format (wrap in a\n * Workbook template).\n *\n * @packageDocumentation\n */\n\nimport type { Vault } from '@noy-db/hub'\n\nexport interface AsXMLOptions {\n /** Collection to export. */\n readonly collection: string\n /** Root element name. Default `'Records'`. */\n readonly rootElement?: string\n /** Per-record element name. Default pascal-cased singular of collection. */\n readonly recordElement?: string\n /** Explicit field list — defaults to inferred columns. */\n readonly fields?: readonly string[]\n /** Include `<?xml version=\"1.0\" encoding=\"UTF-8\"?>` declaration. Default `true`. */\n readonly xmlDeclaration?: boolean\n /** Pretty-print with 2-space indentation. Default `true`. */\n readonly pretty?: boolean\n /** Optional XML namespace on the root element. */\n readonly namespace?: string\n /** Optional namespace prefix. Used together with `namespace`. */\n readonly namespacePrefix?: string\n}\n\nexport interface AsXMLDownloadOptions extends AsXMLOptions {\n /** Filename offered to the browser. Default `'<collection>.xml'`. */\n readonly filename?: string\n}\n\nexport interface AsXMLWriteOptions extends AsXMLOptions {\n /** Required for Node file-write — Tier 3 risk gate. */\n readonly acknowledgeRisks: true\n}\n\nexport async function toString(vault: Vault, options: AsXMLOptions): Promise<string> {\n vault.assertCanExport('plaintext', 'xml')\n\n const records: unknown[] = []\n for await (const chunk of vault.exportStream({ granularity: 'collection' })) {\n if (chunk.collection === options.collection) {\n records.push(...chunk.records)\n break\n }\n }\n\n const rootName = options.rootElement ?? 'Records'\n const recordName = options.recordElement ?? pascalSingular(options.collection)\n const fields = options.fields ?? inferFields(records)\n const pretty = options.pretty !== false\n const declaration = options.xmlDeclaration !== false\n const indent = pretty ? ' ' : ''\n const nl = pretty ? '\\n' : ''\n\n const parts: string[] = []\n if (declaration) parts.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>' + nl)\n\n const nsAttr = options.namespace\n ? options.namespacePrefix\n ? ` xmlns:${options.namespacePrefix}=\"${escapeAttr(options.namespace)}\"`\n : ` xmlns=\"${escapeAttr(options.namespace)}\"`\n : ''\n const rootTag = options.namespacePrefix\n ? `${options.namespacePrefix}:${rootName}`\n : rootName\n const recordTag = options.namespacePrefix\n ? `${options.namespacePrefix}:${recordName}`\n : recordName\n\n parts.push(`<${rootTag}${nsAttr}>`)\n for (const record of records) {\n parts.push(nl + indent + `<${recordTag}>`)\n for (const field of fields) {\n const value = (record as Record<string, unknown>)[field]\n if (value === undefined) continue\n const tag = escapeElementName(field)\n parts.push(nl + indent + indent + `<${tag}>${escapeText(serializeValue(value))}</${tag}>`)\n }\n parts.push(nl + indent + `</${recordTag}>`)\n }\n parts.push(nl + `</${rootTag}>`)\n return parts.join('')\n}\n\nexport async function download(vault: Vault, options: AsXMLDownloadOptions): Promise<void> {\n const xml = await toString(vault, options)\n const filename = options.filename ?? `${options.collection}.xml`\n const blob = new Blob([xml], { type: 'application/xml;charset=utf-8' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n a.click()\n URL.revokeObjectURL(url)\n}\n\nexport async function write(vault: Vault, path: string, options: AsXMLWriteOptions): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-xml.write: acknowledgeRisks: true is required for on-disk plaintext output. ' +\n 'See docs/patterns/as-exports.md §\"The three tiers of \\\\\"plaintext out\\\\\"\"',\n )\n }\n const xml = await toString(vault, options)\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, xml, 'utf-8')\n}\n\n// ─── XML formatting internals ───────────────────────────────────────────\n\nconst XML_ENTITIES: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n}\n\nfunction escapeText(s: string): string {\n let out = s.replace(/[&<>]/g, ch => XML_ENTITIES[ch]!)\n // Strip XML-invalid control characters (U+0000–U+0008, U+000B, U+000C, U+000E–U+001F).\n // Done as a char-by-char scan to keep the regex free of control-character literals\n // (which eslint's no-control-regex flags).\n let cleaned = ''\n for (let i = 0; i < out.length; i++) {\n const code = out.charCodeAt(i)\n if (\n (code >= 0x00 && code <= 0x08) ||\n code === 0x0B ||\n code === 0x0C ||\n (code >= 0x0E && code <= 0x1F)\n ) continue\n cleaned += out[i]\n }\n out = cleaned\n return out\n}\n\nfunction escapeAttr(s: string): string {\n return s.replace(/[&<>\"']/g, ch => XML_ENTITIES[ch]!)\n}\n\nfunction escapeElementName(name: string): string {\n // XML element names must start with a letter/underscore and contain only\n // letters, digits, hyphens, underscores, dots. Replace everything else.\n const safe = name.replace(/[^A-Za-z0-9_.-]/g, '_')\n return /^[A-Za-z_]/.test(safe) ? safe : `_${safe}`\n}\n\nfunction serializeValue(value: unknown): string {\n if (value === null) return ''\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n return JSON.stringify(value)\n}\n\nfunction inferFields(records: readonly unknown[]): string[] {\n const seen = new Set<string>()\n const fields: string[] = []\n for (const r of records) {\n if (r && typeof r === 'object') {\n for (const k of Object.keys(r)) {\n if (k === '_v' || k === '_ts' || k === '_by' || k === '_iv' || k === '_data' || k === '_noydb') continue\n if (!seen.has(k)) {\n seen.add(k)\n fields.push(k)\n }\n }\n }\n }\n return fields\n}\n\nfunction pascalSingular(collection: string): string {\n const singular = collection.endsWith('s') ? collection.slice(0, -1) : collection\n return singular.charAt(0).toUpperCase() + singular.slice(1)\n}\n\n// ─── Reader ──────────────────────────────────────\n\nimport { diffVault, type VaultDiff } from '@noy-db/hub'\nimport { XMLParser, XMLValidator } from 'fast-xml-parser'\n\nexport type ImportPolicy = 'merge' | 'replace' | 'insert-only'\n\nexport interface AsXMLImportOptions {\n /** Target collection. Required — XML has no native collection grouping. */\n readonly collection: string\n /**\n * Optional explicit record-element name. When omitted, the reader\n * picks the first repeated child of the root. (Writer's default is\n * `pascalSingular(collection)` — pass that here for symmetric\n * round-tripping if you wrote with a custom `recordElement`.)\n */\n readonly recordElement?: string\n /**\n * Optional field type hints. Cells are returned as strings unless\n * overridden. Same shape as `as-csv.fromString`'s `columnTypes`.\n */\n readonly fieldTypes?: Record<string, 'string' | 'number' | 'boolean'>\n /** Field carrying the record id. Default `'id'`. */\n readonly idKey?: string\n /** Reconciliation policy. Default `'merge'`. */\n readonly policy?: ImportPolicy\n}\n\nexport interface AsXMLImportPlan {\n readonly plan: VaultDiff\n readonly policy: ImportPolicy\n apply(): Promise<void>\n}\n\n/**\n * Parse XML into records and build an import plan. Inverts what\n * `toString()` writes: root → recordElement[] → field elements with\n * text content. Field values default to strings; pass `fieldTypes` to\n * coerce numbers / booleans on read.\n *\n * Throws on malformed XML — fast-xml-parser is invoked in strict mode\n * so unbalanced tags or invalid character sequences fail fast.\n *\n * Capability: `assertCanImport('plaintext', 'xml')`.\n * Atomicity: `apply()` runs inside `vault.noydb.transaction()`.\n */\nexport async function fromString(\n vault: Vault,\n xml: string,\n options: AsXMLImportOptions,\n): Promise<AsXMLImportPlan> {\n vault.assertCanImport('plaintext', 'xml')\n\n const policy: ImportPolicy = options.policy ?? 'merge'\n const idKey = options.idKey ?? 'id'\n const types = options.fieldTypes ?? {}\n\n const parser = new XMLParser({\n ignoreAttributes: true,\n parseTagValue: false, // keep raw strings — we coerce ourselves\n parseAttributeValue: false,\n trimValues: true,\n // Only force-as-array the record element so a single-record XML still\n // produces an array. We'll narrow this when we discover the element name.\n })\n\n // Strict validation first — fast-xml-parser is permissive by default\n // (silently accepts unbalanced tags). XMLValidator returns true on\n // success or an error object describing the position of the failure.\n const validation = XMLValidator.validate(xml)\n if (validation !== true) {\n const err = validation.err\n throw new Error(\n `as-xml.fromString: input is not valid XML (${err.code} at line ${err.line}: ${err.msg})`,\n )\n }\n\n let parsed: unknown\n try {\n parsed = parser.parse(xml)\n } catch (err) {\n throw new Error(`as-xml.fromString: input is not valid XML (${(err as Error).message})`)\n }\n\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('as-xml.fromString: parser produced a non-object root')\n }\n\n // Drill into the root. fast-xml-parser surfaces `<Root><Item/>...</Root>` as\n // `{ Root: { Item: [...] | {...} } }`. Strip namespace prefixes from element\n // names by splitting on `:` so the writer's `<ns:Records>` round-trips.\n const root = stripNamespacePrefixes(parsed as Record<string, unknown>)\n const rootKeys = Object.keys(root)\n if (rootKeys.length === 0) {\n return emptyPlan(vault, options.collection, policy, idKey)\n }\n // Pick first non-`?xml` root child — fast-xml-parser ignores the\n // declaration by default but be defensive.\n const rootName = rootKeys.find((k) => !k.startsWith('?')) ?? rootKeys[0]!\n const rootBody = stripIfObject(root[rootName])\n\n if (rootBody === null) {\n return emptyPlan(vault, options.collection, policy, idKey)\n }\n\n const recordElName = options.recordElement\n ? stripPrefix(options.recordElement)\n : pickRecordElementName(rootBody)\n\n if (recordElName === null) {\n return emptyPlan(vault, options.collection, policy, idKey)\n }\n\n const recordsRaw = rootBody[recordElName]\n const recordsArr = recordsRaw === undefined ? []\n : Array.isArray(recordsRaw) ? recordsRaw : [recordsRaw]\n\n const records: Record<string, unknown>[] = []\n for (const r of recordsArr) {\n if (r === null || typeof r !== 'object' || Array.isArray(r)) continue\n const flat = stripNamespacePrefixes(r as Record<string, unknown>)\n const record: Record<string, unknown> = {}\n for (const [field, raw] of Object.entries(flat)) {\n // The XML parser exposes text-only elements as primitives or\n // empty objects (when the element is `<tag/>`). Normalize.\n const text = textValue(raw)\n record[field] = coerce(text, types[field])\n }\n records.push(record)\n }\n\n const plan = await diffVault(vault, { [options.collection]: records }, {\n collections: [options.collection],\n idKey,\n })\n\n return {\n plan,\n policy,\n async apply(): Promise<void> {\n // Routes through txStrategy seam — throws clearly when\n // withTransactions() isn't opted in. Atomicity rolls back any\n // partial writes if a put fails mid-batch.\n await vault.noydb.transaction((tx) => {\n const txVault = tx.vault(vault.name)\n for (const entry of plan.added) {\n txVault.collection(entry.collection).put(entry.id, entry.record)\n }\n if (policy !== 'insert-only') {\n for (const entry of plan.modified) {\n txVault.collection(entry.collection).put(entry.id, entry.record)\n }\n }\n if (policy === 'replace') {\n for (const entry of plan.deleted) {\n txVault.collection(entry.collection).delete(entry.id)\n }\n }\n })\n },\n }\n}\n\nasync function emptyPlan(\n vault: Vault,\n collection: string,\n policy: ImportPolicy,\n idKey: string,\n): Promise<AsXMLImportPlan> {\n const plan = await diffVault(vault, { [collection]: [] }, { collections: [collection], idKey })\n return { plan, policy, async apply() { /* nothing to do */ } }\n}\n\nfunction stripPrefix(name: string): string {\n const idx = name.indexOf(':')\n return idx === -1 ? name : name.slice(idx + 1)\n}\n\nfunction stripNamespacePrefixes(obj: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(obj)) {\n out[stripPrefix(k)] = v\n }\n return out\n}\n\nfunction stripIfObject(v: unknown): Record<string, unknown> | null {\n if (v === null || typeof v !== 'object' || Array.isArray(v)) return null\n return stripNamespacePrefixes(v as Record<string, unknown>)\n}\n\n/**\n * Pick the first non-attribute key in `body` — that's the per-record\n * element. Skips namespace-attribute keys like `@_xmlns:foo` (defensive\n * even though `ignoreAttributes: true` should drop them).\n */\nfunction pickRecordElementName(body: Record<string, unknown>): string | null {\n for (const k of Object.keys(body)) {\n if (k.startsWith('@_') || k.startsWith('?')) continue\n return k\n }\n return null\n}\n\n/**\n * Extract the text content from a parsed-element value. The parser\n * surfaces `<a>x</a>` as the string `\"x\"`, `<a/>` as the empty string,\n * and `<a><b>...</b></a>` as an object — we treat the last case as\n * \"not a leaf\" and stringify whatever's there.\n */\nfunction textValue(raw: unknown): string {\n if (raw === null || raw === undefined) return ''\n if (typeof raw === 'string') return raw\n if (typeof raw === 'number' || typeof raw === 'boolean') return String(raw)\n return ''\n}\n\nfunction coerce(cell: string, type?: 'string' | 'number' | 'boolean'): unknown {\n if (type === 'number') {\n if (cell === '') return undefined\n const n = Number(cell)\n return Number.isFinite(n) ? n : cell\n }\n if (type === 'boolean') {\n if (cell === 'true') return true\n if (cell === 'false') return false\n return cell\n }\n return cell\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noy-db/as-xml",
|
|
3
|
+
"version": "0.1.0-pre.3",
|
|
4
|
+
"description": "XML plaintext export for noy-db — decrypts records and formats as XML with RFC-compliant entity escaping. For legacy systems, banking batch imports, SOAP endpoints, and accounting software that requires XML. Gated by RFC #249 canExportPlaintext.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "vLannaAi <vicio@lanna.ai>",
|
|
7
|
+
"homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/as-xml#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/vLannaAi/noy-db.git",
|
|
11
|
+
"directory": "packages/as-xml"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/vLannaAi/noy-db/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"require": {
|
|
25
|
+
"types": "./dist/index.d.cts",
|
|
26
|
+
"default": "./dist/index.cjs"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@noy-db/hub": "0.1.0-pre.3"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"fast-xml-parser": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"@noy-db/hub": "0.1.0-pre.3"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"noy-db",
|
|
53
|
+
"as-xml",
|
|
54
|
+
"xml",
|
|
55
|
+
"export",
|
|
56
|
+
"legacy",
|
|
57
|
+
"plaintext"
|
|
58
|
+
],
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public",
|
|
61
|
+
"tag": "latest"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsup",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"lint": "eslint src/",
|
|
67
|
+
"typecheck": "tsc --noEmit"
|
|
68
|
+
}
|
|
69
|
+
}
|