Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
ts-csv-builder
Advanced tools
CSVBuilder is a generic utility that aims to simplify the creation of CSV files in your code, providing a simple and intuitive interface to build files step-by-step in a type-safe way.
First, if you know the layout of your CSV file, you can define a type CSVTemplate extends Record<string, any>
, which can then be provided as a type-variable of your builder. For example, if you want to build a csv-file with the following columns:
Country | Capital | Country and Capital | Capital River | Population |
---|---|---|---|---|
Spain | Madrid | Spain - Madrid | N/A | 47,780,000 |
Poland | Warsaw | Poland - Warsaw | Vistula | 36,820,000 |
Hungary | Budapest | Hungary - Budapest | Danube | 9,643,000 |
You can represent this as the following template, and provide it to the CSVBuilder
import { CSVBuilder } from 'ts-csv-builder'
type CSVTemplate = {
Country: string;
Capital: string;
Population: number;
"Country and Capital": string;
"Capital River": string | undefined;
};
const builder = new CSVBuilder<CSVTemplate>()
This will provide you with the proper typing when you start building your file.
You can set the following options on the builder:
Option | Type | Default | Description |
---|---|---|---|
separator | string | , | String value used to separate field values in the resulting CSV string. |
eol | string | \n | String value used to separate the rows in the resulting CSV string. |
fieldWrapper | string | " | String value used for wrapping field values in case special characters occur. |
headerSeparator | string | undefined | undefined | If not undefined, column names will be split on the provided string and subheaders will be created in the resulting CSV string. |
emptyValue | string | null | null | Value to replace empty (null | undefined ) column values with across the resulting CSV string. |
removeEmptyColumns | boolean | false | If true then empty columns (with only null | undefined values) will be dropped before converting to a CSV string. |
These can be set via the following:
builder.setBuilderOptions({emptyValue: "N/A"})
Only the properties that you explicitly set will be updated from their default values.
In some CSV files, you may want to create a headers which span across multiple rows. The header separator option let's you achieve this easily. For example, if you want to create a CSV with the following layout:
Header 1 | Header 1 | Header 2 |
---|---|---|
Subheader 1 | Subheader 2 | |
... | ... | ... |
You can create a template such as this in your code:
type Template = {
`Header 1.Subheader 1`: any,
`Header 1.Subheader 2`: any,
`Header 2`: any,
}
and set the builder options to with the appropiate header separator:
const builder = new CSVBuilder<Template>().setBuilderOptions({
headerSeparator: '.'
})
This will then correctly, split the headers and, in case the number of split values are not equal across all columns names, pad the rows with empty string for missing subheaders.
You can add a column's data to the builder with the .createColumn
method.
const countries = ["Hungary", "Poland", "Spain"];
builder.createColumn('Country', countries)
:warning: Subsequent
.createColumn
calls must provide an array with the same length as the initial one, otherwise the builder will raise an error.
To map the values of an existing column to create another one, you can use .mapColumn
:
const capitals: Record<string, string> = {
Hungary: "Budapest",
Poland: "Warsaw",
Spain: "Madrid",
};
const populations: Record<string, number> = {
Hungary: 9643000,
Poland: 36820000,
Spain: 47780000,
};
const rivers: Record<string, string> = {
Budapest: "Danube",
Warsaw: "Vistula",
};
builder
.mapColumn("Country", "Capital", (country) => capitals[country])
.mapColumn("Country", "Population", (country) => populations[country])
.mapColumn("Capital", "Capital River", (capital) => rivers[capital])
Where the callback-function's types are restricted to map from string => number.
:bulb: You can use
.mapColumn
to map from the same column to itself, if you want to modify the values in it.
You can also map the values in all existing columns to another one:
builder.mapColumns(
"Country and Capital",
({ Country: country, Capital: capital }) => `${country} - ${capital}`
)
Columns specifically can have some options configured, which will take presedence over the builder options.
Option | Type | Default | Description |
---|---|---|---|
priority | number | Number.MAX_SAFE_INTEGER | Column priority will determine the column ordering when calling the .sortColumns builder method. Lower priority columns will be ordered first. Columns without a set priority will be ordered last. |
emptyValue | string | null | null | Value to replace empty (null | undefined ) column values with. Takes precedence over the builder level option. |
removeIfEmpty | boolean | false | Remove the column from the resulting CSV, if it only contrains empty (null | undefined ) values. |
transform | CSVColumnTransform<Template extends CSVTemplate, Column extends keyof Template> | undefined | Callback function to transform the column values only in the resulting CSV string, but not while building. |
Set the column options in the following way:
builder
.setColumnOptions("Country", { priority: 1 })
.setColumnOptions("Capital", { priority: 2 })
.setColumnOptions("Country and Capital", { priority: 3 })
.setColumnOptions("Capital River", { priority: 4, emptyValue: "N/A" })
.setColumnOptions("Population", {
transform: (value: number): string => value.toLocaleString();
})
As mentioned in Column options, the column priority will decide the columns position in the final CSV. To actually order the columns, do:
builder.sortColumns()
Sorting rows work in the same fashion as the regular Javascript Array.prototype.sort()
function, where you have to provide a callback which takes a: CSVTemplate
and b: CSVTemplate
and returns a number.
For example, to sort our example by population, we can do:
builder
.sortRows(
({ Population: populationA }, { Population: populationB }) =>
populationB - populationA
);
In some cases, it might come handy to create large CSV files in multiple smaller batches. This is made easy by the ability to concatenate builders with the same template into one.
For example:
type CSVTemplate {
a: string,
}
const builder = new CSVBuilder<CSVTemplate>().createColumn("a", ["x"])
const otherBuilder = new CSVBuilder<CSVTemplate>().createColumn("a", ["y"])
builder.concat(otherBuilder)
This will mutate the builder which invokes the .concat
method.
Internal data, column options and builder options will be concatenated. In the case of options, if both builders define options for the same column or the same builder option, the options of the builder invoking .concat
will take precedence.
To merge more than 2 builders, it's handy to use the CSVBuilder.merge
static method. This could especially be useful when the data required for the builders can only be obtained in smaller batches (for example, from a database or API), allowing us to asynchonously create multiple smaller builders and merge them.
For example:
const batchIds = [[0, 999], [1000, 1999], [2000, 2999]] // and so on
type CSVTemplate = {
"User ID": number,
"User Email": string,
}
const builders = await Promise.all(batchIds.map(async ([fromId, toId]) => {
const users = await usersDB.getUsers({fromId, toId})
return new CSVBuilder<CSVTemplate>()
.createColumn("User ID", users.map(({id}) => id))
.createColumn("User Email", users.map(({email}) => email))
}))
const builder = CSVBuilder.merge(...builders)
:warning: Because
CSVBuilder.merge
uses.concat
under the hood, the builder provided as the first argument will be mutated by this.
To get the final string result of the built CSV, call .getString()
.
const csvString = builder.getString()
To get number of columns and rows of the resulting CSV, use .getDimensions()
.
const { nRows, nCols } = builder.getDimensions()
And to get both the resulting string and the dimensions, call .getStringAndDimensions()
const [csvString, dims] = builder.getStringAndDimensions()
import { CSVBuilder } from 'ts-csv-builder'
const countries = ["Hungary", "Poland", "Spain"];
const capitals: Record<string, string> = {
Hungary: "Budapest",
Poland: "Warsaw",
Spain: "Madrid",
};
const populations: Record<string, number> = {
Hungary: 9643000,
Poland: 36820000,
Spain: 47780000,
};
const rivers: Record<string, string> = {
Budapest: "Danube",
Warsaw: "Vistula",
};
type CSVTemplate = {
Country: string;
Capital: string;
Population: number;
"Country and Capital": string;
"Capital River": string | undefined;
};
const csvString = new CSVBuilder<CSVTemplate>()
.createColumn('Country', countries)
.mapColumn("Country", "Capital", (country) => capitals[country])
.mapColumn("Country", "Population", (country) => populations[country])
.mapColumn("Capital", "Capital River", (capital) => rivers[capital])
.mapColumns(
"Country and Capital",
({ Country: country, Capital: capital }) => `${country} - ${capital}`
)
.setColumnOptions("Country", { priority: 1 })
.setColumnOptions("Capital", { priority: 2 })
.setColumnOptions("Country and Capital", { priority: 3 })
.setColumnOptions("Capital River", { priority: 4, emptyValue: "N/A" })
.setColumnOptions("Population", {
transform: (value: number): string => value.toLocaleString();
})
.sortColumns()
.sortRows(
({ Population: populationA }, { Population: populationB }) =>
populationB - populationA
)
.getString();
In many cases, you will not know the exact set of columns your CSV file will have at the time of writing your code - columns will need to be created dynamically and the template is not known exactly at compile time. Sometimes, the naming of dynamic columns may follow some pattern though. If that is the case, template literaly types can still provide some level of type-safety. Take the following example, where we want to produce a CSV containing students' percentile marks, and we want to have a column for each subject they took.
Student name | ... Final Mark |
---|---|
John Doe | ...final marks |
For this, we can create the following template:
type Subject = "Maths" | "Literature" | "Arts"
type Template = {
"Student name": string,
} & {[K in Subject as `${K} Final Mark`]: number}
Or, if you cannot make assertions about the subject, simply do:
type Template = {
"Student name": string,
`${string} Final Mark`: number,
}
This way typescript will at least help you to make sure your columns are following the correct patterns and will still give you some nice typings.
In case you cannot make any assertions at all about the format of your CSV file, you can simply leave the type variable empty (as it will default to Record<string, any>
). This way you will unfortunately not have any nice types while writing your code, but you can still use the builder.
1.0.6
FAQs
Utility package for building CSV files.
The npm package ts-csv-builder receives a total of 98 weekly downloads. As such, ts-csv-builder popularity was classified as not popular.
We found that ts-csv-builder demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.