@noy-db/as-xlsx 0.2.0-pre.23 → 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.d.cts CHANGED
@@ -268,6 +268,45 @@ interface AsXlsxSheetOptions {
268
268
  * global LANG cell via `VLOOKUP(code, …, MATCH(LANG, …))`.
269
269
  */
270
270
  readonly dictFields?: Record<string, string>;
271
+ /**
272
+ * Multi-vault export only: pull FK-referenced fields from a supporting vault
273
+ * into this sheet as extra columns. Each entry resolves `localField` on the
274
+ * primary record against the `keyField` of another vault's collection (matched
275
+ * by `from.label`/`from.collection`), and emits the `pick` field value as
276
+ * `column`. Appended AFTER the declared `columns` in declaration order.
277
+ * Unresolved FK (row not in the supporting-vault index) → empty cell.
278
+ * Ignored by single-vault `toBytes`.
279
+ */
280
+ readonly denormalize?: readonly MultiVaultDenormColumn[];
281
+ }
282
+ /**
283
+ * One denormalized column in a multi-vault export: pulls a field from a
284
+ * supporting vault into the primary sheet via an in-memory join.
285
+ *
286
+ * @example
287
+ * ```ts
288
+ * {
289
+ * column: 'entityName', // new column header in the primary sheet
290
+ * localField: 'entityId', // FK field on the primary record
291
+ * from: { label: 'directory', collection: 'entities', keyField: 'id', pick: 'name' },
292
+ * }
293
+ * ```
294
+ */
295
+ interface MultiVaultDenormColumn {
296
+ /** Header of the new column to append to the primary sheet. */
297
+ readonly column: string;
298
+ /** Field on the primary record whose value is the FK. */
299
+ readonly localField: string;
300
+ readonly from: {
301
+ /** Label of the supporting vault entry (matches `MultiVaultXlsxEntry.label`). */
302
+ readonly label: string;
303
+ /** Collection in the supporting vault. */
304
+ readonly collection: string;
305
+ /** Field in the supporting record that is the join key (usually `'id'`). */
306
+ readonly keyField: string;
307
+ /** Field in the supporting record to copy as the denorm value. */
308
+ readonly pick: string;
309
+ };
271
310
  }
272
311
  /** One aggregate column in a smart-mode summary sheet (#414 P3). */
