@noy-db/as-xlsx 0.2.0-pre.21 → 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.d.ts CHANGED
@@ -154,6 +154,30 @@ interface ReadXlsxResult {
154
154
  */
155
155
  declare function readXlsx(bytes: Uint8Array): Promise<ReadXlsxResult>;
156
156
 
157
+ type InferredType = 'string' | 'number' | 'boolean' | 'date';
158
+ interface InferredField {
159
+ readonly type: InferredType;
160
+ /** Target collection (sheet) name when this field looks like a foreign key. */
161
+ readonly references?: string;
162
+ }
163
+ interface InferredCollection {
164
+ readonly idField: string;
165
+ readonly fields: Record<string, InferredField>;
166
+ }
167
+ interface InferredSchema {
168
+ readonly collections: Record<string, InferredCollection>;
169
+ }
170
+ /**
171
+ * Infer a {@link InferredSchema} from an `.xlsx` byte stream. Sheets whose name
172
+ * starts with `_` (our smart-export meta sheets) are skipped; pass `skipSheets`
173
+ * to exclude more (e.g. summary sheets).
174
+ */
175
+ declare function inferSchema(bytes: Uint8Array, options?: {
176
+ skipSheets?: readonly string[];
177
+ }): Promise<InferredSchema>;
178
+ /** Emit a Zod schema snippet (guidance) from an {@link InferredSchema}. */
179
+ declare function zodSourceFor(schema: InferredSchema): string;
180
+
157
181
  /**
158
182
  * **@noy-db/as-xlsx** — Excel spreadsheet plaintext export for noy-db.
159
183
  *
@@ -274,6 +298,13 @@ interface AsXlsxOptions {
274
298
  readonly sheets: readonly AsXlsxSheetOptions[];
275
299
  /** Smart mode only: groupBy summary sheets (live SUMIFS/COUNTIFS/AVERAGEIFS). */
276
300
  readonly summaries?: readonly AsXlsxSummarySpec[];
301
+ /**
302
+ * Smart mode only: summary formula dialect. `'excel'` (default) emits
303
+ * cross-compatible per-row SUMIFS/COUNTIFS/AVERAGEIFS. `'sheets'` emits a
304
+ * single Google-Sheets `QUERY` formula per summary — **Sheets-only** (QUERY
305
+ * errors in Excel); use when the target is Google Sheets.
306
+ */
307
+ readonly dialect?: 'excel' | 'sheets';
277
308
  /**
278
309
  * Smart-workbook mode (#414). Emits a relational workbook instead of a flat
279
310
  * dump:
@@ -372,6 +403,14 @@ interface AsXlsxImportOptions {
372
403
  * Unknown labels (no match in any locale) pass through as-is.
373
404
  */
374
405
  readonly dicts?: Readonly<Record<string, readonly DictEntry[]>>;
406
+ /**
407
+ * Read a sheet produced by smart export (#414 P4). Reverses the smart layout:
408
+ * reconstructs i18n fields from their per-locale columns (`<f>__<loc>` →
409
+ * `{ loc: value }`), and drops derived columns — the i18n display column and
410
+ * every `<f>__label` (FK/dict) formula column. Code columns (the real values)
411
+ * pass through. Maps onto the existing collection schema (Mode A).
412
+ */
413
+ readonly smart?: boolean;
375
414
  }
