@noy-db/as-xlsx 0.2.0-pre.23 → 0.2.0-pre.25
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 +91 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -1
- package/dist/index.d.ts +95 -1
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
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 the outward orchestration layer (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 the outward orchestration layer (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
|