273
312
  interface AsXlsxSummaryAggregate {
@@ -351,6 +390,61 @@ declare function download(vault: Vault, options: AsXlsxDownloadOptions): Promise
351
390
  * plaintext xlsx persists past the process (Tier 3 egress).
352
391
  */
353
392
  declare function write(vault: Vault, path: string, options: AsXlsxWriteOptions): Promise<void>;
393
+ /**
394
+ * One vault entry in a multi-vault export. Supplies the pre-opened
395
+ * vault, the sheet specs to render for it, and an optional closure
396
+ * (per-collection id allowlist computed externally by the orchestrator).
397
+ */
398
+ interface MultiVaultXlsxEntry {
399
+ readonly vault: Vault;
400
+ readonly sheets: readonly AsXlsxSheetOptions[];
401
+ /**
402
+ * Optional per-collection id allowlist (e.g. from walkCrossVaultClosure).
403
+ * When set, only rows whose `id` appears in the set are exported for that
404
+ * collection. Omit to export all rows.
405
+ */
406
+ readonly closure?: ReadonlyMap<string, ReadonlySet<string>>;
407
+ /**
408
+ * Optional display label for sheet-name prefixing.
409
+ * Defaults to `vault.name`.
410
+ */
411
+ readonly label?: string;
412
+ }
413
+ /** Options for {@link toBytesMultiVault}. */
414
+ interface MultiVaultXlsxOptions {
415
+ /**
416
+ * Separator inserted between the vault label and the sheet name when
417
+ * building tab names. Default `'_'`. Names are then truncated to 31 chars
418
+ * (Excel limit) by `writeXlsx`.
419
+ */
420
+ readonly sheetSeparator?: string;
421
+ }
422
+ /**
423
+ * Build an `.xlsx` byte stream spanning **multiple vaults**. Each entry
424
+ * supplies a pre-opened vault with its sheet specs and an optional
425
+ * per-collection id-closure (rows filtered to exactly those ids). A
426
+ * `_manifest` sheet is prepended that lists every vault-collection pair
427
+ * and its exported record count.
428
+ *
429
+ * ## Auth
430
+ * Every vault in `entries` must independently hold `assertCanExport('plaintext','xlsx')`.
431
+ * The check fires fail-fast before any rows are materialised.
432
+ *
433
+ * ## Architecture
434
+ * This function is **edge-pure** — it takes pre-opened vaults and a
435
+ * pre-computed closure; it performs no cross-vault FK walk itself.
436
+ * Cross-vault orchestration lives in `@klum-db/lobby` (allowed direction).
437
+ *
438
+ * ## Two-pass execution (when `denormalize` is declared)
439
+ * **Pass 1** — load and closure-filter ALL entries' rows, building a
440
+ * `Map<"${label}/${collection}", Map<keyValue, row>>` index keyed by each
441
+ * sheet's `keyField` (default `'id'`). Only closure-filtered rows are indexed,
442
+ * so an FK pointing outside the closure yields an empty cell (correct: that
443
+ * row was not referenced / not exported).
444
+ * **Pass 2** — emit each sheet; for sheets with `denormalize`, append each
445
+ * denorm column AFTER the declared columns by resolving the index lookup.
446
+ */
447
+ declare function toBytesMultiVault(entries: readonly MultiVaultXlsxEntry[], options?: MultiVaultXlsxOptions): Promise<Uint8Array>;
354
448
 
355
449
  type ImportPolicy = 'merge' | 'replace' | 'insert-only';
356
450
  /**
@@ -441,4 +535,4 @@ interface AsXlsxImportPlan {
441
535
  */
442
536
  declare function fromBytes(vault: Vault, bytes: Uint8Array, options: AsXlsxImportOptions): Promise<AsXlsxImportPlan>;
443
537
 
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 };
538
+ 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 MultiVaultDenormColumn, type MultiVaultXlsxEntry, type MultiVaultXlsxOptions, 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, toBytesMultiVault, write, writeXlsx, zodSourceFor };
package/dist/index.d.ts CHANGED
@@ -268,6 +268,45 @@ interface AsXlsxSheetOptions {
268
268
  * global LANG cell via `VLOOKUP(code, …, MATCH(LANG, …))`.
269
269
  */
270
270
  readonly dictFields?: Record<string, string>;
271
+ /**
272
+ * Multi-vault export only: pull FK-referenced fields from a supporting vault
273
+ * into this sheet as extra columns. Each entry resolves `localField` on the
274
+ * primary record against the `keyField` of another vault's collection (matched
275
+ * by `from.label`/`from.collection`), and emits the `pick` field value as
276
+ * `column`. Appended AFTER the declared `columns` in declaration order.
277
+ * Unresolved FK (row not in the supporting-vault index) → empty cell.
278
+ * Ignored by single-vault `toBytes`.
279
+ */
280
+ readonly denormalize?: readonly MultiVaultDenormColumn[];
281
+ }
282
+ /**
283
+ * One denormalized column in a multi-vault export: pulls a field from a
284
+ * supporting vault into the primary sheet via an in-memory join.
285
+ *
286
+ * @example
287
+ * ```ts
288
+ * {
289
+ * column: 'entityName', // new column header in the primary sheet
290
+ * localField: 'entityId', // FK field on the primary record
291
+ * from: { label: 'directory', collection: 'entities', keyField: 'id', pick: 'name' },
292
+ * }
293
+ * ```
294
+ */
295
+ interface MultiVaultDenormColumn {
296
+ /** Header of the new column to append to the primary sheet. */
297
+ readonly column: string;
298
+ /** Field on the primary record whose value is the FK. */
299
+ readonly localField: string;
300
+ readonly from: {
301
+ /** Label of the supporting vault entry (matches `MultiVaultXlsxEntry.label`). */
302
+ readonly label: string;
303
+ /** Collection in the supporting vault. */
304
+ readonly collection: string;
305
+ /** Field in the supporting record that is the join key (usually `'id'`). */
306
+ readonly keyField: string;
307
+ /** Field in the supporting record to copy as the denorm value. */
308
+ readonly pick: string;
309
+ };
271
310
  }
272
311
  /** One aggregate column in a smart-mode summary sheet (#414 P3). */
