@noy-db/as-xlsx 0.1.0-pre.3

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.
@@ -0,0 +1,274 @@
1
+ import { DictEntry, VaultDiff, Vault } from '@noy-db/hub';
2
+
3
+ /**
4
+ * Minimal zero-dependency XLSX writer.
5
+ *
6
+ * An `.xlsx` file is a ZIP archive (Office Open XML / OOXML) with
7
+ * SpreadsheetML inside. This writer emits the six parts needed for
8
+ * a valid worksheet and hands them to `@noy-db/as-zip`'s
9
+ * `writeZip()` to assemble the final `.xlsx` bytes.
10
+ *
11
+ * ## Emitted parts
12
+ *
13
+ * ```
14
+ * [Content_Types].xml # MIME descriptors
15
+ * _rels/.rels # root → workbook pointer
16
+ * xl/workbook.xml # sheet list
17
+ * xl/_rels/workbook.xml.rels # sheet-part pointers
18
+ * xl/worksheets/sheet<N>.xml # cell data
19
+ * xl/sharedStrings.xml # string pool (Unicode-safe)
20
+ * ```
21
+ *
22
+ * Strings route through the shared-string table (`sharedStrings.xml`)
23
+ * rather than being inlined on cells, which is:
24
+ *
25
+ * 1. Slightly more compact when strings repeat (client names,
26
+ * status labels, locale codes).
27
+ * 2. Consistent with how Excel writes its own files — some
28
+ * strict-OOXML readers refuse inline strings.
29
+ *
30
+ * Numbers, booleans, and dates are written as typed cells; strings
31
+ * and everything else fall back to the shared-string path.
32
+ *
33
+ * ## Not supported
34
+ *
35
+ * - Cell styles (fonts, colours, borders, number formats).
36
+ * - Formulas, merged cells, frozen panes, auto-filter.
37
+ * - Charts, images, drawings.
38
+ * - Zip64 / archives > 4 GiB.
39
+ *
40
+ * @module
41
+ */
42
+ /** One row in a sheet. Values are coerced per type at emit time. */
43
+ type XlsxRow = ReadonlyArray<unknown>;
44
+ /** One sheet in a workbook. */
45
+ interface XlsxSheet {
46
+ /** Sheet tab name — Excel caps at 31 chars; we truncate with `…`. */
47
+ readonly name: string;
48
+ /** Header row, rendered as row 1. Omit to skip the header. */
49
+ readonly header?: readonly string[];
50
+ /** Data rows — each is an array aligned with `header` if present. */
51
+ readonly rows: readonly XlsxRow[];
52
+ }
53
+ /**
54
+ * Build a complete `.xlsx` byte stream from the supplied sheet data.
55
+ * Pure — no I/O beyond the internal zip concatenation.
56
+ */
57
+ declare function writeXlsx(sheets: readonly XlsxSheet[]): Promise<Uint8Array>;
58
+ /**
59
+ * Convert a 1-based column index to Excel A1 letter notation.
60
+ * 1 → A, 26 → Z, 27 → AA, 702 → ZZ, 703 → AAA.
61
+ */
62
+ declare function colLetter(n: number): string;
63
+
64
+ /**
65
+ * Minimal OOXML reader. Inverse of `writeXlsx` in `xlsx.ts`.
66
+ *
67
+ * Walks the three parts the writer emits:
68
+ *
69
+ * - `xl/sharedStrings.xml` → string table (idx → string)
70
+ * - `xl/workbook.xml` → sheet name list (with sheetId)
71
+ * - `xl/_rels/workbook.xml.rels` → sheetId → sheet part path
72
+ * - `xl/worksheets/sheet<N>.xml` → cell data
73
+ *
74
+ * Cell types matched to the writer's emission rules:
75
+ * - no `t` attribute → number (`<v>` is parsed as Number)
76
+ * - `t="s"` → shared-string ref
77
+ * - `t="b"` → boolean (`1` ↔ true, `0` ↔ false)
78
+ * - empty `<c />` → undefined
79
+ *
80
+ * Excel date serials are NOT auto-converted — the writer outputs ISO-
81
+ * 8601 strings via the shared-string path, so dates round-trip as
82
+ * strings unless the consumer opts into `dateFields` coercion (handled
83
+ * one layer up in `index.ts:fromBytes`).
84
+ *
85
+ * @module
86
+ */
87
+ /** A row of cell values keyed by column letter (`'A' → 'foo'`). */
88
+ type ReadXlsxRow = Record<string, unknown>;
89
+ interface ReadXlsxSheet {
90
+ /** Sheet tab name from `xl/workbook.xml`. */
91
+ readonly name: string;
92
+ /** All rows in declaration order. Columns indexed by Excel letter. */
93
+ readonly rows: readonly ReadXlsxRow[];
94
+ }
95
+ interface ReadXlsxResult {
96
+ readonly sheets: readonly ReadXlsxSheet[];
97
+ }
98
+ /**
99
+ * Decode an `.xlsx` (OOXML) byte stream into per-sheet row data. The
100
+ * caller decides what to do with the rows (header inference, type
101
+ * coercion, record building) — the reader stays format-only.
102
+ *
103
+ * Throws on malformed XML, missing parts, or sheet-id mismatches.
104
+ */
105
+ declare function readXlsx(bytes: Uint8Array): Promise<ReadXlsxResult>;
106
+
107
+ /**
108
+ * **@noy-db/as-xlsx** — Excel spreadsheet plaintext export for noy-db.
109
+ *
110
+ * Produces a real `.xlsx` file (Office Open XML / OOXML) from one
111
+ * or more noy-db collections. Opens natively in Excel, Numbers,
112
+ * LibreOffice Calc, Google Sheets, and every modern spreadsheet
113
+ * tool.
114
+ *
115
+ * Zero runtime dependencies — the XLSX encoder builds the required
116
+ * SpreadsheetML parts and assembles them with
117
+ * `@noy-db/as-zip`'s `writeZip()` (STORE method; most xlsx
118
+ * contents are XML text which Excel compresses at open time anyway).
119
+ *
120
+ * Part of the `@noy-db/as-*` portable-artefact family, plaintext
121
+ * tier. See [`docs/patterns/as-exports.md`](https://github.com/vLannaAi/noy-db/blob/main/docs/patterns/as-exports.md).
122
+ *
123
+ * ## Authorisation
124
+ *
125
+ * Every call is gated by `assertCanExport('plaintext', 'xlsx')`.
126
+ *
127
+ * ```ts
128
+ * await db.grant('firm', {
129
+ * userId: 'accountant', role: 'viewer', passphrase: '…',
130
+ * exportCapability: { plaintext: ['xlsx'] },
131
+ * })
132
+ * ```
133
+ *
134
+ * @packageDocumentation
135
+ */
136
+
137
+ /** Per-sheet options for the noy-db consumer API. */
138
+ interface AsXlsxSheetOptions {
139
+ /**
140
+ * Sheet tab name. Excel caps at 31 chars; longer names are
141
+ * truncated with `…`. Duplicates are suffixed `(2)`, `(3)`.
142
+ */
143
+ readonly name: string;
144
+ /** Source collection. Must be in the caller's read ACL. */
145
+ readonly collection: string;
146
+ /**
147
+ * Field list + order. When omitted, columns are inferred from
148
+ * the union of keys across all records (first-record-wins order).
149
+ */
150
+ readonly columns?: readonly string[];
151
+ /**
152
+ * Optional predicate against each decrypted record. Runs after
153
+ * decryption; doesn't reduce I/O.
154
+ */
155
+ readonly filter?: (record: unknown) => boolean;
156
+ }
157
+ /** Single-collection convenience — passed where a sheet-list is accepted. */
158
+ interface AsXlsxOptions {
159
+ /** One or more sheets. At least one required. */
160
+ readonly sheets: readonly AsXlsxSheetOptions[];
161
+ }
162
+ /** Options for `download()` — adds optional filename. */
163
+ interface AsXlsxDownloadOptions extends AsXlsxOptions {
164
+ /** Filename offered to the browser. Default `'export.xlsx'`. */
165
+ readonly filename?: string;
166
+ }
167
+ /** Options for `write()` — requires explicit risk acknowledgement. */
168
+ interface AsXlsxWriteOptions extends AsXlsxOptions {
169
+ /** Tier 3 egress — see `docs/patterns/as-exports.md`. */
170
+ readonly acknowledgeRisks: true;
171
+ }
172
+ /**
173
+ * Convenience — single-collection shorthand. Equivalent to
174
+ * `toBytes(vault, { sheets: [{ name: collectionName, collection: collectionName }] })`.
175
+ */
176
+ declare function toBytesFromCollection(vault: Vault, collectionName: string): Promise<Uint8Array>;
177
+ /**
178
+ * Build the `.xlsx` byte stream from one or more sheets. Pure
179
+ * beyond the auth check + store reads.
180
+ */
181
+ declare function toBytes(vault: Vault, options: AsXlsxOptions): Promise<Uint8Array>;
182
+ /**
183
+ * Browser download. Requires a browser-like environment with
184
+ * `URL.createObjectURL` + `document.createElement`.
185
+ */
186
+ declare function download(vault: Vault, options: AsXlsxDownloadOptions): Promise<void>;
187
+ /**
188
+ * Node file-write. Requires `acknowledgeRisks: true` because the
189
+ * plaintext xlsx persists past the process (Tier 3 egress).
190
+ */
191
+ declare function write(vault: Vault, path: string, options: AsXlsxWriteOptions): Promise<void>;
192
+
193
+ type ImportPolicy = 'merge' | 'replace' | 'insert-only';
194
+ /**
195
+ * Thrown when a dict field contains two different keys whose labels are
196
+ * identical in any locale — making label→key inversion ambiguous.
197
+ *
198
+ * @example
199
+ * dict has: { key: 'a', labels: { en: 'Open' } } and { key: 'b', labels: { th: 'Open' } }
200
+ * → XlsxDictAmbiguityError('status', 'Open')
201
+ */
202
+ declare class XlsxDictAmbiguityError extends Error {
203
+ readonly column: string;
204
+ readonly label: string;
205
+ constructor(column: string, label: string);
206
+ }
207
+ interface AsXlsxImportOptions {
208
+ /** Target collection. xlsx has no native collection grouping. */
209
+ readonly collection: string;
210
+ /**
211
+ * Sheet name to read. Defaults to the first sheet in the workbook.
212
+ */
213
+ readonly sheet?: string;
214
+ /**
215
+ * 1-based header row index. Default `1` (first row).
216
+ */
217
+ readonly headerRow?: number;
218
+ /**
219
+ * Optional field type hints. xlsx cells already have a type
220
+ * (number, boolean, shared-string), so this is for the few cases
221
+ * where the writer's emission rules don't preserve intent —
222
+ * notably ISO-date strings the writer routed through the shared-
223
+ * string path. `'date'` parses the value with `new Date()` and
224
+ * keeps the result as an ISO-8601 string for stable round-tripping.
225
+ */
226
+ readonly fieldTypes?: Record<string, 'string' | 'number' | 'boolean' | 'date'>;
227
+ /** Field carrying the record id. Default `'id'`. */
228
+ readonly idKey?: string;
229
+ /** Reconciliation policy. Default `'merge'`. */
230
+ readonly policy?: ImportPolicy;
231
+ /**
232
+ * Per-field dict definitions for label→key inversion. When a column
233
+ * header matches a key here, cell values are matched against every
234
+ * locale label in the dict entries; matching labels are replaced by
235
+ * their stable key before building the ImportPlan.
236
+ *
237
+ * Takes precedence over any vault dictionary with the same name.
238
+ * For fields not listed here, `fromBytes` automatically tries
239
+ * `vault.dictionary(fieldName).list()` as a fallback.
240
+ *
241
+ * Unknown labels (no match in any locale) pass through as-is.
242
+ */
243
+ readonly dicts?: Readonly<Record<string, readonly DictEntry[]>>;
244
+ }
245
+ interface AsXlsxImportPlan {
246
+ readonly plan: VaultDiff;
247
+ readonly policy: ImportPolicy;
248
+ apply(): Promise<void>;
249
+ }
250
+ /**
251
+ * Build an import plan from an `.xlsx` byte stream. Inverts what
252
+ * `toBytes()` writes — the first row is the header, subsequent rows
253
+ * are records keyed by the column letters in the header row.
254
+ *
255
+ * Capability: `assertCanImport('plaintext', 'xlsx')`.
256
+ * Atomicity: `apply()` runs inside `vault.noydb.transaction()`.
257
+ *
258
+ * **Not supported (matches the writer scope):**
259
+ * - Cell styles / number formats / date format codes
260
+ * - Formulas, merged cells, frozen panes
261
+ * - Inline strings → handled defensively (since some upstream tools
262
+ * emit them) but the writer never produces them
263
+ * - Excel date serials → not auto-detected; pass `fieldTypes: { ts:
264
+ * 'date' }` to coerce a numeric serial to ISO. Date round-trip via
265
+ * the writer (which emits ISO strings) works without a hint.
266
+ *
267
+ * **Dict-label inversion** — supply `dicts` per field (or populate vault
268
+ * dictionaries with `withI18n()`) and the reader automatically inverts
269
+ * human labels back to their stable keys. Ambiguous labels throw
270
+ * `XlsxDictAmbiguityError`; unknown labels pass through unchanged.
271
+ */
272
+ declare function fromBytes(vault: Vault, bytes: Uint8Array, options: AsXlsxImportOptions): Promise<AsXlsxImportPlan>;
273
+
274
+ export { type AsXlsxDownloadOptions, type AsXlsxImportOptions, type AsXlsxImportPlan, type AsXlsxOptions, type AsXlsxSheetOptions, type AsXlsxWriteOptions, type ImportPolicy, type ReadXlsxResult, type ReadXlsxRow, type ReadXlsxSheet, XlsxDictAmbiguityError, type XlsxRow, type XlsxSheet, colLetter, download, fromBytes, readXlsx, toBytes, toBytesFromCollection, write, writeXlsx };