@noy-db/as-xlsx 0.2.0-pre.3 → 0.2.0-pre.30
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.cjs +539 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +241 -3
- package/dist/index.d.ts +241 -3
- package/dist/index.js +533 -5
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -33,12 +33,17 @@ __export(index_exports, {
|
|
|
33
33
|
XlsxDictAmbiguityError: () => XlsxDictAmbiguityError,
|
|
34
34
|
colLetter: () => colLetter,
|
|
35
35
|
download: () => download,
|
|
36
|
+
formula: () => formula,
|
|
36
37
|
fromBytes: () => fromBytes,
|
|
38
|
+
inferSchema: () => inferSchema,
|
|
37
39
|
readXlsx: () => readXlsx,
|
|
40
|
+
styled: () => styled,
|
|
38
41
|
toBytes: () => toBytes,
|
|
39
42
|
toBytesFromCollection: () => toBytesFromCollection,
|
|
43
|
+
toBytesMultiVault: () => toBytesMultiVault,
|
|
40
44
|
write: () => write,
|
|
41
|
-
writeXlsx: () => writeXlsx
|
|
45
|
+
writeXlsx: () => writeXlsx,
|
|
46
|
+
zodSourceFor: () => zodSourceFor
|
|
42
47
|
});
|
|
43
48
|
module.exports = __toCommonJS(index_exports);
|
|
44
49
|
|
|
@@ -46,7 +51,19 @@ module.exports = __toCommonJS(index_exports);
|
|
|
46
51
|
var import_as_zip = require("@noy-db/as-zip");
|
|
47
52
|
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
48
53
|
var ENCODER = new TextEncoder();
|
|
49
|
-
|
|
54
|
+
function formula(f, cachedValue) {
|
|
55
|
+
return cachedValue === void 0 ? { __xlsxFormula: f } : { __xlsxFormula: f, v: cachedValue };
|
|
56
|
+
}
|
|
57
|
+
function isFormulaCell(v) {
|
|
58
|
+
return typeof v === "object" && v !== null && typeof v.__xlsxFormula === "string";
|
|
59
|
+
}
|
|
60
|
+
function styled(value, numberFormat) {
|
|
61
|
+
return { __xlsxStyle: numberFormat, v: value };
|
|
62
|
+
}
|
|
63
|
+
function isStyledCell(v) {
|
|
64
|
+
return typeof v === "object" && v !== null && typeof v.__xlsxStyle === "string";
|
|
65
|
+
}
|
|
66
|
+
async function writeXlsx(sheets, options = {}) {
|
|
50
67
|
if (sheets.length === 0) {
|
|
51
68
|
throw new Error("writeXlsx: at least one sheet is required");
|
|
52
69
|
}
|
|
@@ -71,6 +88,16 @@ async function writeXlsx(sheets) {
|
|
|
71
88
|
stringIndex.set(s, idx);
|
|
72
89
|
return idx;
|
|
73
90
|
};
|
|
91
|
+
const numFmtCodes = [];
|
|
92
|
+
const styleIndexByCode = /* @__PURE__ */ new Map();
|
|
93
|
+
const internStyle = (code) => {
|
|
94
|
+
const existing = styleIndexByCode.get(code);
|
|
95
|
+
if (existing !== void 0) return existing;
|
|
96
|
+
const idx = numFmtCodes.length + 1;
|
|
97
|
+
numFmtCodes.push(code);
|
|
98
|
+
styleIndexByCode.set(code, idx);
|
|
99
|
+
return idx;
|
|
100
|
+
};
|
|
74
101
|
const sheetXmls = safeSheets.map((sheet) => {
|
|
75
102
|
const lines = [
|
|
76
103
|
XML_HEADER,
|
|
@@ -100,12 +127,42 @@ async function writeXlsx(sheets) {
|
|
|
100
127
|
}
|
|
101
128
|
for (const row of sheet.rows) {
|
|
102
129
|
rowNum++;
|
|
103
|
-
const cells = row.map((value, i) => cellXml(value, i + 1, rowNum, internString)).join("");
|
|
130
|
+
const cells = row.map((value, i) => cellXml(value, i + 1, rowNum, internString, internStyle)).join("");
|
|
104
131
|
lines.push(`<row r="${rowNum}">${cells}</row>`);
|
|
105
132
|
}
|
|
106
|
-
lines.push("</sheetData>"
|
|
133
|
+
lines.push("</sheetData>");
|
|
134
|
+
if (sheet.validations && sheet.validations.length > 0) {
|
|
135
|
+
lines.push(`<dataValidations count="${sheet.validations.length}">`);
|
|
136
|
+
for (const dv of sheet.validations) {
|
|
137
|
+
const f1 = dv.formula1 ?? `"${(dv.values ?? []).join(",")}"`;
|
|
138
|
+
lines.push(
|
|
139
|
+
`<dataValidation type="list" allowBlank="1" showInputMessage="1" showErrorMessage="1" sqref="${escapeXmlAttr(dv.sqref)}"><formula1>${escapeXmlText(f1)}</formula1></dataValidation>`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
lines.push("</dataValidations>");
|
|
143
|
+
}
|
|
144
|
+
lines.push("</worksheet>");
|
|
107
145
|
return lines.join("");
|
|
108
146
|
});
|
|
147
|
+
const hasStyles = numFmtCodes.length > 0;
|
|
148
|
+
const stylesXml = hasStyles ? [
|
|
149
|
+
XML_HEADER,
|
|
150
|
+
'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">',
|
|
151
|
+
`<numFmts count="${numFmtCodes.length}">`,
|
|
152
|
+
...numFmtCodes.map((code, i) => `<numFmt numFmtId="${164 + i}" formatCode="${escapeXmlAttr(code)}"/>`),
|
|
153
|
+
"</numFmts>",
|
|
154
|
+
'<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>',
|
|
155
|
+
'<fills count="1"><fill><patternFill patternType="none"/></fill></fills>',
|
|
156
|
+
'<borders count="1"><border/></borders>',
|
|
157
|
+
'<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>',
|
|
158
|
+
`<cellXfs count="${numFmtCodes.length + 1}">`,
|
|
159
|
+
'<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>',
|
|
160
|
+
...numFmtCodes.map(
|
|
161
|
+
(_, i) => `<xf numFmtId="${164 + i}" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>`
|
|
162
|
+
),
|
|
163
|
+
"</cellXfs>",
|
|
164
|
+
"</styleSheet>"
|
|
165
|
+
].join("") : "";
|
|
109
166
|
const sheetEntries = safeSheets.map((s, i) => ({
|
|
110
167
|
index: i + 1,
|
|
111
168
|
id: `rId${i + 1}`,
|
|
@@ -122,6 +179,7 @@ async function writeXlsx(sheets) {
|
|
|
122
179
|
(s) => `<Override PartName="/${s.path}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
123
180
|
),
|
|
124
181
|
'<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>',
|
|
182
|
+
...hasStyles ? ['<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'] : [],
|
|
125
183
|
"</Types>"
|
|
126
184
|
].join("");
|
|
127
185
|
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>';
|
|
@@ -133,6 +191,13 @@ async function writeXlsx(sheets) {
|
|
|
133
191
|
(s) => `<sheet name="${escapeXmlAttr(s.name)}" sheetId="${s.index}" r:id="${s.id}"/>`
|
|
134
192
|
),
|
|
135
193
|
"</sheets>",
|
|
194
|
+
...options.definedNames && options.definedNames.length > 0 ? [
|
|
195
|
+
"<definedNames>",
|
|
196
|
+
...options.definedNames.map(
|
|
197
|
+
(d) => `<definedName name="${escapeXmlAttr(d.name)}">${escapeXmlText(d.ref)}</definedName>`
|
|
198
|
+
),
|
|
199
|
+
"</definedNames>"
|
|
200
|
+
] : [],
|
|
136
201
|
"</workbook>"
|
|
137
202
|
].join("");
|
|
138
203
|
const workbookRels = [
|
|
@@ -142,6 +207,7 @@ async function writeXlsx(sheets) {
|
|
|
142
207
|
(s) => `<Relationship Id="${s.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${s.index}.xml"/>`
|
|
143
208
|
),
|
|
144
209
|
`<Relationship Id="rIdSharedStrings" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>`,
|
|
210
|
+
...hasStyles ? [`<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`] : [],
|
|
145
211
|
"</Relationships>"
|
|
146
212
|
].join("");
|
|
147
213
|
const sharedStringsXml = [
|
|
@@ -156,12 +222,28 @@ async function writeXlsx(sheets) {
|
|
|
156
222
|
{ path: "xl/workbook.xml", bytes: ENCODER.encode(workbookXml) },
|
|
157
223
|
{ path: "xl/_rels/workbook.xml.rels", bytes: ENCODER.encode(workbookRels) },
|
|
158
224
|
{ path: "xl/sharedStrings.xml", bytes: ENCODER.encode(sharedStringsXml) },
|
|
225
|
+
...hasStyles ? [{ path: "xl/styles.xml", bytes: ENCODER.encode(stylesXml) }] : [],
|
|
159
226
|
...sheetEntries.map((s, i) => ({ path: s.path, bytes: ENCODER.encode(sheetXmls[i] ?? "") }))
|
|
160
227
|
];
|
|
161
228
|
return await (0, import_as_zip.writeZip)(entries);
|
|
162
229
|
}
|
|
163
|
-
function cellXml(value, colIdx, rowNum, intern) {
|
|
230
|
+
function cellXml(value, colIdx, rowNum, intern, internStyle) {
|
|
164
231
|
const ref = `${colLetter(colIdx)}${rowNum}`;
|
|
232
|
+
if (isStyledCell(value)) {
|
|
233
|
+
const s2 = internStyle(value.__xlsxStyle);
|
|
234
|
+
const v = value.v;
|
|
235
|
+
if (v === null || v === void 0 || v === "") return `<c r="${ref}" s="${s2}"/>`;
|
|
236
|
+
if (typeof v === "number" && Number.isFinite(v)) return `<c r="${ref}" s="${s2}"><v>${v}</v></c>`;
|
|
237
|
+
if (typeof v === "boolean") return `<c r="${ref}" s="${s2}" t="b"><v>${v ? 1 : 0}</v></c>`;
|
|
238
|
+
return `<c r="${ref}" s="${s2}" t="s"><v>${intern(typeof v === "string" ? v : String(v))}</v></c>`;
|
|
239
|
+
}
|
|
240
|
+
if (isFormulaCell(value)) {
|
|
241
|
+
const f = escapeXmlText(value.__xlsxFormula);
|
|
242
|
+
if (value.v === void 0) return `<c r="${ref}"><f>${f}</f></c>`;
|
|
243
|
+
if (typeof value.v === "number" && Number.isFinite(value.v)) return `<c r="${ref}"><f>${f}</f><v>${value.v}</v></c>`;
|
|
244
|
+
if (typeof value.v === "boolean") return `<c r="${ref}" t="b"><f>${f}</f><v>${value.v ? 1 : 0}</v></c>`;
|
|
245
|
+
return `<c r="${ref}" t="str"><f>${f}</f><v>${escapeXmlText(String(value.v))}</v></c>`;
|
|
246
|
+
}
|
|
165
247
|
if (value === null || value === void 0 || value === "") return `<c r="${ref}"/>`;
|
|
166
248
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
167
249
|
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
@@ -386,6 +468,92 @@ function stringAttr(raw) {
|
|
|
386
468
|
return "";
|
|
387
469
|
}
|
|
388
470
|
|
|
471
|
+
// src/infer.ts
|
|
472
|
+
var ISO_DATE = /^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2})?/;
|
|
473
|
+
function inferType(vals) {
|
|
474
|
+
if (vals.length === 0) return "string";
|
|
475
|
+
if (vals.every((v) => typeof v === "boolean")) return "boolean";
|
|
476
|
+
if (vals.every((v) => typeof v === "number")) return "number";
|
|
477
|
+
if (vals.every((v) => typeof v === "string" && ISO_DATE.test(v))) return "date";
|
|
478
|
+
return "string";
|
|
479
|
+
}
|
|
480
|
+
async function inferSchema(bytes, options = {}) {
|
|
481
|
+
const skip = new Set(options.skipSheets ?? []);
|
|
482
|
+
const decoded = await readXlsx(bytes);
|
|
483
|
+
const sheets = [];
|
|
484
|
+
for (const sh of decoded.sheets) {
|
|
485
|
+
if (sh.name.startsWith("_") || skip.has(sh.name)) continue;
|
|
486
|
+
const headerRow = sh.rows[0] ?? {};
|
|
487
|
+
const colToField = /* @__PURE__ */ new Map();
|
|
488
|
+
const fields = [];
|
|
489
|
+
for (const [letter, val] of Object.entries(headerRow)) {
|
|
490
|
+
const name = typeof val === "string" ? val.trim() : typeof val === "number" || typeof val === "boolean" ? String(val) : "";
|
|
491
|
+
if (!name) continue;
|
|
492
|
+
colToField.set(letter, name);
|
|
493
|
+
fields.push(name);
|
|
494
|
+
}
|
|
495
|
+
const records = sh.rows.slice(1).map((row) => {
|
|
496
|
+
const rec = {};
|
|
497
|
+
for (const [letter, val] of Object.entries(row)) {
|
|
498
|
+
const f = colToField.get(letter);
|
|
499
|
+
if (f !== void 0) rec[f] = val;
|
|
500
|
+
}
|
|
501
|
+
return rec;
|
|
502
|
+
});
|
|
503
|
+
sheets.push({ name: sh.name, fields, records });
|
|
504
|
+
}
|
|
505
|
+
const valuesOf = (s, f) => s.records.map((r) => r[f]).filter((v) => v != null && v !== "");
|
|
506
|
+
const idFieldOf = (s) => {
|
|
507
|
+
if (s.fields.includes("id")) return "id";
|
|
508
|
+
for (const f of s.fields) {
|
|
509
|
+
const vals = valuesOf(s, f);
|
|
510
|
+
if (vals.length > 0 && new Set(vals.map((v) => String(v))).size === vals.length) return f;
|
|
511
|
+
}
|
|
512
|
+
return s.fields[0] ?? "id";
|
|
513
|
+
};
|
|
514
|
+
const meta = sheets.map((s) => ({ s, idField: idFieldOf(s) }));
|
|
515
|
+
const idValuesByName = /* @__PURE__ */ new Map();
|
|
516
|
+
for (const { s, idField } of meta) {
|
|
517
|
+
idValuesByName.set(s.name, new Set(valuesOf(s, idField).map((v) => String(v))));
|
|
518
|
+
}
|
|
519
|
+
const collections = {};
|
|
520
|
+
for (const { s, idField } of meta) {
|
|
521
|
+
const fields = {};
|
|
522
|
+
for (const f of s.fields) {
|
|
523
|
+
const vals = valuesOf(s, f);
|
|
524
|
+
const type = inferType(vals);
|
|
525
|
+
let references;
|
|
526
|
+
if (f !== idField && type === "string" && vals.length > 0) {
|
|
527
|
+
const distinct = new Set(vals.map((v) => String(v)));
|
|
528
|
+
for (const { s: other } of meta) {
|
|
529
|
+
if (other.name === s.name) continue;
|
|
530
|
+
const ids = idValuesByName.get(other.name);
|
|
531
|
+
if (ids && ids.size > 0 && [...distinct].every((v) => ids.has(v))) {
|
|
532
|
+
references = other.name;
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
fields[f] = references ? { type, references } : { type };
|
|
538
|
+
}
|
|
539
|
+
collections[s.name] = { idField, fields };
|
|
540
|
+
}
|
|
541
|
+
return { collections };
|
|
542
|
+
}
|
|
543
|
+
function zodSourceFor(schema) {
|
|
544
|
+
const zType = (t) => t === "number" ? "z.number()" : t === "boolean" ? "z.boolean()" : t === "date" ? "z.string().datetime()" : "z.string()";
|
|
545
|
+
const blocks = [`import { z } from 'zod'`];
|
|
546
|
+
for (const [name, c] of Object.entries(schema.collections)) {
|
|
547
|
+
const lines = Object.entries(c.fields).map(
|
|
548
|
+
([f, d]) => ` ${f}: ${zType(d.type)},${d.references ? ` // \u2192 ${d.references}` : ""}`
|
|
549
|
+
);
|
|
550
|
+
blocks.push(`export const ${name}Schema = z.object({
|
|
551
|
+
${lines.join("\n")}
|
|
552
|
+
})`);
|
|
553
|
+
}
|
|
554
|
+
return blocks.join("\n\n");
|
|
555
|
+
}
|
|
556
|
+
|
|
389
557
|
// src/index.ts
|
|
390
558
|
var import_hub = require("@noy-db/hub");
|
|
391
559
|
async function toBytesFromCollection(vault, collectionName) {
|
|
@@ -398,6 +566,10 @@ async function toBytes(vault, options) {
|
|
|
398
566
|
if (options.sheets.length === 0) {
|
|
399
567
|
throw new Error("as-xlsx: at least one sheet is required");
|
|
400
568
|
}
|
|
569
|
+
if (options.smart) {
|
|
570
|
+
const { sheets, definedNames } = await buildSmartSheets(vault, options);
|
|
571
|
+
return writeXlsx(sheets, { definedNames });
|
|
572
|
+
}
|
|
401
573
|
const materialisedSheets = [];
|
|
402
574
|
for (const sheetOpt of options.sheets) {
|
|
403
575
|
const collection = vault.collection(sheetOpt.collection);
|
|
@@ -441,6 +613,339 @@ async function write(vault, path, options) {
|
|
|
441
613
|
const { writeFile } = await import("fs/promises");
|
|
442
614
|
await writeFile(path, bytes);
|
|
443
615
|
}
|
|
616
|
+
async function toBytesMultiVault(entries, options = {}) {
|
|
617
|
+
const sep = options.sheetSeparator ?? "_";
|
|
618
|
+
for (const entry of entries) {
|
|
619
|
+
entry.vault.assertCanExport("plaintext", "xlsx");
|
|
620
|
+
}
|
|
621
|
+
const loaded = [];
|
|
622
|
+
const denormIndex = /* @__PURE__ */ new Map();
|
|
623
|
+
for (const entry of entries) {
|
|
624
|
+
const prefix = entry.label ?? entry.vault.name;
|
|
625
|
+
for (const s of entry.sheets) {
|
|
626
|
+
const all = await entry.vault.collection(s.collection).list();
|
|
627
|
+
const records = [];
|
|
628
|
+
for (const item of all) {
|
|
629
|
+
const r = item;
|
|
630
|
+
const allow = entry.closure?.get(s.collection);
|
|
631
|
+
if (allow && !allow.has(String(r.id))) continue;
|
|
632
|
+
if (s.filter && !s.filter(r)) continue;
|
|
633
|
+
records.push(r);
|
|
634
|
+
}
|
|
635
|
+
loaded.push({ entry, prefix, sheetOpt: s, records });
|
|
636
|
+
const indexKey = `${prefix}/${s.collection}`;
|
|
637
|
+
const collectionIndex = denormIndex.get(indexKey) ?? /* @__PURE__ */ new Map();
|
|
638
|
+
denormIndex.set(indexKey, collectionIndex);
|
|
639
|
+
for (const r of records) {
|
|
640
|
+
const idVal = r.id;
|
|
641
|
+
if (idVal != null) collectionIndex.set(safeStringify(idVal), r);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
for (const ls of loaded) {
|
|
646
|
+
for (const d of ls.sheetOpt.denormalize ?? []) {
|
|
647
|
+
if (d.from.keyField === "id") continue;
|
|
648
|
+
const indexKey = `${d.from.label}/${d.from.collection}`;
|
|
649
|
+
const idx = denormIndex.get(indexKey);
|
|
650
|
+
if (!idx) continue;
|
|
651
|
+
const srcLoaded = loaded.find(
|
|
652
|
+
(l) => l.prefix === d.from.label && l.sheetOpt.collection === d.from.collection
|
|
653
|
+
);
|
|
654
|
+
if (!srcLoaded) continue;
|
|
655
|
+
for (const r of srcLoaded.records) {
|
|
656
|
+
const kv = r[d.from.keyField];
|
|
657
|
+
if (kv != null) idx.set(safeStringify(kv), r);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const allSheets = [];
|
|
662
|
+
const manifestRows = [];
|
|
663
|
+
for (const { prefix, sheetOpt: s, records } of loaded) {
|
|
664
|
+
const baseColumns = s.columns ?? inferColumns(records);
|
|
665
|
+
const denormDefs = s.denormalize ?? [];
|
|
666
|
+
if (denormDefs.length === 0) {
|
|
667
|
+
const sheetName2 = `${prefix}${sep}${s.name}`;
|
|
668
|
+
allSheets.push(buildFlatSheet(sheetName2, baseColumns, records));
|
|
669
|
+
manifestRows.push([prefix, s.collection, records.length]);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
const allColumns = [...baseColumns, ...denormDefs.map((d) => d.column)];
|
|
673
|
+
const sheetName = `${prefix}${sep}${s.name}`;
|
|
674
|
+
const rows = records.map((r) => {
|
|
675
|
+
const baseCells = baseColumns.map((c) => r[c] ?? null);
|
|
676
|
+
const denormCells = denormDefs.map((d) => {
|
|
677
|
+
const idxKey = `${d.from.label}/${d.from.collection}`;
|
|
678
|
+
const idx = denormIndex.get(idxKey);
|
|
679
|
+
if (!idx) return "";
|
|
680
|
+
const fkVal = r[d.localField];
|
|
681
|
+
if (fkVal == null) return "";
|
|
682
|
+
const supporting = idx.get(safeStringify(fkVal));
|
|
683
|
+
if (!supporting) return "";
|
|
684
|
+
return supporting[d.from.pick] ?? "";
|
|
685
|
+
});
|
|
686
|
+
return [...baseCells, ...denormCells];
|
|
687
|
+
});
|
|
688
|
+
allSheets.push({ name: sheetName, header: allColumns, rows });
|
|
689
|
+
manifestRows.push([prefix, s.collection, records.length]);
|
|
690
|
+
}
|
|
691
|
+
allSheets.unshift({
|
|
692
|
+
name: "_manifest",
|
|
693
|
+
header: ["Vault", "Collection", "Records"],
|
|
694
|
+
rows: manifestRows
|
|
695
|
+
});
|
|
696
|
+
return writeXlsx(allSheets);
|
|
697
|
+
}
|
|
698
|
+
function buildFlatSheet(name, columns, records) {
|
|
699
|
+
return {
|
|
700
|
+
name,
|
|
701
|
+
header: [...columns],
|
|
702
|
+
rows: records.map((r) => columns.map((c) => r[c] ?? null))
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function safeStringify(v) {
|
|
706
|
+
if (typeof v === "string") return v;
|
|
707
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
|
|
708
|
+
if (v == null) return "";
|
|
709
|
+
try {
|
|
710
|
+
return JSON.stringify(v) ?? "";
|
|
711
|
+
} catch {
|
|
712
|
+
return "";
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function asCached(v) {
|
|
716
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "string") return v;
|
|
717
|
+
return safeStringify(v);
|
|
718
|
+
}
|
|
719
|
+
async function buildSmartSheets(vault, options) {
|
|
720
|
+
const snapshot = await vault.dumpSchema();
|
|
721
|
+
const sheetNameByCollection = /* @__PURE__ */ new Map();
|
|
722
|
+
for (const s of options.sheets) sheetNameByCollection.set(s.collection, s.name);
|
|
723
|
+
const mats = [];
|
|
724
|
+
const localeSet = /* @__PURE__ */ new Set();
|
|
725
|
+
for (const sheetOpt of options.sheets) {
|
|
726
|
+
const collection = vault.collection(sheetOpt.collection);
|
|
727
|
+
const list = await collection.list();
|
|
728
|
+
const records = [];
|
|
729
|
+
for (const item of list) {
|
|
730
|
+
const r = item;
|
|
731
|
+
if (sheetOpt.filter && !sheetOpt.filter(r)) continue;
|
|
732
|
+
records.push(r);
|
|
733
|
+
}
|
|
734
|
+
const base = sheetOpt.columns ?? inferColumns(records);
|
|
735
|
+
const cols = ["id", ...base.filter((c) => c !== "id")];
|
|
736
|
+
const labelCol = cols.find((c) => c !== "id");
|
|
737
|
+
const labelMap = /* @__PURE__ */ new Map();
|
|
738
|
+
if (labelCol) {
|
|
739
|
+
for (const r of records) if (r.id != null) labelMap.set(safeStringify(r.id), r[labelCol]);
|
|
740
|
+
}
|
|
741
|
+
const i18nFields = (sheetOpt.i18nFields ?? []).filter((f) => cols.includes(f));
|
|
742
|
+
for (const f of i18nFields) {
|
|
743
|
+
for (const r of records) {
|
|
744
|
+
const v = r[f];
|
|
745
|
+
if (v && typeof v === "object") for (const loc of Object.keys(v)) localeSet.add(loc);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
mats.push({ opt: sheetOpt, records, cols, labelMap, i18nFields });
|
|
749
|
+
}
|
|
750
|
+
const matByCollection = new Map(mats.map((m) => [m.opt.collection, m]));
|
|
751
|
+
const dictEntries = /* @__PURE__ */ new Map();
|
|
752
|
+
for (const m of mats) {
|
|
753
|
+
for (const [field, dictName] of Object.entries(m.opt.dictFields ?? {})) {
|
|
754
|
+
if (!m.cols.includes(field) || dictEntries.has(dictName)) continue;
|
|
755
|
+
dictEntries.set(dictName, await vault.dictionary(dictName).list());
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
for (const entries of dictEntries.values()) {
|
|
759
|
+
for (const e of entries) for (const loc of Object.keys(e.labels)) localeSet.add(loc);
|
|
760
|
+
}
|
|
761
|
+
const locales = [...localeSet].sort();
|
|
762
|
+
const hasLang = locales.length > 0;
|
|
763
|
+
const defaultLocale = locales.includes("en") ? "en" : locales[0] ?? "en";
|
|
764
|
+
const ifChain = (refOf) => {
|
|
765
|
+
if (locales.length === 0) return '""';
|
|
766
|
+
let expr = refOf(locales[0]);
|
|
767
|
+
for (let k = locales.length - 1; k >= 0; k--) {
|
|
768
|
+
expr = `IF(LANG="${locales[k]}",${refOf(locales[k])},${expr})`;
|
|
769
|
+
}
|
|
770
|
+
return expr;
|
|
771
|
+
};
|
|
772
|
+
const sheetMeta = /* @__PURE__ */ new Map();
|
|
773
|
+
const dataSheets = mats.map((m) => {
|
|
774
|
+
const refs = snapshot.collections[m.opt.collection]?.refs ?? {};
|
|
775
|
+
const fields = snapshot.collections[m.opt.collection]?.fields ?? {};
|
|
776
|
+
const i18nSet = new Set(m.i18nFields);
|
|
777
|
+
const refFields = Object.keys(refs).filter((f) => m.cols.includes(f) && matByCollection.has(refs[f].target));
|
|
778
|
+
const baseCols = m.cols.filter((c) => !i18nSet.has(c));
|
|
779
|
+
const i18nFlat = m.i18nFields.flatMap((f) => [f, ...locales.map((l) => `${f}__${l}`)]);
|
|
780
|
+
const dictPairs = Object.entries(m.opt.dictFields ?? {}).filter(([f]) => m.cols.includes(f));
|
|
781
|
+
const header = [
|
|
782
|
+
...baseCols,
|
|
783
|
+
...i18nFlat,
|
|
784
|
+
...dictPairs.map(([f]) => `${f}__label`),
|
|
785
|
+
...refFields.map((f) => `${f}__label`)
|
|
786
|
+
];
|
|
787
|
+
const colIndex = /* @__PURE__ */ new Map();
|
|
788
|
+
header.forEach((h, i) => colIndex.set(h, i + 1));
|
|
789
|
+
sheetMeta.set(m.opt.collection, { name: m.opt.name, colIndex });
|
|
790
|
+
const lastRow = Math.max(m.records.length + 1, 2);
|
|
791
|
+
const validations = [];
|
|
792
|
+
for (const field of baseCols) {
|
|
793
|
+
const colL = colLetter(colIndex.get(field));
|
|
794
|
+
const sqref = `${colL}2:${colL}${lastRow}`;
|
|
795
|
+
const explicit = m.opt.dropdowns?.[field];
|
|
796
|
+
if (explicit && explicit.length > 0) {
|
|
797
|
+
validations.push({ sqref, values: explicit });
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
const rf = refs[field];
|
|
801
|
+
if (rf && matByCollection.has(rf.target)) {
|
|
802
|
+
const targetSheet = sheetNameByCollection.get(rf.target);
|
|
803
|
+
const targetRows = Math.max(matByCollection.get(rf.target).records.length + 1, 2);
|
|
804
|
+
validations.push({ sqref, formula1: `'${targetSheet}'!$A$2:$A$${targetRows}` });
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const enumVals = fields[field]?.constraints?.["values"];
|
|
808
|
+
if (Array.isArray(enumVals) && enumVals.length > 0) validations.push({ sqref, values: enumVals.map((v) => safeStringify(v)) });
|
|
809
|
+
}
|
|
810
|
+
const rows = m.records.map((r, i) => {
|
|
811
|
+
const rowNum = i + 2;
|
|
812
|
+
const baseCells = baseCols.map((c) => {
|
|
813
|
+
const raw = c === "id" ? r.id ?? null : r[c] ?? null;
|
|
814
|
+
const fmt = m.opt.numberFormats?.[c];
|
|
815
|
+
if (fmt === void 0) return raw;
|
|
816
|
+
const num = typeof raw === "string" && raw.trim() !== "" && Number.isFinite(Number(raw)) ? Number(raw) : raw;
|
|
817
|
+
return styled(num, fmt);
|
|
818
|
+
});
|
|
819
|
+
const i18nCells = m.i18nFields.flatMap((f) => {
|
|
820
|
+
const rawMap = r[f] && typeof r[f] === "object" ? r[f] : {};
|
|
821
|
+
const display = formula(
|
|
822
|
+
ifChain((l) => `${colLetter(colIndex.get(`${f}__${l}`))}${rowNum}`),
|
|
823
|
+
asCached(rawMap[defaultLocale] ?? "")
|
|
824
|
+
);
|
|
825
|
+
return [display, ...locales.map((l) => rawMap[l] ?? null)];
|
|
826
|
+
});
|
|
827
|
+
const dictCells = dictPairs.map(([f, dn]) => {
|
|
828
|
+
const codeRef = `${colLetter(colIndex.get(f))}${rowNum}`;
|
|
829
|
+
const lk = `_Lookups_${dn}`;
|
|
830
|
+
const f1 = `IFERROR(VLOOKUP(${codeRef},'${lk}'!$A:$ZZ,MATCH(LANG,'${lk}'!$1:$1,0),FALSE),${codeRef})`;
|
|
831
|
+
const code = r[f];
|
|
832
|
+
const entry = code == null ? void 0 : dictEntries.get(dn)?.find((e) => e.key === safeStringify(code));
|
|
833
|
+
return formula(f1, asCached(entry?.labels[defaultLocale] ?? (code ?? "")));
|
|
834
|
+
});
|
|
835
|
+
const refCells = refFields.map((f) => {
|
|
836
|
+
const target = refs[f].target;
|
|
837
|
+
const targetSheet = sheetNameByCollection.get(target);
|
|
838
|
+
const targetMat = matByCollection.get(target);
|
|
839
|
+
const codeRef = `${colLetter(colIndex.get(f))}${rowNum}`;
|
|
840
|
+
const f1 = `IFERROR(VLOOKUP(${codeRef},'${targetSheet}'!$A:$ZZ,2,FALSE),"")`;
|
|
841
|
+
const code = r[f];
|
|
842
|
+
const cached = code == null ? "" : asCached(targetMat.labelMap.get(safeStringify(code)));
|
|
843
|
+
return formula(f1, cached);
|
|
844
|
+
});
|
|
845
|
+
return [...baseCells, ...i18nCells, ...dictCells, ...refCells];
|
|
846
|
+
});
|
|
847
|
+
return {
|
|
848
|
+
name: m.opt.name,
|
|
849
|
+
header,
|
|
850
|
+
rows,
|
|
851
|
+
...validations.length > 0 ? { validations } : {},
|
|
852
|
+
...m.opt.widths !== void 0 ? { widths: m.opt.widths } : {}
|
|
853
|
+
};
|
|
854
|
+
});
|
|
855
|
+
const manifest = {
|
|
856
|
+
name: "_manifest",
|
|
857
|
+
header: ["Collection", "Records", "Refs"],
|
|
858
|
+
rows: mats.map((m) => {
|
|
859
|
+
const refs = snapshot.collections[m.opt.collection]?.refs ?? {};
|
|
860
|
+
return [m.opt.name, m.records.length, Object.entries(refs).map(([f, r]) => `${f}\u2192${r.target}`).join(", ")];
|
|
861
|
+
})
|
|
862
|
+
};
|
|
863
|
+
const dialect = options.dialect ?? "excel";
|
|
864
|
+
const summarySheets = [];
|
|
865
|
+
for (const spec of options.summaries ?? []) {
|
|
866
|
+
const src = sheetMeta.get(spec.from);
|
|
867
|
+
const srcMat = matByCollection.get(spec.from);
|
|
868
|
+
const gCol = src?.colIndex.get(spec.groupBy);
|
|
869
|
+
if (!src || !srcMat || gCol === void 0) continue;
|
|
870
|
+
const gLetter = colLetter(gCol);
|
|
871
|
+
if (dialect === "sheets") {
|
|
872
|
+
const selects = [];
|
|
873
|
+
const labels = [];
|
|
874
|
+
for (const a of spec.aggregates) {
|
|
875
|
+
if (a.op === "count") {
|
|
876
|
+
selects.push(`COUNT(${gLetter})`);
|
|
877
|
+
labels.push(`COUNT(${gLetter}) '${a.label}'`);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
const vIdx = a.field ? src.colIndex.get(a.field) : void 0;
|
|
881
|
+
if (vIdx === void 0) continue;
|
|
882
|
+
const vL = colLetter(vIdx);
|
|
883
|
+
const fn = a.op === "sum" ? "SUM" : "AVG";
|
|
884
|
+
selects.push(`${fn}(${vL})`);
|
|
885
|
+
labels.push(`${fn}(${vL}) '${a.label}'`);
|
|
886
|
+
}
|
|
887
|
+
const q = `QUERY('${src.name}'!A:ZZ, "SELECT ${gLetter}, ${selects.join(", ")} GROUP BY ${gLetter} LABEL ${labels.join(", ")}", 1)`;
|
|
888
|
+
summarySheets.push({ name: spec.name, rows: [[formula(q)]] });
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
const seen = /* @__PURE__ */ new Set();
|
|
892
|
+
const groups = [];
|
|
893
|
+
for (const r of srcMat.records) {
|
|
894
|
+
const k = safeStringify(r[spec.groupBy]);
|
|
895
|
+
if (!seen.has(k)) {
|
|
896
|
+
seen.add(k);
|
|
897
|
+
groups.push(r[spec.groupBy]);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const header = [spec.groupBy, ...spec.aggregates.map((a) => a.label)];
|
|
901
|
+
const rows = groups.map((g, i) => {
|
|
902
|
+
const rowNum = i + 2;
|
|
903
|
+
const groupRecs = srcMat.records.filter((r) => safeStringify(r[spec.groupBy]) === safeStringify(g));
|
|
904
|
+
const cells = [g];
|
|
905
|
+
for (const a of spec.aggregates) {
|
|
906
|
+
if (a.op === "count") {
|
|
907
|
+
cells.push(formula(`COUNTIFS('${src.name}'!$${gLetter}:$${gLetter},$A${rowNum})`, groupRecs.length));
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const vIdx = a.field ? src.colIndex.get(a.field) : void 0;
|
|
911
|
+
if (vIdx === void 0) {
|
|
912
|
+
cells.push(null);
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const vLetter = colLetter(vIdx);
|
|
916
|
+
const nums = groupRecs.map((r) => Number(r[a.field])).filter((n) => Number.isFinite(n));
|
|
917
|
+
const sum = nums.reduce((s, n) => s + n, 0);
|
|
918
|
+
const cached = a.op === "sum" ? sum : nums.length ? sum / nums.length : 0;
|
|
919
|
+
const fn = a.op === "sum" ? "SUMIFS" : "AVERAGEIFS";
|
|
920
|
+
cells.push(
|
|
921
|
+
formula(`${fn}('${src.name}'!$${vLetter}:$${vLetter},'${src.name}'!$${gLetter}:$${gLetter},$A${rowNum})`, cached)
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
return cells;
|
|
925
|
+
});
|
|
926
|
+
summarySheets.push({ name: spec.name, header, rows });
|
|
927
|
+
}
|
|
928
|
+
const lookupSheets = [...dictEntries.entries()].map(([dn, entries]) => ({
|
|
929
|
+
name: `_Lookups_${dn}`,
|
|
930
|
+
header: ["Code", ...locales],
|
|
931
|
+
rows: entries.map((e) => [e.key, ...locales.map((l) => e.labels[l] ?? "")])
|
|
932
|
+
}));
|
|
933
|
+
const settingsSheets = [];
|
|
934
|
+
const definedNames = [];
|
|
935
|
+
if (hasLang) {
|
|
936
|
+
settingsSheets.push({
|
|
937
|
+
name: "_settings",
|
|
938
|
+
header: ["Setting", "Value"],
|
|
939
|
+
rows: [["Language", defaultLocale]],
|
|
940
|
+
validations: [{ sqref: "B2:B2", values: locales }]
|
|
941
|
+
});
|
|
942
|
+
definedNames.push({ name: "LANG", ref: `'_settings'!$B$2` });
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
sheets: [...settingsSheets, ...lookupSheets, manifest, ...summarySheets, ...dataSheets],
|
|
946
|
+
definedNames
|
|
947
|
+
};
|
|
948
|
+
}
|
|
444
949
|
function inferColumns(records) {
|
|
445
950
|
const seen = /* @__PURE__ */ new Set();
|
|
446
951
|
const out = [];
|
|
@@ -512,6 +1017,13 @@ async function fromBytes(vault, bytes, options) {
|
|
|
512
1017
|
} catch {
|
|
513
1018
|
}
|
|
514
1019
|
}
|
|
1020
|
+
const i18nBases = /* @__PURE__ */ new Set();
|
|
1021
|
+
if (options.smart) {
|
|
1022
|
+
for (const field of colToField.values()) {
|
|
1023
|
+
const m = /^(.+)__(.+)$/.exec(field);
|
|
1024
|
+
if (m && m[2] !== "label") i18nBases.add(m[1]);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
515
1027
|
const records = [];
|
|
516
1028
|
for (let i = headerRowIdx + 1; i < allRows.length; i++) {
|
|
517
1029
|
const row = allRows[i];
|
|
@@ -520,6 +1032,22 @@ async function fromBytes(vault, bytes, options) {
|
|
|
520
1032
|
for (const [col, value] of Object.entries(row)) {
|
|
521
1033
|
const field = colToField.get(col);
|
|
522
1034
|
if (field === void 0) continue;
|
|
1035
|
+
if (options.smart) {
|
|
1036
|
+
if (field.endsWith("__label")) continue;
|
|
1037
|
+
const lm = /^(.+)__(.+)$/.exec(field);
|
|
1038
|
+
if (lm && lm[2] !== "label" && i18nBases.has(lm[1])) {
|
|
1039
|
+
const base = lm[1];
|
|
1040
|
+
const coerced2 = coerceXlsxCell(value, types[base]);
|
|
1041
|
+
if (coerced2 !== void 0 && coerced2 !== "") {
|
|
1042
|
+
const map = record[base] ?? {};
|
|
1043
|
+
map[lm[2]] = coerced2;
|
|
1044
|
+
record[base] = map;
|
|
1045
|
+
hasAny = true;
|
|
1046
|
+
}
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (i18nBases.has(field)) continue;
|
|
1050
|
+
}
|
|
523
1051
|
const coerced = coerceXlsxCell(value, types[field]);
|
|
524
1052
|
if (coerced !== void 0) {
|
|
525
1053
|
const invMap = invertMaps.get(field);
|
|
@@ -627,11 +1155,16 @@ function excelSerialToMs(serial) {
|
|
|
627
1155
|
XlsxDictAmbiguityError,
|
|
628
1156
|
colLetter,
|
|
629
1157
|
download,
|
|
1158
|
+
formula,
|
|
630
1159
|
fromBytes,
|
|
1160
|
+
inferSchema,
|
|
631
1161
|
readXlsx,
|
|
1162
|
+
styled,
|
|
632
1163
|
toBytes,
|
|
633
1164
|
toBytesFromCollection,
|
|
1165
|
+
toBytesMultiVault,
|
|
634
1166
|
write,
|
|
635
|
-
writeXlsx
|
|
1167
|
+
writeXlsx,
|
|
1168
|
+
zodSourceFor
|
|
636
1169
|
});
|
|
637
1170
|
//# sourceMappingURL=index.cjs.map
|