@noy-db/as-xml 0.6.0 → 0.7.0-pre.0
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/README.md +1 -1
- package/dist/index.d.ts +31 -22
- package/dist/index.js +35 -70
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ pnpm add @noy-db/hub @noy-db/as-xml
|
|
|
14
14
|
|
|
15
15
|
## What it is
|
|
16
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
|
|
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 `vault.assertCanExport('plaintext', …)`.
|
|
18
18
|
|
|
19
19
|
## Status
|
|
20
20
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ImportPolicy, ImportPlan, NoydbFormat } from '@noy-db/hub/as';
|
|
2
|
+
export { ImportPolicy } from '@noy-db/hub/as';
|
|
3
|
+
import { Vault } from '@noy-db/hub';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* **@noy-db/as-xml** — XML plaintext export for noy-db.
|
|
@@ -68,11 +70,35 @@ interface AsXMLWriteOptions extends AsXMLOptions {
|
|
|
68
70
|
/** Required for Node file-write — Tier 3 risk gate. */
|
|
69
71
|
readonly acknowledgeRisks: true;
|
|
70
72
|
}
|
|
71
|
-
|
|
73
|
+
/** Options an XML format instance carries. Read concerns live on `vault.export`. */
|
|
74
|
+
interface AsXMLFormatOptions {
|
|
75
|
+
readonly rootElement?: string;
|
|
76
|
+
readonly recordElement?: string;
|
|
77
|
+
readonly fields?: readonly string[];
|
|
78
|
+
readonly xmlDeclaration?: boolean;
|
|
79
|
+
readonly pretty?: boolean;
|
|
80
|
+
readonly namespace?: string;
|
|
81
|
+
readonly namespacePrefix?: string;
|
|
82
|
+
/** Per-field coercion on decode. */
|
|
83
|
+
readonly fieldTypes?: Readonly<Record<string, 'string' | 'number' | 'boolean'>>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The XML format — the `as-*` port instance.
|
|
87
|
+
*
|
|
88
|
+
* ```ts
|
|
89
|
+
* const xml = await vault.export(asXml(), { collections: ['invoices'] })
|
|
90
|
+
* const plan = await vault.import(asXml(), xml, { collection: 'invoices' })
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
declare function asXml(options?: AsXMLFormatOptions): NoydbFormat<string>;
|
|
94
|
+
/** Browser download. Hub gates, reads and redacts; this wraps the bytes. */
|
|
72
95
|
declare function download(vault: Vault, options: AsXMLDownloadOptions): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Node file write. Not in hub because `hub-portable` forbids Node builtins
|
|
98
|
+
* there. The gate, the read and the redaction all moved.
|
|
99
|
+
*/
|
|
73
100
|
declare function write(vault: Vault, path: string, options: AsXMLWriteOptions): Promise<void>;
|
|
74
101
|
|
|
75
|
-
type ImportPolicy = 'merge' | 'replace' | 'insert-only';
|
|
76
102
|
interface AsXMLImportOptions {
|
|
77
103
|
/** Target collection. Required — XML has no native collection grouping. */
|
|
78
104
|
readonly collection: string;
|
|
@@ -93,23 +119,6 @@ interface AsXMLImportOptions {
|
|
|
93
119
|
/** Reconciliation policy. Default `'merge'`. */
|
|
94
120
|
readonly policy?: ImportPolicy;
|
|
95
121
|
}
|
|
96
|
-
|
|
97
|
-
readonly plan: VaultDiff;
|
|
98
|
-
readonly policy: ImportPolicy;
|
|
99
|
-
apply(): Promise<void>;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Parse XML into records and build an import plan. Inverts what
|
|
103
|
-
* `toString()` writes: root → recordElement[] → field elements with
|
|
104
|
-
* text content. Field values default to strings; pass `fieldTypes` to
|
|
105
|
-
* coerce numbers / booleans on read.
|
|
106
|
-
*
|
|
107
|
-
* Throws on malformed XML — fast-xml-parser is invoked in strict mode
|
|
108
|
-
* so unbalanced tags or invalid character sequences fail fast.
|
|
109
|
-
*
|
|
110
|
-
* Capability: `assertCanImport('plaintext', 'xml')`.
|
|
111
|
-
* Atomicity: `apply()` runs inside `vault.noydb.transaction()`.
|
|
112
|
-
*/
|
|
113
|
-
declare function fromString(vault: Vault, xml: string, options: AsXMLImportOptions): Promise<AsXMLImportPlan>;
|
|
122
|
+
type AsXMLImportPlan = ImportPlan;
|
|
114
123
|
|
|
115
|
-
export { type AsXMLDownloadOptions, type AsXMLImportOptions, type AsXMLImportPlan, type AsXMLOptions, type AsXMLWriteOptions,
|
|
124
|
+
export { type AsXMLDownloadOptions, type AsXMLFormatOptions, type AsXMLImportOptions, type AsXMLImportPlan, type AsXMLOptions, type AsXMLWriteOptions, asXml, download, write };
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { applyListProjection } from "@noy-db/hub";
|
|
3
|
-
import { diffVault } from "@noy-db/hub";
|
|
4
2
|
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const records = [];
|
|
8
|
-
for await (const chunk of vault.exportStream({ granularity: "collection" })) {
|
|
9
|
-
if (chunk.collection === options.collection) {
|
|
10
|
-
records.push(...chunk.records);
|
|
11
|
-
break;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
if (options.redact !== void 0 && options.redact !== false) {
|
|
15
|
-
const desc = vault.collection(options.collection).describe();
|
|
16
|
-
const projectionOpts = options.redact === true ? void 0 : { sensitivity: options.redact.sensitivity };
|
|
17
|
-
records.splice(0, records.length, ...records.map((r) => applyListProjection(desc, r, projectionOpts)));
|
|
18
|
-
}
|
|
3
|
+
function encodeXml(chunks, options) {
|
|
4
|
+
const records = chunks.flatMap((c) => c.records);
|
|
19
5
|
const rootName = options.rootElement ?? "Records";
|
|
20
|
-
const recordName = options.recordElement ?? pascalSingular(
|
|
6
|
+
const recordName = options.recordElement ?? pascalSingular(chunks[0]?.collection ?? "Record");
|
|
21
7
|
const fields = options.fields ?? inferFields(records);
|
|
22
8
|
const pretty = options.pretty !== false;
|
|
23
9
|
const declaration = options.xmlDeclaration !== false;
|
|
@@ -42,26 +28,39 @@ async function toString(vault, options) {
|
|
|
42
28
|
parts.push(nl + `</${rootTag}>`);
|
|
43
29
|
return parts.join("");
|
|
44
30
|
}
|
|
31
|
+
function asXml(options = {}) {
|
|
32
|
+
return {
|
|
33
|
+
id: "xml",
|
|
34
|
+
extension: "xml",
|
|
35
|
+
mimeType: "application/xml;charset=utf-8",
|
|
36
|
+
tier: "plaintext",
|
|
37
|
+
encode: (chunks) => encodeXml(chunks, options),
|
|
38
|
+
decode: (input) => decodeXml(input, options)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function readOpts(o) {
|
|
42
|
+
return {
|
|
43
|
+
...o.collection ? { collections: [o.collection] } : {},
|
|
44
|
+
...o.redact !== void 0 && o.redact !== false ? { redact: o.redact === true ? true : { sensitivity: o.redact.sensitivity } } : {}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
45
47
|
async function download(vault, options) {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const url = URL.createObjectURL(blob);
|
|
48
|
+
const fmt = asXml(options);
|
|
49
|
+
const xml = await vault.export(fmt, readOpts(options));
|
|
50
|
+
const url = URL.createObjectURL(new Blob([xml], { type: fmt.mimeType }));
|
|
50
51
|
const a = document.createElement("a");
|
|
51
52
|
a.href = url;
|
|
52
|
-
a.download = filename
|
|
53
|
+
a.download = options.filename ?? `${options.collection}.${fmt.extension}`;
|
|
53
54
|
a.click();
|
|
54
55
|
URL.revokeObjectURL(url);
|
|
55
56
|
}
|
|
56
57
|
async function write(vault, path, options) {
|
|
57
58
|
if (options.acknowledgeRisks !== true) {
|
|
58
|
-
throw new Error(
|
|
59
|
-
'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\\""'
|
|
60
|
-
);
|
|
59
|
+
throw new Error("as-xml.write: acknowledgeRisks: true is required for on-disk plaintext output.");
|
|
61
60
|
}
|
|
62
|
-
const xml = await
|
|
61
|
+
const xml = await vault.export(asXml(options), readOpts(options));
|
|
63
62
|
const { writeFile } = await import("fs/promises");
|
|
64
|
-
await writeFile(path, xml, "
|
|
63
|
+
await writeFile(path, xml, "utf8");
|
|
65
64
|
}
|
|
66
65
|
var XML_ENTITIES = {
|
|
67
66
|
"&": "&",
|
|
@@ -115,10 +114,7 @@ function pascalSingular(collection) {
|
|
|
115
114
|
const singular = collection.endsWith("s") ? collection.slice(0, -1) : collection;
|
|
116
115
|
return singular.charAt(0).toUpperCase() + singular.slice(1);
|
|
117
116
|
}
|
|
118
|
-
|
|
119
|
-
vault.assertCanImport("plaintext", "xml");
|
|
120
|
-
const policy = options.policy ?? "merge";
|
|
121
|
-
const idKey = options.idKey ?? "id";
|
|
117
|
+
function decodeXml(xml, options) {
|
|
122
118
|
const types = options.fieldTypes ?? {};
|
|
123
119
|
const parser = new XMLParser({
|
|
124
120
|
ignoreAttributes: true,
|
|
@@ -133,31 +129,31 @@ async function fromString(vault, xml, options) {
|
|
|
133
129
|
if (validation !== true) {
|
|
134
130
|
const err = validation.err;
|
|
135
131
|
throw new Error(
|
|
136
|
-
`as-xml
|
|
132
|
+
`as-xml decode: input is not valid XML (${err.code} at line ${err.line}: ${err.msg})`
|
|
137
133
|
);
|
|
138
134
|
}
|
|
139
135
|
let parsed;
|
|
140
136
|
try {
|
|
141
137
|
parsed = parser.parse(xml);
|
|
142
138
|
} catch (err) {
|
|
143
|
-
throw new Error(`as-xml
|
|
139
|
+
throw new Error(`as-xml decode: input is not valid XML (${err.message})`);
|
|
144
140
|
}
|
|
145
141
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
146
|
-
throw new Error("as-xml
|
|
142
|
+
throw new Error("as-xml decode: parser produced a non-object root");
|
|
147
143
|
}
|
|
148
144
|
const root = stripNamespacePrefixes(parsed);
|
|
149
145
|
const rootKeys = Object.keys(root);
|
|
150
146
|
if (rootKeys.length === 0) {
|
|
151
|
-
return
|
|
147
|
+
return [{ collection: "", records: [] }];
|
|
152
148
|
}
|
|
153
149
|
const rootName = rootKeys.find((k) => !k.startsWith("?")) ?? rootKeys[0];
|
|
154
150
|
const rootBody = stripIfObject(root[rootName]);
|
|
155
151
|
if (rootBody === null) {
|
|
156
|
-
return
|
|
152
|
+
return [{ collection: "", records: [] }];
|
|
157
153
|
}
|
|
158
154
|
const recordElName = options.recordElement ? stripPrefix(options.recordElement) : pickRecordElementName(rootBody);
|
|
159
155
|
if (recordElName === null) {
|
|
160
|
-
return
|
|
156
|
+
return [{ collection: "", records: [] }];
|
|
161
157
|
}
|
|
162
158
|
const recordsRaw = rootBody[recordElName];
|
|
163
159
|
const recordsArr = recordsRaw === void 0 ? [] : Array.isArray(recordsRaw) ? recordsRaw : [recordsRaw];
|
|
@@ -172,37 +168,7 @@ async function fromString(vault, xml, options) {
|
|
|
172
168
|
}
|
|
173
169
|
records.push(record);
|
|
174
170
|
}
|
|
175
|
-
|
|
176
|
-
collections: [options.collection],
|
|
177
|
-
idKey
|
|
178
|
-
});
|
|
179
|
-
return {
|
|
180
|
-
plan,
|
|
181
|
-
policy,
|
|
182
|
-
async apply() {
|
|
183
|
-
await vault.noydb.transaction((tx) => {
|
|
184
|
-
const txVault = tx.vault(vault.name);
|
|
185
|
-
for (const entry of plan.added) {
|
|
186
|
-
txVault.collection(entry.collection).put(entry.id, entry.record, { reason: "import:xml" });
|
|
187
|
-
}
|
|
188
|
-
if (policy !== "insert-only") {
|
|
189
|
-
for (const entry of plan.modified) {
|
|
190
|
-
txVault.collection(entry.collection).put(entry.id, entry.record, { reason: "import:xml" });
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
if (policy === "replace") {
|
|
194
|
-
for (const entry of plan.deleted) {
|
|
195
|
-
txVault.collection(entry.collection).delete(entry.id);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
async function emptyPlan(vault, collection, policy, idKey) {
|
|
203
|
-
const plan = await diffVault(vault, { [collection]: [] }, { collections: [collection], idKey });
|
|
204
|
-
return { plan, policy, async apply() {
|
|
205
|
-
} };
|
|
171
|
+
return [{ collection: "", records }];
|
|
206
172
|
}
|
|
207
173
|
function stripPrefix(name) {
|
|
208
174
|
const idx = name.indexOf(":");
|
|
@@ -246,9 +212,8 @@ function coerce(cell, type) {
|
|
|
246
212
|
return cell;
|
|
247
213
|
}
|
|
248
214
|
export {
|
|
215
|
+
asXml,
|
|
249
216
|
download,
|
|
250
|
-
fromString,
|
|
251
|
-
toString,
|
|
252
217
|
write
|
|
253
218
|
};
|
|
254
219
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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, CollectionDescription } from '@noy-db/hub'\nimport { applyListProjection } 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 * Apply the hub's `applyListProjection` read-projection before\n * serialising records. `true` redacts only `classifiedFields` (mask /\n * omit / rider, per the field's preset). The object form additionally\n * redacts fields carrying a plain `fieldMeta` `sensitivity: 'pii' |\n * 'secret'` tag, per `sensitivity: 'omit' | 'mask'`.\n *\n * Caveat: `describe()` reflects the declarations of *this session's*\n * collection instance — redaction only takes effect when the\n * collection was opened (this call or earlier in the session) with\n * its `classifiedFields` / `fieldMeta` options. This is presentation-\n * layer redaction; it never affects what's on disk. Sealed handles\n * are unaffected either way — they always serialize as `'[sealed]'`,\n * so ciphertext never leaks regardless of this option. Rider companion\n * fields (e.g. `pan_last4`) remain visible as their own elements —\n * they are safe write-time projections.\n */\n readonly redact?: boolean | { readonly sensitivity: 'omit' | 'mask' }\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 if (options.redact !== undefined && options.redact !== false) {\n const desc: CollectionDescription = vault.collection(options.collection).describe()\n const projectionOpts = options.redact === true ? undefined\n : { sensitivity: options.redact.sensitivity }\n records.splice(0, records.length, ...records.map((r) => applyListProjection(desc, r as Record<string, unknown>, projectionOpts)))\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 transactionsStrategy 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, { reason: 'import:xml' })\n }\n if (policy !== 'insert-only') {\n for (const entry of plan.modified) {\n txVault.collection(entry.collection).put(entry.id, entry.record, { reason: 'import:xml' })\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":[]}
|
|
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 {\n ExportChunk,\n NoydbFormat,\n DecodedChunk,\n FormatExportOptions,\n} from '@noy-db/hub/as'\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 * Apply the hub's `applyListProjection` read-projection before\n * serialising records. `true` redacts only `classifiedFields` (mask /\n * omit / rider, per the field's preset). The object form additionally\n * redacts fields carrying a plain `fieldMeta` `sensitivity: 'pii' |\n * 'secret'` tag, per `sensitivity: 'omit' | 'mask'`.\n *\n * Caveat: `describe()` reflects the declarations of *this session's*\n * collection instance — redaction only takes effect when the\n * collection was opened (this call or earlier in the session) with\n * its `classifiedFields` / `fieldMeta` options. This is presentation-\n * layer redaction; it never affects what's on disk. Sealed handles\n * are unaffected either way — they always serialize as `'[sealed]'`,\n * so ciphertext never leaks regardless of this option. Rider companion\n * fields (e.g. `pan_last4`) remain visible as their own elements —\n * they are safe write-time projections.\n */\n readonly redact?: boolean | { readonly sensitivity: 'omit' | 'mask' }\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\n/**\n * The pure encoder — records in, XML out. Already gated and already redacted\n * by hub; it has no vault (ADR 0004).\n */\nfunction encodeXml(chunks: readonly ExportChunk[], options: AsXMLFormatOptions): string {\n const records: unknown[] = chunks.flatMap((c) => c.records)\n\n const rootName = options.rootElement ?? 'Records'\n // The chunk carries its collection name, so the element default survives the\n // inversion without the format needing to be told which collection it got.\n const recordName = options.recordElement ?? pascalSingular(chunks[0]?.collection ?? 'Record')\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\n/** Options an XML format instance carries. Read concerns live on `vault.export`. */\nexport interface AsXMLFormatOptions {\n readonly rootElement?: string\n readonly recordElement?: string\n readonly fields?: readonly string[]\n readonly xmlDeclaration?: boolean\n readonly pretty?: boolean\n readonly namespace?: string\n readonly namespacePrefix?: string\n /** Per-field coercion on decode. */\n readonly fieldTypes?: Readonly<Record<string, 'string' | 'number' | 'boolean'>>\n}\n\n/**\n * The XML format — the `as-*` port instance.\n *\n * ```ts\n * const xml = await vault.export(asXml(), { collections: ['invoices'] })\n * const plan = await vault.import(asXml(), xml, { collection: 'invoices' })\n * ```\n */\nexport function asXml(options: AsXMLFormatOptions = {}): NoydbFormat<string> {\n return {\n id: 'xml',\n extension: 'xml',\n mimeType: 'application/xml;charset=utf-8',\n tier: 'plaintext',\n encode: (chunks) => encodeXml(chunks, options),\n decode: (input) => decodeXml(input, options),\n }\n}\n\n/** Only the keys actually set — `exactOptionalPropertyTypes` is on. */\nfunction readOpts(o: AsXMLOptions): FormatExportOptions {\n return {\n ...(o.collection ? { collections: [o.collection] } : {}),\n ...(o.redact !== undefined && o.redact !== false\n ? { redact: o.redact === true ? true : { sensitivity: o.redact.sensitivity } }\n : {}),\n }\n}\n\n/** Browser download. Hub gates, reads and redacts; this wraps the bytes. */\nexport async function download(vault: Vault, options: AsXMLDownloadOptions): Promise<void> {\n const fmt = asXml(options)\n const xml = await vault.export(fmt, readOpts(options))\n const url = URL.createObjectURL(new Blob([xml], { type: fmt.mimeType }))\n const a = document.createElement('a')\n a.href = url\n a.download = options.filename ?? `${options.collection}.${fmt.extension}`\n a.click()\n URL.revokeObjectURL(url)\n}\n\n/**\n * Node file write. Not in hub because `hub-portable` forbids Node builtins\n * there. The gate, the read and the redaction all moved.\n */\nexport async function write(vault: Vault, path: string, options: AsXMLWriteOptions): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error('as-xml.write: acknowledgeRisks: true is required for on-disk plaintext output.')\n }\n const xml = await vault.export(asXml(options), readOpts(options))\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, xml, 'utf8')\n}\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 { XMLParser, XMLValidator } from 'fast-xml-parser'\n\n// Hub-owned as of 0.7 (ADR 0004). This line replaced a local declaration that\n// existed identically in six as-* packages, with nothing comparing them.\nimport type { ImportPolicy, ImportPlan } from '@noy-db/hub/as'\nexport type { ImportPolicy }\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 type AsXMLImportPlan = ImportPlan\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 */\n/**\n * The pure decoder — XML in, records out. No vault, no gate, no diff: hub\n * gates, plans against the live vault and owns `apply()`.\n */\nfunction decodeXml(xml: string, options: AsXMLFormatOptions): readonly DecodedChunk[] {\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 decode: 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 decode: 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 decode: 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 [{ collection: '', records: [] }]\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 [{ collection: '', records: [] }]\n }\n\n const recordElName = options.recordElement\n ? stripPrefix(options.recordElement)\n : pickRecordElementName(rootBody)\n\n if (recordElName === null) {\n return [{ collection: '', records: [] }]\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 // XML carries no collection name; hub resolves it from\n // `vault.import(fmt, xml, { collection })`.\n return [{ collection: '', records }]\n}\n\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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noy-db/as-xml",
|
|
3
|
-
"version": "0.
|
|
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
|
|
3
|
+
"version": "0.7.0-pre.0",
|
|
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 `vault.assertCanExport('plaintext', …)`.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vLannaAi <vicio@lanna.ai>",
|
|
7
7
|
"homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/as-xml#readme",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"node": ">=22.0.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@noy-db/hub": "0.
|
|
35
|
+
"@noy-db/hub": "0.7.0-pre.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"fast-xml-parser": "^5.0.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "^22.0.0",
|
|
42
|
-
"@noy-db/hub": "0.
|
|
42
|
+
"@noy-db/hub": "0.7.0-pre.0"
|
|
43
43
|
},
|
|
44
44
|
"keywords": [
|
|
45
45
|
"noy-db",
|