376
415
  interface AsXlsxImportPlan {
377
416
  readonly plan: VaultDiff;
@@ -402,4 +441,4 @@ interface AsXlsxImportPlan {
402
441
  */
403
442
  declare function fromBytes(vault: Vault, bytes: Uint8Array, options: AsXlsxImportOptions): Promise<AsXlsxImportPlan>;
404
443
 
405
- export { type AsXlsxDownloadOptions, type AsXlsxImportOptions, type AsXlsxImportPlan, type AsXlsxOptions, type AsXlsxSheetOptions, type AsXlsxSummaryAggregate, type AsXlsxSummarySpec, type AsXlsxWriteOptions, type ImportPolicy, type ReadXlsxResult, type ReadXlsxRow, type ReadXlsxSheet, XlsxDictAmbiguityError, type XlsxFormulaCell, type XlsxRow, type XlsxSheet, type XlsxStyledCell, type XlsxValidation, colLetter, download, formula, fromBytes, readXlsx, styled, toBytes, toBytesFromCollection, write, writeXlsx };
444
+ export { type AsXlsxDownloadOptions, type AsXlsxImportOptions, type AsXlsxImportPlan, type AsXlsxOptions, type AsXlsxSheetOptions, type AsXlsxSummaryAggregate, type AsXlsxSummarySpec, type AsXlsxWriteOptions, type ImportPolicy, type InferredCollection, type InferredField, type InferredSchema, type InferredType, type ReadXlsxResult, type ReadXlsxRow, type ReadXlsxSheet, XlsxDictAmbiguityError, type XlsxFormulaCell, type XlsxRow, type XlsxSheet, type XlsxStyledCell, type XlsxValidation, colLetter, download, formula, fromBytes, inferSchema, readXlsx, styled, toBytes, toBytesFromCollection, write, writeXlsx, zodSourceFor };
package/dist/index.js CHANGED
@@ -419,6 +419,92 @@ function stringAttr(raw) {
419
419
  return "";
420
420
  }
421
421
 
422
+ // src/infer.ts
423
+ var ISO_DATE = /^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2})?/;
424
+ function inferType(vals) {
425
+ if (vals.length === 0) return "string";
426
+ if (vals.every((v) => typeof v === "boolean")) return "boolean";
427
+ if (vals.every((v) => typeof v === "number")) return "number";
428
+ if (vals.every((v) => typeof v === "string" && ISO_DATE.test(v))) return "date";
429
+ return "string";
430
+ }
431
+ async function inferSchema(bytes, options = {}) {
432
+ const skip = new Set(options.skipSheets ?? []);
433
+ const decoded = await readXlsx(bytes);
434
+ const sheets = [];
435
+ for (const sh of decoded.sheets) {
436
+ if (sh.name.startsWith("_") || skip.has(sh.name)) continue;
437
+ const headerRow = sh.rows[0] ?? {};
438
+ const colToField = /* @__PURE__ */ new Map();
439
+ const fields = [];
440
+ for (const [letter, val] of Object.entries(headerRow)) {
441
+ const name = typeof val === "string" ? val.trim() : typeof val === "number" || typeof val === "boolean" ? String(val) : "";
442
+ if (!name) continue;
443
+ colToField.set(letter, name);
444
+ fields.push(name);
445
+ }
446
+ const records = sh.rows.slice(1).map((row) => {
447
+ const rec = {};
448
+ for (const [letter, val] of Object.entries(row)) {
449
+ const f = colToField.get(letter);
450
+ if (f !== void 0) rec[f] = val;
451
+ }
452
+ return rec;
453
+ });
454
+ sheets.push({ name: sh.name, fields, records });
455
+ }
456
+ const valuesOf = (s, f) => s.records.map((r) => r[f]).filter((v) => v != null && v !== "");
457
+ const idFieldOf = (s) => {
458
+ if (s.fields.includes("id")) return "id";
459
+ for (const f of s.fields) {
460
+ const vals = valuesOf(s, f);
461
+ if (vals.length > 0 && new Set(vals.map((v) => String(v))).size === vals.length) return f;
462
+ }
463
+ return s.fields[0] ?? "id";
464
+ };
465
+ const meta = sheets.map((s) => ({ s, idField: idFieldOf(s) }));
466
+ const idValuesByName = /* @__PURE__ */ new Map();
467
+ for (const { s, idField } of meta) {
468
+ idValuesByName.set(s.name, new Set(valuesOf(s, idField).map((v) => String(v))));
469
+ }
470
+ const collections = {};
471
+ for (const { s, idField } of meta) {
472
+ const fields = {};
473
+ for (const f of s.fields) {
474
+ const vals = valuesOf(s, f);
475
+ const type = inferType(vals);
476
+ let references;
477
+ if (f !== idField && type === "string" && vals.length > 0) {
478
+ const distinct = new Set(vals.map((v) => String(v)));
479
+ for (const { s: other } of meta) {
480
+ if (other.name === s.name) continue;
481
+ const ids = idValuesByName.get(other.name);
482
+ if (ids && ids.size > 0 && [...distinct].every((v) => ids.has(v))) {
483
+ references = other.name;
484
+ break;
485
+ }
486
+ }
487
+ }
488
+ fields[f] = references ? { type, references } : { type };
489
+ }
490
+ collections[s.name] = { idField, fields };
491
+ }
492
+ return { collections };
493
+ }
494
+ function zodSourceFor(schema) {
495
+ const zType = (t) => t === "number" ? "z.number()" : t === "boolean" ? "z.boolean()" : t === "date" ? "z.string().datetime()" : "z.string()";
496
+ const blocks = [`import { z } from 'zod'`];
497
+ for (const [name, c] of Object.entries(schema.collections)) {
498
+ const lines = Object.entries(c.fields).map(
499
+ ([f, d]) => ` ${f}: ${zType(d.type)},${d.references ? ` // \u2192 ${d.references}` : ""}`
500
+ );
501
+ blocks.push(`export const ${name}Schema = z.object({
502
+ ${lines.join("\n")}
503
+ })`);
504
+ }
505
+ return blocks.join("\n\n");
506
+ }
507
+
422
508
  // src/index.ts
