@noy-db/as-xlsx 0.2.0-pre.20 → 0.2.0-pre.23

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