273
312
  interface AsXlsxSummaryAggregate {
@@ -351,6 +390,61 @@ declare function download(vault: Vault, options: AsXlsxDownloadOptions): Promise
351
390
  * plaintext xlsx persists past the process (Tier 3 egress).
352
391
  */
353
392
  declare function write(vault: Vault, path: string, options: AsXlsxWriteOptions): Promise<void>;
393
+ /**
394
+ * One vault entry in a multi-vault export. Supplies the pre-opened
395
+ * vault, the sheet specs to render for it, and an optional closure
396
+ * (per-collection id allowlist computed externally by the orchestrator).
397
+ */
398
+ interface MultiVaultXlsxEntry {
399
+ readonly vault: Vault;
400
+ readonly sheets: readonly AsXlsxSheetOptions[];
401
+ /**
402
+ * Optional per-collection id allowlist (e.g. from walkCrossVaultClosure).
403
+ * When set, only rows whose `id` appears in the set are exported for that
404
+ * collection. Omit to export all rows.
405
+ */
406
+ readonly closure?: ReadonlyMap<string, ReadonlySet<string>>;
407
+ /**
408
+ * Optional display label for sheet-name prefixing.
409
+ * Defaults to `vault.name`.
410
+ */
411
+ readonly label?: string;
412
+ }
413
+ /** Options for {@link toBytesMultiVault}. */
414
+ interface MultiVaultXlsxOptions {
415
+ /**
416
+ * Separator inserted between the vault label and the sheet name when
417
+ * building tab names. Default `'_'`. Names are then truncated to 31 chars
418
+ * (Excel limit) by `writeXlsx`.
419
+ */
420
+ readonly sheetSeparator?: string;
421
+ }
422
+ /**
423
+ * Build an `.xlsx` byte stream spanning **multiple vaults**. Each entry
424
+ * supplies a pre-opened vault with its sheet specs and an optional
425
+ * per-collection id-closure (rows filtered to exactly those ids). A
426
+ * `_manifest` sheet is prepended that lists every vault-collection pair
427
+ * and its exported record count.
428
+ *
429
+ * ## Auth
430
+ * Every vault in `entries` must independently hold `assertCanExport('plaintext','xlsx')`.
431
+ * The check fires fail-fast before any rows are materialised.
432
+ *
433
+ * ## Architecture
434
+ * This function is **edge-pure** — it takes pre-opened vaults and a
435
+ * pre-computed closure; it performs no cross-vault FK walk itself.
436
+ * Cross-vault orchestration lives in `@klum-db/lobby` (allowed direction).
437
+ *
438
+ * ## Two-pass execution (when `denormalize` is declared)
439
+ * **Pass 1** — load and closure-filter ALL entries' rows, building a
440
+ * `Map<"${label}/${collection}", Map<keyValue, row>>` index keyed by each
441
+ * sheet's `keyField` (default `'id'`). Only closure-filtered rows are indexed,
442
+ * so an FK pointing outside the closure yields an empty cell (correct: that
443
+ * row was not referenced / not exported).
444
+ * **Pass 2** — emit each sheet; for sheets with `denormalize`, append each
445
+ * denorm column AFTER the declared columns by resolving the index lookup.
446
+ */
447
+ declare function toBytesMultiVault(entries: readonly MultiVaultXlsxEntry[], options?: MultiVaultXlsxOptions): Promise<Uint8Array>;
354
448
 
355
449
  type ImportPolicy = 'merge' | 'replace' | 'insert-only';
356
450
  /**
@@ -441,4 +535,4 @@ interface AsXlsxImportPlan {
441
535
  */
442
536
  declare function fromBytes(vault: Vault, bytes: Uint8Array, options: AsXlsxImportOptions): Promise<AsXlsxImportPlan>;
443
537
 
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 };
538
+ 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 MultiVaultDenormColumn, type MultiVaultXlsxEntry, type MultiVaultXlsxOptions, 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, toBytesMultiVault, write, writeXlsx, zodSourceFor };
package/dist/index.js CHANGED
@@ -564,6 +564,95 @@ async function write(vault, path, options) {
564
564
  const { writeFile } = await import("fs/promises");
565
565
  await writeFile(path, bytes);
566
566
  }
