@yedoma-labs/ysyr-import 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,517 @@
1
+ // src/columns.ts
2
+ /** Trimmed text. */
3
+ function string(options = {}) {
4
+ return {
5
+ required: options.required,
6
+ parse: (raw) => raw.trim()
7
+ };
8
+ }
9
+ /** Whole number within the safe integer range, e.g. `-42` or `+7`. */
10
+ function int(options = {}) {
11
+ return {
12
+ required: options.required,
13
+ parse: (raw) => {
14
+ const text = raw.trim();
15
+ if (!/^[+-]?\d+$/.test(text)) throw new Error("expected an integer");
16
+ const value = Number(text);
17
+ if (!Number.isSafeInteger(value)) throw new Error("integer is outside the safe range");
18
+ return value;
19
+ }
20
+ };
21
+ }
22
+ /** Finite decimal number with `.` as decimal separator, e.g. `12`, `-0.5`, `1e3`. */
23
+ function number(options = {}) {
24
+ return {
25
+ required: options.required,
26
+ parse: (raw) => {
27
+ const text = raw.trim();
28
+ if (!/^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(text)) throw new Error("expected a number");
29
+ const value = Number(text);
30
+ if (!Number.isFinite(value)) throw new Error("expected a finite number");
31
+ return value;
32
+ }
33
+ };
34
+ }
35
+ /** Accepts `true`, `false`, `1` or `0` (case-insensitive). */
36
+ function boolean(options = {}) {
37
+ return {
38
+ required: options.required,
39
+ parse: (raw) => {
40
+ const text = raw.trim().toLowerCase();
41
+ if (text === "true" || text === "1") return true;
42
+ if (text === "false" || text === "0") return false;
43
+ throw new Error("expected true, false, 1 or 0");
44
+ }
45
+ };
46
+ }
47
+ /** ISO-8601 calendar date `YYYY-MM-DD`, returned as a `Date` at UTC midnight. */
48
+ function date(options = {}) {
49
+ return {
50
+ required: options.required,
51
+ parse: (raw) => {
52
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw.trim());
53
+ if (!match) throw new Error("expected a date in YYYY-MM-DD format");
54
+ const year = Number(match[1]);
55
+ const month = Number(match[2]);
56
+ const day = Number(match[3]);
57
+ const value = /* @__PURE__ */ new Date(0);
58
+ value.setUTCFullYear(year, month - 1, day);
59
+ if (value.getUTCFullYear() !== year || value.getUTCMonth() !== month - 1 || value.getUTCDate() !== day) throw new Error("expected a valid calendar date");
60
+ return value;
61
+ }
62
+ };
63
+ }
64
+ /**
65
+ * One of the given values, compared case-sensitively after trimming.
66
+ * Named `enumOf` because `enum` is a reserved word in JavaScript.
67
+ */
68
+ function enumOf(values, options = {}) {
69
+ if (values.length === 0) throw new TypeError("enumOf needs at least one value");
70
+ const allowed = new Set(values);
71
+ return {
72
+ required: options.required,
73
+ parse: (raw) => {
74
+ const text = raw.trim();
75
+ if (!allowed.has(text)) throw new Error(`expected one of: ${values.join(", ")}`);
76
+ return text;
77
+ }
78
+ };
79
+ }
80
+ // src/errors.ts
81
+ /**
82
+ * Typed error that aborts the whole import. Thrown for a missing required column,
83
+ * for exceeded input limits, and for a header line with broken quoting.
84
+ */
85
+ var ImportError = class extends Error {
86
+ code;
87
+ columns;
88
+ limit;
89
+ max;
90
+ constructor(code, message, details = {}) {
91
+ super(message);
92
+ this.name = "ImportError";
93
+ this.code = code;
94
+ this.columns = details.columns ?? [];
95
+ this.limit = details.limit;
96
+ this.max = details.max;
97
+ }
98
+ };
99
+ function limitExceeded(limit, max) {
100
+ return new ImportError("LIMIT_EXCEEDED", `input exceeds ${limit} (${max})`, {
101
+ limit,
102
+ max
103
+ });
104
+ }
105
+ // src/csv-parser.ts
106
+ const BOM = 65279;
107
+ const DQUOTE = 34;
108
+ const CR = 13;
109
+ const LF = 10;
110
+ const DEFAULT_MAX_BYTES = 10485760;
111
+ const DEFAULT_MAX_ROWS = 1e5;
112
+ const DEFAULT_MAX_FIELD_LENGTH = 1e4;
113
+ const AUTO_DELIMITERS = [
114
+ ",",
115
+ ";",
116
+ " "
117
+ ];
118
+ /**
119
+ * Parses CSV text into data records (RFC 4180 quoting, `\n`, `\r\n` and `\r` line endings,
120
+ * UTF-8 BOM removed). The header line is consumed and not returned.
121
+ */
122
+ function parseCsvRecords(text, options = {}) {
123
+ return [...readCsv(text, options).records];
124
+ }
125
+ /** Parses CSV text into header names and data records. Used by `importRows`. */
126
+ function readCsv(text, options = {}) {
127
+ const limits = resolveLimits(options);
128
+ if (byteLength(text) > limits.maxBytes) throw limitExceeded("maxBytes", limits.maxBytes);
129
+ const src = text.charCodeAt(0) === BOM ? text.slice(1) : text;
130
+ const raw = scanRecords(src, resolveDelimiter(src, options));
131
+ let header;
132
+ let dataRaw;
133
+ const explicit = Array.isArray(options.header) ? options.header : void 0;
134
+ if (explicit !== void 0) {
135
+ header = explicit;
136
+ dataRaw = raw;
137
+ } else {
138
+ if (options.header !== void 0 && options.header !== true) throw new TypeError("header must be true or an array of column names");
139
+ const first = raw[0];
140
+ if (first?.quoteError !== void 0) throw new ImportError("INVALID_HEADER", "header line has broken quoting; the columns cannot be determined");
141
+ header = first ? first.values.map((name) => name.trim()) : [];
142
+ dataRaw = raw.slice(1);
143
+ }
144
+ assertFieldLengths(header, limits);
145
+ const records = dataRaw.map((record, index) => ({
146
+ row: index + 1,
147
+ line: record.line,
148
+ values: record.values,
149
+ quoteError: record.quoteError
150
+ }));
151
+ enforceRowLimits(records, limits);
152
+ return {
153
+ header,
154
+ records
155
+ };
156
+ }
157
+ /** Throws LIMIT_EXCEEDED if the rows or any field exceed the configured limits. */
158
+ function enforceRowLimits(records, limits) {
159
+ if (records.length > limits.maxRows) throw limitExceeded("maxRows", limits.maxRows);
160
+ for (const record of records) assertFieldLengths(record.values, limits);
161
+ }
162
+ function resolveLimits(options) {
163
+ return {
164
+ maxBytes: nonNegativeInteger(options.maxBytes, DEFAULT_MAX_BYTES, "maxBytes"),
165
+ maxRows: nonNegativeInteger(options.maxRows, DEFAULT_MAX_ROWS, "maxRows"),
166
+ maxFieldLength: nonNegativeInteger(options.maxFieldLength, DEFAULT_MAX_FIELD_LENGTH, "maxFieldLength")
167
+ };
168
+ }
169
+ /** UTF-8 length of a string, computed without encoding it. */
170
+ function byteLength(text) {
171
+ let bytes = 0;
172
+ for (let i = 0; i < text.length; i++) {
173
+ const code = text.charCodeAt(i);
174
+ if (code < 128) bytes += 1;
175
+ else if (code < 2048) bytes += 2;
176
+ else if (code >= 55296 && code <= 56319 && isLowSurrogate(text.charCodeAt(i + 1))) {
177
+ bytes += 4;
178
+ i++;
179
+ } else bytes += 3;
180
+ }
181
+ return bytes;
182
+ }
183
+ function isLowSurrogate(code) {
184
+ return code >= 56320 && code <= 57343;
185
+ }
186
+ function nonNegativeInteger(value, fallback, name) {
187
+ if (value === void 0) return fallback;
188
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} must be a non-negative integer`);
189
+ return value;
190
+ }
191
+ function assertFieldLengths(values, limits) {
192
+ for (const value of values) if (value.length > limits.maxFieldLength) throw limitExceeded("maxFieldLength", limits.maxFieldLength);
193
+ }
194
+ function resolveDelimiter(text, options) {
195
+ if (options.autoDetectDelimiter === true) {
196
+ if (options.delimiter !== void 0) throw new TypeError("use either delimiter or autoDetectDelimiter, not both");
197
+ return detectDelimiter(text);
198
+ }
199
+ const delimiter = options.delimiter ?? ",";
200
+ if (delimiter.length !== 1 || delimiter === "\"" || delimiter === "\r" || delimiter === "\n") throw new TypeError("delimiter must be a single character other than quote or newline");
201
+ return delimiter.charCodeAt(0);
202
+ }
203
+ /**
204
+ * Picks the candidate that occurs most often outside quotes in the first line.
205
+ * Ties are resolved in the order `,` `;` tab. Falls back to `,` if none occurs.
206
+ */
207
+ function detectDelimiter(text) {
208
+ const counts = AUTO_DELIMITERS.map(() => 0);
209
+ let inQuotes = false;
210
+ for (let i = 0; i < text.length; i++) {
211
+ const ch = text.charAt(i);
212
+ if (ch === "\"") inQuotes = !inQuotes;
213
+ else if (ch === "\n" || ch === "\r") {
214
+ if (!inQuotes) break;
215
+ } else if (!inQuotes) {
216
+ const index = AUTO_DELIMITERS.indexOf(ch);
217
+ if (index >= 0) counts[index] = (counts[index] ?? 0) + 1;
218
+ }
219
+ }
220
+ let best = 0;
221
+ for (let i = 1; i < counts.length; i++) if ((counts[i] ?? 0) > (counts[best] ?? 0)) best = i;
222
+ return (counts[best] ?? 0) === 0 ? 44 : (AUTO_DELIMITERS[best] ?? ",").charCodeAt(0);
223
+ }
224
+ function isTerminator(code, delimiter) {
225
+ return code === delimiter || code === LF || code === CR;
226
+ }
227
+ /**
228
+ * Splits the text into records. Blank lines are skipped. Quoting errors do not stop the scan:
229
+ * the affected record is flagged with `quoteError` and parsing continues on the next line.
230
+ */
231
+ function scanRecords(text, delimiter) {
232
+ const out = [];
233
+ const n = text.length;
234
+ let i = 0;
235
+ let line = 1;
236
+ while (i < n) {
237
+ const startLine = line;
238
+ const values = [];
239
+ let quoteError;
240
+ let quoted = false;
241
+ let done = false;
242
+ while (!done) {
243
+ let value;
244
+ if (text.charCodeAt(i) === DQUOTE) {
245
+ quoted = true;
246
+ i++;
247
+ let parts = "";
248
+ let segment = i;
249
+ let closed = false;
250
+ while (i < n) {
251
+ const code = text.charCodeAt(i);
252
+ if (code === DQUOTE) {
253
+ if (text.charCodeAt(i + 1) === DQUOTE) {
254
+ parts += text.slice(segment, i + 1);
255
+ i += 2;
256
+ segment = i;
257
+ continue;
258
+ }
259
+ parts += text.slice(segment, i);
260
+ i++;
261
+ closed = true;
262
+ break;
263
+ }
264
+ if (code === LF) line++;
265
+ else if (code === CR) {
266
+ if (text.charCodeAt(i + 1) === LF) i++;
267
+ line++;
268
+ }
269
+ i++;
270
+ }
271
+ if (!closed) {
272
+ parts += text.slice(segment, n);
273
+ quoteError ??= "unterminated quoted field";
274
+ } else if (i < n && !isTerminator(text.charCodeAt(i), delimiter)) {
275
+ quoteError ??= "unexpected character after closing quote";
276
+ const start = i;
277
+ while (i < n && !isTerminator(text.charCodeAt(i), delimiter)) i++;
278
+ parts += text.slice(start, i);
279
+ }
280
+ value = parts;
281
+ } else {
282
+ const start = i;
283
+ while (i < n && !isTerminator(text.charCodeAt(i), delimiter)) i++;
284
+ value = text.slice(start, i);
285
+ }
286
+ values.push(value);
287
+ if (i >= n) done = true;
288
+ else if (text.charCodeAt(i) === delimiter) i++;
289
+ else {
290
+ i += text.charCodeAt(i) === CR && text.charCodeAt(i + 1) === LF ? 2 : 1;
291
+ line++;
292
+ done = true;
293
+ }
294
+ }
295
+ if (!(!quoted && values.length === 1 && values[0] === "" && quoteError === void 0)) out.push({
296
+ line: startLine,
297
+ values,
298
+ quoteError
299
+ });
300
+ }
301
+ return out;
302
+ }
303
+ // src/sanitize.ts
304
+ /** Maximum length of any message or column name placed into a `RowError`. */
305
+ const MAX_TEXT_LENGTH = 100;
306
+ /**
307
+ * True for control characters, line/paragraph separators and bidi controls. Such characters are
308
+ * replaced by a space so that messages cannot inject log lines or spoof surrounding output.
309
+ */
310
+ function isUnsafe(code) {
311
+ return code <= 31 || code >= 127 && code <= 159 || code === 8232 || code === 8233 || code >= 8234 && code <= 8238 || code >= 8294 && code <= 8297;
312
+ }
313
+ /** Replaces unsafe characters and truncates to at most 100 characters (with a trailing ellipsis). */
314
+ function sanitizeText(text) {
315
+ let clean = "";
316
+ for (let i = 0; i < text.length; i++) {
317
+ const code = text.charCodeAt(i);
318
+ clean += isUnsafe(code) ? " " : text.charAt(i);
319
+ }
320
+ const chars = Array.from(clean);
321
+ if (chars.length <= MAX_TEXT_LENGTH) return clean;
322
+ return `${chars.slice(0, 99).join("")}…`;
323
+ }
324
+ // src/import-rows.ts
325
+ /**
326
+ * Imports CSV rows with partial success. Valid rows go to `accepted`, broken rows are collected
327
+ * in `rejected` with row number, line number, column and reason.
328
+ *
329
+ * Throws `ImportError` for whole-file problems (missing required column, limits exceeded).
330
+ * Storing the accepted rows is up to the caller.
331
+ */
332
+ function importRows(options) {
333
+ if (options.text !== void 0 === (options.records !== void 0)) throw new TypeError("importRows: pass exactly one of `text` or `records`");
334
+ let header;
335
+ let records;
336
+ if (options.text !== void 0) {
337
+ const doc = readCsv(options.text, options);
338
+ header = doc.header;
339
+ records = doc.records;
340
+ } else {
341
+ if (!Array.isArray(options.header)) throw new TypeError("importRows: `header` with the column names is required for `records`");
342
+ header = options.header;
343
+ records = options.records ?? [];
344
+ enforceRowLimits(records, resolveLimits(options));
345
+ }
346
+ const entries = Object.entries(options.columns);
347
+ const headerIndex = /* @__PURE__ */ new Map();
348
+ header.forEach((name, index) => {
349
+ if (!headerIndex.has(name)) headerIndex.set(name, index);
350
+ });
351
+ const missing = entries.filter(([key, rule]) => rule.required === true && !headerIndex.has(key)).map(([key]) => key);
352
+ if (missing.length > 0) throw new ImportError("MISSING_COLUMN", `required column(s) missing from header: ${missing.join(", ")}`, { columns: missing });
353
+ const schemaKeys = new Set(entries.map(([key]) => key));
354
+ const unmapped = [];
355
+ header.forEach((name, index) => {
356
+ const primary = headerIndex.get(name) === index;
357
+ if (!primary || !schemaKeys.has(name)) unmapped.push({
358
+ index,
359
+ name: sanitizeText(name),
360
+ duplicate: !primary
361
+ });
362
+ });
363
+ const context = {
364
+ entries,
365
+ headerIndex,
366
+ headerLength: header.length,
367
+ unmapped,
368
+ rejectUnknown: options.unknownColumns === "reject",
369
+ validate: options.validate
370
+ };
371
+ const accepted = [];
372
+ const rejected = [];
373
+ let rejectedRows = 0;
374
+ for (const record of records) {
375
+ const { errors, value } = checkRecord(record, context);
376
+ if (value !== void 0) {
377
+ accepted.push(value);
378
+ options.onRow?.({
379
+ ok: true,
380
+ row: record.row,
381
+ line: record.line,
382
+ value
383
+ });
384
+ } else {
385
+ for (const error of errors) rejected.push(error);
386
+ rejectedRows++;
387
+ options.onRow?.({
388
+ ok: false,
389
+ row: record.row,
390
+ line: record.line,
391
+ errors
392
+ });
393
+ }
394
+ }
395
+ return {
396
+ accepted,
397
+ rejected,
398
+ summary: {
399
+ total: records.length,
400
+ accepted: accepted.length,
401
+ rejected: rejectedRows
402
+ }
403
+ };
404
+ }
405
+ function checkRecord(record, context) {
406
+ const base = {
407
+ row: record.row,
408
+ line: record.line
409
+ };
410
+ if (record.quoteError !== void 0) return {
411
+ errors: [{
412
+ ...base,
413
+ code: "PARSE_QUOTE",
414
+ message: sanitizeText(record.quoteError)
415
+ }],
416
+ value: void 0
417
+ };
418
+ if (record.values.length !== context.headerLength) {
419
+ const message = `expected ${context.headerLength} fields, found ${record.values.length}`;
420
+ return {
421
+ errors: [{
422
+ ...base,
423
+ code: "WRONG_FIELD_COUNT",
424
+ message
425
+ }],
426
+ value: void 0
427
+ };
428
+ }
429
+ const errors = [];
430
+ const row = {};
431
+ for (const [key, rule] of context.entries) {
432
+ const index = context.headerIndex.get(key);
433
+ const raw = index === void 0 ? "" : record.values[index] ?? "";
434
+ if (raw.trim() === "") {
435
+ if (rule.required === true) errors.push({
436
+ ...base,
437
+ column: key,
438
+ code: "REQUIRED",
439
+ message: "value is required"
440
+ });
441
+ row[key] = void 0;
442
+ continue;
443
+ }
444
+ try {
445
+ row[key] = rule.parse(raw);
446
+ } catch (error) {
447
+ errors.push({
448
+ ...base,
449
+ column: sanitizeText(key),
450
+ code: "PARSE",
451
+ message: sanitizeText(messageOf(error))
452
+ });
453
+ }
454
+ }
455
+ if (context.rejectUnknown) {
456
+ for (const column of context.unmapped) if ((record.values[column.index] ?? "").trim() !== "") errors.push({
457
+ ...base,
458
+ column: column.name,
459
+ code: "UNKNOWN_COLUMN",
460
+ message: column.duplicate ? "duplicate column name" : "column is not part of the schema"
461
+ });
462
+ }
463
+ if (errors.length > 0) return {
464
+ errors,
465
+ value: void 0
466
+ };
467
+ const value = row;
468
+ if (context.validate !== void 0) try {
469
+ const message = context.validate(value);
470
+ if (message) errors.push({
471
+ ...base,
472
+ code: "VALIDATION",
473
+ message: sanitizeText(message)
474
+ });
475
+ } catch (error) {
476
+ errors.push({
477
+ ...base,
478
+ code: "VALIDATION",
479
+ message: sanitizeText(messageOf(error))
480
+ });
481
+ }
482
+ return {
483
+ errors,
484
+ value: errors.length === 0 ? value : void 0
485
+ };
486
+ }
487
+ function messageOf(error) {
488
+ return error instanceof Error ? error.message : "invalid value";
489
+ }
490
+ // src/report.ts
491
+ const REPORT_HEADER = "line,row,column,code,message";
492
+ /**
493
+ * Renders rejected rows as CSV with the header `line,row,column,code,message` and CRLF line endings.
494
+ *
495
+ * CSV injection protection: any text cell starting with `=`, `+`, `-`, `@`, tab or CR is prefixed
496
+ * with `'`, so spreadsheet applications do not evaluate it as a formula. Cells are quoted per
497
+ * RFC 4180 when they contain a comma, quote or line break.
498
+ */
499
+ function toErrorReportCsv(rejected) {
500
+ const lines = [REPORT_HEADER];
501
+ for (const error of rejected) lines.push([
502
+ String(error.line),
503
+ String(error.row),
504
+ escapeCell(error.column ?? ""),
505
+ escapeCell(error.code),
506
+ escapeCell(error.message)
507
+ ].join(","));
508
+ return `${lines.join("\r\n")}\r\n`;
509
+ }
510
+ function escapeCell(value) {
511
+ const guarded = /^[=+\-@\t\r]/.test(value) ? `'${value}` : value;
512
+ if (/[",\r\n]/.test(guarded)) return `"${guarded.replace(/"/g, "\"\"")}"`;
513
+ return guarded;
514
+ }
515
+ export { ImportError, boolean, date, enumOf, importRows, int, number, parseCsvRecords, string, toErrorReportCsv };
516
+
517
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/columns.ts","../src/errors.ts","../src/csv-parser.ts","../src/sanitize.ts","../src/import-rows.ts","../src/report.ts"],"sourcesContent":["/**\n * Rule for one column. `parse` receives the raw cell text and returns the typed value or throws.\n * The thrown error's message is reported as a `PARSE` row error.\n */\nexport interface ColumnRule<T = unknown, R extends boolean = boolean> {\n\t/** Empty cells are a `REQUIRED` error. Optional columns yield `undefined` for empty cells. */\n\treadonly required?: R;\n\treadonly parse: (raw: string) => T;\n}\n\nexport interface ColumnOptions<R extends boolean = boolean> {\n\treadonly required?: R;\n}\n\n/** Column name to rule. */\nexport type Schema = Readonly<Record<string, ColumnRule<unknown, boolean>>>;\n\ntype ValueOf<R> = R extends { readonly parse: (raw: string) => infer T } ? T : never;\ntype IsRequired<R> = R extends { readonly required?: infer Q }\n\t? [NonNullable<Q>] extends [true]\n\t\t? true\n\t\t: false\n\t: false;\n\n/** Row type produced by a schema: required columns are `T`, optional columns `T | undefined`. */\nexport type InferRow<S extends Schema> = {\n\t-readonly [K in keyof S]: IsRequired<S[K]> extends true\n\t\t? ValueOf<S[K]>\n\t\t: ValueOf<S[K]> | undefined;\n};\n\n/** Trimmed text. */\nexport function string<const R extends boolean = false>(\n\toptions: ColumnOptions<R> = {},\n): ColumnRule<string, R> {\n\treturn { required: options.required, parse: (raw) => raw.trim() };\n}\n\n/** Whole number within the safe integer range, e.g. `-42` or `+7`. */\nexport function int<const R extends boolean = false>(\n\toptions: ColumnOptions<R> = {},\n): ColumnRule<number, R> {\n\treturn {\n\t\trequired: options.required,\n\t\tparse: (raw) => {\n\t\t\tconst text = raw.trim();\n\t\t\tif (!/^[+-]?\\d+$/.test(text)) {\n\t\t\t\tthrow new Error(\"expected an integer\");\n\t\t\t}\n\t\t\tconst value = Number(text);\n\t\t\tif (!Number.isSafeInteger(value)) {\n\t\t\t\tthrow new Error(\"integer is outside the safe range\");\n\t\t\t}\n\t\t\treturn value;\n\t\t},\n\t};\n}\n\n/** Finite decimal number with `.` as decimal separator, e.g. `12`, `-0.5`, `1e3`. */\nexport function number<const R extends boolean = false>(\n\toptions: ColumnOptions<R> = {},\n): ColumnRule<number, R> {\n\treturn {\n\t\trequired: options.required,\n\t\tparse: (raw) => {\n\t\t\tconst text = raw.trim();\n\t\t\tif (!/^[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][+-]?\\d+)?$/.test(text)) {\n\t\t\t\tthrow new Error(\"expected a number\");\n\t\t\t}\n\t\t\tconst value = Number(text);\n\t\t\tif (!Number.isFinite(value)) {\n\t\t\t\tthrow new Error(\"expected a finite number\");\n\t\t\t}\n\t\t\treturn value;\n\t\t},\n\t};\n}\n\n/** Accepts `true`, `false`, `1` or `0` (case-insensitive). */\nexport function boolean<const R extends boolean = false>(\n\toptions: ColumnOptions<R> = {},\n): ColumnRule<boolean, R> {\n\treturn {\n\t\trequired: options.required,\n\t\tparse: (raw) => {\n\t\t\tconst text = raw.trim().toLowerCase();\n\t\t\tif (text === \"true\" || text === \"1\") {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (text === \"false\" || text === \"0\") {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tthrow new Error(\"expected true, false, 1 or 0\");\n\t\t},\n\t};\n}\n\n/** ISO-8601 calendar date `YYYY-MM-DD`, returned as a `Date` at UTC midnight. */\nexport function date<const R extends boolean = false>(\n\toptions: ColumnOptions<R> = {},\n): ColumnRule<Date, R> {\n\treturn {\n\t\trequired: options.required,\n\t\tparse: (raw) => {\n\t\t\tconst match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(raw.trim());\n\t\t\tif (!match) {\n\t\t\t\tthrow new Error(\"expected a date in YYYY-MM-DD format\");\n\t\t\t}\n\t\t\tconst year = Number(match[1]);\n\t\t\tconst month = Number(match[2]);\n\t\t\tconst day = Number(match[3]);\n\t\t\tconst value = new Date(0);\n\t\t\tvalue.setUTCFullYear(year, month - 1, day);\n\t\t\tif (\n\t\t\t\tvalue.getUTCFullYear() !== year ||\n\t\t\t\tvalue.getUTCMonth() !== month - 1 ||\n\t\t\t\tvalue.getUTCDate() !== day\n\t\t\t) {\n\t\t\t\tthrow new Error(\"expected a valid calendar date\");\n\t\t\t}\n\t\t\treturn value;\n\t\t},\n\t};\n}\n\n/**\n * One of the given values, compared case-sensitively after trimming.\n * Named `enumOf` because `enum` is a reserved word in JavaScript.\n */\nexport function enumOf<const V extends readonly string[], const R extends boolean = false>(\n\tvalues: V,\n\toptions: ColumnOptions<R> = {},\n): ColumnRule<V[number], R> {\n\tif (values.length === 0) {\n\t\tthrow new TypeError(\"enumOf needs at least one value\");\n\t}\n\tconst allowed = new Set<string>(values);\n\treturn {\n\t\trequired: options.required,\n\t\tparse: (raw) => {\n\t\t\tconst text = raw.trim();\n\t\t\tif (!allowed.has(text)) {\n\t\t\t\tthrow new Error(`expected one of: ${values.join(\", \")}`);\n\t\t\t}\n\t\t\treturn text as V[number];\n\t\t},\n\t};\n}\n","/** Whole-file error codes. Row-level problems are reported as `RowError`, never thrown. */\nexport type ImportErrorCode = \"MISSING_COLUMN\" | \"LIMIT_EXCEEDED\" | \"INVALID_HEADER\";\n\n/** Input limits that can be exceeded. */\nexport type LimitName = \"maxBytes\" | \"maxRows\" | \"maxFieldLength\";\n\nexport interface ImportErrorDetails {\n\t/** MISSING_COLUMN: names of the required columns that are absent from the header. */\n\treadonly columns?: readonly string[];\n\t/** LIMIT_EXCEEDED: the limit that was exceeded. */\n\treadonly limit?: LimitName;\n\t/** LIMIT_EXCEEDED: the configured maximum. */\n\treadonly max?: number;\n}\n\n/**\n * Typed error that aborts the whole import. Thrown for a missing required column,\n * for exceeded input limits, and for a header line with broken quoting.\n */\nexport class ImportError extends Error {\n\treadonly code: ImportErrorCode;\n\treadonly columns: readonly string[];\n\treadonly limit: LimitName | undefined;\n\treadonly max: number | undefined;\n\n\tconstructor(code: ImportErrorCode, message: string, details: ImportErrorDetails = {}) {\n\t\tsuper(message);\n\t\tthis.name = \"ImportError\";\n\t\tthis.code = code;\n\t\tthis.columns = details.columns ?? [];\n\t\tthis.limit = details.limit;\n\t\tthis.max = details.max;\n\t}\n}\n\nexport function limitExceeded(limit: LimitName, max: number): ImportError {\n\treturn new ImportError(\"LIMIT_EXCEEDED\", `input exceeds ${limit} (${max})`, { limit, max });\n}\n","import { ImportError, limitExceeded } from \"./errors\";\n\nconst BOM = 0xfeff;\nconst DQUOTE = 0x22;\nconst CR = 0x0d;\nconst LF = 0x0a;\n\nconst DEFAULT_MAX_BYTES = 10 * 1024 * 1024;\nconst DEFAULT_MAX_ROWS = 100_000;\nconst DEFAULT_MAX_FIELD_LENGTH = 10_000;\n\nconst AUTO_DELIMITERS = [\",\", \";\", \"\\t\"] as const;\n\n/** One data record as produced by the parser. */\nexport interface CsvRecord {\n\t/** Data row number, starting at 1. The header line is not counted. Blank lines are skipped. */\n\treadonly row: number;\n\t/** Physical line on which the record starts, starting at 1. */\n\treadonly line: number;\n\t/** Raw field values in column order. */\n\treadonly values: readonly string[];\n\t/**\n\t * Set when the record has broken quoting (unterminated quoted field at end of input, or\n\t * characters after a closing quote). `values` then holds a best-effort recovery.\n\t */\n\treadonly quoteError?: string;\n}\n\nexport interface LimitOptions {\n\t/** Maximum input size in UTF-8 bytes. Default 10 MiB. */\n\treadonly maxBytes?: number;\n\t/** Maximum number of data rows. Default 100000. */\n\treadonly maxRows?: number;\n\t/** Maximum length of a single field in UTF-16 code units. Default 10000. */\n\treadonly maxFieldLength?: number;\n}\n\nexport interface ParseCsvOptions extends LimitOptions {\n\t/** Field delimiter, a single character. Default \",\". */\n\treadonly delimiter?: string;\n\t/** Detect the delimiter among `,` `;` and tab from the first line. Off by default. */\n\treadonly autoDetectDelimiter?: boolean;\n\t/**\n\t * `true` (default): the first record is the header line.\n\t * An array: the input has no header line and these are the column names.\n\t */\n\treadonly header?: true | readonly string[];\n}\n\nexport interface CsvDocument {\n\treadonly header: readonly string[];\n\treadonly records: readonly CsvRecord[];\n}\n\nexport interface Limits {\n\treadonly maxBytes: number;\n\treadonly maxRows: number;\n\treadonly maxFieldLength: number;\n}\n\ninterface RawRecord {\n\treadonly line: number;\n\treadonly values: string[];\n\treadonly quoteError: string | undefined;\n}\n\n/**\n * Parses CSV text into data records (RFC 4180 quoting, `\\n`, `\\r\\n` and `\\r` line endings,\n * UTF-8 BOM removed). The header line is consumed and not returned.\n */\nexport function parseCsvRecords(text: string, options: ParseCsvOptions = {}): CsvRecord[] {\n\treturn [...readCsv(text, options).records];\n}\n\n/** Parses CSV text into header names and data records. Used by `importRows`. */\nexport function readCsv(text: string, options: ParseCsvOptions = {}): CsvDocument {\n\tconst limits = resolveLimits(options);\n\tif (byteLength(text) > limits.maxBytes) {\n\t\tthrow limitExceeded(\"maxBytes\", limits.maxBytes);\n\t}\n\n\tconst src = text.charCodeAt(0) === BOM ? text.slice(1) : text;\n\tconst delimiter = resolveDelimiter(src, options);\n\tconst raw = scanRecords(src, delimiter);\n\n\tlet header: readonly string[];\n\tlet dataRaw: RawRecord[];\n\tconst explicit = Array.isArray(options.header) ? options.header : undefined;\n\tif (explicit !== undefined) {\n\t\theader = explicit;\n\t\tdataRaw = raw;\n\t} else {\n\t\tif (options.header !== undefined && options.header !== true) {\n\t\t\tthrow new TypeError(\"header must be true or an array of column names\");\n\t\t}\n\t\tconst first = raw[0];\n\t\tif (first?.quoteError !== undefined) {\n\t\t\tthrow new ImportError(\n\t\t\t\t\"INVALID_HEADER\",\n\t\t\t\t\"header line has broken quoting; the columns cannot be determined\",\n\t\t\t);\n\t\t}\n\t\theader = first ? first.values.map((name) => name.trim()) : [];\n\t\tdataRaw = raw.slice(1);\n\t}\n\tassertFieldLengths(header, limits);\n\n\tconst records: CsvRecord[] = dataRaw.map((record, index) => ({\n\t\trow: index + 1,\n\t\tline: record.line,\n\t\tvalues: record.values,\n\t\tquoteError: record.quoteError,\n\t}));\n\tenforceRowLimits(records, limits);\n\treturn { header, records };\n}\n\n/** Throws LIMIT_EXCEEDED if the rows or any field exceed the configured limits. */\nexport function enforceRowLimits(records: readonly CsvRecord[], limits: Limits): void {\n\tif (records.length > limits.maxRows) {\n\t\tthrow limitExceeded(\"maxRows\", limits.maxRows);\n\t}\n\tfor (const record of records) {\n\t\tassertFieldLengths(record.values, limits);\n\t}\n}\n\nexport function resolveLimits(options: LimitOptions): Limits {\n\treturn {\n\t\tmaxBytes: nonNegativeInteger(options.maxBytes, DEFAULT_MAX_BYTES, \"maxBytes\"),\n\t\tmaxRows: nonNegativeInteger(options.maxRows, DEFAULT_MAX_ROWS, \"maxRows\"),\n\t\tmaxFieldLength: nonNegativeInteger(\n\t\t\toptions.maxFieldLength,\n\t\t\tDEFAULT_MAX_FIELD_LENGTH,\n\t\t\t\"maxFieldLength\",\n\t\t),\n\t};\n}\n\n/** UTF-8 length of a string, computed without encoding it. */\nexport function byteLength(text: string): number {\n\tlet bytes = 0;\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst code = text.charCodeAt(i);\n\t\tif (code < 0x80) {\n\t\t\tbytes += 1;\n\t\t} else if (code < 0x800) {\n\t\t\tbytes += 2;\n\t\t} else if (code >= 0xd800 && code <= 0xdbff && isLowSurrogate(text.charCodeAt(i + 1))) {\n\t\t\tbytes += 4;\n\t\t\ti++;\n\t\t} else {\n\t\t\tbytes += 3;\n\t\t}\n\t}\n\treturn bytes;\n}\n\nfunction isLowSurrogate(code: number): boolean {\n\treturn code >= 0xdc00 && code <= 0xdfff;\n}\n\nfunction nonNegativeInteger(value: number | undefined, fallback: number, name: string): number {\n\tif (value === undefined) {\n\t\treturn fallback;\n\t}\n\tif (!Number.isSafeInteger(value) || value < 0) {\n\t\tthrow new TypeError(`${name} must be a non-negative integer`);\n\t}\n\treturn value;\n}\n\nfunction assertFieldLengths(values: readonly string[], limits: Limits): void {\n\tfor (const value of values) {\n\t\tif (value.length > limits.maxFieldLength) {\n\t\t\tthrow limitExceeded(\"maxFieldLength\", limits.maxFieldLength);\n\t\t}\n\t}\n}\n\nfunction resolveDelimiter(text: string, options: ParseCsvOptions): number {\n\tif (options.autoDetectDelimiter === true) {\n\t\tif (options.delimiter !== undefined) {\n\t\t\tthrow new TypeError(\"use either delimiter or autoDetectDelimiter, not both\");\n\t\t}\n\t\treturn detectDelimiter(text);\n\t}\n\tconst delimiter = options.delimiter ?? \",\";\n\tif (delimiter.length !== 1 || delimiter === '\"' || delimiter === \"\\r\" || delimiter === \"\\n\") {\n\t\tthrow new TypeError(\"delimiter must be a single character other than quote or newline\");\n\t}\n\treturn delimiter.charCodeAt(0);\n}\n\n/**\n * Picks the candidate that occurs most often outside quotes in the first line.\n * Ties are resolved in the order `,` `;` tab. Falls back to `,` if none occurs.\n */\nfunction detectDelimiter(text: string): number {\n\tconst counts = AUTO_DELIMITERS.map(() => 0);\n\tlet inQuotes = false;\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst ch = text.charAt(i);\n\t\tif (ch === '\"') {\n\t\t\tinQuotes = !inQuotes;\n\t\t} else if (ch === \"\\n\" || ch === \"\\r\") {\n\t\t\tif (!inQuotes) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} else if (!inQuotes) {\n\t\t\tconst index = AUTO_DELIMITERS.indexOf(ch as (typeof AUTO_DELIMITERS)[number]);\n\t\t\tif (index >= 0) {\n\t\t\t\tcounts[index] = (counts[index] ?? 0) + 1;\n\t\t\t}\n\t\t}\n\t}\n\tlet best = 0;\n\tfor (let i = 1; i < counts.length; i++) {\n\t\tif ((counts[i] ?? 0) > (counts[best] ?? 0)) {\n\t\t\tbest = i;\n\t\t}\n\t}\n\treturn (counts[best] ?? 0) === 0 ? 0x2c : (AUTO_DELIMITERS[best] ?? \",\").charCodeAt(0);\n}\n\nfunction isTerminator(code: number, delimiter: number): boolean {\n\treturn code === delimiter || code === LF || code === CR;\n}\n\n/**\n * Splits the text into records. Blank lines are skipped. Quoting errors do not stop the scan:\n * the affected record is flagged with `quoteError` and parsing continues on the next line.\n */\nfunction scanRecords(text: string, delimiter: number): RawRecord[] {\n\tconst out: RawRecord[] = [];\n\tconst n = text.length;\n\tlet i = 0;\n\tlet line = 1;\n\n\twhile (i < n) {\n\t\tconst startLine = line;\n\t\tconst values: string[] = [];\n\t\tlet quoteError: string | undefined;\n\t\tlet quoted = false;\n\t\tlet done = false;\n\n\t\twhile (!done) {\n\t\t\tlet value: string;\n\t\t\tif (text.charCodeAt(i) === DQUOTE) {\n\t\t\t\tquoted = true;\n\t\t\t\ti++;\n\t\t\t\tlet parts = \"\";\n\t\t\t\tlet segment = i;\n\t\t\t\tlet closed = false;\n\t\t\t\twhile (i < n) {\n\t\t\t\t\tconst code = text.charCodeAt(i);\n\t\t\t\t\tif (code === DQUOTE) {\n\t\t\t\t\t\tif (text.charCodeAt(i + 1) === DQUOTE) {\n\t\t\t\t\t\t\tparts += text.slice(segment, i + 1);\n\t\t\t\t\t\t\ti += 2;\n\t\t\t\t\t\t\tsegment = i;\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tparts += text.slice(segment, i);\n\t\t\t\t\t\ti++;\n\t\t\t\t\t\tclosed = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tif (code === LF) {\n\t\t\t\t\t\tline++;\n\t\t\t\t\t} else if (code === CR) {\n\t\t\t\t\t\tif (text.charCodeAt(i + 1) === LF) {\n\t\t\t\t\t\t\ti++;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tline++;\n\t\t\t\t\t}\n\t\t\t\t\ti++;\n\t\t\t\t}\n\t\t\t\tif (!closed) {\n\t\t\t\t\tparts += text.slice(segment, n);\n\t\t\t\t\tquoteError ??= \"unterminated quoted field\";\n\t\t\t\t} else if (i < n && !isTerminator(text.charCodeAt(i), delimiter)) {\n\t\t\t\t\tquoteError ??= \"unexpected character after closing quote\";\n\t\t\t\t\tconst start = i;\n\t\t\t\t\twhile (i < n && !isTerminator(text.charCodeAt(i), delimiter)) {\n\t\t\t\t\t\ti++;\n\t\t\t\t\t}\n\t\t\t\t\tparts += text.slice(start, i);\n\t\t\t\t}\n\t\t\t\tvalue = parts;\n\t\t\t} else {\n\t\t\t\tconst start = i;\n\t\t\t\twhile (i < n && !isTerminator(text.charCodeAt(i), delimiter)) {\n\t\t\t\t\ti++;\n\t\t\t\t}\n\t\t\t\tvalue = text.slice(start, i);\n\t\t\t}\n\t\t\tvalues.push(value);\n\n\t\t\tif (i >= n) {\n\t\t\t\tdone = true;\n\t\t\t} else if (text.charCodeAt(i) === delimiter) {\n\t\t\t\ti++;\n\t\t\t} else {\n\t\t\t\ti += text.charCodeAt(i) === CR && text.charCodeAt(i + 1) === LF ? 2 : 1;\n\t\t\t\tline++;\n\t\t\t\tdone = true;\n\t\t\t}\n\t\t}\n\n\t\tconst blank = !quoted && values.length === 1 && values[0] === \"\" && quoteError === undefined;\n\t\tif (!blank) {\n\t\t\tout.push({ line: startLine, values, quoteError });\n\t\t}\n\t}\n\treturn out;\n}\n","/** Maximum length of any message or column name placed into a `RowError`. */\nconst MAX_TEXT_LENGTH = 100;\n\n/**\n * True for control characters, line/paragraph separators and bidi controls. Such characters are\n * replaced by a space so that messages cannot inject log lines or spoof surrounding output.\n */\nfunction isUnsafe(code: number): boolean {\n\treturn (\n\t\tcode <= 0x1f ||\n\t\t(code >= 0x7f && code <= 0x9f) ||\n\t\tcode === 0x2028 ||\n\t\tcode === 0x2029 ||\n\t\t(code >= 0x202a && code <= 0x202e) ||\n\t\t(code >= 0x2066 && code <= 0x2069)\n\t);\n}\n\n/** Replaces unsafe characters and truncates to at most 100 characters (with a trailing ellipsis). */\nexport function sanitizeText(text: string): string {\n\tlet clean = \"\";\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst code = text.charCodeAt(i);\n\t\tclean += isUnsafe(code) ? \" \" : text.charAt(i);\n\t}\n\tconst chars = Array.from(clean);\n\tif (chars.length <= MAX_TEXT_LENGTH) {\n\t\treturn clean;\n\t}\n\treturn `${chars.slice(0, MAX_TEXT_LENGTH - 1).join(\"\")}…`;\n}\n","import type { ColumnRule, InferRow, Schema } from \"./columns\";\nimport type { CsvRecord, ParseCsvOptions } from \"./csv-parser\";\nimport { enforceRowLimits, readCsv, resolveLimits } from \"./csv-parser\";\nimport { ImportError } from \"./errors\";\nimport { sanitizeText } from \"./sanitize\";\nimport type { ImportSummary, RowError, RowOutcome } from \"./types\";\n\nexport interface ImportOptions<S extends Schema> extends ParseCsvOptions {\n\t/** Column schema. Keys are matched against the header names. */\n\treadonly columns: S;\n\t/** CSV text to parse. Exactly one of `text` or `records` is required. */\n\treadonly text?: string;\n\t/**\n\t * Records from `parseCsvRecords`. Requires `header` with the column names, because the\n\t * records do not carry them.\n\t */\n\treadonly records?: readonly CsvRecord[];\n\t/**\n\t * `ignore` (default): unknown columns are skipped.\n\t * `reject`: a non-empty value in an unknown column is an `UNKNOWN_COLUMN` row error.\n\t */\n\treadonly unknownColumns?: \"ignore\" | \"reject\";\n\t/** Row-level check, run only for rows that passed all column checks. Returns an error message. */\n\treadonly validate?: (row: InferRow<S>) => string | undefined;\n\t/** Called once per row in input order, after the row has been checked. */\n\treadonly onRow?: (outcome: RowOutcome<InferRow<S>>) => void;\n}\n\nexport interface ImportResult<T> {\n\treadonly accepted: T[];\n\t/** All row errors. A row can contribute several entries, one per problem. */\n\treadonly rejected: RowError[];\n\treadonly summary: ImportSummary;\n}\n\ninterface UnmappedColumn {\n\treadonly index: number;\n\treadonly name: string;\n\treadonly duplicate: boolean;\n}\n\ninterface RowContext<S extends Schema> {\n\treadonly entries: readonly [string, ColumnRule<unknown, boolean>][];\n\treadonly headerIndex: ReadonlyMap<string, number>;\n\treadonly headerLength: number;\n\treadonly unmapped: readonly UnmappedColumn[];\n\treadonly rejectUnknown: boolean;\n\treadonly validate: ((row: InferRow<S>) => string | undefined) | undefined;\n}\n\n/**\n * Imports CSV rows with partial success. Valid rows go to `accepted`, broken rows are collected\n * in `rejected` with row number, line number, column and reason.\n *\n * Throws `ImportError` for whole-file problems (missing required column, limits exceeded).\n * Storing the accepted rows is up to the caller.\n */\nexport function importRows<const S extends Schema>(\n\toptions: ImportOptions<S>,\n): ImportResult<InferRow<S>> {\n\tconst hasText = options.text !== undefined;\n\tconst hasRecords = options.records !== undefined;\n\tif (hasText === hasRecords) {\n\t\tthrow new TypeError(\"importRows: pass exactly one of `text` or `records`\");\n\t}\n\n\tlet header: readonly string[];\n\tlet records: readonly CsvRecord[];\n\tif (options.text !== undefined) {\n\t\tconst doc = readCsv(options.text, options);\n\t\theader = doc.header;\n\t\trecords = doc.records;\n\t} else {\n\t\tif (!Array.isArray(options.header)) {\n\t\t\tthrow new TypeError(\"importRows: `header` with the column names is required for `records`\");\n\t\t}\n\t\theader = options.header;\n\t\trecords = options.records ?? [];\n\t\tenforceRowLimits(records, resolveLimits(options));\n\t}\n\n\tconst entries = Object.entries(options.columns) as [string, ColumnRule<unknown, boolean>][];\n\tconst headerIndex = new Map<string, number>();\n\theader.forEach((name, index) => {\n\t\tif (!headerIndex.has(name)) {\n\t\t\theaderIndex.set(name, index);\n\t\t}\n\t});\n\n\tconst missing = entries\n\t\t.filter(([key, rule]) => rule.required === true && !headerIndex.has(key))\n\t\t.map(([key]) => key);\n\tif (missing.length > 0) {\n\t\tthrow new ImportError(\n\t\t\t\"MISSING_COLUMN\",\n\t\t\t`required column(s) missing from header: ${missing.join(\", \")}`,\n\t\t\t{ columns: missing },\n\t\t);\n\t}\n\n\tconst schemaKeys = new Set(entries.map(([key]) => key));\n\tconst unmapped: UnmappedColumn[] = [];\n\theader.forEach((name, index) => {\n\t\tconst primary = headerIndex.get(name) === index;\n\t\tif (!primary || !schemaKeys.has(name)) {\n\t\t\tunmapped.push({ index, name: sanitizeText(name), duplicate: !primary });\n\t\t}\n\t});\n\n\tconst context: RowContext<S> = {\n\t\tentries,\n\t\theaderIndex,\n\t\theaderLength: header.length,\n\t\tunmapped,\n\t\trejectUnknown: options.unknownColumns === \"reject\",\n\t\tvalidate: options.validate,\n\t};\n\n\tconst accepted: InferRow<S>[] = [];\n\tconst rejected: RowError[] = [];\n\tlet rejectedRows = 0;\n\tfor (const record of records) {\n\t\tconst { errors, value } = checkRecord(record, context);\n\t\tif (value !== undefined) {\n\t\t\taccepted.push(value);\n\t\t\toptions.onRow?.({ ok: true, row: record.row, line: record.line, value });\n\t\t} else {\n\t\t\tfor (const error of errors) {\n\t\t\t\trejected.push(error);\n\t\t\t}\n\t\t\trejectedRows++;\n\t\t\toptions.onRow?.({ ok: false, row: record.row, line: record.line, errors });\n\t\t}\n\t}\n\n\treturn {\n\t\taccepted,\n\t\trejected,\n\t\tsummary: { total: records.length, accepted: accepted.length, rejected: rejectedRows },\n\t};\n}\n\nfunction checkRecord<S extends Schema>(\n\trecord: CsvRecord,\n\tcontext: RowContext<S>,\n): { errors: RowError[]; value: InferRow<S> | undefined } {\n\tconst base = { row: record.row, line: record.line };\n\n\tif (record.quoteError !== undefined) {\n\t\treturn {\n\t\t\terrors: [{ ...base, code: \"PARSE_QUOTE\", message: sanitizeText(record.quoteError) }],\n\t\t\tvalue: undefined,\n\t\t};\n\t}\n\tif (record.values.length !== context.headerLength) {\n\t\tconst message = `expected ${context.headerLength} fields, found ${record.values.length}`;\n\t\treturn { errors: [{ ...base, code: \"WRONG_FIELD_COUNT\", message }], value: undefined };\n\t}\n\n\tconst errors: RowError[] = [];\n\tconst row: Record<string, unknown> = {};\n\n\tfor (const [key, rule] of context.entries) {\n\t\tconst index = context.headerIndex.get(key);\n\t\tconst raw = index === undefined ? \"\" : (record.values[index] ?? \"\");\n\t\tif (raw.trim() === \"\") {\n\t\t\tif (rule.required === true) {\n\t\t\t\terrors.push({ ...base, column: key, code: \"REQUIRED\", message: \"value is required\" });\n\t\t\t}\n\t\t\trow[key] = undefined;\n\t\t\tcontinue;\n\t\t}\n\t\ttry {\n\t\t\trow[key] = rule.parse(raw);\n\t\t} catch (error) {\n\t\t\terrors.push({\n\t\t\t\t...base,\n\t\t\t\tcolumn: sanitizeText(key),\n\t\t\t\tcode: \"PARSE\",\n\t\t\t\tmessage: sanitizeText(messageOf(error)),\n\t\t\t});\n\t\t}\n\t}\n\n\tif (context.rejectUnknown) {\n\t\tfor (const column of context.unmapped) {\n\t\t\tconst raw = record.values[column.index] ?? \"\";\n\t\t\tif (raw.trim() !== \"\") {\n\t\t\t\terrors.push({\n\t\t\t\t\t...base,\n\t\t\t\t\tcolumn: column.name,\n\t\t\t\t\tcode: \"UNKNOWN_COLUMN\",\n\t\t\t\t\tmessage: column.duplicate ? \"duplicate column name\" : \"column is not part of the schema\",\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tif (errors.length > 0) {\n\t\treturn { errors, value: undefined };\n\t}\n\n\tconst value = row as InferRow<S>;\n\tif (context.validate !== undefined) {\n\t\ttry {\n\t\t\tconst message = context.validate(value);\n\t\t\tif (message) {\n\t\t\t\terrors.push({ ...base, code: \"VALIDATION\", message: sanitizeText(message) });\n\t\t\t}\n\t\t} catch (error) {\n\t\t\terrors.push({ ...base, code: \"VALIDATION\", message: sanitizeText(messageOf(error)) });\n\t\t}\n\t}\n\treturn { errors, value: errors.length === 0 ? value : undefined };\n}\n\nfunction messageOf(error: unknown): string {\n\treturn error instanceof Error ? error.message : \"invalid value\";\n}\n","import type { RowError } from \"./types\";\n\nconst REPORT_HEADER = \"line,row,column,code,message\";\n\n/**\n * Renders rejected rows as CSV with the header `line,row,column,code,message` and CRLF line endings.\n *\n * CSV injection protection: any text cell starting with `=`, `+`, `-`, `@`, tab or CR is prefixed\n * with `'`, so spreadsheet applications do not evaluate it as a formula. Cells are quoted per\n * RFC 4180 when they contain a comma, quote or line break.\n */\nexport function toErrorReportCsv(rejected: readonly RowError[]): string {\n\tconst lines: string[] = [REPORT_HEADER];\n\tfor (const error of rejected) {\n\t\tlines.push(\n\t\t\t[\n\t\t\t\tString(error.line),\n\t\t\t\tString(error.row),\n\t\t\t\tescapeCell(error.column ?? \"\"),\n\t\t\t\tescapeCell(error.code),\n\t\t\t\tescapeCell(error.message),\n\t\t\t].join(\",\"),\n\t\t);\n\t}\n\treturn `${lines.join(\"\\r\\n\")}\\r\\n`;\n}\n\nfunction escapeCell(value: string): string {\n\tconst guarded = /^[=+\\-@\\t\\r]/.test(value) ? `'${value}` : value;\n\tif (/[\",\\r\\n]/.test(guarded)) {\n\t\treturn `\"${guarded.replace(/\"/g, '\"\"')}\"`;\n\t}\n\treturn guarded;\n}\n"],"mappings":"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"}