@noy-db/as-xlsx 0.2.0-pre.21 → 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.cjs +227 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +134 -1
- package/dist/index.d.ts +134 -1
- package/dist/index.js +223 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.d.cts
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
|
*
|
|
@@ -244,6 +268,45 @@ interface AsXlsxSheetOptions {
|
|
|
244
268
|
* global LANG cell via `VLOOKUP(code, …, MATCH(LANG, …))`.
|
|
245
269
|
*/
|
|
246
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
|
+
};
|
|
247
310
|
}
|
|
248
311
|
/** One aggregate column in a smart-mode summary sheet (#414 P3). */
|
|
249
312
|
interface AsXlsxSummaryAggregate {
|
|
@@ -274,6 +337,13 @@ interface AsXlsxOptions {
|
|
|
274
337
|
readonly sheets: readonly AsXlsxSheetOptions[];
|
|
275
338
|
/** Smart mode only: groupBy summary sheets (live SUMIFS/COUNTIFS/AVERAGEIFS). */
|
|
276
339
|
readonly summaries?: readonly AsXlsxSummarySpec[];
|
|
340
|
+
/**
|
|
341
|
+
* Smart mode only: summary formula dialect. `'excel'` (default) emits
|
|
342
|
+
* cross-compatible per-row SUMIFS/COUNTIFS/AVERAGEIFS. `'sheets'` emits a
|
|
343
|
+
* single Google-Sheets `QUERY` formula per summary — **Sheets-only** (QUERY
|
|
344
|
+
* errors in Excel); use when the target is Google Sheets.
|
|
345
|
+
*/
|
|
346
|
+
readonly dialect?: 'excel' | 'sheets';
|
|
277
347
|
/**
|
|
278
348
|
* Smart-workbook mode (#414). Emits a relational workbook instead of a flat
|
|
279
349
|
* dump:
|
|
@@ -320,6 +390,61 @@ declare function download(vault: Vault, options: AsXlsxDownloadOptions): Promise
|
|
|
320
390
|
* plaintext xlsx persists past the process (Tier 3 egress).
|
|
321
391
|
*/
|
|
322
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>;
|
|
323
448
|
|
|
324
449
|
type ImportPolicy = 'merge' | 'replace' | 'insert-only';
|
|
325
450
|
/**
|
|
@@ -372,6 +497,14 @@ interface AsXlsxImportOptions {
|
|
|
372
497
|
* Unknown labels (no match in any locale) pass through as-is.
|
|
373
498
|
*/
|
|
374
499
|
readonly dicts?: Readonly<Record<string, readonly DictEntry[]>>;
|
|
500
|
+
/**
|
|
501
|
+
* Read a sheet produced by smart export (#414 P4). Reverses the smart layout:
|
|
502
|
+
* reconstructs i18n fields from their per-locale columns (`<f>__<loc>` →
|
|
503
|
+
* `{ loc: value }`), and drops derived columns — the i18n display column and
|
|
504
|
+
* every `<f>__label` (FK/dict) formula column. Code columns (the real values)
|
|
505
|
+
* pass through. Maps onto the existing collection schema (Mode A).
|
|
506
|
+
*/
|
|
507
|
+
readonly smart?: boolean;
|
|
375
508
|
}
|
|
376
509
|
interface AsXlsxImportPlan {
|
|
377
510
|
readonly plan: VaultDiff;
|
|
@@ -402,4 +535,4 @@ interface AsXlsxImportPlan {
|
|
|
402
535
|
*/
|
|
403
536
|
declare function fromBytes(vault: Vault, bytes: Uint8Array, options: AsXlsxImportOptions): Promise<AsXlsxImportPlan>;
|
|
404
537
|
|
|
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 };
|
|
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
|
@@ -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
|
*
|
|
@@ -244,6 +268,45 @@ interface AsXlsxSheetOptions {
|
|
|
244
268
|
* global LANG cell via `VLOOKUP(code, …, MATCH(LANG, …))`.
|
|
245
269
|
*/
|
|
246
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
|
+
};
|
|
247
310
|
}
|
|
248
311
|
/** One aggregate column in a smart-mode summary sheet (#414 P3). */
|
|
249
312
|
interface AsXlsxSummaryAggregate {
|
|
@@ -274,6 +337,13 @@ interface AsXlsxOptions {
|
|
|
274
337
|
readonly sheets: readonly AsXlsxSheetOptions[];
|
|
275
338
|
/** Smart mode only: groupBy summary sheets (live SUMIFS/COUNTIFS/AVERAGEIFS). */
|
|
276
339
|
readonly summaries?: readonly AsXlsxSummarySpec[];
|
|
340
|
+
/**
|
|
341
|
+
* Smart mode only: summary formula dialect. `'excel'` (default) emits
|
|
342
|
+
* cross-compatible per-row SUMIFS/COUNTIFS/AVERAGEIFS. `'sheets'` emits a
|
|
343
|
+
* single Google-Sheets `QUERY` formula per summary — **Sheets-only** (QUERY
|
|
344
|
+
* errors in Excel); use when the target is Google Sheets.
|
|
345
|
+
*/
|
|
346
|
+
readonly dialect?: 'excel' | 'sheets';
|
|
277
347
|
/**
|
|
278
348
|
* Smart-workbook mode (#414). Emits a relational workbook instead of a flat
|
|
279
349
|
* dump:
|
|
@@ -320,6 +390,61 @@ declare function download(vault: Vault, options: AsXlsxDownloadOptions): Promise
|
|
|
320
390
|
* plaintext xlsx persists past the process (Tier 3 egress).
|
|
321
391
|
*/
|
|
322
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>;
|
|
323
448
|
|
|
324
449
|
type ImportPolicy = 'merge' | 'replace' | 'insert-only';
|
|
325
450
|
/**
|
|
@@ -372,6 +497,14 @@ interface AsXlsxImportOptions {
|
|
|
372
497
|
* Unknown labels (no match in any locale) pass through as-is.
|
|
373
498
|
*/
|
|
374
499
|
readonly dicts?: Readonly<Record<string, readonly DictEntry[]>>;
|
|
500
|
+
/**
|
|
501
|
+
* Read a sheet produced by smart export (#414 P4). Reverses the smart layout:
|
|
502
|
+
* reconstructs i18n fields from their per-locale columns (`<f>__<loc>` →
|
|
503
|
+
* `{ loc: value }`), and drops derived columns — the i18n display column and
|
|
504
|
+
* every `<f>__label` (FK/dict) formula column. Code columns (the real values)
|
|
505
|
+
* pass through. Maps onto the existing collection schema (Mode A).
|
|
506
|
+
*/
|
|
507
|
+
readonly smart?: boolean;
|
|
375
508
|
}
|
|
376
509
|
interface AsXlsxImportPlan {
|
|
377
510
|
readonly plan: VaultDiff;
|
|
@@ -402,4 +535,4 @@ interface AsXlsxImportPlan {
|
|
|
402
535
|
*/
|
|
403
536
|
declare function fromBytes(vault: Vault, bytes: Uint8Array, options: AsXlsxImportOptions): Promise<AsXlsxImportPlan>;
|
|
404
537
|
|
|
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 };
|
|
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
|
@@ -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) {
|
|
@@ -478,6 +564,95 @@ async function write(vault, path, options) {
|
|
|
478
564
|
const { writeFile } = await import("fs/promises");
|
|
479
565
|
await writeFile(path, bytes);
|
|
480
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
|
+
}
|
|
481
656
|
function safeStringify(v) {
|
|
482
657
|
if (typeof v === "string") return v;
|
|
483
658
|
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
|
|
@@ -636,6 +811,7 @@ async function buildSmartSheets(vault, options) {
|
|
|
636
811
|
return [m.opt.name, m.records.length, Object.entries(refs).map(([f, r]) => `${f}\u2192${r.target}`).join(", ")];
|
|
637
812
|
})
|
|
638
813
|
};
|
|
814
|
+
const dialect = options.dialect ?? "excel";
|
|
639
815
|
const summarySheets = [];
|
|
640
816
|
for (const spec of options.summaries ?? []) {
|
|
641
817
|
const src = sheetMeta.get(spec.from);
|
|
@@ -643,6 +819,26 @@ async function buildSmartSheets(vault, options) {
|
|
|
643
819
|
const gCol = src?.colIndex.get(spec.groupBy);
|
|
644
820
|
if (!src || !srcMat || gCol === void 0) continue;
|
|
645
821
|
const gLetter = colLetter(gCol);
|
|
822
|
+
if (dialect === "sheets") {
|
|
823
|
+
const selects = [];
|
|
824
|
+
const labels = [];
|
|
825
|
+
for (const a of spec.aggregates) {
|
|
826
|
+
if (a.op === "count") {
|
|
827
|
+
selects.push(`COUNT(${gLetter})`);
|
|
828
|
+
labels.push(`COUNT(${gLetter}) '${a.label}'`);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
const vIdx = a.field ? src.colIndex.get(a.field) : void 0;
|
|
832
|
+
if (vIdx === void 0) continue;
|
|
833
|
+
const vL = colLetter(vIdx);
|
|
834
|
+
const fn = a.op === "sum" ? "SUM" : "AVG";
|
|
835
|
+
selects.push(`${fn}(${vL})`);
|
|
836
|
+
labels.push(`${fn}(${vL}) '${a.label}'`);
|
|
837
|
+
}
|
|
838
|
+
const q = `QUERY('${src.name}'!A:ZZ, "SELECT ${gLetter}, ${selects.join(", ")} GROUP BY ${gLetter} LABEL ${labels.join(", ")}", 1)`;
|
|
839
|
+
summarySheets.push({ name: spec.name, rows: [[formula(q)]] });
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
646
842
|
const seen = /* @__PURE__ */ new Set();
|
|
647
843
|
const groups = [];
|
|
648
844
|
for (const r of srcMat.records) {
|
|
@@ -772,6 +968,13 @@ async function fromBytes(vault, bytes, options) {
|
|
|
772
968
|
} catch {
|
|
773
969
|
}
|
|
774
970
|
}
|
|
971
|
+
const i18nBases = /* @__PURE__ */ new Set();
|
|
972
|
+
if (options.smart) {
|
|
973
|
+
for (const field of colToField.values()) {
|
|
974
|
+
const m = /^(.+)__(.+)$/.exec(field);
|
|
975
|
+
if (m && m[2] !== "label") i18nBases.add(m[1]);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
775
978
|
const records = [];
|
|
776
979
|
for (let i = headerRowIdx + 1; i < allRows.length; i++) {
|
|
777
980
|
const row = allRows[i];
|
|
@@ -780,6 +983,22 @@ async function fromBytes(vault, bytes, options) {
|
|
|
780
983
|
for (const [col, value] of Object.entries(row)) {
|
|
781
984
|
const field = colToField.get(col);
|
|
782
985
|
if (field === void 0) continue;
|
|
986
|
+
if (options.smart) {
|
|
987
|
+
if (field.endsWith("__label")) continue;
|
|
988
|
+
const lm = /^(.+)__(.+)$/.exec(field);
|
|
989
|
+
if (lm && lm[2] !== "label" && i18nBases.has(lm[1])) {
|
|
990
|
+
const base = lm[1];
|
|
991
|
+
const coerced2 = coerceXlsxCell(value, types[base]);
|
|
992
|
+
if (coerced2 !== void 0 && coerced2 !== "") {
|
|
993
|
+
const map = record[base] ?? {};
|
|
994
|
+
map[lm[2]] = coerced2;
|
|
995
|
+
record[base] = map;
|
|
996
|
+
hasAny = true;
|
|
997
|
+
}
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
if (i18nBases.has(field)) continue;
|
|
1001
|
+
}
|
|
783
1002
|
const coerced = coerceXlsxCell(value, types[field]);
|
|
784
1003
|
if (coerced !== void 0) {
|
|
785
1004
|
const invMap = invertMaps.get(field);
|
|
@@ -888,11 +1107,14 @@ export {
|
|
|
888
1107
|
download,
|
|
889
1108
|
formula,
|
|
890
1109
|
fromBytes,
|
|
1110
|
+
inferSchema,
|
|
891
1111
|
readXlsx,
|
|
892
1112
|
styled,
|
|
893
1113
|
toBytes,
|
|
894
1114
|
toBytesFromCollection,
|
|
1115
|
+
toBytesMultiVault,
|
|
895
1116
|
write,
|
|
896
|
-
writeXlsx
|
|
1117
|
+
writeXlsx,
|
|
1118
|
+
zodSourceFor
|
|
897
1119
|
};
|
|
898
1120
|
//# sourceMappingURL=index.js.map
|