@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 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