Skip to content

Commit 50d6613

Browse files
committed
feat(csv-parse): with option columns (fix #461 #464 #466)
1 parent 18bb78a commit 50d6613

File tree

7 files changed

+184
-78
lines changed

7 files changed

+184
-78
lines changed

packages/csv-parse/lib/index.d.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export type ColumnOption<K = string> =
9393
| false
9494
| { name: K };
9595

96-
export interface OptionsNormalized<T = string[]> {
96+
export interface OptionsNormalized<T = string[], U = T> {
9797
auto_parse?: boolean | CastingFunction;
9898
auto_parse_date?: boolean | CastingDateFunction;
9999
/**
@@ -190,7 +190,8 @@ export interface OptionsNormalized<T = string[]> {
190190
/**
191191
* Alter and filter records by executing a user defined function.
192192
*/
193-
on_record?: (record: T, context: InfoRecord) => T | undefined;
193+
on_record?: (record: U, context: InfoRecord) => T | null | undefined;
194+
// on_record?: (record: T, context: InfoRecord) => T | undefined;
194195
/**
195196
* Optional character surrounding a field, one character only, defaults to double quotes.
196197
*/
@@ -258,7 +259,7 @@ Note, could not `extends stream.TransformOptions` because encoding can be
258259
BufferEncoding and undefined as well as null which is not defined in the
259260
extended type.
260261
*/
261-
export interface Options<T = string[]> {
262+
export interface Options<T = string[], U = T> {
262263
/**
263264
* If true, the parser will attempt to convert read data types to native types.
264265
* @deprecated Use {@link cast}
@@ -366,8 +367,8 @@ export interface Options<T = string[]> {
366367
/**
367368
* Alter and filter records by executing a user defined function.
368369
*/
369-
on_record?: (record: T, context: InfoRecord) => T | null | undefined;
370-
onRecord?: (record: T, context: InfoRecord) => T | null | undefined;
370+
on_record?: (record: U, context: InfoRecord) => T | null | undefined | U;
371+
onRecord?: (record: U, context: InfoRecord) => T | null | undefined | U;
371372
/**
372373
* Function called when an error occured if the `skip_records_with_error`
373374
* option is activated.
@@ -479,13 +480,13 @@ export class CsvError extends Error {
479480
);
480481
}
481482

482-
type OptionsWithColumns<T> = Omit<Options<T>, "columns"> & {
483+
export type OptionsWithColumns<T, U = T> = Omit<Options<T, U>, "columns"> & {
483484
columns: Exclude<Options["columns"], undefined | false>;
484485
};
485486

486-
declare function parse<T = unknown>(
487+
declare function parse<T = unknown, U = T>(
487488
input: string | Buffer | Uint8Array,
488-
options: OptionsWithColumns<T>,
489+
options: OptionsWithColumns<T, U>,
489490
callback?: Callback<T>,
490491
): Parser;
491492
declare function parse(
@@ -494,8 +495,8 @@ declare function parse(
494495
callback?: Callback,
495496
): Parser;
496497

497-
declare function parse<T = unknown>(
498-
options: OptionsWithColumns<T>,
498+
declare function parse<T = unknown, U = T>(
499+
options: OptionsWithColumns<T, U>,
499500
callback?: Callback<T>,
500501
): Parser;
501502
declare function parse(options: Options, callback?: Callback): Parser;

packages/csv-parse/lib/stream.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export {
1111
ColumnOption,
1212
Options,
1313
OptionsNormalized,
14+
OptionsWithColumns,
1415
Info,
1516
InfoCallback,
1617
InfoDataSet,

packages/csv-parse/lib/sync.d.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
import { Options } from "./index.js";
1+
import { Options, OptionsWithColumns } from "./index.js";
22

3-
type OptionsWithColumns<T> = Omit<Options<T>, "columns"> & {
4-
columns: Exclude<Options["columns"], undefined | false>;
5-
};
6-
7-
declare function parse<T = unknown>(
3+
declare function parse<T = unknown, U = T>(
84
input: Buffer | string | Uint8Array,
9-
options: OptionsWithColumns<T>,
5+
options: OptionsWithColumns<T, U>,
106
): T[];
117
declare function parse(
128
input: Buffer | string | Uint8Array,
@@ -24,6 +20,7 @@ export {
2420
ColumnOption,
2521
Options,
2622
OptionsNormalized,
23+
OptionsWithColumns,
2724
Info,
2825
InfoCallback,
2926
InfoDataSet,

packages/csv-parse/test/api.arguments.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,11 @@ describe("API arguments", function () {
8989
});
9090

9191
it("options:object, callback:function; write data and get result in callback", function (next) {
92-
const parser = parse({ columns: true }, (err, records) => {
92+
interface RowType {
93+
field_1: string;
94+
field_2: string;
95+
}
96+
const parser = parse<RowType>({ columns: true }, (err, records) => {
9397
if (err) return next(err);
9498
records.should.eql([{ field_1: "value 1", field_2: "value 2" }]);
9599
next();
@@ -123,7 +127,11 @@ describe("API arguments", function () {
123127

124128
describe("3 args", function () {
125129
it("data:string, options:object, callback:function", function (next) {
126-
parse(
130+
interface RowType {
131+
field_1: string;
132+
field_2: string;
133+
}
134+
parse<RowType>(
127135
"field_1,field_2\nvalue 1,value 2",
128136
{ columns: true },
129137
(err, records) => {
@@ -135,7 +143,11 @@ describe("API arguments", function () {
135143
});
136144

137145
it("data:buffer, options:object, callback:function", function (next) {
138-
parse(
146+
interface RowType {
147+
field_1: string;
148+
field_2: string;
149+
}
150+
parse<RowType>(
139151
Buffer.from("field_1,field_2\nvalue 1,value 2"),
140152
{ columns: true },
141153
(err, records) => {

packages/csv-parse/test/api.sync.ts

Lines changed: 103 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,74 +3,120 @@ import dedent from "dedent";
33
import { parse } from "../lib/sync.js";
44

55
describe("API sync", function () {
6-
it("take a string and return records", function () {
7-
const records = parse("field_1,field_2\nvalue 1,value 2");
8-
records.should.eql([
9-
["field_1", "field_2"],
10-
["value 1", "value 2"],
11-
]);
12-
});
6+
describe("content", function () {
7+
it("take a string and return records", function () {
8+
const records = parse("field_1,field_2\nvalue 1,value 2");
9+
records.should.eql([
10+
["field_1", "field_2"],
11+
["value 1", "value 2"],
12+
]);
13+
});
1314

14-
it("take a buffer and return records", function () {
15-
const records = parse(Buffer.from("field_1,field_2\nvalue 1,value 2"));
16-
records.should.eql([
17-
["field_1", "field_2"],
18-
["value 1", "value 2"],
19-
]);
20-
});
15+
it("take a buffer and return records", function () {
16+
const records = parse(Buffer.from("field_1,field_2\nvalue 1,value 2"));
17+
records.should.eql([
18+
["field_1", "field_2"],
19+
["value 1", "value 2"],
20+
]);
21+
});
2122

22-
it("take a Uint8Array and return records", function () {
23-
const records = parse(
24-
new TextEncoder().encode("field_1,field_2\nvalue 1,value 2"),
25-
);
26-
records.should.eql([
27-
["field_1", "field_2"],
28-
["value 1", "value 2"],
29-
]);
23+
it("take a Uint8Array and return records", function () {
24+
const records = parse(
25+
new TextEncoder().encode("field_1,field_2\nvalue 1,value 2"),
26+
);
27+
records.should.eql([
28+
["field_1", "field_2"],
29+
["value 1", "value 2"],
30+
]);
31+
});
3032
});
3133

32-
it("honors columns option", function () {
33-
const records = parse("field_1,field_2\nvalue 1,value 2", {
34-
columns: true,
34+
describe("options", function () {
35+
it("`columns` option without generic", function () {
36+
// Parse returns unknown[]
37+
const records = parse("field_1,field_2\nvalue 1,value 2", {
38+
columns: true,
39+
});
40+
records.should.eql([{ field_1: "value 1", field_2: "value 2" }]);
41+
});
42+
43+
it("`columns` option with generic", function () {
44+
// Parse returns Record[]
45+
interface Record {
46+
field_1: string;
47+
field_2: string;
48+
}
49+
const records: Record[] = parse<Record>(
50+
"field_1,field_2\nvalue 1,value 2",
51+
{
52+
columns: true,
53+
},
54+
);
55+
records.should.eql([{ field_1: "value 1", field_2: "value 2" }]);
3556
});
36-
records.should.eql([{ field_1: "value 1", field_2: "value 2" }]);
37-
});
3857

39-
it("honors objname option", function () {
40-
const records = parse("field_1,field_2\nname 1,value 1\nname 2,value 2", {
41-
objname: "field_1",
42-
columns: true,
58+
it("`columns` and `on_record` options with generic", function () {
59+
// Parse returns Record[]
60+
interface RecordOriginal {
61+
field_a: string;
62+
field_b: string;
63+
}
64+
interface Record {
65+
field_1: string;
66+
field_2: string;
67+
}
68+
const records: Record[] = parse<Record, RecordOriginal>(
69+
"field_1,field_2\nvalue 1,value 2",
70+
{
71+
columns: true,
72+
on_record: (record: RecordOriginal) => ({
73+
field_1: record.field_a,
74+
field_2: record.field_b,
75+
}),
76+
},
77+
);
78+
records.should.eql([{ field_1: "value 1", field_2: "value 2" }]);
4379
});
44-
records.should.eql({
45-
"name 1": { field_1: "name 1", field_2: "value 1" },
46-
"name 2": { field_1: "name 2", field_2: "value 2" },
80+
81+
it("`objname` option", function () {
82+
// Not good, parse returns unknown[]
83+
const records = parse("field_1,field_2\nname 1,value 1\nname 2,value 2", {
84+
objname: "field_1",
85+
columns: true,
86+
});
87+
records.should.eql({
88+
"name 1": { field_1: "name 1", field_2: "value 1" },
89+
"name 2": { field_1: "name 2", field_2: "value 2" },
90+
});
4791
});
48-
});
4992

50-
it("honors to_line", function () {
51-
const records = parse("1\n2\n3\n4", { to_line: 2 });
52-
records.should.eql([["1"], ["2"]]);
93+
it("`to_line` option", function () {
94+
const records = parse("1\n2\n3\n4", { to_line: 2 });
95+
records.should.eql([["1"], ["2"]]);
96+
});
5397
});
5498

55-
it("catch errors", function () {
56-
try {
57-
parse("A,B\nB\nC,K", { trim: true });
58-
throw Error("Error not catched");
59-
} catch (err) {
60-
if (!err) throw Error("Invalid assessment");
61-
(err as Error).message.should.eql(
62-
"Invalid Record Length: expect 2, got 1 on line 2",
63-
);
64-
}
65-
});
99+
describe("errors", function () {
100+
it("catch errors", function () {
101+
try {
102+
parse("A,B\nB\nC,K", { trim: true });
103+
throw Error("Error not catched");
104+
} catch (err) {
105+
if (!err) throw Error("Invalid assessment");
106+
(err as Error).message.should.eql(
107+
"Invalid Record Length: expect 2, got 1 on line 2",
108+
);
109+
}
110+
});
66111

67-
it("catch err in last line while flushing", function () {
68-
(() => {
69-
parse(dedent`
70-
headerA, headerB
71-
A2, B2
72-
A1, B1, C2, D2
73-
`);
74-
}).should.throw("Invalid Record Length: expect 2, got 4 on line 3");
112+
it("catch err in last line while flushing", function () {
113+
(() => {
114+
parse(dedent`
115+
headerA, headerB
116+
A2, B2
117+
A1, B1, C2, D2
118+
`);
119+
}).should.throw("Invalid Record Length: expect 2, got 4 on line 3");
120+
});
75121
});
76122
});

packages/csv-parse/test/api.types.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ describe("API Types", function () {
487487
{
488488
columns: true,
489489
},
490-
(error, records: unknown[] | undefined) => {
490+
(error, records: unknown[]) => {
491491
records;
492492
next(error);
493493
},
@@ -506,5 +506,25 @@ describe("API Types", function () {
506506
},
507507
);
508508
});
509+
510+
it("Exposes U[] and T if columns and on_record are specified", function (next) {
511+
type PersonOriginal = { surname: string; age: number };
512+
parse<Person, PersonOriginal>(
513+
"",
514+
{
515+
columns: true,
516+
on_record: (record: PersonOriginal) => {
517+
return {
518+
name: record.surname,
519+
age: record.age,
520+
};
521+
},
522+
},
523+
(error, records: Person[]) => {
524+
records;
525+
next(error);
526+
},
527+
);
528+
});
509529
});
510530
});

packages/csv-parse/test/option.on_record.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ describe("Option `on_record`", function () {
88
"a,b",
99
{
1010
on_record: (record) => {
11+
console.log(record[1]);
1112
return [record[1], record[0]];
1213
},
1314
},
@@ -86,6 +87,34 @@ describe("Option `on_record`", function () {
8687
},
8788
);
8889
});
90+
91+
it("with option `columns` (fix #461 #464 #466)", function (next) {
92+
type RowTypeOriginal = { a: string; b: string };
93+
type RowTypeFinal = { prop_a: string; prop_b: string };
94+
parse<RowTypeFinal, RowTypeOriginal>(
95+
"a,1\nb,2",
96+
{
97+
on_record: (record: RowTypeOriginal): RowTypeFinal => ({
98+
prop_a: record.a,
99+
prop_b: record.b,
100+
}),
101+
columns: ["a", "b"],
102+
},
103+
function (err, records: RowTypeFinal[]) {
104+
records.should.eql([
105+
{
106+
prop_a: "a",
107+
prop_b: "1",
108+
},
109+
{
110+
prop_a: "b",
111+
prop_b: "2",
112+
},
113+
]);
114+
next();
115+
},
116+
);
117+
});
89118
});
90119

91120
describe("context", function () {

0 commit comments

Comments
 (0)