423
509
  import { diffVault } from "@noy-db/hub";
424
510
  async function toBytesFromCollection(vault, collectionName) {
@@ -636,6 +722,7 @@ async function buildSmartSheets(vault, options) {
636
722
  return [m.opt.name, m.records.length, Object.entries(refs).map(([f, r]) => `${f}\u2192${r.target}`).join(", ")];
637
723
  })
638
724
  };
725
+ const dialect = options.dialect ?? "excel";
639
726
  const summarySheets = [];
640
727
  for (const spec of options.summaries ?? []) {
641
728
  const src = sheetMeta.get(spec.from);
@@ -643,6 +730,26 @@ async function buildSmartSheets(vault, options) {
643
730
  const gCol = src?.colIndex.get(spec.groupBy);
644
731
  if (!src || !srcMat || gCol === void 0) continue;
645
732
  const gLetter = colLetter(gCol);
733
+ if (dialect === "sheets") {
734
+ const selects = [];
735
+ const labels = [];
736
+ for (const a of spec.aggregates) {
737
+ if (a.op === "count") {
738
+ selects.push(`COUNT(${gLetter})`);
739
+ labels.push(`COUNT(${gLetter}) '${a.label}'`);
740
+ continue;
741
+ }
742
+ const vIdx = a.field ? src.colIndex.get(a.field) : void 0;
743
+ if (vIdx === void 0) continue;
744
+ const vL = colLetter(vIdx);
745
+ const fn = a.op === "sum" ? "SUM" : "AVG";
746
+ selects.push(`${fn}(${vL})`);
747
+ labels.push(`${fn}(${vL}) '${a.label}'`);
748
+ }
749
+ const q = `QUERY('${src.name}'!A:ZZ, "SELECT ${gLetter}, ${selects.join(", ")} GROUP BY ${gLetter} LABEL ${labels.join(", ")}", 1)`;
750
+ summarySheets.push({ name: spec.name, rows: [[formula(q)]] });
751
+ continue;
752
+ }
646
753
  const seen = /* @__PURE__ */ new Set();
647
754
  const groups = [];
648
755
  for (const r of srcMat.records) {
@@ -772,6 +879,13 @@ async function fromBytes(vault, bytes, options) {
772
879
  } catch {
773
880
  }
774
881
  }
882
+ const i18nBases = /* @__PURE__ */ new Set();
883
+ if (options.smart) {
884
+ for (const field of colToField.values()) {
885
+ const m = /^(.+)__(.+)$/.exec(field);
886
+ if (m && m[2] !== "label") i18nBases.add(m[1]);
887
+ }
888
+ }
775
889
  const records = [];
776
890
  for (let i = headerRowIdx + 1; i < allRows.length; i++) {
777
891
  const row = allRows[i];
@@ -780,6 +894,22 @@ async function fromBytes(vault, bytes, options) {
780
894
  for (const [col, value] of Object.entries(row)) {
781
895
  const field = colToField.get(col);
782
896
  if (field === void 0) continue;
897
+ if (options.smart) {
898
+ if (field.endsWith("__label")) continue;
899
+ const lm = /^(.+)__(.+)$/.exec(field);
900
+ if (lm && lm[2] !== "label" && i18nBases.has(lm[1])) {
901
+ const base = lm[1];
902
+ const coerced2 = coerceXlsxCell(value, types[base]);
903
+ if (coerced2 !== void 0 && coerced2 !== "") {
904
+ const map = record[base] ?? {};
905
+ map[lm[2]] = coerced2;
906
+ record[base] = map;
907
+ hasAny = true;
908
+ }
909
+ continue;
910
+ }
911
+ if (i18nBases.has(field)) continue;
912
+ }
783
913
  const coerced = coerceXlsxCell(value, types[field]);
784
914
  if (coerced !== void 0) {
785
915
  const invMap = invertMaps.get(field);
@@ -888,11 +1018,13 @@ export {
888
1018
  download,
889
1019
  formula,
890
1020
  fromBytes,
1021
+ inferSchema,
891
1022
  readXlsx,
892
1023
  styled,
893
1024
  toBytes,
894
1025
  toBytesFromCollection,
895
1026
  write,
896
- writeXlsx
1027
+ writeXlsx,
1028
+ zodSourceFor
897
1029
  };
898
1030
  //# sourceMappingURL=index.js.map