sheet-to-schema 1.0.0 → 1.1.0-rc.2
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/README.md +77 -11
- package/dist/browser/index.d.mts +41 -0
- package/dist/browser/index.d.ts +41 -0
- package/dist/browser/index.js +617 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/index.mjs +586 -0
- package/dist/browser/index.mjs.map +1 -0
- package/dist/import-error-Co6lRAqv.d.mts +166 -0
- package/dist/import-error-Co6lRAqv.d.ts +166 -0
- package/dist/index.d.mts +103 -6
- package/dist/index.d.ts +103 -6
- package/dist/index.js +204 -89
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +203 -90
- package/dist/index.mjs.map +1 -1
- package/dist/node/index.d.mts +71 -188
- package/dist/node/index.d.ts +71 -188
- package/dist/node/index.js +319 -113
- package/dist/node/index.js.map +1 -1
- package/dist/node/index.mjs +319 -115
- package/dist/node/index.mjs.map +1 -1
- package/package.json +14 -6
package/README.md
CHANGED
|
@@ -11,8 +11,10 @@ one record per row, or per group of rows, reporting every cell it could not use.
|
|
|
11
11
|
with memory bounded by a batch, and serves the import over HTTP.
|
|
12
12
|
- **The same engine on both sides.** A file imports to the same records in the browser and
|
|
13
13
|
on the server, because both run the same function over the same plan.
|
|
14
|
+
- **The same reader on both sides.** `sheet-to-schema/browser` reads a CSV, TSV or `.xlsx`
|
|
15
|
+
file in a browser into exactly the rows the server reads from the same bytes.
|
|
14
16
|
|
|
15
|
-
> **Status: stable (1.0.0)
|
|
17
|
+
> **Status: stable (1.0.0); 1.1.0-rc.2 on `next`.** It is the import engine of
|
|
16
18
|
> [Dynamic Entity](https://github.com/berserker5619/Dynamic-Entity#readme) 3.0, extracted so it can be used without it.
|
|
17
19
|
> Members marked `@internal` are not covered by semver and may change in any release.
|
|
18
20
|
|
|
@@ -20,8 +22,8 @@ one record per row, or per group of rows, reporting every cell it could not use.
|
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
24
|
npm install sheet-to-schema
|
|
23
|
-
# Only for sheet-to-schema/node, and only the ones you use:
|
|
24
|
-
npm install exceljs # to read or write .xlsx
|
|
25
|
+
# Only for sheet-to-schema/node and /browser, and only the ones you use:
|
|
26
|
+
npm install exceljs # to read or write .xlsx (on a server or in a browser)
|
|
25
27
|
npm install busboy # to receive uploads
|
|
26
28
|
npm install express # for the ready-made router
|
|
27
29
|
```
|
|
@@ -135,8 +137,20 @@ result.warnings;
|
|
|
135
137
|
```
|
|
136
138
|
|
|
137
139
|
Rows of a group that disagree on a parent field keep the first row's value and raise a
|
|
138
|
-
**warning**: the record is still imported. Grouping works in `applyMapping`.
|
|
139
|
-
|
|
140
|
+
**warning**: the record is still imported. Grouping works in `applyMapping`. On the server it
|
|
141
|
+
is opt-in in 1.1: `runImport` and `createImportRouter` group when given `grouping: true`, and
|
|
142
|
+
give the same records as `applyMapping` (a group that straddles a batch is carried into the next
|
|
143
|
+
one and written whole). Without it a grouped plan is refused, as in 1.0. The runner groups
|
|
144
|
+
contiguous rows only (sort the sheet by the key columns); `contiguous: false` works in
|
|
145
|
+
`applyMapping` and is refused by the runner, since it would hold every group to the end of the
|
|
146
|
+
file.
|
|
147
|
+
|
|
148
|
+
Grouping is bounded: a group of more than 10,000 rows, more than 200,000 groups, or (for
|
|
149
|
+
contiguous groups) more than 8 MiB of group keys stops the import with an `ImportFailure`
|
|
150
|
+
(`GROUP_TOO_LARGE` or `TOO_MANY_GROUPS`), as does a key that comes back after its rows ended
|
|
151
|
+
(`GROUP_NOT_CONTIGUOUS`). Raise a limit with
|
|
152
|
+
`applyMapping(rows, plan, adapter, ctx, { groupLimits: { maxGroupRows: 50_000 } })`, or with the
|
|
153
|
+
server's `limits`. Over HTTP these answer 413 and 422.
|
|
140
154
|
|
|
141
155
|
## Dates are never guessed
|
|
142
156
|
|
|
@@ -160,6 +174,42 @@ For anything else, implement `SchemaAdapter`: `id`, `adapterVersion`, and
|
|
|
160
174
|
its own cells (`coerce`), finish a record (`finalize`) and check one (`validate`). That is how
|
|
161
175
|
Dynamic Entity's form configs run on this engine.
|
|
162
176
|
|
|
177
|
+
## In a browser: `sheet-to-schema/browser`
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { applyMapping, decimalMarkFor } from 'sheet-to-schema';
|
|
181
|
+
import { readSheetFile } from 'sheet-to-schema/browser';
|
|
182
|
+
|
|
183
|
+
// `file` from an <input type="file">: a CSV, a TSV or an .xlsx, decided by its bytes.
|
|
184
|
+
const sheet = await readSheetFile(file, { filename: file.name });
|
|
185
|
+
const result = applyMapping(sheet.rows, plan, adapter, undefined, { decimal: decimalMarkFor(sheet.delimiter) });
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`readSheetFile` returns the grid the server's `readSheet` reads from the same file: the same
|
|
189
|
+
headers, the same rows with the same typed cells (a date cell is a `Date`, a formula is its
|
|
190
|
+
cached result), the same separator, and the same refusals, with the server's error codes and
|
|
191
|
+
messages. Pass `sheet.delimiter` to `decimalMarkFor`, as the server does, so a semicolon file
|
|
192
|
+
reads `1,5` as 1.5 on both sides. `readXlsx` is the same for a caller that already knows the
|
|
193
|
+
file is a workbook.
|
|
194
|
+
|
|
195
|
+
- **A CSV needs nothing installed.** `exceljs` is loaded with `import()` the first time a
|
|
196
|
+
workbook is read, so a CSV-only app never downloads it. Without it, a workbook fails with
|
|
197
|
+
what to install.
|
|
198
|
+
- **The limits are the server's**, with its defaults: `maxBytes`, `maxRows`, `maxColumns`,
|
|
199
|
+
`maxCellLength` and `maxZipEntries`, through `limits`.
|
|
200
|
+
- **A workbook's zip is checked by what it declares** (entry count, sizes, compression ratio)
|
|
201
|
+
before ExcelJS sees it. The server also checks what each entry actually inflates to; the
|
|
202
|
+
browser does not, because there the file is the user's own and only their tab is at risk.
|
|
203
|
+
- **The first sheet, header row 1**, as on the server.
|
|
204
|
+
|
|
205
|
+
Known limitations of the browser path:
|
|
206
|
+
|
|
207
|
+
- **It needs `DecompressionStream`** (every current browser, and Node 20) to read a workbook;
|
|
208
|
+
without it, a workbook fails with a message saying so. A CSV needs nothing.
|
|
209
|
+
- **A broken archive may be described differently.** Both readers refuse it, but the server
|
|
210
|
+
reads entries front to back and the browser reads the zip's directory, so the message for a
|
|
211
|
+
damaged `.xlsx` can differ. Every well-formed workbook in the parity fixtures reads the same.
|
|
212
|
+
|
|
163
213
|
## On a server: `sheet-to-schema/node`
|
|
164
214
|
|
|
165
215
|
```ts
|
|
@@ -172,6 +222,8 @@ app.use(
|
|
|
172
222
|
createImportRouter(express.Router(), {
|
|
173
223
|
bindings: { order: { adapter, ctx: undefined, kind: 'json-schema' } },
|
|
174
224
|
dateOrder: 'DMY',
|
|
225
|
+
// Opt-in in 1.1: import plans that group rows into one record (previews then say "group").
|
|
226
|
+
grouping: true,
|
|
175
227
|
// Called once per batch, and awaited. It must be idempotent: a client will retry.
|
|
176
228
|
onImport: async (records, { name }) => saveAll(name, records),
|
|
177
229
|
}),
|
|
@@ -203,13 +255,18 @@ createImportRouter(express.Router(), {
|
|
|
203
255
|
Four things to know before deploying it:
|
|
204
256
|
|
|
205
257
|
1. **An import is not transactional.** A file that fails at row 30,000 has written 29,999
|
|
206
|
-
records. Run `/validate` first, and make your writer idempotent.
|
|
258
|
+
records. Run `/validate` first, and make your writer idempotent. A grouped import can stop
|
|
259
|
+
on a group limit or a key that comes back, after earlier groups were written; `/validate`
|
|
260
|
+
runs the same pipeline and finds that first.
|
|
207
261
|
2. **Authentication, authorization and concurrency are yours.** The router checks none.
|
|
208
|
-
3. **Every limit is on by default**: bytes, rows, columns, cell size, zip expansion,
|
|
209
|
-
total time. See `DEFAULT_LIMITS`; pass `limits` to change one.
|
|
210
|
-
4. **A client must check
|
|
211
|
-
|
|
212
|
-
|
|
262
|
+
3. **Every limit is on by default**: bytes, rows, columns, cell size, zip expansion, group
|
|
263
|
+
size and count, idle and total time. See `DEFAULT_LIMITS`; pass `limits` to change one.
|
|
264
|
+
4. **A client must check what the server reads.** `planForServer(plan, preview)` returns the
|
|
265
|
+
plan to send to that server, or the reason not to: a server from before plan v2 would ignore
|
|
266
|
+
`group` and import one record per row, and a 1.0 server reads plan v2 but refuses `group`.
|
|
267
|
+
A preview lists `planVersions` and, from 1.1, `features` (`["group"]` on a router created
|
|
268
|
+
with `grouping: true`); a missing `features` means the server will not group. The 1.0 form, `planForServer(plan, preview.planVersions)`,
|
|
269
|
+
still works and checks plan versions only.
|
|
213
270
|
|
|
214
271
|
## Problem codes
|
|
215
272
|
|
|
@@ -221,6 +278,15 @@ Four things to know before deploying it:
|
|
|
221
278
|
|
|
222
279
|
`PLAN_PROBLEM_CODES`, `CELL_PROBLEM_CODES` and `RECORD_PROBLEM_CODES` list them all.
|
|
223
280
|
|
|
281
|
+
A grouped import can also stop as a whole, with an `ImportFailure` (`IMPORT_FAILURE_CODES`).
|
|
282
|
+
`applyMapping` and `runImport` throw it; over HTTP it is the error body's `code`:
|
|
283
|
+
|
|
284
|
+
| Code | HTTP | Raised when |
|
|
285
|
+
| --- | --- | --- |
|
|
286
|
+
| `GROUP_NOT_CONTIGUOUS` | 422 | A group's key comes back after its rows ended. `details` has the key and both rows. |
|
|
287
|
+
| `GROUP_TOO_LARGE` | 413 | A group has more than `maxGroupRows` rows. |
|
|
288
|
+
| `TOO_MANY_GROUPS` | 413 | More than `maxGroups` groups, or more than `maxGroupKeyBytes` of group keys. |
|
|
289
|
+
|
|
224
290
|
## Licence
|
|
225
291
|
|
|
226
292
|
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SheetGrid, CsvDelimiter } from 'sheet-to-schema';
|
|
2
|
+
import { I as ImportLimits } from '../import-error-Co6lRAqv.mjs';
|
|
3
|
+
export { a as ImportError, b as ImportErrorCode } from '../import-error-Co6lRAqv.mjs';
|
|
4
|
+
|
|
5
|
+
/** The limits a browser read enforces. Each one left out is the server's default. */
|
|
6
|
+
type BrowserLimits = Pick<ImportLimits, 'maxBytes' | 'maxRows' | 'maxColumns' | 'maxCellLength' | 'maxZipEntries'>;
|
|
7
|
+
interface ReadSheetFileOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The file's name. A delimiter hint only, as on the server: once the bytes have said the file
|
|
10
|
+
* is text, a name ending in `.tsv` means tab. It never decides the format.
|
|
11
|
+
*/
|
|
12
|
+
filename?: string;
|
|
13
|
+
limits?: Partial<BrowserLimits>;
|
|
14
|
+
}
|
|
15
|
+
/** What `readSheetFile` reads: the grid, what the bytes turned out to be, and a CSV's separator. */
|
|
16
|
+
interface SheetFile extends SheetGrid {
|
|
17
|
+
format: 'csv' | 'xlsx';
|
|
18
|
+
/** The separator a CSV turned out to use; pass it to `decimalMarkFor`. Absent for a workbook. */
|
|
19
|
+
delimiter?: CsvDelimiter;
|
|
20
|
+
}
|
|
21
|
+
/** What a file can arrive as: a `File` or other `Blob`, or its bytes. */
|
|
22
|
+
type SheetFileInput = Blob | ArrayBuffer | Uint8Array;
|
|
23
|
+
/**
|
|
24
|
+
* Read a CSV, TSV or `.xlsx` file into exactly the grid the server's `readSheet` reads from the
|
|
25
|
+
* same bytes: the same headers, the same rows with the same typed cells, the same separator,
|
|
26
|
+
* and the same refusals with the same codes and messages.
|
|
27
|
+
*
|
|
28
|
+
* @throws ImportError `TOO_LARGE`, `SHEET_TOO_LARGE`, `UNSUPPORTED_FORMAT`, `MALFORMED_FILE`,
|
|
29
|
+
* `ARCHIVE_REFUSED`, as the server would. A workbook read without `exceljs` installed throws a
|
|
30
|
+
* plain `Error` saying what to install.
|
|
31
|
+
*/
|
|
32
|
+
declare function readSheetFile(input: SheetFileInput, options?: ReadSheetFileOptions): Promise<SheetFile>;
|
|
33
|
+
/**
|
|
34
|
+
* Read an `.xlsx` file by the server's rules, for a caller that already knows the format.
|
|
35
|
+
*
|
|
36
|
+
* @throws ImportError `UNSUPPORTED_FORMAT` for anything that is not a zip, and whatever
|
|
37
|
+
* `readSheetFile` throws for a workbook.
|
|
38
|
+
*/
|
|
39
|
+
declare function readXlsx(input: SheetFileInput, options?: Omit<ReadSheetFileOptions, 'filename'>): Promise<SheetGrid>;
|
|
40
|
+
|
|
41
|
+
export { type BrowserLimits, type ReadSheetFileOptions, type SheetFile, type SheetFileInput, readSheetFile, readXlsx };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SheetGrid, CsvDelimiter } from 'sheet-to-schema';
|
|
2
|
+
import { I as ImportLimits } from '../import-error-Co6lRAqv.js';
|
|
3
|
+
export { a as ImportError, b as ImportErrorCode } from '../import-error-Co6lRAqv.js';
|
|
4
|
+
|
|
5
|
+
/** The limits a browser read enforces. Each one left out is the server's default. */
|
|
6
|
+
type BrowserLimits = Pick<ImportLimits, 'maxBytes' | 'maxRows' | 'maxColumns' | 'maxCellLength' | 'maxZipEntries'>;
|
|
7
|
+
interface ReadSheetFileOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The file's name. A delimiter hint only, as on the server: once the bytes have said the file
|
|
10
|
+
* is text, a name ending in `.tsv` means tab. It never decides the format.
|
|
11
|
+
*/
|
|
12
|
+
filename?: string;
|
|
13
|
+
limits?: Partial<BrowserLimits>;
|
|
14
|
+
}
|
|
15
|
+
/** What `readSheetFile` reads: the grid, what the bytes turned out to be, and a CSV's separator. */
|
|
16
|
+
interface SheetFile extends SheetGrid {
|
|
17
|
+
format: 'csv' | 'xlsx';
|
|
18
|
+
/** The separator a CSV turned out to use; pass it to `decimalMarkFor`. Absent for a workbook. */
|
|
19
|
+
delimiter?: CsvDelimiter;
|
|
20
|
+
}
|
|
21
|
+
/** What a file can arrive as: a `File` or other `Blob`, or its bytes. */
|
|
22
|
+
type SheetFileInput = Blob | ArrayBuffer | Uint8Array;
|
|
23
|
+
/**
|
|
24
|
+
* Read a CSV, TSV or `.xlsx` file into exactly the grid the server's `readSheet` reads from the
|
|
25
|
+
* same bytes: the same headers, the same rows with the same typed cells, the same separator,
|
|
26
|
+
* and the same refusals with the same codes and messages.
|
|
27
|
+
*
|
|
28
|
+
* @throws ImportError `TOO_LARGE`, `SHEET_TOO_LARGE`, `UNSUPPORTED_FORMAT`, `MALFORMED_FILE`,
|
|
29
|
+
* `ARCHIVE_REFUSED`, as the server would. A workbook read without `exceljs` installed throws a
|
|
30
|
+
* plain `Error` saying what to install.
|
|
31
|
+
*/
|
|
32
|
+
declare function readSheetFile(input: SheetFileInput, options?: ReadSheetFileOptions): Promise<SheetFile>;
|
|
33
|
+
/**
|
|
34
|
+
* Read an `.xlsx` file by the server's rules, for a caller that already knows the format.
|
|
35
|
+
*
|
|
36
|
+
* @throws ImportError `UNSUPPORTED_FORMAT` for anything that is not a zip, and whatever
|
|
37
|
+
* `readSheetFile` throws for a workbook.
|
|
38
|
+
*/
|
|
39
|
+
declare function readXlsx(input: SheetFileInput, options?: Omit<ReadSheetFileOptions, 'filename'>): Promise<SheetGrid>;
|
|
40
|
+
|
|
41
|
+
export { type BrowserLimits, type ReadSheetFileOptions, type SheetFile, type SheetFileInput, readSheetFile, readXlsx };
|