@dynamic-entity/server 2.4.1 → 3.0.0-rc.1

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/express.js CHANGED
@@ -2,940 +2,20 @@
2
2
 
3
3
  var core = require('@dynamic-entity/core');
4
4
  var express = require('express');
5
- var Busboy = require('busboy');
6
- var ExcelJS = require('exceljs');
7
- var stream = require('stream');
8
- var zlib = require('zlib');
5
+ var node = require('sheet-to-schema/node');
9
6
 
10
7
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
8
 
12
9
  var express__default = /*#__PURE__*/_interopDefault(express);
13
- var Busboy__default = /*#__PURE__*/_interopDefault(Busboy);
14
- var ExcelJS__default = /*#__PURE__*/_interopDefault(ExcelJS);
15
- var zlib__default = /*#__PURE__*/_interopDefault(zlib);
16
10
 
17
11
  // src/express.ts
18
-
19
- // src/errors.ts
20
- var STATUS = {
21
- NO_FILE: 400,
22
- TOO_MANY_FILES: 400,
23
- TOO_LARGE: 413,
24
- UNSUPPORTED_FORMAT: 415,
25
- MALFORMED_FILE: 400,
26
- ARCHIVE_REFUSED: 400,
27
- SHEET_TOO_LARGE: 413,
28
- UNKNOWN_ENTITY: 404,
29
- INVALID_PLAN: 400,
30
- TIMEOUT: 408,
31
- IMPORT_FAILED: 500,
32
- INTERNAL: 500
33
- };
34
- var ImportError = class extends Error {
35
- code;
36
- status;
37
- details;
38
- constructor(code, message, options = {}) {
39
- super(message);
40
- this.name = "ImportError";
41
- this.code = code;
42
- this.status = STATUS[code] ?? 500;
43
- this.details = options.details;
44
- if (options.cause !== void 0) this.cause = options.cause;
45
- }
46
- };
47
- function toErrorBody(error) {
48
- if (error instanceof ImportError) {
49
- return {
50
- status: error.status,
51
- body: {
52
- error: {
53
- code: error.code,
54
- message: error.message,
55
- ...error.details === void 0 ? {} : { details: error.details }
56
- }
57
- }
58
- };
59
- }
60
- return {
61
- status: 500,
62
- body: { error: { code: "INTERNAL", message: "The import could not be completed." } }
63
- };
64
- }
65
-
66
- // src/bytes.ts
67
- async function* toByteStream(source) {
68
- if (source instanceof Uint8Array) {
69
- yield source;
70
- return;
71
- }
72
- if (Symbol.asyncIterator in source) {
73
- for await (const chunk of source) yield asBytes(chunk);
74
- return;
75
- }
76
- for (const chunk of source) yield asBytes(chunk);
77
- }
78
- function asBytes(chunk) {
79
- return typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
80
- }
81
- async function* limitBytes(source, maxBytes) {
82
- let seen = 0;
83
- for await (const chunk of source) {
84
- seen += chunk.byteLength;
85
- if (seen > maxBytes) {
86
- throw new ImportError("TOO_LARGE", `The file is larger than the ${maxBytes} byte limit.`);
87
- }
88
- yield chunk;
89
- }
90
- }
91
- async function peek(source, n) {
92
- const iterator = source[Symbol.asyncIterator]();
93
- const taken = [];
94
- let size = 0;
95
- while (size < n) {
96
- const next = await iterator.next();
97
- if (next.done) break;
98
- taken.push(next.value);
99
- size += next.value.byteLength;
100
- }
101
- const head = Buffer.concat(taken.map((c) => Buffer.from(c.buffer, c.byteOffset, c.byteLength)));
102
- async function* replay() {
103
- for (const chunk of taken) yield chunk;
104
- for (let next = await iterator.next(); !next.done; next = await iterator.next()) {
105
- yield next.value;
106
- }
107
- }
108
- return { head: head.subarray(0, n), stream: replay() };
109
- }
110
- function destroySource(source) {
111
- const candidate = source;
112
- try {
113
- if (typeof candidate?.destroy === "function") candidate.destroy();
114
- else if (typeof candidate?.return === "function") candidate.return();
115
- } catch {
116
- }
117
- }
118
-
119
- // src/limits.ts
120
- var DEFAULT_LIMITS = {
121
- maxBytes: 10 * 1024 * 1024,
122
- maxFields: 16,
123
- maxFieldBytes: 1024 * 1024,
124
- maxFieldNameBytes: 200,
125
- maxFiles: 1,
126
- maxRows: 2e5,
127
- maxColumns: 512,
128
- maxCellLength: 32768,
129
- maxUncompressedBytes: 200 * 1024 * 1024,
130
- maxCompressionRatio: 200,
131
- maxZipEntries: 512,
132
- batchSize: 500,
133
- maxReportedErrors: 200,
134
- sampleRows: 5,
135
- idleTimeoutMs: 3e4,
136
- totalTimeoutMs: 10 * 6e4
137
- };
138
- function resolveLimits(overrides) {
139
- const out = { ...DEFAULT_LIMITS };
140
- if (!overrides) return out;
141
- for (const key of Object.keys(DEFAULT_LIMITS)) {
142
- const value = overrides[key];
143
- if (typeof value === "number" && Number.isFinite(value) && value > 0) {
144
- out[key] = Math.floor(value);
145
- }
146
- }
147
- return out;
148
- }
149
- function receiveUpload(request, limits) {
150
- return new Promise((resolve, reject) => {
151
- let settled = false;
152
- let file = null;
153
- const fail = (error) => {
154
- if (settled) {
155
- file?.destroy(error);
156
- return;
157
- }
158
- settled = true;
159
- request.unpipe(parser);
160
- reject(error);
161
- };
162
- let parser;
163
- try {
164
- parser = Busboy__default.default({
165
- headers: request.headers,
166
- limits: {
167
- fileSize: limits.maxBytes,
168
- files: limits.maxFiles,
169
- fields: limits.maxFields,
170
- fieldSize: limits.maxFieldBytes,
171
- fieldNameSize: limits.maxFieldNameBytes,
172
- // A part that is neither a field nor a file still costs a parse.
173
- parts: limits.maxFields + limits.maxFiles
174
- }
175
- });
176
- } catch (cause) {
177
- reject(new ImportError("MALFORMED_FILE", "This is not a multipart upload.", { cause }));
178
- return;
179
- }
180
- const fields = {};
181
- let sawFile = false;
182
- parser.on("field", (name, value, info) => {
183
- if (info.nameTruncated || info.valueTruncated) {
184
- fail(new ImportError("TOO_LARGE", `The "${safeFieldName(name)}" field is too long.`));
185
- return;
186
- }
187
- if (Object.prototype.hasOwnProperty.call(fields, name)) {
188
- fail(new ImportError("MALFORMED_FILE", `The upload repeats the "${safeFieldName(name)}" field.`));
189
- return;
190
- }
191
- fields[name] = value;
192
- });
193
- parser.on("file", (_name, stream, info) => {
194
- if (sawFile) {
195
- stream.resume();
196
- fail(new ImportError("TOO_MANY_FILES", "Send one file at a time."));
197
- return;
198
- }
199
- sawFile = true;
200
- file = stream;
201
- stream.on("error", () => void 0);
202
- stream.on("limit", () => {
203
- stream.destroy(
204
- new ImportError("TOO_LARGE", `The file is larger than the ${limits.maxBytes} byte limit.`)
205
- );
206
- });
207
- if (settled) {
208
- stream.resume();
209
- return;
210
- }
211
- settled = true;
212
- resolve({ fields, filename: String(info.filename ?? ""), stream });
213
- });
214
- parser.on("filesLimit", () => fail(new ImportError("TOO_MANY_FILES", "Send one file at a time.")));
215
- parser.on("fieldsLimit", () => fail(new ImportError("TOO_LARGE", "The upload has too many fields.")));
216
- parser.on("partsLimit", () => fail(new ImportError("TOO_LARGE", "The upload has too many parts.")));
217
- parser.on(
218
- "error",
219
- (cause) => fail(new ImportError("MALFORMED_FILE", "The upload could not be read.", { cause }))
220
- );
221
- parser.on("close", () => {
222
- if (!sawFile) fail(new ImportError("NO_FILE", "No file was uploaded."));
223
- });
224
- request.on("close", () => {
225
- if (!request.complete) fail(new ImportError("TIMEOUT", "The upload did not finish."));
226
- });
227
- request.pipe(parser);
228
- });
229
- }
230
- function safeFieldName(name) {
231
- return String(name).replace(/[^\w.-]/g, "").slice(0, 40) || "unnamed";
232
- }
233
- function readPlanField(fields) {
234
- const raw = fields["plan"];
235
- if (raw === void 0) {
236
- throw new ImportError(
237
- "INVALID_PLAN",
238
- 'No mapping plan was sent. The "plan" field must come before the file part.'
239
- );
240
- }
241
- try {
242
- return JSON.parse(raw);
243
- } catch (cause) {
244
- throw new ImportError("INVALID_PLAN", "The mapping plan is not valid JSON.", { cause });
245
- }
246
- }
247
- var LOCAL_HEADER = 67324752;
248
- var CENTRAL_HEADER = 33639248;
249
- var END_OF_CENTRAL = 101010256;
250
- var DATA_DESCRIPTOR = 134695760;
251
- var HEADER_SIZE = 30;
252
- var HAS_DATA_DESCRIPTOR = 8;
253
- var ZIP64_SENTINEL = 4294967295;
254
- var SLICE = 64 * 1024;
255
- var STORED = 0;
256
- var DEFLATED = 8;
257
- var ZIP32_MAX = 4294967295;
258
- var WANTED = [
259
- "[Content_Types].xml",
260
- "_rels/.rels",
261
- "xl/_rels/workbook.xml.rels",
262
- "xl/workbook.xml",
263
- "xl/styles.xml",
264
- "xl/sharedStrings.xml"
265
- ];
266
- var WORKSHEET = /^xl\/worksheets\/sheet(\d+)\.xml$/;
267
- function sheetNumber(name) {
268
- const match = WORKSHEET.exec(name);
269
- return match ? Number(match[1]) : null;
270
- }
271
- var refuse = (message) => {
272
- throw new ImportError("ARCHIVE_REFUSED", message);
273
- };
274
- var CRC_TABLE = (() => {
275
- const table = new Int32Array(256);
276
- for (let i = 0; i < 256; i++) {
277
- let c = i;
278
- for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
279
- table[i] = c;
280
- }
281
- return table;
282
- })();
283
- function crc32(current, bytes) {
284
- let c = ~current;
285
- for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
286
- return ~c >>> 0;
287
- }
288
- var ZipScanner = class {
289
- constructor(iterator) {
290
- this.iterator = iterator;
291
- }
292
- iterator;
293
- buffer = Buffer.alloc(0);
294
- eof = false;
295
- get available() {
296
- return this.buffer.length;
297
- }
298
- /** Pull until at least `n` bytes are buffered. False means the stream ended first. */
299
- async need(n) {
300
- while (this.buffer.length < n && !this.eof) {
301
- const next = await this.iterator.next();
302
- if (next.done) {
303
- this.eof = true;
304
- break;
305
- }
306
- const chunk = Buffer.from(next.value.buffer, next.value.byteOffset, next.value.byteLength);
307
- this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;
308
- }
309
- return this.buffer.length >= n;
310
- }
311
- peek(n) {
312
- return this.buffer.subarray(0, Math.min(n, this.buffer.length));
313
- }
314
- take(n) {
315
- const chunk = this.buffer.subarray(0, n);
316
- this.buffer = this.buffer.subarray(n);
317
- return chunk;
318
- }
319
- pushBack(bytes) {
320
- if (bytes.length) this.buffer = Buffer.concat([bytes, this.buffer]);
321
- }
322
- };
323
- function writeChunk(inflater, chunk) {
324
- return new Promise((resolve, reject) => {
325
- const onError = (error) => {
326
- inflater.removeListener("error", onError);
327
- reject(error);
328
- };
329
- inflater.once("error", onError);
330
- inflater.write(chunk, (error) => {
331
- inflater.removeListener("error", onError);
332
- if (error) reject(error);
333
- else resolve();
334
- });
335
- });
336
- }
337
- var settle = () => new Promise((resolve) => setImmediate(resolve));
338
- async function readDeflated(scanner, budget, limits, name, declared, keep) {
339
- const retain = (slice) => {
340
- budget.retained += slice.length;
341
- if (budget.retained > limits.maxBytes) {
342
- refuse(`The workbook is larger than the ${limits.maxBytes} byte limit.`);
343
- }
344
- };
345
- const inflater = zlib__default.default.createInflateRaw();
346
- const kept = [];
347
- let produced = 0;
348
- let consumed = 0;
349
- let crc = 0;
350
- let ended = false;
351
- let failure = null;
352
- let breach = null;
353
- const check = () => {
354
- if (produced > limits.maxUncompressedBytes || budget.total + produced > limits.maxUncompressedBytes) {
355
- return new ImportError(
356
- "ARCHIVE_REFUSED",
357
- `The workbook inflates to more than the ${limits.maxUncompressedBytes} byte limit.`
358
- );
359
- }
360
- if (produced > 64 * 1024 && produced / Math.max(consumed, 1) > limits.maxCompressionRatio) {
361
- return new ImportError(
362
- "ARCHIVE_REFUSED",
363
- `An entry in the workbook inflates more than ${limits.maxCompressionRatio} times \u2014 refusing it.`
364
- );
365
- }
366
- return null;
367
- };
368
- inflater.on("data", (chunk) => {
369
- produced += chunk.length;
370
- crc = crc32(crc, chunk);
371
- if (!breach) breach = check();
372
- });
373
- inflater.on("end", () => {
374
- ended = true;
375
- });
376
- inflater.on("error", (error) => {
377
- failure = error;
378
- });
379
- try {
380
- for (; ; ) {
381
- const remaining = declared === null ? Number.POSITIVE_INFINITY : declared - consumed;
382
- if (remaining <= 0) break;
383
- if (ended || failure || breach) break;
384
- if (!await scanner.need(1)) refuse(`The workbook ends part-way through "${name}".`);
385
- const slice = Buffer.from(scanner.take(Math.min(SLICE, remaining, scanner.available)));
386
- consumed += slice.length;
387
- await writeChunk(inflater, slice);
388
- if (keep) {
389
- retain(slice);
390
- kept.push(slice);
391
- }
392
- await settle();
393
- if (ended && declared === null) {
394
- const overshoot = consumed - inflater.bytesWritten;
395
- if (overshoot > 0) {
396
- scanner.pushBack(slice.subarray(slice.length - overshoot));
397
- consumed -= overshoot;
398
- if (keep) {
399
- kept[kept.length - 1] = slice.subarray(0, slice.length - overshoot);
400
- budget.retained -= overshoot;
401
- }
402
- }
403
- }
404
- }
405
- while (declared !== null && consumed < declared && !breach) {
406
- if (!await scanner.need(1)) refuse(`The workbook ends part-way through "${name}".`);
407
- const slice = Buffer.from(scanner.take(Math.min(declared - consumed, SLICE, scanner.available)));
408
- consumed += slice.length;
409
- if (keep) {
410
- retain(slice);
411
- kept.push(slice);
412
- }
413
- }
414
- } catch (error) {
415
- if (error instanceof ImportError) throw error;
416
- throw new ImportError("MALFORMED_FILE", "The workbook could not be read.", { cause: error });
417
- } finally {
418
- inflater.destroy();
419
- }
420
- if (breach) throw breach;
421
- if (failure) {
422
- throw new ImportError("MALFORMED_FILE", "The workbook could not be read.", { cause: failure });
423
- }
424
- budget.total += produced;
425
- return { data: keep ? Buffer.concat(kept) : Buffer.alloc(0), inflated: produced, crc };
426
- }
427
- async function readStored(scanner, budget, limits, name, size, keep) {
428
- const kept = [];
429
- let crc = 0;
430
- let left = size;
431
- while (left > 0) {
432
- if (!await scanner.need(1)) refuse(`The workbook ends part-way through "${name}".`);
433
- const slice = Buffer.from(scanner.take(Math.min(left, SLICE, scanner.available)));
434
- crc = crc32(crc, slice);
435
- if (keep) {
436
- budget.retained += slice.length;
437
- if (budget.retained > limits.maxBytes) {
438
- refuse(`The workbook is larger than the ${limits.maxBytes} byte limit.`);
439
- }
440
- kept.push(slice);
441
- }
442
- left -= slice.length;
443
- budget.total += slice.length;
444
- if (budget.total > limits.maxUncompressedBytes) {
445
- refuse(`The workbook inflates to more than the ${limits.maxUncompressedBytes} byte limit.`);
446
- }
447
- }
448
- return { data: keep ? Buffer.concat(kept) : Buffer.alloc(0), inflated: size, crc };
449
- }
450
- async function skipDescriptor(scanner) {
451
- if (!await scanner.need(4)) return;
452
- const signed = scanner.peek(4).readUInt32LE(0) === DATA_DESCRIPTOR;
453
- const base = signed ? 4 : 0;
454
- for (const width of [base + 12, base + 20]) {
455
- if (!await scanner.need(width + 4)) break;
456
- const next = scanner.peek(width + 4).readUInt32LE(width);
457
- if (next === LOCAL_HEADER || next === CENTRAL_HEADER) {
458
- scanner.take(width);
459
- return;
460
- }
461
- }
462
- if (await scanner.need(base + 12)) scanner.take(base + 12);
463
- }
464
- function repack(entries) {
465
- const parts = [];
466
- const directory = [];
467
- let offset = 0;
468
- for (const entry of entries) {
469
- const name = Buffer.from(entry.name, "utf8");
470
- const local = Buffer.alloc(HEADER_SIZE);
471
- local.writeUInt32LE(LOCAL_HEADER, 0);
472
- local.writeUInt16LE(20, 4);
473
- local.writeUInt16LE(0, 6);
474
- local.writeUInt16LE(entry.method, 8);
475
- local.writeUInt32LE(entry.crc, 14);
476
- local.writeUInt32LE(entry.data.length, 18);
477
- local.writeUInt32LE(entry.inflated, 22);
478
- local.writeUInt16LE(name.length, 26);
479
- parts.push(local, name, entry.data);
480
- const central = Buffer.alloc(46);
481
- central.writeUInt32LE(CENTRAL_HEADER, 0);
482
- central.writeUInt16LE(20, 4);
483
- central.writeUInt16LE(20, 6);
484
- central.writeUInt16LE(entry.method, 10);
485
- central.writeUInt32LE(entry.crc, 16);
486
- central.writeUInt32LE(entry.data.length, 20);
487
- central.writeUInt32LE(entry.inflated, 24);
488
- central.writeUInt16LE(name.length, 28);
489
- central.writeUInt32LE(offset, 42);
490
- directory.push(central, name);
491
- offset += local.length + name.length + entry.data.length;
492
- }
493
- const directoryBytes = Buffer.concat(directory);
494
- const end = Buffer.alloc(22);
495
- end.writeUInt32LE(END_OF_CENTRAL, 0);
496
- end.writeUInt16LE(entries.length, 8);
497
- end.writeUInt16LE(entries.length, 10);
498
- end.writeUInt32LE(directoryBytes.length, 12);
499
- end.writeUInt32LE(offset, 16);
500
- return Buffer.concat([...parts, directoryBytes, end]);
501
- }
502
- async function guardZip(source, limits) {
503
- const scanner = new ZipScanner(source[Symbol.asyncIterator]());
504
- const budget = { total: 0, retained: 0, entries: 0 };
505
- const kept = /* @__PURE__ */ new Map();
506
- let sheet = null;
507
- for (; ; ) {
508
- if (!await scanner.need(4)) break;
509
- if (scanner.peek(4).readUInt32LE(0) !== LOCAL_HEADER) break;
510
- if (!await scanner.need(HEADER_SIZE)) {
511
- refuse("The workbook ends part-way through an entry header.");
512
- }
513
- const header = Buffer.from(scanner.peek(HEADER_SIZE));
514
- const flags = header.readUInt16LE(6);
515
- const method = header.readUInt16LE(8);
516
- const declaredCompressed = header.readUInt32LE(18);
517
- const nameLength = header.readUInt16LE(26);
518
- const extraLength = header.readUInt16LE(28);
519
- if (++budget.entries > limits.maxZipEntries) {
520
- refuse(`The workbook has more than ${limits.maxZipEntries} entries.`);
521
- }
522
- if (!await scanner.need(HEADER_SIZE + nameLength + extraLength)) {
523
- refuse("The workbook ends part-way through an entry header.");
524
- }
525
- const name = scanner.peek(HEADER_SIZE + nameLength).subarray(HEADER_SIZE).toString("utf8");
526
- scanner.take(HEADER_SIZE + nameLength + extraLength);
527
- const number = sheetNumber(name);
528
- const keep = number === null ? WANTED.includes(name) && !kept.has(name) : sheet === null || number < sheet.number;
529
- if (keep && number !== null && sheet) {
530
- budget.retained -= kept.get(sheet.name)?.data.length ?? 0;
531
- kept.delete(sheet.name);
532
- sheet = null;
533
- }
534
- const streamed = (flags & HAS_DATA_DESCRIPTOR) !== 0 || declaredCompressed === ZIP64_SENTINEL;
535
- let read;
536
- if (method === STORED) {
537
- if (streamed) {
538
- refuse(`Entry "${name}" is stored without a declared size.`);
539
- }
540
- read = await readStored(scanner, budget, limits, name, declaredCompressed, keep);
541
- } else if (method === DEFLATED) {
542
- read = await readDeflated(
543
- scanner,
544
- budget,
545
- limits,
546
- name,
547
- streamed ? null : declaredCompressed,
548
- keep
549
- );
550
- if (streamed) await skipDescriptor(scanner);
551
- } else {
552
- return refuse(`Entry "${name}" uses compression method ${method}, which is not supported.`);
553
- }
554
- if (keep) {
555
- if (read.data.length > ZIP32_MAX || read.inflated > ZIP32_MAX) {
556
- refuse(`Entry "${name}" is too large to repack without zip64.`);
557
- }
558
- kept.set(name, { name, method, data: read.data, inflated: read.inflated, crc: read.crc });
559
- if (number !== null) sheet = { name, number };
560
- }
561
- }
562
- if (!sheet) {
563
- if (!kept.has("xl/workbook.xml")) {
564
- throw new ImportError(
565
- "UNSUPPORTED_FORMAT",
566
- "This is a zip archive that is not an Excel workbook. Upload an .xlsx or .csv file."
567
- );
568
- }
569
- throw new ImportError("MALFORMED_FILE", "This workbook has no worksheet.");
570
- }
571
- const ordered = [...WANTED, sheet.name].map((wanted) => kept.get(wanted)).filter((entry) => entry !== void 0);
572
- return repack(ordered);
12
+ function suggestionFor(headers, config, lang) {
13
+ const arrayBound = core.arrayBoundFor(headers, config, null, lang);
14
+ const { columns } = core.deriveImportColumns(config, { lang, maxArrayRows: arrayBound });
15
+ return { suggestion: core.suggestMapping(headers, columns, config?.entity ?? ""), arrayBound };
573
16
  }
574
-
575
- // src/xlsx-source.ts
576
- function cellValue(raw) {
577
- if (raw === null || raw === void 0) return "";
578
- if (raw instanceof Date) return raw;
579
- if (typeof raw !== "object") return raw;
580
- const cell = raw;
581
- if (Array.isArray(cell["richText"])) {
582
- return cell["richText"].map((part) => String(part?.text ?? "")).join("");
583
- }
584
- if ("formula" in cell || "sharedFormula" in cell) return cellValue(cell["result"]);
585
- if ("error" in cell) return String(cell["error"]);
586
- if ("text" in cell) return String(cell["text"]);
587
- return "";
588
- }
589
- function rowCells(values) {
590
- if (!Array.isArray(values)) return [];
591
- const out = [];
592
- for (let i = 1; i < values.length; i++) out.push(cellValue(values[i]));
593
- return out;
594
- }
595
- async function* xlsxRows(source, limits) {
596
- const archive = await guardZip(source, limits);
597
- const guarded = stream.Readable.from([archive]);
598
- const reader = new ExcelJS__default.default.stream.xlsx.WorkbookReader(guarded, {
599
- // Nothing decorative is parsed. Each of these is XML this import has no use for, and
600
- // every part not parsed is a part that cannot be hostile.
601
- entries: "ignore",
602
- hyperlinks: "ignore",
603
- // Cached because a string cell is a reference into this table and an unresolved reference
604
- // is not a value. See the note at the top about what that costs.
605
- sharedStrings: "cache",
606
- // Cached because it is what tells a numeric cell it is a date.
607
- styles: "cache",
608
- worksheets: "emit"
609
- });
610
- let expected = 1;
611
- let yielded = false;
612
- try {
613
- for await (const worksheet of reader) {
614
- for await (const row of worksheet) {
615
- const number = typeof row.number === "number" ? row.number : expected;
616
- if (number - 1 > limits.maxRows) {
617
- throw new ImportError("SHEET_TOO_LARGE", `The sheet has more than ${limits.maxRows} rows.`);
618
- }
619
- while (expected < number) {
620
- yield [];
621
- expected++;
622
- }
623
- yield rowCells(row.values);
624
- yielded = true;
625
- expected = number + 1;
626
- }
627
- break;
628
- }
629
- if (!yielded) {
630
- throw new ImportError("MALFORMED_FILE", "This workbook has no readable rows.");
631
- }
632
- } catch (error) {
633
- if (error instanceof ImportError) throw error;
634
- throw new ImportError("MALFORMED_FILE", "This is not a readable workbook.", { cause: error });
635
- } finally {
636
- guarded.destroy();
637
- }
638
- }
639
-
640
- // src/read-sheet.ts
641
- var MAGIC_BYTES = 8;
642
- var ZIP = [80, 75];
643
- var OLE2 = [208, 207, 17, 224];
644
- var UTF16_LE = [255, 254];
645
- var UTF16_BE = [254, 255];
646
- var startsWith = (bytes, signature) => signature.every((byte, i) => bytes[i] === byte);
647
- function detectFormat(head) {
648
- if (startsWith(head, ZIP)) return "xlsx";
649
- if (startsWith(head, OLE2)) {
650
- throw new ImportError(
651
- "UNSUPPORTED_FORMAT",
652
- "This is a pre-2007 Excel file (.xls). Save it as .xlsx or .csv and try again."
653
- );
654
- }
655
- if (startsWith(head, UTF16_LE) || startsWith(head, UTF16_BE)) {
656
- throw new ImportError(
657
- "UNSUPPORTED_FORMAT",
658
- "This file is UTF-16 encoded. Save it as UTF-8 CSV and try again."
659
- );
660
- }
661
- return "csv";
662
- }
663
- async function* csvRows(stream, limits, found, forced) {
664
- const decoder = new TextDecoder("utf-8");
665
- const maxHeader = limits.maxCellLength * limits.maxColumns + limits.maxColumns;
666
- let held = "";
667
- let reader = null;
668
- const open = (headerLine) => {
669
- found.delimiter = forced ?? core.detectDelimiter(headerLine);
670
- return core.createCsvReader({ delimiter: found.delimiter });
671
- };
672
- for await (const chunk of stream) {
673
- const text = decoder.decode(chunk, { stream: true });
674
- if (!reader) {
675
- held += text;
676
- const line = core.completeFirstLine(held);
677
- if (line === null) {
678
- if (held.length > maxHeader) {
679
- throw new ImportError("SHEET_TOO_LARGE", "The header row is longer than any row may be.");
680
- }
681
- continue;
682
- }
683
- reader = open(line);
684
- for (const row of reader.push(held)) yield row;
685
- held = "";
686
- continue;
687
- }
688
- for (const row of reader.push(text)) yield row;
689
- }
690
- const rest = held + decoder.decode();
691
- reader ??= open(core.completeFirstLine(rest) ?? rest);
692
- for (const row of reader.push(rest)) yield row;
693
- for (const row of reader.end()) yield row;
694
- }
695
- async function* guardRows(rows, width, limits) {
696
- let seen = 0;
697
- for await (const row of rows) {
698
- if (++seen > limits.maxRows) {
699
- throw new ImportError(
700
- "SHEET_TOO_LARGE",
701
- `The sheet has more than ${limits.maxRows} rows.`
702
- );
703
- }
704
- if (row.length > limits.maxColumns) {
705
- throw new ImportError(
706
- "SHEET_TOO_LARGE",
707
- `Row ${seen + 1} has more than ${limits.maxColumns} columns.`
708
- );
709
- }
710
- for (const cell of row) {
711
- if (typeof cell === "string" && cell.length > limits.maxCellLength) {
712
- throw new ImportError(
713
- "SHEET_TOO_LARGE",
714
- `A cell in row ${seen + 1} is longer than ${limits.maxCellLength} characters.`
715
- );
716
- }
717
- }
718
- yield core.padRow(row, width);
719
- }
720
- }
721
- async function readSheet(options) {
722
- const limits = resolveLimits(options.limits);
723
- try {
724
- const counted = limitBytes(toByteStream(options.stream), limits.maxBytes);
725
- const { head, stream } = await peek(counted, MAGIC_BYTES);
726
- const format = detectFormat(head);
727
- const found = {};
728
- const forced = /\.tsv$/i.test(options.filename ?? "") ? " " : void 0;
729
- const rows = format === "xlsx" ? xlsxRows(stream, limits) : csvRows(stream, limits, found, forced);
730
- const iterator = rows[Symbol.asyncIterator]();
731
- const first = await iterator.next();
732
- const headers = first.done ? [] : first.value.map(core.cellText);
733
- if (headers.length > limits.maxColumns) {
734
- throw new ImportError(
735
- "SHEET_TOO_LARGE",
736
- `The sheet has more than ${limits.maxColumns} columns.`
737
- );
738
- }
739
- const remaining = drain(iterator);
740
- return {
741
- format,
742
- headers,
743
- rows: closing(guardRows(remaining, headers.length, limits), options.stream),
744
- ...found.delimiter ? { delimiter: found.delimiter } : {}
745
- };
746
- } catch (error) {
747
- destroySource(options.stream);
748
- throw error;
749
- }
750
- }
751
- async function* drain(iterator) {
752
- for (let next = await iterator.next(); !next.done; next = await iterator.next()) {
753
- yield next.value;
754
- }
755
- }
756
- async function* closing(rows, source) {
757
- let drained = false;
758
- try {
759
- yield* rows;
760
- drained = true;
761
- } finally {
762
- if (!drained) destroySource(source);
763
- }
764
- }
765
-
766
- // src/run-import.ts
767
- function checkPlan(plan, config, lang) {
768
- return core.validateMappingPlan(plan, config, {
769
- lang,
770
- // The same derivation `applyMapping` uses, or the two would disagree about which refs
771
- // exist and the pre-flight would pass a plan the run then rejects.
772
- includeReadonly: true,
773
- includeSystemDefault: true
774
- });
775
- }
776
- async function runImport(options) {
777
- const limits = resolveLimits(options.limits);
778
- const { plan, config, onBatch } = options;
779
- const planProblems = checkPlan(plan, config, options.lang);
780
- if (planProblems.some((problem) => problem.level === "error")) {
781
- destroySource(options.stream);
782
- return {
783
- imported: 0,
784
- skipped: 0,
785
- failed: 0,
786
- errors: [],
787
- errorCount: 0,
788
- truncated: false,
789
- planProblems,
790
- rowsRead: 0
791
- };
792
- }
793
- const sheet = await readSheet({
794
- stream: options.stream,
795
- filename: options.filename,
796
- limits
797
- });
798
- const decimal = options.decimal ?? core.decimalMarkFor(sheet.delimiter);
799
- const errors = [];
800
- let errorCount = 0;
801
- let imported = 0;
802
- let skipped = 0;
803
- let failed = 0;
804
- let rowsRead = 0;
805
- let batchIndex = 0;
806
- let batch = [];
807
- let firstRow = 2;
808
- const flush = async () => {
809
- if (!batch.length) return;
810
- const rows = batch;
811
- batch = [];
812
- const result = core.applyMapping(rows, plan, config, {
813
- lang: options.lang,
814
- decimal,
815
- rules: options.rules,
816
- lookups: options.lookups,
817
- firstRowNumber: firstRow
818
- });
819
- skipped += result.skipped;
820
- errorCount += result.errors.length;
821
- failed += new Set(result.errors.map((error) => error.row)).size;
822
- for (const error of result.errors) {
823
- if (errors.length >= limits.maxReportedErrors) break;
824
- errors.push(error);
825
- }
826
- if (result.records.length && onBatch) {
827
- const info = { index: batchIndex, firstRow, imported };
828
- try {
829
- await onBatch(result.records, info);
830
- } catch (cause) {
831
- throw new ImportError(
832
- "IMPORT_FAILED",
833
- "The import stopped partway through. Rows before the failure may already have been written \u2014 this import is not transactional.",
834
- { cause }
835
- );
836
- }
837
- }
838
- imported += result.records.length;
839
- firstRow += rows.length;
840
- batchIndex++;
841
- };
842
- try {
843
- for await (const row of sheet.rows) {
844
- rowsRead++;
845
- batch.push(row);
846
- if (batch.length >= limits.batchSize) await flush();
847
- }
848
- await flush();
849
- } catch (error) {
850
- destroySource(options.stream);
851
- throw error;
852
- }
853
- return {
854
- imported,
855
- skipped,
856
- failed,
857
- errors,
858
- errorCount,
859
- truncated: errorCount > errors.length,
860
- planProblems,
861
- rowsRead,
862
- format: sheet.format
863
- };
864
- }
865
- async function previewSheet(options) {
866
- const limits = resolveLimits(options.limits);
867
- const sheet = await readSheet({
868
- stream: options.stream,
869
- filename: options.filename,
870
- limits
871
- });
872
- const sample = [];
873
- let rowCount = 0;
874
- try {
875
- for await (const row of sheet.rows) {
876
- rowCount++;
877
- if (sample.length < limits.sampleRows) sample.push(row.map(core.cellText));
878
- }
879
- } catch (error) {
880
- destroySource(options.stream);
881
- throw error;
882
- }
883
- const arrayBound = core.arrayBoundFor(sheet.headers, options.config, null, options.lang);
884
- const { columns } = core.deriveImportColumns(options.config, { lang: options.lang, maxArrayRows: arrayBound });
885
- return {
886
- headers: sheet.headers,
887
- sample,
888
- rowCount,
889
- suggestion: core.suggestMapping(sheet.headers, columns, options.config?.entity ?? ""),
890
- format: sheet.format,
891
- ...sheet.delimiter ? { delimiter: sheet.delimiter } : {},
892
- arrayBound
893
- };
894
- }
895
- var TEMPLATE_EXTENSION = { csv: "csv", xlsx: "xlsx" };
896
- var TEMPLATE_MEDIA_TYPE = {
897
- csv: "text/csv; charset=utf-8",
898
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
899
- };
900
- async function writeTemplate(options) {
901
- const limits = resolveLimits(options.limits);
902
- const columns = options.spec.columns;
903
- if (columns.length > limits.maxColumns) {
904
- throw new ImportError(
905
- "SHEET_TOO_LARGE",
906
- `A template of ${columns.length} columns is past the ${limits.maxColumns} column limit.`
907
- );
908
- }
909
- if (options.format === "csv") {
910
- await writeCsv(options.spec, options.out);
911
- return;
912
- }
913
- await writeXlsx(options.spec, options.out);
914
- }
915
- function writeCsv(spec, out) {
916
- const csv = core.toCsv(spec.columns.map((column) => column.header));
917
- return new Promise((resolve, reject) => {
918
- out.end(csv, () => resolve());
919
- out.on("error", reject);
920
- });
921
- }
922
- async function writeXlsx(spec, out) {
923
- const workbook = new ExcelJS__default.default.stream.xlsx.WorkbookWriter({ stream: out, useStyles: true });
924
- const sheet = workbook.addWorksheet(sheetName(spec.sheetName));
925
- const { columns, notes } = spec;
926
- const row = sheet.addRow(columns.map((column) => core.escapeFormula(column.header)));
927
- row.eachCell((cell, index) => {
928
- cell.font = { bold: true };
929
- const note = notes[index - 1];
930
- if (note) cell.note = note;
931
- });
932
- row.commit();
933
- sheet.commit();
934
- await workbook.commit();
935
- }
936
- function sheetName(name) {
937
- const cleaned = String(name).replace(/[[\]:*?/\\]/g, " ").trim();
938
- return (cleaned || "Import").slice(0, 31);
17
+ function templateSheetOf(spec) {
18
+ return { sheetName: spec.sheetName, headers: spec.columns.map((column) => column.header), notes: spec.notes };
939
19
  }
940
20
 
941
21
  // src/express.ts
@@ -961,157 +41,48 @@ Pass them as \`lookups\` to createImportRouter. Without them those columns would
961
41
  function ownValue(map, key) {
962
42
  return Object.hasOwn(map, key) ? map[key] : void 0;
963
43
  }
964
- function readFormat(value) {
965
- if (value === void 0 || value === "csv") return "csv";
966
- if (value === "xlsx") return "xlsx";
967
- throw new ImportError("UNSUPPORTED_FORMAT", "Supported template formats are csv and xlsx.");
968
- }
969
- function readFields(value) {
970
- const raw = value === void 0 ? [] : Array.isArray(value) ? value : [value];
971
- const fields = raw.flatMap((item) => String(item).split(",")).map((item) => item.trim()).filter(Boolean);
972
- return fields.length ? fields : void 0;
973
- }
974
- function templateFilename(entity, format) {
975
- const safe = String(entity).replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[-.]+/, "").slice(0, 64);
976
- return `${safe || "import"}-template.${TEMPLATE_EXTENSION[format]}`;
977
- }
978
- function applyTimeouts(request, response, limits) {
979
- const abandon = (message) => () => {
980
- request.destroy(new ImportError("TIMEOUT", message));
981
- };
982
- const socket = request.socket;
983
- const previous = socket?.timeout;
984
- request.setTimeout(limits.idleTimeoutMs, abandon("The upload stalled."));
985
- const total = setTimeout(abandon("The upload took too long."), limits.totalTimeoutMs);
986
- total.unref?.();
987
- response.on("close", () => {
988
- clearTimeout(total);
989
- if (socket && !socket.destroyed) socket.setTimeout(previous ?? 0);
990
- });
991
- }
992
- function sendFailure(response, error) {
993
- if (response.headersSent) {
994
- response.destroy();
995
- return;
996
- }
997
- for (const header of ["Content-Type", "Content-Disposition", "Content-Length"]) {
998
- response.removeHeader(header);
999
- }
1000
- const { status, body } = toErrorBody(error);
1001
- response.status(status).json(body);
1002
- }
44
+ var templateFilename = node.templateFilename;
45
+ var sendFailure = node.sendFailure;
1003
46
  function createImportRouter(options) {
1004
- const limits = resolveLimits(options.limits);
1005
- const router = express__default.default.Router();
1006
47
  if (typeof options.configs !== "function") {
1007
48
  for (const [entity, config] of Object.entries(options.configs)) {
1008
49
  checkLookups(entity, config, options.lookups);
1009
50
  }
1010
51
  }
1011
52
  const languageOf = (request) => typeof options.lang === "function" ? options.lang(request) : options.lang ?? "en";
1012
- const configFor = async (request) => {
1013
- const entity = String(request.params["entity"] ?? "");
53
+ const bindingFor = async (entity, request) => {
1014
54
  const config = typeof options.configs === "function" ? await options.configs(entity, request) : ownValue(options.configs, entity);
1015
- if (!config) throw new ImportError("UNKNOWN_ENTITY", "No import is configured for that entity.");
55
+ if (!config) throw new node.ImportError("UNKNOWN_ENTITY", "No import is configured for that entity.");
1016
56
  if (typeof options.configs === "function") checkLookups(entity, config, options.lookups);
1017
- return { entity, config };
57
+ const lang = languageOf(request);
58
+ const adapter = core.entityFormConfigAdapter(config, {
59
+ rules: options.rules && ownValue(options.rules, entity),
60
+ lookups: options.lookups
61
+ });
62
+ return {
63
+ adapter,
64
+ ctx: { lang },
65
+ kind: "dynamic-entity",
66
+ config,
67
+ // Named from the config. The route segment only when the config names nothing.
68
+ templateName: config.entity || entity,
69
+ template: ({ lang: language, fields }) => templateSheetOf(core.buildTemplateSpec(config, { lang: language, fields })),
70
+ // What the browser suggests for the same headers, and the one number it was sized with.
71
+ previewExtras: (preview) => suggestionFor(preview.headers, config, lang),
72
+ // A 2.x plan that names no entity is read against this config, as 2.x read it.
73
+ plan: (plan) => core.planForConfig(plan, config)
74
+ };
1018
75
  };
1019
- const respond = (result, written) => ({
1020
- written,
1021
- imported: result.imported,
1022
- skipped: result.skipped,
1023
- failed: result.failed,
1024
- rowsRead: result.rowsRead,
1025
- errors: result.errors,
1026
- errorCount: result.errorCount,
1027
- truncated: result.truncated,
1028
- planProblems: result.planProblems,
1029
- engineVersion: core.CORE_VERSION
76
+ return node.createImportRouter(express__default.default.Router(), {
77
+ bindings: bindingFor,
78
+ param: "entity",
79
+ // What a Dynamic Entity client compares with its own `CORE_VERSION`, as it always has.
80
+ engineVersion: core.CORE_VERSION,
81
+ dateOrder: options.dateOrder,
82
+ limits: options.limits,
83
+ lang: options.lang,
84
+ onImport: (records, { index, firstRow, imported, name, binding, request }) => options.onImport(records, { index, firstRow, imported, entity: name, config: binding.config, request })
1030
85
  });
1031
- const route = (handler) => (request, response) => {
1032
- applyTimeouts(request, response, limits);
1033
- handler(request, response).catch((error) => sendFailure(response, error));
1034
- };
1035
- router.get(
1036
- "/:entity/template",
1037
- route(async (request, response) => {
1038
- const { entity, config } = await configFor(request);
1039
- const format = readFormat(request.query["format"]);
1040
- const spec = core.buildTemplateSpec(config, {
1041
- lang: languageOf(request),
1042
- fields: readFields(request.query["fields"])
1043
- });
1044
- response.setHeader("Content-Type", TEMPLATE_MEDIA_TYPE[format]);
1045
- response.setHeader(
1046
- "Content-Disposition",
1047
- `attachment; filename="${templateFilename(config.entity || entity, format)}"`
1048
- );
1049
- await writeTemplate({ spec, format, out: response, limits });
1050
- })
1051
- );
1052
- router.post(
1053
- "/:entity/preview",
1054
- route(async (request, response) => {
1055
- const { config } = await configFor(request);
1056
- const upload = await receiveUpload(request, limits);
1057
- const preview = await previewSheet({
1058
- stream: upload.stream,
1059
- filename: upload.filename,
1060
- config,
1061
- lang: languageOf(request),
1062
- limits
1063
- });
1064
- const body = {
1065
- headers: preview.headers,
1066
- sample: preview.sample,
1067
- suggestion: preview.suggestion,
1068
- rowCount: preview.rowCount,
1069
- arrayBound: preview.arrayBound,
1070
- ...preview.delimiter ? { delimiter: preview.delimiter } : {},
1071
- engineVersion: core.CORE_VERSION
1072
- };
1073
- response.json(body);
1074
- })
1075
- );
1076
- router.post(
1077
- "/:entity/validate",
1078
- route(async (request, response) => {
1079
- response.json(await execute(request, options, limits, languageOf, void 0));
1080
- })
1081
- );
1082
- router.post(
1083
- "/:entity/import",
1084
- route(async (request, response) => {
1085
- const onBatch = async (records, info, entity, config) => {
1086
- await options.onImport(records, { ...info, entity, config, request });
1087
- };
1088
- response.json(await execute(request, options, limits, languageOf, onBatch));
1089
- })
1090
- );
1091
- async function execute(request, routerOptions, resolved, language, write) {
1092
- const { entity, config } = await configFor(request);
1093
- const upload = await receiveUpload(request, resolved);
1094
- let plan;
1095
- try {
1096
- plan = readPlanField(upload.fields);
1097
- } catch (error) {
1098
- destroySource(upload.stream);
1099
- throw error;
1100
- }
1101
- const result = await runImport({
1102
- stream: upload.stream,
1103
- filename: upload.filename,
1104
- plan,
1105
- config,
1106
- rules: routerOptions.rules && ownValue(routerOptions.rules, entity),
1107
- lookups: routerOptions.lookups,
1108
- lang: language(request),
1109
- limits: resolved,
1110
- onBatch: write && ((records, info) => write(records, info, entity, config))
1111
- });
1112
- return respond(result, write !== void 0);
1113
- }
1114
- return router;
1115
86
  }
1116
87
 
1117
88
  exports.createImportRouter = createImportRouter;