@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.cjs +136 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +133 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
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
|