@dynamic-entity/server 2.5.0 → 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.d.mts +16 -16
- package/express.d.ts +16 -16
- package/express.js +38 -1072
- package/express.js.map +1 -1
- package/express.mjs +38 -1069
- package/express.mjs.map +1 -1
- package/index.d.mts +55 -140
- package/index.d.ts +55 -140
- package/index.js +50 -813
- package/index.js.map +1 -1
- package/index.mjs +49 -809
- package/index.mjs.map +1 -1
- package/package.json +5 -4
- package/run-import-BUClq6BR.d.mts +187 -0
- package/run-import-BUClq6BR.d.ts +187 -0
- package/write-template-Dqu5b00D.d.mts +0 -355
- package/write-template-Dqu5b00D.d.ts +0 -355
package/express.js
CHANGED
|
@@ -2,941 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
var core = require('@dynamic-entity/core');
|
|
4
4
|
var express = require('express');
|
|
5
|
-
var
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
dateOrder: options.dateOrder,
|
|
816
|
-
rules: options.rules,
|
|
817
|
-
lookups: options.lookups,
|
|
818
|
-
firstRowNumber: firstRow
|
|
819
|
-
});
|
|
820
|
-
skipped += result.skipped;
|
|
821
|
-
errorCount += result.errors.length;
|
|
822
|
-
failed += new Set(result.errors.map((error) => error.row)).size;
|
|
823
|
-
for (const error of result.errors) {
|
|
824
|
-
if (errors.length >= limits.maxReportedErrors) break;
|
|
825
|
-
errors.push(error);
|
|
826
|
-
}
|
|
827
|
-
if (result.records.length && onBatch) {
|
|
828
|
-
const info = { index: batchIndex, firstRow, imported };
|
|
829
|
-
try {
|
|
830
|
-
await onBatch(result.records, info);
|
|
831
|
-
} catch (cause) {
|
|
832
|
-
throw new ImportError(
|
|
833
|
-
"IMPORT_FAILED",
|
|
834
|
-
"The import stopped partway through. Rows before the failure may already have been written \u2014 this import is not transactional.",
|
|
835
|
-
{ cause }
|
|
836
|
-
);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
imported += result.records.length;
|
|
840
|
-
firstRow += rows.length;
|
|
841
|
-
batchIndex++;
|
|
842
|
-
};
|
|
843
|
-
try {
|
|
844
|
-
for await (const row of sheet.rows) {
|
|
845
|
-
rowsRead++;
|
|
846
|
-
batch.push(row);
|
|
847
|
-
if (batch.length >= limits.batchSize) await flush();
|
|
848
|
-
}
|
|
849
|
-
await flush();
|
|
850
|
-
} catch (error) {
|
|
851
|
-
destroySource(options.stream);
|
|
852
|
-
throw error;
|
|
853
|
-
}
|
|
854
|
-
return {
|
|
855
|
-
imported,
|
|
856
|
-
skipped,
|
|
857
|
-
failed,
|
|
858
|
-
errors,
|
|
859
|
-
errorCount,
|
|
860
|
-
truncated: errorCount > errors.length,
|
|
861
|
-
planProblems,
|
|
862
|
-
rowsRead,
|
|
863
|
-
format: sheet.format
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
async function previewSheet(options) {
|
|
867
|
-
const limits = resolveLimits(options.limits);
|
|
868
|
-
const sheet = await readSheet({
|
|
869
|
-
stream: options.stream,
|
|
870
|
-
filename: options.filename,
|
|
871
|
-
limits
|
|
872
|
-
});
|
|
873
|
-
const sample = [];
|
|
874
|
-
let rowCount = 0;
|
|
875
|
-
try {
|
|
876
|
-
for await (const row of sheet.rows) {
|
|
877
|
-
rowCount++;
|
|
878
|
-
if (sample.length < limits.sampleRows) sample.push(row.map(core.cellText));
|
|
879
|
-
}
|
|
880
|
-
} catch (error) {
|
|
881
|
-
destroySource(options.stream);
|
|
882
|
-
throw error;
|
|
883
|
-
}
|
|
884
|
-
const arrayBound = core.arrayBoundFor(sheet.headers, options.config, null, options.lang);
|
|
885
|
-
const { columns } = core.deriveImportColumns(options.config, { lang: options.lang, maxArrayRows: arrayBound });
|
|
886
|
-
return {
|
|
887
|
-
headers: sheet.headers,
|
|
888
|
-
sample,
|
|
889
|
-
rowCount,
|
|
890
|
-
suggestion: core.suggestMapping(sheet.headers, columns, options.config?.entity ?? ""),
|
|
891
|
-
format: sheet.format,
|
|
892
|
-
...sheet.delimiter ? { delimiter: sheet.delimiter } : {},
|
|
893
|
-
arrayBound
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
var TEMPLATE_EXTENSION = { csv: "csv", xlsx: "xlsx" };
|
|
897
|
-
var TEMPLATE_MEDIA_TYPE = {
|
|
898
|
-
csv: "text/csv; charset=utf-8",
|
|
899
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
900
|
-
};
|
|
901
|
-
async function writeTemplate(options) {
|
|
902
|
-
const limits = resolveLimits(options.limits);
|
|
903
|
-
const columns = options.spec.columns;
|
|
904
|
-
if (columns.length > limits.maxColumns) {
|
|
905
|
-
throw new ImportError(
|
|
906
|
-
"SHEET_TOO_LARGE",
|
|
907
|
-
`A template of ${columns.length} columns is past the ${limits.maxColumns} column limit.`
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
if (options.format === "csv") {
|
|
911
|
-
await writeCsv(options.spec, options.out);
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
await writeXlsx(options.spec, options.out);
|
|
915
|
-
}
|
|
916
|
-
function writeCsv(spec, out) {
|
|
917
|
-
const csv = core.toCsv(spec.columns.map((column) => column.header));
|
|
918
|
-
return new Promise((resolve, reject) => {
|
|
919
|
-
out.end(csv, () => resolve());
|
|
920
|
-
out.on("error", reject);
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
async function writeXlsx(spec, out) {
|
|
924
|
-
const workbook = new ExcelJS__default.default.stream.xlsx.WorkbookWriter({ stream: out, useStyles: true });
|
|
925
|
-
const sheet = workbook.addWorksheet(sheetName(spec.sheetName));
|
|
926
|
-
const { columns, notes } = spec;
|
|
927
|
-
const row = sheet.addRow(columns.map((column) => core.escapeFormula(column.header)));
|
|
928
|
-
row.eachCell((cell, index) => {
|
|
929
|
-
cell.font = { bold: true };
|
|
930
|
-
const note = notes[index - 1];
|
|
931
|
-
if (note) cell.note = note;
|
|
932
|
-
});
|
|
933
|
-
row.commit();
|
|
934
|
-
sheet.commit();
|
|
935
|
-
await workbook.commit();
|
|
936
|
-
}
|
|
937
|
-
function sheetName(name) {
|
|
938
|
-
const cleaned = String(name).replace(/[[\]:*?/\\]/g, " ").trim();
|
|
939
|
-
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 };
|
|
940
19
|
}
|
|
941
20
|
|
|
942
21
|
// src/express.ts
|
|
@@ -962,161 +41,48 @@ Pass them as \`lookups\` to createImportRouter. Without them those columns would
|
|
|
962
41
|
function ownValue(map, key) {
|
|
963
42
|
return Object.hasOwn(map, key) ? map[key] : void 0;
|
|
964
43
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
if (value === "xlsx") return "xlsx";
|
|
968
|
-
throw new ImportError("UNSUPPORTED_FORMAT", "Supported template formats are csv and xlsx.");
|
|
969
|
-
}
|
|
970
|
-
function readFields(value) {
|
|
971
|
-
const raw = value === void 0 ? [] : Array.isArray(value) ? value : [value];
|
|
972
|
-
const fields = raw.flatMap((item) => String(item).split(",")).map((item) => item.trim()).filter(Boolean);
|
|
973
|
-
return fields.length ? fields : void 0;
|
|
974
|
-
}
|
|
975
|
-
function templateFilename(entity, format) {
|
|
976
|
-
const safe = String(entity).replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[-.]+/, "").slice(0, 64);
|
|
977
|
-
return `${safe || "import"}-template.${TEMPLATE_EXTENSION[format]}`;
|
|
978
|
-
}
|
|
979
|
-
function applyTimeouts(request, response, limits) {
|
|
980
|
-
const abandon = (message) => () => {
|
|
981
|
-
request.destroy(new ImportError("TIMEOUT", message));
|
|
982
|
-
};
|
|
983
|
-
const socket = request.socket;
|
|
984
|
-
const previous = socket?.timeout;
|
|
985
|
-
request.setTimeout(limits.idleTimeoutMs, abandon("The upload stalled."));
|
|
986
|
-
const total = setTimeout(abandon("The upload took too long."), limits.totalTimeoutMs);
|
|
987
|
-
total.unref?.();
|
|
988
|
-
response.on("close", () => {
|
|
989
|
-
clearTimeout(total);
|
|
990
|
-
if (socket && !socket.destroyed) socket.setTimeout(previous ?? 0);
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
function sendFailure(response, error) {
|
|
994
|
-
if (response.headersSent) {
|
|
995
|
-
response.destroy();
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
for (const header of ["Content-Type", "Content-Disposition", "Content-Length"]) {
|
|
999
|
-
response.removeHeader(header);
|
|
1000
|
-
}
|
|
1001
|
-
const { status, body } = toErrorBody(error);
|
|
1002
|
-
response.status(status).json(body);
|
|
1003
|
-
}
|
|
44
|
+
var templateFilename = node.templateFilename;
|
|
45
|
+
var sendFailure = node.sendFailure;
|
|
1004
46
|
function createImportRouter(options) {
|
|
1005
|
-
const limits = resolveLimits(options.limits);
|
|
1006
|
-
const router = express__default.default.Router();
|
|
1007
47
|
if (typeof options.configs !== "function") {
|
|
1008
48
|
for (const [entity, config] of Object.entries(options.configs)) {
|
|
1009
49
|
checkLookups(entity, config, options.lookups);
|
|
1010
50
|
}
|
|
1011
51
|
}
|
|
1012
52
|
const languageOf = (request) => typeof options.lang === "function" ? options.lang(request) : options.lang ?? "en";
|
|
1013
|
-
const
|
|
1014
|
-
const entity = String(request.params["entity"] ?? "");
|
|
53
|
+
const bindingFor = async (entity, request) => {
|
|
1015
54
|
const config = typeof options.configs === "function" ? await options.configs(entity, request) : ownValue(options.configs, entity);
|
|
1016
|
-
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.");
|
|
1017
56
|
if (typeof options.configs === "function") checkLookups(entity, config, options.lookups);
|
|
1018
|
-
|
|
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
|
+
};
|
|
1019
75
|
};
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
planProblems: result.planProblems,
|
|
1030
|
-
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 })
|
|
1031
85
|
});
|
|
1032
|
-
const route = (handler) => (request, response) => {
|
|
1033
|
-
applyTimeouts(request, response, limits);
|
|
1034
|
-
handler(request, response).catch((error) => sendFailure(response, error));
|
|
1035
|
-
};
|
|
1036
|
-
router.get(
|
|
1037
|
-
"/:entity/template",
|
|
1038
|
-
route(async (request, response) => {
|
|
1039
|
-
const { entity, config } = await configFor(request);
|
|
1040
|
-
const format = readFormat(request.query["format"]);
|
|
1041
|
-
const spec = core.buildTemplateSpec(config, {
|
|
1042
|
-
lang: languageOf(request),
|
|
1043
|
-
fields: readFields(request.query["fields"])
|
|
1044
|
-
});
|
|
1045
|
-
response.setHeader("Content-Type", TEMPLATE_MEDIA_TYPE[format]);
|
|
1046
|
-
response.setHeader(
|
|
1047
|
-
"Content-Disposition",
|
|
1048
|
-
`attachment; filename="${templateFilename(config.entity || entity, format)}"`
|
|
1049
|
-
);
|
|
1050
|
-
await writeTemplate({ spec, format, out: response, limits });
|
|
1051
|
-
})
|
|
1052
|
-
);
|
|
1053
|
-
router.post(
|
|
1054
|
-
"/:entity/preview",
|
|
1055
|
-
route(async (request, response) => {
|
|
1056
|
-
const { config } = await configFor(request);
|
|
1057
|
-
const upload = await receiveUpload(request, limits);
|
|
1058
|
-
const preview = await previewSheet({
|
|
1059
|
-
stream: upload.stream,
|
|
1060
|
-
filename: upload.filename,
|
|
1061
|
-
config,
|
|
1062
|
-
lang: languageOf(request),
|
|
1063
|
-
limits
|
|
1064
|
-
});
|
|
1065
|
-
const body = {
|
|
1066
|
-
headers: preview.headers,
|
|
1067
|
-
sample: preview.sample,
|
|
1068
|
-
suggestion: preview.suggestion,
|
|
1069
|
-
rowCount: preview.rowCount,
|
|
1070
|
-
arrayBound: preview.arrayBound,
|
|
1071
|
-
...preview.delimiter ? { delimiter: preview.delimiter } : {},
|
|
1072
|
-
// The order every import through this router is read in, so the client reads its
|
|
1073
|
-
// sample the same way and can refuse to commit when it was told another.
|
|
1074
|
-
...options.dateOrder ? { dateOrder: options.dateOrder } : {},
|
|
1075
|
-
engineVersion: core.CORE_VERSION
|
|
1076
|
-
};
|
|
1077
|
-
response.json(body);
|
|
1078
|
-
})
|
|
1079
|
-
);
|
|
1080
|
-
router.post(
|
|
1081
|
-
"/:entity/validate",
|
|
1082
|
-
route(async (request, response) => {
|
|
1083
|
-
response.json(await execute(request, options, limits, languageOf, void 0));
|
|
1084
|
-
})
|
|
1085
|
-
);
|
|
1086
|
-
router.post(
|
|
1087
|
-
"/:entity/import",
|
|
1088
|
-
route(async (request, response) => {
|
|
1089
|
-
const onBatch = async (records, info, entity, config) => {
|
|
1090
|
-
await options.onImport(records, { ...info, entity, config, request });
|
|
1091
|
-
};
|
|
1092
|
-
response.json(await execute(request, options, limits, languageOf, onBatch));
|
|
1093
|
-
})
|
|
1094
|
-
);
|
|
1095
|
-
async function execute(request, routerOptions, resolved, language, write) {
|
|
1096
|
-
const { entity, config } = await configFor(request);
|
|
1097
|
-
const upload = await receiveUpload(request, resolved);
|
|
1098
|
-
let plan;
|
|
1099
|
-
try {
|
|
1100
|
-
plan = readPlanField(upload.fields);
|
|
1101
|
-
} catch (error) {
|
|
1102
|
-
destroySource(upload.stream);
|
|
1103
|
-
throw error;
|
|
1104
|
-
}
|
|
1105
|
-
const result = await runImport({
|
|
1106
|
-
stream: upload.stream,
|
|
1107
|
-
filename: upload.filename,
|
|
1108
|
-
plan,
|
|
1109
|
-
config,
|
|
1110
|
-
rules: routerOptions.rules && ownValue(routerOptions.rules, entity),
|
|
1111
|
-
lookups: routerOptions.lookups,
|
|
1112
|
-
dateOrder: routerOptions.dateOrder,
|
|
1113
|
-
lang: language(request),
|
|
1114
|
-
limits: resolved,
|
|
1115
|
-
onBatch: write && ((records, info) => write(records, info, entity, config))
|
|
1116
|
-
});
|
|
1117
|
-
return respond(result, write !== void 0);
|
|
1118
|
-
}
|
|
1119
|
-
return router;
|
|
1120
86
|
}
|
|
1121
87
|
|
|
1122
88
|
exports.createImportRouter = createImportRouter;
|