567
+ async function toBytesMultiVault(entries, options = {}) {
568
+ const sep = options.sheetSeparator ?? "_";
569
+ for (const entry of entries) {
570
+ entry.vault.assertCanExport("plaintext", "xlsx");
571
+ }
572
+ const loaded = [];
573
+ const denormIndex = /* @__PURE__ */ new Map();
574
+ for (const entry of entries) {
575
+ const prefix = entry.label ?? entry.vault.name;
576
+ for (const s of entry.sheets) {
577
+ const all = await entry.vault.collection(s.collection).list();
578
+ const records = [];
579
+ for (const item of all) {
580
+ const r = item;
581
+ const allow = entry.closure?.get(s.collection);
582
+ if (allow && !allow.has(String(r.id))) continue;
583
+ if (s.filter && !s.filter(r)) continue;
584
+ records.push(r);
585
+ }
586
+ loaded.push({ entry, prefix, sheetOpt: s, records });
587
+ const indexKey = `${prefix}/${s.collection}`;
588
+ const collectionIndex = denormIndex.get(indexKey) ?? /* @__PURE__ */ new Map();
589
+ denormIndex.set(indexKey, collectionIndex);
590
+ for (const r of records) {
591
+ const idVal = r.id;
592
+ if (idVal != null) collectionIndex.set(safeStringify(idVal), r);
593
+ }
594
+ }
595
+ }
596
+ for (const ls of loaded) {
597
+ for (const d of ls.sheetOpt.denormalize ?? []) {
598
+ if (d.from.keyField === "id") continue;
599
+ const indexKey = `${d.from.label}/${d.from.collection}`;
600
+ const idx = denormIndex.get(indexKey);
601
+ if (!idx) continue;
602
+ const srcLoaded = loaded.find(
603
+ (l) => l.prefix === d.from.label && l.sheetOpt.collection === d.from.collection
604
+ );
605
+ if (!srcLoaded) continue;
606
+ for (const r of srcLoaded.records) {
607
+ const kv = r[d.from.keyField];
608
+ if (kv != null) idx.set(safeStringify(kv), r);
609
+ }
610
+ }
611
+ }
612
+ const allSheets = [];
613
+ const manifestRows = [];
614
+ for (const { prefix, sheetOpt: s, records } of loaded) {
615
+ const baseColumns = s.columns ?? inferColumns(records);
616
+ const denormDefs = s.denormalize ?? [];
617
+ if (denormDefs.length === 0) {
618
+ const sheetName2 = `${prefix}${sep}${s.name}`;
619
+ allSheets.push(buildFlatSheet(sheetName2, baseColumns, records));
620
+ manifestRows.push([prefix, s.collection, records.length]);
621
+ continue;
622
+ }
623
+ const allColumns = [...baseColumns, ...denormDefs.map((d) => d.column)];
624
+ const sheetName = `${prefix}${sep}${s.name}`;
625
+ const rows = records.map((r) => {
626
+ const baseCells = baseColumns.map((c) => r[c] ?? null);
627
+ const denormCells = denormDefs.map((d) => {
628
+ const idxKey = `${d.from.label}/${d.from.collection}`;
629
+ const idx = denormIndex.get(idxKey);
630
+ if (!idx) return "";
631
+ const fkVal = r[d.localField];
632
+ if (fkVal == null) return "";
633
+ const supporting = idx.get(safeStringify(fkVal));
634
+ if (!supporting) return "";
635
+ return supporting[d.from.pick] ?? "";
636
+ });
637
+ return [...baseCells, ...denormCells];
638
+ });
639
+ allSheets.push({ name: sheetName, header: allColumns, rows });
640
+ manifestRows.push([prefix, s.collection, records.length]);
641
+ }
642
+ allSheets.unshift({
643
+ name: "_manifest",
644
+ header: ["Vault", "Collection", "Records"],
645
+ rows: manifestRows
646
+ });
647
+ return writeXlsx(allSheets);
648
+ }
649
+ function buildFlatSheet(name, columns, records) {
650
+ return {
651
+ name,
652
+ header: [...columns],
653
+ rows: records.map((r) => columns.map((c) => r[c] ?? null))
654
+ };
655
+ }
567
656
  function safeStringify(v) {
568
657
  if (typeof v === "string") return v;
569
658
  if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
@@ -1023,6 +1112,7 @@ export {
1023
1112
  styled,
1024
1113
  toBytes,
1025
1114
  toBytesFromCollection,
1115
+ toBytesMultiVault,
1026
1116
  write,
1027
1117
  writeXlsx,
1028
1118
  zodSourceFor