@noy-db/as-xlsx 0.2.0-pre.9 → 0.3.0-pre.10
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/dist/index.d.ts +270 -3
- package/dist/index.js +547 -7
- package/dist/index.js.map +1 -1
- package/package.json +9 -16
- package/dist/index.cjs +0 -637
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -300
package/dist/index.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { applyListProjection } from "@noy-db/hub";
|
|
3
|
+
|
|
1
4
|
// src/xlsx.ts
|
|
2
5
|
import { writeZip } from "@noy-db/as-zip";
|
|
3
6
|
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
4
7
|
var ENCODER = new TextEncoder();
|
|
5
|
-
|
|
8
|
+
function formula(f, cachedValue) {
|
|
9
|
+
return cachedValue === void 0 ? { __xlsxFormula: f } : { __xlsxFormula: f, v: cachedValue };
|
|
10
|
+
}
|
|
11
|
+
function isFormulaCell(v) {
|
|
12
|
+
return typeof v === "object" && v !== null && typeof v.__xlsxFormula === "string";
|
|
13
|
+
}
|
|
14
|
+
function styled(value, numberFormat) {
|
|
15
|
+
return { __xlsxStyle: numberFormat, v: value };
|
|
16
|
+
}
|
|
17
|
+
function isStyledCell(v) {
|
|
18
|
+
return typeof v === "object" && v !== null && typeof v.__xlsxStyle === "string";
|
|
19
|
+
}
|
|
20
|
+
async function writeXlsx(sheets, options = {}) {
|
|
6
21
|
if (sheets.length === 0) {
|
|
7
22
|
throw new Error("writeXlsx: at least one sheet is required");
|
|
8
23
|
}
|
|
@@ -27,6 +42,16 @@ async function writeXlsx(sheets) {
|
|
|
27
42
|
stringIndex.set(s, idx);
|
|
28
43
|
return idx;
|
|
29
44
|
};
|
|
45
|
+
const numFmtCodes = [];
|
|
46
|
+
const styleIndexByCode = /* @__PURE__ */ new Map();
|
|
47
|
+
const internStyle = (code) => {
|
|
48
|
+
const existing = styleIndexByCode.get(code);
|
|
49
|
+
if (existing !== void 0) return existing;
|
|
50
|
+
const idx = numFmtCodes.length + 1;
|
|
51
|
+
numFmtCodes.push(code);
|
|
52
|
+
styleIndexByCode.set(code, idx);
|
|
53
|
+
return idx;
|
|
54
|
+
};
|
|
30
55
|
const sheetXmls = safeSheets.map((sheet) => {
|
|
31
56
|
const lines = [
|
|
32
57
|
XML_HEADER,
|
|
@@ -56,12 +81,42 @@ async function writeXlsx(sheets) {
|
|
|
56
81
|
}
|
|
57
82
|
for (const row of sheet.rows) {
|
|
58
83
|
rowNum++;
|
|
59
|
-
const cells = row.map((value, i) => cellXml(value, i + 1, rowNum, internString)).join("");
|
|
84
|
+
const cells = row.map((value, i) => cellXml(value, i + 1, rowNum, internString, internStyle)).join("");
|
|
60
85
|
lines.push(`<row r="${rowNum}">${cells}</row>`);
|
|
61
86
|
}
|
|
62
|
-
lines.push("</sheetData>"
|
|
87
|
+
lines.push("</sheetData>");
|
|
88
|
+
if (sheet.validations && sheet.validations.length > 0) {
|
|
89
|
+
lines.push(`<dataValidations count="${sheet.validations.length}">`);
|
|
90
|
+
for (const dv of sheet.validations) {
|
|
91
|
+
const f1 = dv.formula1 ?? `"${(dv.values ?? []).join(",")}"`;
|
|
92
|
+
lines.push(
|
|
93
|
+
`<dataValidation type="list" allowBlank="1" showInputMessage="1" showErrorMessage="1" sqref="${escapeXmlAttr(dv.sqref)}"><formula1>${escapeXmlText(f1)}</formula1></dataValidation>`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
lines.push("</dataValidations>");
|
|
97
|
+
}
|
|
98
|
+
lines.push("</worksheet>");
|
|
63
99
|
return lines.join("");
|
|
64
100
|
});
|
|
101
|
+
const hasStyles = numFmtCodes.length > 0;
|
|
102
|
+
const stylesXml = hasStyles ? [
|
|
103
|
+
XML_HEADER,
|
|
104
|
+
'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">',
|
|
105
|
+
`<numFmts count="${numFmtCodes.length}">`,
|
|
106
|
+
...numFmtCodes.map((code, i) => `<numFmt numFmtId="${164 + i}" formatCode="${escapeXmlAttr(code)}"/>`),
|
|
107
|
+
"</numFmts>",
|
|
108
|
+
'<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>',
|
|
109
|
+
'<fills count="1"><fill><patternFill patternType="none"/></fill></fills>',
|
|
110
|
+
'<borders count="1"><border/></borders>',
|
|
111
|
+
'<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>',
|
|
112
|
+
`<cellXfs count="${numFmtCodes.length + 1}">`,
|
|
113
|
+
'<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>',
|
|
114
|
+
...numFmtCodes.map(
|
|
115
|
+
(_, i) => `<xf numFmtId="${164 + i}" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>`
|
|
116
|
+
),
|
|
117
|
+
"</cellXfs>",
|
|
118
|
+
"</styleSheet>"
|
|
119
|
+
].join("") : "";
|
|
65
120
|
const sheetEntries = safeSheets.map((s, i) => ({
|
|
66
121
|
index: i + 1,
|
|
67
122
|
id: `rId${i + 1}`,
|
|
@@ -78,6 +133,7 @@ async function writeXlsx(sheets) {
|
|
|
78
133
|
(s) => `<Override PartName="/${s.path}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
79
134
|
),
|
|
80
135
|
'<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>',
|
|
136
|
+
...hasStyles ? ['<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'] : [],
|
|
81
137
|
"</Types>"
|
|
82
138
|
].join("");
|
|
83
139
|
const rootRels = XML_HEADER + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>';
|
|
@@ -89,6 +145,13 @@ async function writeXlsx(sheets) {
|
|
|
89
145
|
(s) => `<sheet name="${escapeXmlAttr(s.name)}" sheetId="${s.index}" r:id="${s.id}"/>`
|
|
90
146
|
),
|
|
91
147
|
"</sheets>",
|
|
148
|
+
...options.definedNames && options.definedNames.length > 0 ? [
|
|
149
|
+
"<definedNames>",
|
|
150
|
+
...options.definedNames.map(
|
|
151
|
+
(d) => `<definedName name="${escapeXmlAttr(d.name)}">${escapeXmlText(d.ref)}</definedName>`
|
|
152
|
+
),
|
|
153
|
+
"</definedNames>"
|
|
154
|
+
] : [],
|
|
92
155
|
"</workbook>"
|
|
93
156
|
].join("");
|
|
94
157
|
const workbookRels = [
|
|
@@ -98,6 +161,7 @@ async function writeXlsx(sheets) {
|
|
|
98
161
|
(s) => `<Relationship Id="${s.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${s.index}.xml"/>`
|
|
99
162
|
),
|
|
100
163
|
`<Relationship Id="rIdSharedStrings" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>`,
|
|
164
|
+
...hasStyles ? [`<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`] : [],
|
|
101
165
|
"</Relationships>"
|
|
102
166
|
].join("");
|
|
103
167
|
const sharedStringsXml = [
|
|
@@ -112,12 +176,28 @@ async function writeXlsx(sheets) {
|
|
|
112
176
|
{ path: "xl/workbook.xml", bytes: ENCODER.encode(workbookXml) },
|
|
113
177
|
{ path: "xl/_rels/workbook.xml.rels", bytes: ENCODER.encode(workbookRels) },
|
|
114
178
|
{ path: "xl/sharedStrings.xml", bytes: ENCODER.encode(sharedStringsXml) },
|
|
179
|
+
...hasStyles ? [{ path: "xl/styles.xml", bytes: ENCODER.encode(stylesXml) }] : [],
|
|
115
180
|
...sheetEntries.map((s, i) => ({ path: s.path, bytes: ENCODER.encode(sheetXmls[i] ?? "") }))
|
|
116
181
|
];
|
|
117
182
|
return await writeZip(entries);
|
|
118
183
|
}
|
|
119
|
-
function cellXml(value, colIdx, rowNum, intern) {
|
|
184
|
+
function cellXml(value, colIdx, rowNum, intern, internStyle) {
|
|
120
185
|
const ref = `${colLetter(colIdx)}${rowNum}`;
|
|
186
|
+
if (isStyledCell(value)) {
|
|
187
|
+
const s2 = internStyle(value.__xlsxStyle);
|
|
188
|
+
const v = value.v;
|
|
189
|
+
if (v === null || v === void 0 || v === "") return `<c r="${ref}" s="${s2}"/>`;
|
|
190
|
+
if (typeof v === "number" && Number.isFinite(v)) return `<c r="${ref}" s="${s2}"><v>${v}</v></c>`;
|
|
191
|
+
if (typeof v === "boolean") return `<c r="${ref}" s="${s2}" t="b"><v>${v ? 1 : 0}</v></c>`;
|
|
192
|
+
return `<c r="${ref}" s="${s2}" t="s"><v>${intern(typeof v === "string" ? v : String(v))}</v></c>`;
|
|
193
|
+
}
|
|
194
|
+
if (isFormulaCell(value)) {
|
|
195
|
+
const f = escapeXmlText(value.__xlsxFormula);
|
|
196
|
+
if (value.v === void 0) return `<c r="${ref}"><f>${f}</f></c>`;
|
|
197
|
+
if (typeof value.v === "number" && Number.isFinite(value.v)) return `<c r="${ref}"><f>${f}</f><v>${value.v}</v></c>`;
|
|
198
|
+
if (typeof value.v === "boolean") return `<c r="${ref}" t="b"><f>${f}</f><v>${value.v ? 1 : 0}</v></c>`;
|
|
199
|
+
return `<c r="${ref}" t="str"><f>${f}</f><v>${escapeXmlText(String(value.v))}</v></c>`;
|
|
200
|
+
}
|
|
121
201
|
if (value === null || value === void 0 || value === "") return `<c r="${ref}"/>`;
|
|
122
202
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
123
203
|
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
@@ -342,6 +422,92 @@ function stringAttr(raw) {
|
|
|
342
422
|
return "";
|
|
343
423
|
}
|
|
344
424
|
|
|
425
|
+
// src/infer.ts
|
|
426
|
+
var ISO_DATE = /^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2})?/;
|
|
427
|
+
function inferType(vals) {
|
|
428
|
+
if (vals.length === 0) return "string";
|
|
429
|
+
if (vals.every((v) => typeof v === "boolean")) return "boolean";
|
|
430
|
+
if (vals.every((v) => typeof v === "number")) return "number";
|
|
431
|
+
if (vals.every((v) => typeof v === "string" && ISO_DATE.test(v))) return "date";
|
|
432
|
+
return "string";
|
|
433
|
+
}
|
|
434
|
+
async function inferSchema(bytes, options = {}) {
|
|
435
|
+
const skip = new Set(options.skipSheets ?? []);
|
|
436
|
+
const decoded = await readXlsx(bytes);
|
|
437
|
+
const sheets = [];
|
|
438
|
+
for (const sh of decoded.sheets) {
|
|
439
|
+
if (sh.name.startsWith("_") || skip.has(sh.name)) continue;
|
|
440
|
+
const headerRow = sh.rows[0] ?? {};
|
|
441
|
+
const colToField = /* @__PURE__ */ new Map();
|
|
442
|
+
const fields = [];
|
|
443
|
+
for (const [letter, val] of Object.entries(headerRow)) {
|
|
444
|
+
const name = typeof val === "string" ? val.trim() : typeof val === "number" || typeof val === "boolean" ? String(val) : "";
|
|
445
|
+
if (!name) continue;
|
|
446
|
+
colToField.set(letter, name);
|
|
447
|
+
fields.push(name);
|
|
448
|
+
}
|
|
449
|
+
const records = sh.rows.slice(1).map((row) => {
|
|
450
|
+
const rec = {};
|
|
451
|
+
for (const [letter, val] of Object.entries(row)) {
|
|
452
|
+
const f = colToField.get(letter);
|
|
453
|
+
if (f !== void 0) rec[f] = val;
|
|
454
|
+
}
|
|
455
|
+
return rec;
|
|
456
|
+
});
|
|
457
|
+
sheets.push({ name: sh.name, fields, records });
|
|
458
|
+
}
|
|
459
|
+
const valuesOf = (s, f) => s.records.map((r) => r[f]).filter((v) => v != null && v !== "");
|
|
460
|
+
const idFieldOf = (s) => {
|
|
461
|
+
if (s.fields.includes("id")) return "id";
|
|
462
|
+
for (const f of s.fields) {
|
|
463
|
+
const vals = valuesOf(s, f);
|
|
464
|
+
if (vals.length > 0 && new Set(vals.map((v) => String(v))).size === vals.length) return f;
|
|
465
|
+
}
|
|
466
|
+
return s.fields[0] ?? "id";
|
|
467
|
+
};
|
|
468
|
+
const meta = sheets.map((s) => ({ s, idField: idFieldOf(s) }));
|
|
469
|
+
const idValuesByName = /* @__PURE__ */ new Map();
|
|
470
|
+
for (const { s, idField } of meta) {
|
|
471
|
+
idValuesByName.set(s.name, new Set(valuesOf(s, idField).map((v) => String(v))));
|
|
472
|
+
}
|
|
473
|
+
const collections = {};
|
|
474
|
+
for (const { s, idField } of meta) {
|
|
475
|
+
const fields = {};
|
|
476
|
+
for (const f of s.fields) {
|
|
477
|
+
const vals = valuesOf(s, f);
|
|
478
|
+
const type = inferType(vals);
|
|
479
|
+
let references;
|
|
480
|
+
if (f !== idField && type === "string" && vals.length > 0) {
|
|
481
|
+
const distinct = new Set(vals.map((v) => String(v)));
|
|
482
|
+
for (const { s: other } of meta) {
|
|
483
|
+
if (other.name === s.name) continue;
|
|
484
|
+
const ids = idValuesByName.get(other.name);
|
|
485
|
+
if (ids && ids.size > 0 && [...distinct].every((v) => ids.has(v))) {
|
|
486
|
+
references = other.name;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
fields[f] = references ? { type, references } : { type };
|
|
492
|
+
}
|
|
493
|
+
collections[s.name] = { idField, fields };
|
|
494
|
+
}
|
|
495
|
+
return { collections };
|
|
496
|
+
}
|
|
497
|
+
function zodSourceFor(schema) {
|
|
498
|
+
const zType = (t) => t === "number" ? "z.number()" : t === "boolean" ? "z.boolean()" : t === "date" ? "z.string().datetime()" : "z.string()";
|
|
499
|
+
const blocks = [`import { z } from 'zod'`];
|
|
500
|
+
for (const [name, c] of Object.entries(schema.collections)) {
|
|
501
|
+
const lines = Object.entries(c.fields).map(
|
|
502
|
+
([f, d]) => ` ${f}: ${zType(d.type)},${d.references ? ` // \u2192 ${d.references}` : ""}`
|
|
503
|
+
);
|
|
504
|
+
blocks.push(`export const ${name}Schema = z.object({
|
|
505
|
+
${lines.join("\n")}
|
|
506
|
+
})`);
|
|
507
|
+
}
|
|
508
|
+
return blocks.join("\n\n");
|
|
509
|
+
}
|
|
510
|
+
|
|
345
511
|
// src/index.ts
|
|
346
512
|
import { diffVault } from "@noy-db/hub";
|
|
347
513
|
async function toBytesFromCollection(vault, collectionName) {
|
|
@@ -354,6 +520,10 @@ async function toBytes(vault, options) {
|
|
|
354
520
|
if (options.sheets.length === 0) {
|
|
355
521
|
throw new Error("as-xlsx: at least one sheet is required");
|
|
356
522
|
}
|
|
523
|
+
if (options.smart) {
|
|
524
|
+
const { sheets, definedNames } = await buildSmartSheets(vault, options);
|
|
525
|
+
return writeXlsx(sheets, { definedNames });
|
|
526
|
+
}
|
|
357
527
|
const materialisedSheets = [];
|
|
358
528
|
for (const sheetOpt of options.sheets) {
|
|
359
529
|
const collection = vault.collection(sheetOpt.collection);
|
|
@@ -364,11 +534,12 @@ async function toBytes(vault, options) {
|
|
|
364
534
|
if (sheetOpt.filter && !sheetOpt.filter(r)) continue;
|
|
365
535
|
records.push(r);
|
|
366
536
|
}
|
|
367
|
-
const
|
|
537
|
+
const projected = projectRecords(vault, sheetOpt.collection, records, options.redact);
|
|
538
|
+
const columns = sheetOpt.columns ?? inferColumns(projected);
|
|
368
539
|
materialisedSheets.push({
|
|
369
540
|
name: sheetOpt.name,
|
|
370
541
|
header: columns,
|
|
371
|
-
rows:
|
|
542
|
+
rows: projected.map((r) => columns.map((c) => r[c] ?? null)),
|
|
372
543
|
...sheetOpt.widths !== void 0 ? { widths: sheetOpt.widths } : {}
|
|
373
544
|
});
|
|
374
545
|
}
|
|
@@ -397,6 +568,347 @@ async function write(vault, path, options) {
|
|
|
397
568
|
const { writeFile } = await import("fs/promises");
|
|
398
569
|
await writeFile(path, bytes);
|
|
399
570
|
}
|
|
571
|
+
async function toBytesMultiVault(entries, options = {}) {
|
|
572
|
+
const sep = options.sheetSeparator ?? "_";
|
|
573
|
+
for (const entry of entries) {
|
|
574
|
+
entry.vault.assertCanExport("plaintext", "xlsx");
|
|
575
|
+
}
|
|
576
|
+
const loaded = [];
|
|
577
|
+
const denormIndex = /* @__PURE__ */ new Map();
|
|
578
|
+
for (const entry of entries) {
|
|
579
|
+
const prefix = entry.label ?? entry.vault.name;
|
|
580
|
+
for (const s of entry.sheets) {
|
|
581
|
+
const all = await entry.vault.collection(s.collection).list();
|
|
582
|
+
const records = [];
|
|
583
|
+
for (const item of all) {
|
|
584
|
+
const r = item;
|
|
585
|
+
const allow = entry.closure?.get(s.collection);
|
|
586
|
+
if (allow && !allow.has(String(r.id))) continue;
|
|
587
|
+
if (s.filter && !s.filter(r)) continue;
|
|
588
|
+
records.push(r);
|
|
589
|
+
}
|
|
590
|
+
const projected = projectRecords(entry.vault, s.collection, records, entry.redact);
|
|
591
|
+
loaded.push({ entry, prefix, sheetOpt: s, records: projected });
|
|
592
|
+
const indexKey = `${prefix}/${s.collection}`;
|
|
593
|
+
const collectionIndex = denormIndex.get(indexKey) ?? /* @__PURE__ */ new Map();
|
|
594
|
+
denormIndex.set(indexKey, collectionIndex);
|
|
595
|
+
for (const r of projected) {
|
|
596
|
+
const idVal = r.id;
|
|
597
|
+
if (idVal != null) collectionIndex.set(safeStringify(idVal), r);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (const ls of loaded) {
|
|
602
|
+
for (const d of ls.sheetOpt.denormalize ?? []) {
|
|
603
|
+
if (d.from.keyField === "id") continue;
|
|
604
|
+
const indexKey = `${d.from.label}/${d.from.collection}`;
|
|
605
|
+
const idx = denormIndex.get(indexKey);
|
|
606
|
+
if (!idx) continue;
|
|
607
|
+
const srcLoaded = loaded.find(
|
|
608
|
+
(l) => l.prefix === d.from.label && l.sheetOpt.collection === d.from.collection
|
|
609
|
+
);
|
|
610
|
+
if (!srcLoaded) continue;
|
|
611
|
+
for (const r of srcLoaded.records) {
|
|
612
|
+
const kv = r[d.from.keyField];
|
|
613
|
+
if (kv != null) idx.set(safeStringify(kv), r);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const allSheets = [];
|
|
618
|
+
const manifestRows = [];
|
|
619
|
+
for (const { prefix, sheetOpt: s, records } of loaded) {
|
|
620
|
+
const baseColumns = s.columns ?? inferColumns(records);
|
|
621
|
+
const denormDefs = s.denormalize ?? [];
|
|
622
|
+
if (denormDefs.length === 0) {
|
|
623
|
+
const sheetName2 = `${prefix}${sep}${s.name}`;
|
|
624
|
+
allSheets.push(buildFlatSheet(sheetName2, baseColumns, records));
|
|
625
|
+
manifestRows.push([prefix, s.collection, records.length]);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const allColumns = [...baseColumns, ...denormDefs.map((d) => d.column)];
|
|
629
|
+
const sheetName = `${prefix}${sep}${s.name}`;
|
|
630
|
+
const rows = records.map((r) => {
|
|
631
|
+
const baseCells = baseColumns.map((c) => r[c] ?? null);
|
|
632
|
+
const denormCells = denormDefs.map((d) => {
|
|
633
|
+
const idxKey = `${d.from.label}/${d.from.collection}`;
|
|
634
|
+
const idx = denormIndex.get(idxKey);
|
|
635
|
+
if (!idx) return "";
|
|
636
|
+
const fkVal = r[d.localField];
|
|
637
|
+
if (fkVal == null) return "";
|
|
638
|
+
const supporting = idx.get(safeStringify(fkVal));
|
|
639
|
+
if (!supporting) return "";
|
|
640
|
+
return supporting[d.from.pick] ?? "";
|
|
641
|
+
});
|
|
642
|
+
return [...baseCells, ...denormCells];
|
|
643
|
+
});
|
|
644
|
+
allSheets.push({ name: sheetName, header: allColumns, rows });
|
|
645
|
+
manifestRows.push([prefix, s.collection, records.length]);
|
|
646
|
+
}
|
|
647
|
+
allSheets.unshift({
|
|
648
|
+
name: "_manifest",
|
|
649
|
+
header: ["Vault", "Collection", "Records"],
|
|
650
|
+
rows: manifestRows
|
|
651
|
+
});
|
|
652
|
+
return writeXlsx(allSheets);
|
|
653
|
+
}
|
|
654
|
+
function projectRecords(vault, collectionName, records, redact) {
|
|
655
|
+
if (redact === void 0 || redact === false) return [...records];
|
|
656
|
+
const desc = vault.collection(collectionName).describe();
|
|
657
|
+
const projectionOpts = redact === true ? void 0 : { sensitivity: redact.sensitivity };
|
|
658
|
+
return records.map((r) => applyListProjection(desc, r, projectionOpts));
|
|
659
|
+
}
|
|
660
|
+
function buildFlatSheet(name, columns, records) {
|
|
661
|
+
return {
|
|
662
|
+
name,
|
|
663
|
+
header: [...columns],
|
|
664
|
+
rows: records.map((r) => columns.map((c) => r[c] ?? null))
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function safeStringify(v) {
|
|
668
|
+
if (typeof v === "string") return v;
|
|
669
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
|
|
670
|
+
if (v == null) return "";
|
|
671
|
+
try {
|
|
672
|
+
return JSON.stringify(v) ?? "";
|
|
673
|
+
} catch {
|
|
674
|
+
return "";
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function asCached(v) {
|
|
678
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "string") return v;
|
|
679
|
+
return safeStringify(v);
|
|
680
|
+
}
|
|
681
|
+
async function buildSmartSheets(vault, options) {
|
|
682
|
+
const snapshot = await vault.dumpSchema();
|
|
683
|
+
const sheetNameByCollection = /* @__PURE__ */ new Map();
|
|
684
|
+
for (const s of options.sheets) sheetNameByCollection.set(s.collection, s.name);
|
|
685
|
+
const mats = [];
|
|
686
|
+
const localeSet = /* @__PURE__ */ new Set();
|
|
687
|
+
for (const sheetOpt of options.sheets) {
|
|
688
|
+
const collection = vault.collection(sheetOpt.collection);
|
|
689
|
+
const list = await collection.list();
|
|
690
|
+
const records = [];
|
|
691
|
+
for (const item of list) {
|
|
692
|
+
const r = item;
|
|
693
|
+
if (sheetOpt.filter && !sheetOpt.filter(r)) continue;
|
|
694
|
+
records.push(r);
|
|
695
|
+
}
|
|
696
|
+
const projected = projectRecords(vault, sheetOpt.collection, records, options.redact);
|
|
697
|
+
const base = sheetOpt.columns ?? inferColumns(projected);
|
|
698
|
+
const cols = ["id", ...base.filter((c) => c !== "id")];
|
|
699
|
+
const labelCol = cols.find((c) => c !== "id");
|
|
700
|
+
const labelMap = /* @__PURE__ */ new Map();
|
|
701
|
+
if (labelCol) {
|
|
702
|
+
for (const r of projected) if (r.id != null) labelMap.set(safeStringify(r.id), r[labelCol]);
|
|
703
|
+
}
|
|
704
|
+
const i18nFields = (sheetOpt.i18nFields ?? []).filter((f) => cols.includes(f));
|
|
705
|
+
for (const f of i18nFields) {
|
|
706
|
+
for (const r of projected) {
|
|
707
|
+
const v = r[f];
|
|
708
|
+
if (v && typeof v === "object") for (const loc of Object.keys(v)) localeSet.add(loc);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
mats.push({ opt: sheetOpt, records: projected, cols, labelMap, i18nFields });
|
|
712
|
+
}
|
|
713
|
+
const matByCollection = new Map(mats.map((m) => [m.opt.collection, m]));
|
|
714
|
+
const dictEntries = /* @__PURE__ */ new Map();
|
|
715
|
+
for (const m of mats) {
|
|
716
|
+
for (const [field, dictName] of Object.entries(m.opt.dictFields ?? {})) {
|
|
717
|
+
if (!m.cols.includes(field) || dictEntries.has(dictName)) continue;
|
|
718
|
+
dictEntries.set(dictName, await vault.dictionary(dictName).list());
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
for (const entries of dictEntries.values()) {
|
|
722
|
+
for (const e of entries) for (const loc of Object.keys(e.labels)) localeSet.add(loc);
|
|
723
|
+
}
|
|
724
|
+
const locales = [...localeSet].sort();
|
|
725
|
+
const hasLang = locales.length > 0;
|
|
726
|
+
const defaultLocale = locales.includes("en") ? "en" : locales[0] ?? "en";
|
|
727
|
+
const ifChain = (refOf) => {
|
|
728
|
+
if (locales.length === 0) return '""';
|
|
729
|
+
let expr = refOf(locales[0]);
|
|
730
|
+
for (let k = locales.length - 1; k >= 0; k--) {
|
|
731
|
+
expr = `IF(LANG="${locales[k]}",${refOf(locales[k])},${expr})`;
|
|
732
|
+
}
|
|
733
|
+
return expr;
|
|
734
|
+
};
|
|
735
|
+
const sheetMeta = /* @__PURE__ */ new Map();
|
|
736
|
+
const dataSheets = mats.map((m) => {
|
|
737
|
+
const refs = snapshot.collections[m.opt.collection]?.refs ?? {};
|
|
738
|
+
const fields = snapshot.collections[m.opt.collection]?.fields ?? {};
|
|
739
|
+
const i18nSet = new Set(m.i18nFields);
|
|
740
|
+
const refFields = Object.keys(refs).filter((f) => m.cols.includes(f) && matByCollection.has(refs[f].target));
|
|
741
|
+
const baseCols = m.cols.filter((c) => !i18nSet.has(c));
|
|
742
|
+
const i18nFlat = m.i18nFields.flatMap((f) => [f, ...locales.map((l) => `${f}__${l}`)]);
|
|
743
|
+
const dictPairs = Object.entries(m.opt.dictFields ?? {}).filter(([f]) => m.cols.includes(f));
|
|
744
|
+
const header = [
|
|
745
|
+
...baseCols,
|
|
746
|
+
...i18nFlat,
|
|
747
|
+
...dictPairs.map(([f]) => `${f}__label`),
|
|
748
|
+
...refFields.map((f) => `${f}__label`)
|
|
749
|
+
];
|
|
750
|
+
const colIndex = /* @__PURE__ */ new Map();
|
|
751
|
+
header.forEach((h, i) => colIndex.set(h, i + 1));
|
|
752
|
+
sheetMeta.set(m.opt.collection, { name: m.opt.name, colIndex });
|
|
753
|
+
const lastRow = Math.max(m.records.length + 1, 2);
|
|
754
|
+
const validations = [];
|
|
755
|
+
for (const field of baseCols) {
|
|
756
|
+
const colL = colLetter(colIndex.get(field));
|
|
757
|
+
const sqref = `${colL}2:${colL}${lastRow}`;
|
|
758
|
+
const explicit = m.opt.dropdowns?.[field];
|
|
759
|
+
if (explicit && explicit.length > 0) {
|
|
760
|
+
validations.push({ sqref, values: explicit });
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const rf = refs[field];
|
|
764
|
+
if (rf && matByCollection.has(rf.target)) {
|
|
765
|
+
const targetSheet = sheetNameByCollection.get(rf.target);
|
|
766
|
+
const targetRows = Math.max(matByCollection.get(rf.target).records.length + 1, 2);
|
|
767
|
+
validations.push({ sqref, formula1: `'${targetSheet}'!$A$2:$A$${targetRows}` });
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const enumVals = fields[field]?.constraints?.["values"];
|
|
771
|
+
if (Array.isArray(enumVals) && enumVals.length > 0) validations.push({ sqref, values: enumVals.map((v) => safeStringify(v)) });
|
|
772
|
+
}
|
|
773
|
+
const rows = m.records.map((r, i) => {
|
|
774
|
+
const rowNum = i + 2;
|
|
775
|
+
const baseCells = baseCols.map((c) => {
|
|
776
|
+
const raw = c === "id" ? r.id ?? null : r[c] ?? null;
|
|
777
|
+
const fmt = m.opt.numberFormats?.[c];
|
|
778
|
+
if (fmt === void 0) return raw;
|
|
779
|
+
const num = typeof raw === "string" && raw.trim() !== "" && Number.isFinite(Number(raw)) ? Number(raw) : raw;
|
|
780
|
+
return styled(num, fmt);
|
|
781
|
+
});
|
|
782
|
+
const i18nCells = m.i18nFields.flatMap((f) => {
|
|
783
|
+
const rawMap = r[f] && typeof r[f] === "object" ? r[f] : {};
|
|
784
|
+
const display = formula(
|
|
785
|
+
ifChain((l) => `${colLetter(colIndex.get(`${f}__${l}`))}${rowNum}`),
|
|
786
|
+
asCached(rawMap[defaultLocale] ?? "")
|
|
787
|
+
);
|
|
788
|
+
return [display, ...locales.map((l) => rawMap[l] ?? null)];
|
|
789
|
+
});
|
|
790
|
+
const dictCells = dictPairs.map(([f, dn]) => {
|
|
791
|
+
const codeRef = `${colLetter(colIndex.get(f))}${rowNum}`;
|
|
792
|
+
const lk = `_Lookups_${dn}`;
|
|
793
|
+
const f1 = `IFERROR(VLOOKUP(${codeRef},'${lk}'!$A:$ZZ,MATCH(LANG,'${lk}'!$1:$1,0),FALSE),${codeRef})`;
|
|
794
|
+
const code = r[f];
|
|
795
|
+
const entry = code == null ? void 0 : dictEntries.get(dn)?.find((e) => e.key === safeStringify(code));
|
|
796
|
+
return formula(f1, asCached(entry?.labels[defaultLocale] ?? (code ?? "")));
|
|
797
|
+
});
|
|
798
|
+
const refCells = refFields.map((f) => {
|
|
799
|
+
const target = refs[f].target;
|
|
800
|
+
const targetSheet = sheetNameByCollection.get(target);
|
|
801
|
+
const targetMat = matByCollection.get(target);
|
|
802
|
+
const codeRef = `${colLetter(colIndex.get(f))}${rowNum}`;
|
|
803
|
+
const f1 = `IFERROR(VLOOKUP(${codeRef},'${targetSheet}'!$A:$ZZ,2,FALSE),"")`;
|
|
804
|
+
const code = r[f];
|
|
805
|
+
const cached = code == null ? "" : asCached(targetMat.labelMap.get(safeStringify(code)));
|
|
806
|
+
return formula(f1, cached);
|
|
807
|
+
});
|
|
808
|
+
return [...baseCells, ...i18nCells, ...dictCells, ...refCells];
|
|
809
|
+
});
|
|
810
|
+
return {
|
|
811
|
+
name: m.opt.name,
|
|
812
|
+
header,
|
|
813
|
+
rows,
|
|
814
|
+
...validations.length > 0 ? { validations } : {},
|
|
815
|
+
...m.opt.widths !== void 0 ? { widths: m.opt.widths } : {}
|
|
816
|
+
};
|
|
817
|
+
});
|
|
818
|
+
const manifest = {
|
|
819
|
+
name: "_manifest",
|
|
820
|
+
header: ["Collection", "Records", "Refs"],
|
|
821
|
+
rows: mats.map((m) => {
|
|
822
|
+
const refs = snapshot.collections[m.opt.collection]?.refs ?? {};
|
|
823
|
+
return [m.opt.name, m.records.length, Object.entries(refs).map(([f, r]) => `${f}\u2192${r.target}`).join(", ")];
|
|
824
|
+
})
|
|
825
|
+
};
|
|
826
|
+
const dialect = options.dialect ?? "excel";
|
|
827
|
+
const summarySheets = [];
|
|
828
|
+
for (const spec of options.summaries ?? []) {
|
|
829
|
+
const src = sheetMeta.get(spec.from);
|
|
830
|
+
const srcMat = matByCollection.get(spec.from);
|
|
831
|
+
const gCol = src?.colIndex.get(spec.groupBy);
|
|
832
|
+
if (!src || !srcMat || gCol === void 0) continue;
|
|
833
|
+
const gLetter = colLetter(gCol);
|
|
834
|
+
if (dialect === "sheets") {
|
|
835
|
+
const selects = [];
|
|
836
|
+
const labels = [];
|
|
837
|
+
for (const a of spec.aggregates) {
|
|
838
|
+
if (a.op === "count") {
|
|
839
|
+
selects.push(`COUNT(${gLetter})`);
|
|
840
|
+
labels.push(`COUNT(${gLetter}) '${a.label}'`);
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
const vIdx = a.field ? src.colIndex.get(a.field) : void 0;
|
|
844
|
+
if (vIdx === void 0) continue;
|
|
845
|
+
const vL = colLetter(vIdx);
|
|
846
|
+
const fn = a.op === "sum" ? "SUM" : "AVG";
|
|
847
|
+
selects.push(`${fn}(${vL})`);
|
|
848
|
+
labels.push(`${fn}(${vL}) '${a.label}'`);
|
|
849
|
+
}
|
|
850
|
+
const q = `QUERY('${src.name}'!A:ZZ, "SELECT ${gLetter}, ${selects.join(", ")} GROUP BY ${gLetter} LABEL ${labels.join(", ")}", 1)`;
|
|
851
|
+
summarySheets.push({ name: spec.name, rows: [[formula(q)]] });
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const seen = /* @__PURE__ */ new Set();
|
|
855
|
+
const groups = [];
|
|
856
|
+
for (const r of srcMat.records) {
|
|
857
|
+
const k = safeStringify(r[spec.groupBy]);
|
|
858
|
+
if (!seen.has(k)) {
|
|
859
|
+
seen.add(k);
|
|
860
|
+
groups.push(r[spec.groupBy]);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const header = [spec.groupBy, ...spec.aggregates.map((a) => a.label)];
|
|
864
|
+
const rows = groups.map((g, i) => {
|
|
865
|
+
const rowNum = i + 2;
|
|
866
|
+
const groupRecs = srcMat.records.filter((r) => safeStringify(r[spec.groupBy]) === safeStringify(g));
|
|
867
|
+
const cells = [g];
|
|
868
|
+
for (const a of spec.aggregates) {
|
|
869
|
+
if (a.op === "count") {
|
|
870
|
+
cells.push(formula(`COUNTIFS('${src.name}'!$${gLetter}:$${gLetter},$A${rowNum})`, groupRecs.length));
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const vIdx = a.field ? src.colIndex.get(a.field) : void 0;
|
|
874
|
+
if (vIdx === void 0) {
|
|
875
|
+
cells.push(null);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
const vLetter = colLetter(vIdx);
|
|
879
|
+
const nums = groupRecs.map((r) => Number(r[a.field])).filter((n) => Number.isFinite(n));
|
|
880
|
+
const sum = nums.reduce((s, n) => s + n, 0);
|
|
881
|
+
const cached = a.op === "sum" ? sum : nums.length ? sum / nums.length : 0;
|
|
882
|
+
const fn = a.op === "sum" ? "SUMIFS" : "AVERAGEIFS";
|
|
883
|
+
cells.push(
|
|
884
|
+
formula(`${fn}('${src.name}'!$${vLetter}:$${vLetter},'${src.name}'!$${gLetter}:$${gLetter},$A${rowNum})`, cached)
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
return cells;
|
|
888
|
+
});
|
|
889
|
+
summarySheets.push({ name: spec.name, header, rows });
|
|
890
|
+
}
|
|
891
|
+
const lookupSheets = [...dictEntries.entries()].map(([dn, entries]) => ({
|
|
892
|
+
name: `_Lookups_${dn}`,
|
|
893
|
+
header: ["Code", ...locales],
|
|
894
|
+
rows: entries.map((e) => [e.key, ...locales.map((l) => e.labels[l] ?? "")])
|
|
895
|
+
}));
|
|
896
|
+
const settingsSheets = [];
|
|
897
|
+
const definedNames = [];
|
|
898
|
+
if (hasLang) {
|
|
899
|
+
settingsSheets.push({
|
|
900
|
+
name: "_settings",
|
|
901
|
+
header: ["Setting", "Value"],
|
|
902
|
+
rows: [["Language", defaultLocale]],
|
|
903
|
+
validations: [{ sqref: "B2:B2", values: locales }]
|
|
904
|
+
});
|
|
905
|
+
definedNames.push({ name: "LANG", ref: `'_settings'!$B$2` });
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
sheets: [...settingsSheets, ...lookupSheets, manifest, ...summarySheets, ...dataSheets],
|
|
909
|
+
definedNames
|
|
910
|
+
};
|
|
911
|
+
}
|
|
400
912
|
function inferColumns(records) {
|
|
401
913
|
const seen = /* @__PURE__ */ new Set();
|
|
402
914
|
const out = [];
|
|
@@ -468,6 +980,13 @@ async function fromBytes(vault, bytes, options) {
|
|
|
468
980
|
} catch {
|
|
469
981
|
}
|
|
470
982
|
}
|
|
983
|
+
const i18nBases = /* @__PURE__ */ new Set();
|
|
984
|
+
if (options.smart) {
|
|
985
|
+
for (const field of colToField.values()) {
|
|
986
|
+
const m = /^(.+)__(.+)$/.exec(field);
|
|
987
|
+
if (m && m[2] !== "label") i18nBases.add(m[1]);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
471
990
|
const records = [];
|
|
472
991
|
for (let i = headerRowIdx + 1; i < allRows.length; i++) {
|
|
473
992
|
const row = allRows[i];
|
|
@@ -476,6 +995,22 @@ async function fromBytes(vault, bytes, options) {
|
|
|
476
995
|
for (const [col, value] of Object.entries(row)) {
|
|
477
996
|
const field = colToField.get(col);
|
|
478
997
|
if (field === void 0) continue;
|
|
998
|
+
if (options.smart) {
|
|
999
|
+
if (field.endsWith("__label")) continue;
|
|
1000
|
+
const lm = /^(.+)__(.+)$/.exec(field);
|
|
1001
|
+
if (lm && lm[2] !== "label" && i18nBases.has(lm[1])) {
|
|
1002
|
+
const base = lm[1];
|
|
1003
|
+
const coerced2 = coerceXlsxCell(value, types[base]);
|
|
1004
|
+
if (coerced2 !== void 0 && coerced2 !== "") {
|
|
1005
|
+
const map = record[base] ?? {};
|
|
1006
|
+
map[lm[2]] = coerced2;
|
|
1007
|
+
record[base] = map;
|
|
1008
|
+
hasAny = true;
|
|
1009
|
+
}
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (i18nBases.has(field)) continue;
|
|
1013
|
+
}
|
|
479
1014
|
const coerced = coerceXlsxCell(value, types[field]);
|
|
480
1015
|
if (coerced !== void 0) {
|
|
481
1016
|
const invMap = invertMaps.get(field);
|
|
@@ -582,11 +1117,16 @@ export {
|
|
|
582
1117
|
XlsxDictAmbiguityError,
|
|
583
1118
|
colLetter,
|
|
584
1119
|
download,
|
|
1120
|
+
formula,
|
|
585
1121
|
fromBytes,
|
|
1122
|
+
inferSchema,
|
|
586
1123
|
readXlsx,
|
|
1124
|
+
styled,
|
|
587
1125
|
toBytes,
|
|
588
1126
|
toBytesFromCollection,
|
|
1127
|
+
toBytesMultiVault,
|
|
589
1128
|
write,
|
|
590
|
-
writeXlsx
|
|
1129
|
+
writeXlsx,
|
|
1130
|
+
zodSourceFor
|
|
591
1131
|
};
|
|
592
1132
|
//# sourceMappingURL=index.js.map
|