@noy-db/as-xlsx 0.2.0-pre.21 → 0.2.0-pre.24
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 +227 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +134 -1
- package/dist/index.d.ts +134 -1
- package/dist/index.js +223 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -35,12 +35,15 @@ __export(index_exports, {
|
|
|
35
35
|
download: () => download,
|
|
36
36
|
formula: () => formula,
|
|
37
37
|
fromBytes: () => fromBytes,
|
|
38
|
+
inferSchema: () => inferSchema,
|
|
38
39
|
readXlsx: () => readXlsx,
|
|
39
40
|
styled: () => styled,
|
|
40
41
|
toBytes: () => toBytes,
|
|
41
42
|
toBytesFromCollection: () => toBytesFromCollection,
|
|
43
|
+
toBytesMultiVault: () => toBytesMultiVault,
|
|
42
44
|
write: () => write,
|
|
43
|
-
writeXlsx: () => writeXlsx
|
|
45
|
+
writeXlsx: () => writeXlsx,
|
|
46
|
+
zodSourceFor: () => zodSourceFor
|
|
44
47
|
});
|
|
45
48
|
module.exports = __toCommonJS(index_exports);
|
|
46
49
|
|
|
@@ -465,6 +468,92 @@ function stringAttr(raw) {
|
|
|
465
468
|
return "";
|
|
466
469
|
}
|
|
467
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
|
+
|
|
468
557
|
// src/index.ts
|
|
469
558
|
var import_hub = require("@noy-db/hub");
|
|
470
559
|
async function toBytesFromCollection(vault, collectionName) {
|
|
@@ -524,6 +613,95 @@ async function write(vault, path, options) {
|
|
|
524
613
|
const { writeFile } = await import("fs/promises");
|
|
525
614
|
await writeFile(path, bytes);
|
|
526
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
|
+
}
|
|
527
705
|
function safeStringify(v) {
|
|
528
706
|
if (typeof v === "string") return v;
|
|
529
707
|
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
|
|
@@ -682,6 +860,7 @@ async function buildSmartSheets(vault, options) {
|
|
|
682
860
|
return [m.opt.name, m.records.length, Object.entries(refs).map(([f, r]) => `${f}\u2192${r.target}`).join(", ")];
|
|
683
861
|
})
|
|
684
862
|
};
|
|
863
|
+
const dialect = options.dialect ?? "excel";
|
|
685
864
|
const summarySheets = [];
|
|
686
865
|
for (const spec of options.summaries ?? []) {
|
|
687
866
|
const src = sheetMeta.get(spec.from);
|
|
@@ -689,6 +868,26 @@ async function buildSmartSheets(vault, options) {
|
|
|
689
868
|
const gCol = src?.colIndex.get(spec.groupBy);
|
|
690
869
|
if (!src || !srcMat || gCol === void 0) continue;
|
|
691
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
|
+
}
|
|
692
891
|
const seen = /* @__PURE__ */ new Set();
|
|
693
892
|
const groups = [];
|
|
694
893
|
for (const r of srcMat.records) {
|
|
@@ -818,6 +1017,13 @@ async function fromBytes(vault, bytes, options) {
|
|
|
818
1017
|
} catch {
|
|
819
1018
|
}
|
|
820
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
|
+
}
|
|
821
1027
|
const records = [];
|
|
822
1028
|
for (let i = headerRowIdx + 1; i < allRows.length; i++) {
|
|
823
1029
|
const row = allRows[i];
|
|
@@ -826,6 +1032,22 @@ async function fromBytes(vault, bytes, options) {
|
|
|
826
1032
|
for (const [col, value] of Object.entries(row)) {
|
|
827
1033
|
const field = colToField.get(col);
|
|
828
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
|
+
}
|
|
829
1051
|
const coerced = coerceXlsxCell(value, types[field]);
|
|
830
1052
|
if (coerced !== void 0) {
|
|
831
1053
|
const invMap = invertMaps.get(field);
|
|
@@ -935,11 +1157,14 @@ function excelSerialToMs(serial) {
|
|
|
935
1157
|
download,
|
|
936
1158
|
formula,
|
|
937
1159
|
fromBytes,
|
|
1160
|
+
inferSchema,
|
|
938
1161
|
readXlsx,
|
|
939
1162
|
styled,
|
|
940
1163
|
toBytes,
|
|
941
1164
|
toBytesFromCollection,
|
|
1165
|
+
toBytesMultiVault,
|
|
942
1166
|
write,
|
|
943
|
-
writeXlsx
|
|
1167
|
+
writeXlsx,
|
|
1168
|
+
zodSourceFor
|
|
944
1169
|
});
|
|
945
1170
|
//# sourceMappingURL=index.cjs.map
|