csv-stream-lite 0.1.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/EXAMPLES.md +197 -0
- package/LICENSE +21 -0
- package/README.md +273 -0
- package/dist/byte-buffer.d.ts +112 -0
- package/dist/byte-buffer.js +282 -0
- package/dist/defaults.d.ts +8 -0
- package/dist/defaults.js +8 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +30 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/parser.d.ts +293 -0
- package/dist/parser.js +596 -0
- package/dist/stringify.d.ts +91 -0
- package/dist/stringify.js +186 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +20 -0
- package/package.json +69 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import { ByteBuffer } from './byte-buffer.js';
|
|
2
|
+
import { DEFAULT_CHUNK_SIZE } from './defaults.js';
|
|
3
|
+
import { TooFewColumnsError, TooManyColumnsError } from './errors.js';
|
|
4
|
+
import { CsvStringify } from './stringify.js';
|
|
5
|
+
import { bytesToString } from './utils.js';
|
|
6
|
+
const UTF_8_BOM = [0xef, 0xbb, 0xbf];
|
|
7
|
+
/**
|
|
8
|
+
* Map of character names to their byte values for CSV parsing.
|
|
9
|
+
* Contains commonly used characters in CSV syntax.
|
|
10
|
+
*/
|
|
11
|
+
const BYTE_MAP = {
|
|
12
|
+
quotes: 34,
|
|
13
|
+
comma: 44,
|
|
14
|
+
backslash: 92,
|
|
15
|
+
space: 32,
|
|
16
|
+
tab: 9,
|
|
17
|
+
carriageReturn: 13,
|
|
18
|
+
lineFeed: 10,
|
|
19
|
+
};
|
|
20
|
+
const isLineEnd = (byte) => {
|
|
21
|
+
return byte === BYTE_MAP.lineFeed || byte === BYTE_MAP.carriageReturn;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Abstract base class for CSV entities that supports both synchronous and asynchronous parsing.
|
|
25
|
+
* Provides common functionality for reading and streaming CSV data.
|
|
26
|
+
*
|
|
27
|
+
* @typeParam T - The type returned by read operations
|
|
28
|
+
* @typeParam S - The type yielded by stream operations (defaults to T)
|
|
29
|
+
*/
|
|
30
|
+
export class CsvEntity {
|
|
31
|
+
byteBuffer;
|
|
32
|
+
separator = ',';
|
|
33
|
+
escapeChar = '"';
|
|
34
|
+
consumed = false;
|
|
35
|
+
/**
|
|
36
|
+
* Creates a new CSV entity.
|
|
37
|
+
*
|
|
38
|
+
* @param asyncIterable - Optional byte stream or buffer to parse
|
|
39
|
+
* @param options - Configuration options for parsing
|
|
40
|
+
*/
|
|
41
|
+
constructor(asyncIterable, options) {
|
|
42
|
+
this.byteBuffer =
|
|
43
|
+
asyncIterable instanceof ByteBuffer
|
|
44
|
+
? asyncIterable
|
|
45
|
+
: new ByteBuffer(asyncIterable);
|
|
46
|
+
if (options?.separator) {
|
|
47
|
+
this.separator = options.separator;
|
|
48
|
+
}
|
|
49
|
+
if (options?.escapeChar) {
|
|
50
|
+
this.escapeChar = options.escapeChar;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
set maxBufferSize(size) {
|
|
54
|
+
this.byteBuffer.maxBufferSize = size;
|
|
55
|
+
}
|
|
56
|
+
get maxBufferSize() {
|
|
57
|
+
return this.byteBuffer.maxBufferSize;
|
|
58
|
+
}
|
|
59
|
+
set allowBufferToBeExceeded(value) {
|
|
60
|
+
this.byteBuffer.allowBufferToBeExceeded = value;
|
|
61
|
+
}
|
|
62
|
+
get allowBufferToBeExceeded() {
|
|
63
|
+
return this.byteBuffer.allowBufferToBeExceeded;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Reads and parses the entire entity synchronously.
|
|
67
|
+
* Marks the entity as consumed after reading.
|
|
68
|
+
*
|
|
69
|
+
* @returns The parsed result of type T
|
|
70
|
+
*/
|
|
71
|
+
read() {
|
|
72
|
+
const res = this.parse();
|
|
73
|
+
this.consumed = true;
|
|
74
|
+
return res;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Reads and parses the entire entity asynchronously.
|
|
78
|
+
* Marks the entity as consumed after reading.
|
|
79
|
+
*
|
|
80
|
+
* @returns A promise that resolves to the parsed result of type T
|
|
81
|
+
*/
|
|
82
|
+
async readAsync() {
|
|
83
|
+
const res = await this.parseAsync();
|
|
84
|
+
this.consumed = true;
|
|
85
|
+
return res;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Returns a synchronous generator that yields chunks of type S.
|
|
89
|
+
* Marks the entity as consumed when iteration completes.
|
|
90
|
+
*
|
|
91
|
+
* @returns A generator that yields values of type S
|
|
92
|
+
*/
|
|
93
|
+
*stream() {
|
|
94
|
+
const res = yield* this.streamImpl();
|
|
95
|
+
this.consumed = true;
|
|
96
|
+
return res;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Returns an asynchronous generator that yields chunks of type S.
|
|
100
|
+
* Handles buffering and automatically reads more data as needed.
|
|
101
|
+
*
|
|
102
|
+
* @returns An async generator that yields values of type S
|
|
103
|
+
*/
|
|
104
|
+
async *streamAsync() {
|
|
105
|
+
while (true) {
|
|
106
|
+
const stream = this.stream();
|
|
107
|
+
let currentChunk = undefined;
|
|
108
|
+
while (true) {
|
|
109
|
+
currentChunk = this.byteBuffer.resetOnFail(() => stream.next());
|
|
110
|
+
if (currentChunk === undefined) {
|
|
111
|
+
break; // Need more data
|
|
112
|
+
}
|
|
113
|
+
if (currentChunk.done) {
|
|
114
|
+
return; // No more data
|
|
115
|
+
}
|
|
116
|
+
yield currentChunk.value;
|
|
117
|
+
}
|
|
118
|
+
await this.byteBuffer.readStreamAsync();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Consumes the entity if it hasn't been consumed yet.
|
|
123
|
+
* This ensures the buffer advances to the end of this entity.
|
|
124
|
+
*/
|
|
125
|
+
consume() {
|
|
126
|
+
if (!this.consumed) {
|
|
127
|
+
this.read();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Asynchronously consumes the entity if it hasn't been consumed yet.
|
|
132
|
+
* This ensures the buffer advances to the end of this entity.
|
|
133
|
+
*
|
|
134
|
+
* @returns A promise that resolves when consumption is complete
|
|
135
|
+
*/
|
|
136
|
+
async consumeAsync() {
|
|
137
|
+
if (!this.consumed) {
|
|
138
|
+
await this.readAsync();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Makes this entity iterable using the synchronous stream.
|
|
143
|
+
*
|
|
144
|
+
* @returns A generator that yields values of type S
|
|
145
|
+
*/
|
|
146
|
+
[Symbol.iterator]() {
|
|
147
|
+
return this.stream();
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Makes this entity async iterable using the asynchronous stream.
|
|
151
|
+
*
|
|
152
|
+
* @returns An async generator that yields values of type S
|
|
153
|
+
*/
|
|
154
|
+
[Symbol.asyncIterator]() {
|
|
155
|
+
return this.streamAsync();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Represents a single CSV cell that can be read as a string or streamed in chunks.
|
|
160
|
+
* Handles quoted cells and escape sequences according to CSV standards.
|
|
161
|
+
*/
|
|
162
|
+
export class CsvCell extends CsvEntity {
|
|
163
|
+
chunkSize = DEFAULT_CHUNK_SIZE;
|
|
164
|
+
endOfLineReached = false;
|
|
165
|
+
parse() {
|
|
166
|
+
let str = '';
|
|
167
|
+
for (const part of this) {
|
|
168
|
+
str += part;
|
|
169
|
+
}
|
|
170
|
+
return str;
|
|
171
|
+
}
|
|
172
|
+
async parseAsync() {
|
|
173
|
+
let str = '';
|
|
174
|
+
for await (const part of this) {
|
|
175
|
+
str += part;
|
|
176
|
+
}
|
|
177
|
+
return str;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Reads the cell value and transforms it using the provided function.
|
|
181
|
+
* Special handling for Boolean transformer: converts 'true'/'false' strings to boolean.
|
|
182
|
+
*
|
|
183
|
+
* @typeParam T - The type to transform the cell value into
|
|
184
|
+
* @param transform - Function to transform the cell string into type T
|
|
185
|
+
* @returns The transformed value of type T
|
|
186
|
+
*/
|
|
187
|
+
readAs(transform) {
|
|
188
|
+
const cellStr = this.read();
|
|
189
|
+
if (transform === Boolean) {
|
|
190
|
+
return (String(cellStr).toLowerCase() === 'true');
|
|
191
|
+
}
|
|
192
|
+
return transform(cellStr);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Asynchronously reads the cell value and transforms it using the provided function.
|
|
196
|
+
* Special handling for Boolean transformer: converts 'true'/'false' strings to boolean.
|
|
197
|
+
*
|
|
198
|
+
* @typeParam T - The type to transform the cell value into
|
|
199
|
+
* @param transform - Function to transform the cell string into type T (can be async)
|
|
200
|
+
* @returns A promise that resolves to the transformed value of type T
|
|
201
|
+
*/
|
|
202
|
+
async readAsAsync(transform) {
|
|
203
|
+
const cellStr = await this.readAsync();
|
|
204
|
+
if (transform === Boolean) {
|
|
205
|
+
return (cellStr.toLowerCase() === 'true');
|
|
206
|
+
}
|
|
207
|
+
return await transform(cellStr);
|
|
208
|
+
}
|
|
209
|
+
*streamImpl() {
|
|
210
|
+
const separator = this.separator.charCodeAt(0);
|
|
211
|
+
const escapeChar = this.escapeChar.charCodeAt(0);
|
|
212
|
+
let chunk = [];
|
|
213
|
+
let hadData = false;
|
|
214
|
+
let isEscaped = false;
|
|
215
|
+
const next = this.byteBuffer.peek();
|
|
216
|
+
if (next === null && this.byteBuffer.eof) {
|
|
217
|
+
throw new Error('No more data to read');
|
|
218
|
+
}
|
|
219
|
+
if (next === escapeChar) {
|
|
220
|
+
isEscaped = true;
|
|
221
|
+
this.byteBuffer.expect(escapeChar); // consume opening quote
|
|
222
|
+
}
|
|
223
|
+
while (this.byteBuffer.peek() !== null) {
|
|
224
|
+
const next = this.byteBuffer.peek();
|
|
225
|
+
if (isEscaped) {
|
|
226
|
+
if (next === escapeChar) {
|
|
227
|
+
// Possible end of quoted cell
|
|
228
|
+
const lookahead = this.byteBuffer.peek(1);
|
|
229
|
+
if (lookahead === escapeChar) {
|
|
230
|
+
// Escaped quote
|
|
231
|
+
this.byteBuffer.expect(escapeChar); // consume first quote
|
|
232
|
+
this.byteBuffer.expect(escapeChar); // consume second quote
|
|
233
|
+
chunk.push(escapeChar);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// End of quoted cell
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
if (next === separator || isLineEnd(next)) {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const nextByte = this.byteBuffer.next();
|
|
248
|
+
chunk.push(nextByte);
|
|
249
|
+
if (chunk.length >= this.chunkSize) {
|
|
250
|
+
yield bytesToString(new Uint8Array(chunk));
|
|
251
|
+
chunk = [];
|
|
252
|
+
hadData = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (isEscaped) {
|
|
256
|
+
this.byteBuffer.expect(escapeChar); // consume closing quote
|
|
257
|
+
}
|
|
258
|
+
if (this.byteBuffer.peek() === separator) {
|
|
259
|
+
this.byteBuffer.expect(separator); // consume separator
|
|
260
|
+
}
|
|
261
|
+
while (isLineEnd(this.byteBuffer.peek())) {
|
|
262
|
+
this.byteBuffer.next(); // consume line ending
|
|
263
|
+
this.endOfLineReached = true;
|
|
264
|
+
}
|
|
265
|
+
if (!hadData || chunk.length > 0)
|
|
266
|
+
yield bytesToString(new Uint8Array(chunk));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Represents a single CSV row that can be read as an array of strings or streamed as individual cells.
|
|
271
|
+
* Can also be parsed into an object using a shape definition.
|
|
272
|
+
*
|
|
273
|
+
* @typeParam T - The object type when reading as an object
|
|
274
|
+
* @typeParam O - The output type after optional transformation (defaults to T)
|
|
275
|
+
*/
|
|
276
|
+
export class CsvRow extends CsvEntity {
|
|
277
|
+
parse() {
|
|
278
|
+
const cells = [];
|
|
279
|
+
for (const cell of this) {
|
|
280
|
+
cells.push(cell.read());
|
|
281
|
+
}
|
|
282
|
+
return cells;
|
|
283
|
+
}
|
|
284
|
+
async parseAsync() {
|
|
285
|
+
const cells = [];
|
|
286
|
+
for await (const cell of this) {
|
|
287
|
+
cells.push(await cell.readAsync());
|
|
288
|
+
}
|
|
289
|
+
return cells;
|
|
290
|
+
}
|
|
291
|
+
*streamImpl() {
|
|
292
|
+
while (this.byteBuffer.peek() !== null) {
|
|
293
|
+
const cell = new CsvCell(this.byteBuffer, this);
|
|
294
|
+
yield cell;
|
|
295
|
+
cell.consume(); // Advance the buffer
|
|
296
|
+
if (cell.endOfLineReached) {
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Reads the row as an object using the provided shape definition.
|
|
303
|
+
* Handles column count validation and extra cells based on options.
|
|
304
|
+
*
|
|
305
|
+
* @param options - Configuration for reading the row as an object
|
|
306
|
+
* @returns The parsed object of type O
|
|
307
|
+
* @throws {TooManyColumnsError} If strictColumns is true and extra cells are found
|
|
308
|
+
* @throws {TooFewColumnsError} If strictColumns is true and cells are missing
|
|
309
|
+
*/
|
|
310
|
+
readObject(options) {
|
|
311
|
+
let { shape, headers } = options;
|
|
312
|
+
let obj = {};
|
|
313
|
+
let i = 0;
|
|
314
|
+
const includeExtraCells = options?.includeExtraCells ?? false;
|
|
315
|
+
const strictColumns = options?.strictColumns ?? false;
|
|
316
|
+
if (Array.isArray(shape)) {
|
|
317
|
+
shape = shape.reduce((acc, header) => {
|
|
318
|
+
acc[header] = String;
|
|
319
|
+
return acc;
|
|
320
|
+
}, {});
|
|
321
|
+
}
|
|
322
|
+
for (const cell of this) {
|
|
323
|
+
if (!includeExtraCells && i >= headers.length) {
|
|
324
|
+
if (strictColumns) {
|
|
325
|
+
throw new TooManyColumnsError(`Extra cells found${options?.rowIndex ? ` in row ${options.rowIndex}` : ''} but strictColumns is enabled`);
|
|
326
|
+
}
|
|
327
|
+
cell.consume();
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
const header = i >= headers.length
|
|
331
|
+
? `extra_cell_${i - headers.length + 1}`
|
|
332
|
+
: headers[i];
|
|
333
|
+
obj[header] = cell.read();
|
|
334
|
+
}
|
|
335
|
+
i++;
|
|
336
|
+
if (cell.endOfLineReached) {
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (; i < headers.length; i++) {
|
|
341
|
+
if (strictColumns) {
|
|
342
|
+
throw new TooFewColumnsError(`Not enough cells${options?.rowIndex ? ` in row ${options.rowIndex}` : ''} to match headers when strictColumns is enabled`);
|
|
343
|
+
}
|
|
344
|
+
const header = headers[i];
|
|
345
|
+
obj[header] = undefined;
|
|
346
|
+
}
|
|
347
|
+
if (options.transform) {
|
|
348
|
+
obj = options.transform(obj);
|
|
349
|
+
}
|
|
350
|
+
for (const transform in shape) {
|
|
351
|
+
const transformer = shape[transform];
|
|
352
|
+
if (obj[transform] !== undefined) {
|
|
353
|
+
if (transformer === Boolean) {
|
|
354
|
+
obj[transform] = String(obj[transform]) === 'true';
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
obj[transform] = transformer(obj[transform]);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
this.consumed = true;
|
|
362
|
+
return obj;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Asynchronously reads the row as an object using the provided shape definition.
|
|
366
|
+
* Automatically handles buffer refills as needed.
|
|
367
|
+
*
|
|
368
|
+
* @param options - Configuration for reading the row as an object
|
|
369
|
+
* @returns A promise that resolves to the parsed object of type O
|
|
370
|
+
*/
|
|
371
|
+
async readObjectAsync(options) {
|
|
372
|
+
while (true) {
|
|
373
|
+
const value = this.byteBuffer.resetOnFail(() => {
|
|
374
|
+
return this.readObject(options);
|
|
375
|
+
});
|
|
376
|
+
if (value !== undefined) {
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
await this.byteBuffer.readStreamAsync();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Main CSV parser class for parsing complete CSV documents.
|
|
385
|
+
* Supports reading headers, streaming rows, and converting rows to objects.
|
|
386
|
+
*
|
|
387
|
+
* @typeParam T - The object type for each row when reading as objects
|
|
388
|
+
* @typeParam O - The output type after optional transformation (defaults to T)
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* ```typescript
|
|
392
|
+
* // Parse CSV with headers
|
|
393
|
+
* const csv = new Csv(fileStream)
|
|
394
|
+
* for await (const row of csv.streamObjectsAsync()) {
|
|
395
|
+
* console.log(row)
|
|
396
|
+
* }
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
export class Csv extends CsvEntity {
|
|
400
|
+
includeExtraCells = false;
|
|
401
|
+
ignoreUtf8Bom = true;
|
|
402
|
+
headers;
|
|
403
|
+
shape;
|
|
404
|
+
readHeaders = true;
|
|
405
|
+
strictColumns = false;
|
|
406
|
+
transform;
|
|
407
|
+
/**
|
|
408
|
+
* Creates a new CSV parser.
|
|
409
|
+
*
|
|
410
|
+
* @param asyncIterable - Optional byte stream or buffer containing CSV data
|
|
411
|
+
* @param options - Configuration options for CSV parsing
|
|
412
|
+
* @throws {Error} If both headers and shape options are specified
|
|
413
|
+
*/
|
|
414
|
+
constructor(asyncIterable, options) {
|
|
415
|
+
super(asyncIterable, options);
|
|
416
|
+
if (options?.includeExtraCells) {
|
|
417
|
+
this.includeExtraCells = options.includeExtraCells;
|
|
418
|
+
}
|
|
419
|
+
if (options?.ignoreUtf8Bom !== undefined) {
|
|
420
|
+
this.ignoreUtf8Bom = options.ignoreUtf8Bom;
|
|
421
|
+
}
|
|
422
|
+
if (options?.headers) {
|
|
423
|
+
this.headers = options.headers;
|
|
424
|
+
}
|
|
425
|
+
if (options?.readHeaders !== undefined) {
|
|
426
|
+
this.readHeaders = options.readHeaders;
|
|
427
|
+
}
|
|
428
|
+
if (options?.strictColumns !== undefined) {
|
|
429
|
+
this.strictColumns = options.strictColumns;
|
|
430
|
+
}
|
|
431
|
+
if (options?.shape && this.headers) {
|
|
432
|
+
throw new Error('Cannot specify both headers and shape options');
|
|
433
|
+
}
|
|
434
|
+
this.shape = options?.shape;
|
|
435
|
+
this.transform = options?.transform;
|
|
436
|
+
}
|
|
437
|
+
readBom() {
|
|
438
|
+
// Skip UTF-8 BOM if present
|
|
439
|
+
while (this.ignoreUtf8Bom &&
|
|
440
|
+
this.byteBuffer.peek() === UTF_8_BOM[0] &&
|
|
441
|
+
this.byteBuffer.peek(1) === UTF_8_BOM[1] &&
|
|
442
|
+
this.byteBuffer.peek(2) === UTF_8_BOM[2]) {
|
|
443
|
+
this.byteBuffer.next();
|
|
444
|
+
this.byteBuffer.next();
|
|
445
|
+
this.byteBuffer.next();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
parse() {
|
|
449
|
+
return Array.from(this.streamObjects());
|
|
450
|
+
}
|
|
451
|
+
async parseAsync() {
|
|
452
|
+
const results = [];
|
|
453
|
+
for await (const obj of this.streamObjectsAsync()) {
|
|
454
|
+
results.push(obj);
|
|
455
|
+
}
|
|
456
|
+
return results;
|
|
457
|
+
}
|
|
458
|
+
*streamImpl() {
|
|
459
|
+
// Skip UTF-8 BOM if present
|
|
460
|
+
this.readBom();
|
|
461
|
+
while (this.byteBuffer.peek() !== null) {
|
|
462
|
+
const row = new CsvRow(this.byteBuffer, this);
|
|
463
|
+
yield row;
|
|
464
|
+
row.consume(); // Advance the buffer
|
|
465
|
+
}
|
|
466
|
+
this.consumed = true;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Synchronously streams CSV rows as objects.
|
|
470
|
+
* Reads headers from the first row if readHeaders is true.
|
|
471
|
+
*
|
|
472
|
+
* @returns A generator that yields parsed objects of type O
|
|
473
|
+
*
|
|
474
|
+
* @example
|
|
475
|
+
* ```typescript
|
|
476
|
+
* const csv = new Csv(csvString)
|
|
477
|
+
* for (const row of csv.streamObjects()) {
|
|
478
|
+
* console.log(row)
|
|
479
|
+
* }
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
*streamObjects() {
|
|
483
|
+
const stream = this.stream();
|
|
484
|
+
let headers = this.headers ?? [];
|
|
485
|
+
if (this.readHeaders) {
|
|
486
|
+
const headerRow = stream.next();
|
|
487
|
+
if (headerRow.done) {
|
|
488
|
+
return; // No data
|
|
489
|
+
}
|
|
490
|
+
const foundHeaders = headerRow.value.read();
|
|
491
|
+
if (headers.length === 0) {
|
|
492
|
+
headers = foundHeaders;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
let rowIndex = this.readHeaders ? 2 : 1;
|
|
496
|
+
while (true) {
|
|
497
|
+
const currentRow = stream.next();
|
|
498
|
+
if (currentRow.done) {
|
|
499
|
+
return; // No more data
|
|
500
|
+
}
|
|
501
|
+
yield currentRow.value.readObject({
|
|
502
|
+
headers: headers,
|
|
503
|
+
shape: this.shape,
|
|
504
|
+
includeExtraCells: this.includeExtraCells,
|
|
505
|
+
strictColumns: this.strictColumns,
|
|
506
|
+
rowIndex: rowIndex++,
|
|
507
|
+
transform: this.transform,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Asynchronously streams CSV rows as objects.
|
|
513
|
+
* Automatically handles buffer refills as needed.
|
|
514
|
+
*
|
|
515
|
+
* @returns An async generator that yields parsed objects of type O
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* ```typescript
|
|
519
|
+
* const csv = new Csv(fileStream)
|
|
520
|
+
* for await (const row of csv.streamObjectsAsync()) {
|
|
521
|
+
* console.log(row)
|
|
522
|
+
* }
|
|
523
|
+
* ```
|
|
524
|
+
*/
|
|
525
|
+
async *streamObjectsAsync() {
|
|
526
|
+
while (true) {
|
|
527
|
+
const stream = this.streamObjects();
|
|
528
|
+
let currentRow = undefined;
|
|
529
|
+
while (true) {
|
|
530
|
+
currentRow = this.byteBuffer.resetOnFail(() => stream.next());
|
|
531
|
+
if (currentRow === undefined) {
|
|
532
|
+
break; // Need more data
|
|
533
|
+
}
|
|
534
|
+
if (currentRow.done) {
|
|
535
|
+
return; // No more data
|
|
536
|
+
}
|
|
537
|
+
yield currentRow.value;
|
|
538
|
+
}
|
|
539
|
+
await this.byteBuffer.readStreamAsync();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Static method to stringify an array of objects into CSV format.
|
|
544
|
+
* Returns a synchronous generator that yields CSV string chunks.
|
|
545
|
+
*
|
|
546
|
+
* @typeParam T - The input object type
|
|
547
|
+
* @typeParam O - The output object type after optional transformation (defaults to T)
|
|
548
|
+
* @param values - Array of objects to convert to CSV
|
|
549
|
+
* @param options - Optional configuration for CSV stringification
|
|
550
|
+
* @returns A generator that yields CSV string chunks
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* ```typescript
|
|
554
|
+
* const data = [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]
|
|
555
|
+
* for (const chunk of Csv.stringify(data, { headers: ['name', 'age'] })) {
|
|
556
|
+
* process.stdout.write(chunk)
|
|
557
|
+
* }
|
|
558
|
+
* ```
|
|
559
|
+
*/
|
|
560
|
+
static stringify(values, options) {
|
|
561
|
+
return new CsvStringify(values, options).toStream();
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Static method to stringify an array of objects into CSV format asynchronously.
|
|
565
|
+
* Returns an asynchronous generator that yields CSV string chunks.
|
|
566
|
+
*
|
|
567
|
+
* @typeParam T - The input object type
|
|
568
|
+
* @typeParam O - The output object type after optional transformation (defaults to T)
|
|
569
|
+
* @param values - Array of objects to convert to CSV
|
|
570
|
+
* @param options - Optional configuration for CSV stringification
|
|
571
|
+
* @returns An async generator that yields CSV string chunks
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* ```typescript
|
|
575
|
+
* const data = [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]
|
|
576
|
+
* for await (const chunk of Csv.stringifyAsync(data)) {
|
|
577
|
+
* process.stdout.write(chunk)
|
|
578
|
+
* }
|
|
579
|
+
* ```
|
|
580
|
+
*/
|
|
581
|
+
static stringifyAsync(values, options) {
|
|
582
|
+
return new CsvStringify(values, options).toStreamAsync();
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Static method to create a CsvStringify instance for advanced usage.
|
|
586
|
+
*
|
|
587
|
+
* @typeParam T - The input object type
|
|
588
|
+
* @typeParam O - The output object type after optional transformation (defaults to T)
|
|
589
|
+
* @param values - Array of objects to convert to CSV
|
|
590
|
+
* @param options - Optional configuration for CSV stringification
|
|
591
|
+
* @returns A CsvStringify instance
|
|
592
|
+
*/
|
|
593
|
+
static stringifier(values, options) {
|
|
594
|
+
return new CsvStringify(values, options);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { CsvObjectShape, CsvString } from './types.js';
|
|
2
|
+
export interface CsvStringifyOptions<T extends object = object, O extends object = T> {
|
|
3
|
+
/** Optional array of headers to use as the first row. Optionally, pass in a CsvObjectShape to be used as headers */
|
|
4
|
+
headers?: string[] | CsvObjectShape<T>;
|
|
5
|
+
/** Character to separate fields (default: ',') */
|
|
6
|
+
delimiter?: string;
|
|
7
|
+
/** Character to escape special characters (default: '"') */
|
|
8
|
+
escapeChar?: string;
|
|
9
|
+
/** Character to quote fields (default: '"') */
|
|
10
|
+
quoteChar?: string;
|
|
11
|
+
/** Newline character (default: '\n') */
|
|
12
|
+
newline?: string;
|
|
13
|
+
/** Whether to write a UTF-8 BOM at the start of the output (default: false) */
|
|
14
|
+
writeBom?: boolean;
|
|
15
|
+
/** Whether to always write headers, even if there are no records (default: false) */
|
|
16
|
+
alwaysWriteHeaders?: boolean;
|
|
17
|
+
/** Optional transform function to modify each record before stringifying */
|
|
18
|
+
transform?: (row: T) => O;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* CSV stringifier class for converting JavaScript objects into CSV format.
|
|
22
|
+
* Supports both synchronous and asynchronous streaming of CSV output.
|
|
23
|
+
*
|
|
24
|
+
* @typeParam T - The input object type
|
|
25
|
+
* @typeParam O - The output object type after optional transformation (defaults to T)
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const data = [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]
|
|
30
|
+
* const stringifier = new CsvStringify(data, { headers: ['name', 'age'] })
|
|
31
|
+
* const csvString = stringifier.toString()
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare class CsvStringify<T extends object = object, O extends object = T> {
|
|
35
|
+
private values;
|
|
36
|
+
headers?: string[];
|
|
37
|
+
delimiter: string;
|
|
38
|
+
escapeChar: string;
|
|
39
|
+
quoteChar: string;
|
|
40
|
+
newline: string;
|
|
41
|
+
writeBom: boolean;
|
|
42
|
+
alwaysWriteHeaders: boolean;
|
|
43
|
+
transform?: (row: T) => O;
|
|
44
|
+
/**
|
|
45
|
+
* Creates a new CSV stringifier.
|
|
46
|
+
*
|
|
47
|
+
* @param values - Iterable or async iterable of objects to stringify
|
|
48
|
+
* @param options - Optional configuration for CSV stringification
|
|
49
|
+
*/
|
|
50
|
+
constructor(values: Iterable<T> | AsyncIterable<T>, options?: CsvStringifyOptions<T, O>);
|
|
51
|
+
private formatField;
|
|
52
|
+
private recordToString;
|
|
53
|
+
private forceWriteHeaders;
|
|
54
|
+
/**
|
|
55
|
+
* Returns a synchronous generator that yields CSV string chunks.
|
|
56
|
+
*
|
|
57
|
+
* @returns A generator that yields CSV strings
|
|
58
|
+
* @throws {Error} If values is not iterable
|
|
59
|
+
*/
|
|
60
|
+
toStream(): Generator<CsvString<O>>;
|
|
61
|
+
/**
|
|
62
|
+
* Returns an asynchronous generator that yields CSV string chunks.
|
|
63
|
+
*
|
|
64
|
+
* @returns An async generator that yields CSV strings
|
|
65
|
+
*/
|
|
66
|
+
toStreamAsync(): AsyncGenerator<CsvString<O>>;
|
|
67
|
+
/**
|
|
68
|
+
* Converts all values to a single CSV string synchronously.
|
|
69
|
+
*
|
|
70
|
+
* @returns The complete CSV string
|
|
71
|
+
*/
|
|
72
|
+
toString(): CsvString<O>;
|
|
73
|
+
/**
|
|
74
|
+
* Converts all values to a single CSV string asynchronously.
|
|
75
|
+
*
|
|
76
|
+
* @returns A promise that resolves to the complete CSV string
|
|
77
|
+
*/
|
|
78
|
+
toStringAsync(): Promise<CsvString<O>>;
|
|
79
|
+
/**
|
|
80
|
+
* Makes this stringifier iterable using the synchronous stream.
|
|
81
|
+
*
|
|
82
|
+
* @returns A generator that yields CSV strings
|
|
83
|
+
*/
|
|
84
|
+
[Symbol.iterator](): Generator<CsvString<O>>;
|
|
85
|
+
/**
|
|
86
|
+
* Makes this stringifier async iterable using the asynchronous stream.
|
|
87
|
+
*
|
|
88
|
+
* @returns An async generator that yields CSV strings
|
|
89
|
+
*/
|
|
90
|
+
[Symbol.asyncIterator](): AsyncGenerator<CsvString<O>>;
|
|
91
|
+
}
|