forked from dfinity/nns-dapp
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NNS1-3450: Utility function to convert an object into a valid CSV str…
…ing (dfinity#5815) # Motivation NNS1-3450 will require functionality to download .csv files. # Changes - New utility function to convert an object into a string in csv format. # Tests - Unit tests for the utility function # Todos - [ ] Add entry to changelog (if necessary). Not necessary. Nex PR: dfinity#5812
- Loading branch information
Showing
2 changed files
with
148 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { isNullish } from "@dfinity/utils"; | ||
|
||
const escapeCsvValue = (value: unknown): string => { | ||
if (isNullish(value)) return ""; | ||
|
||
let stringValue = String(value); | ||
|
||
const patternForSpecialCharacters = /[",\r\n=+-@|]/; | ||
if (!patternForSpecialCharacters.test(stringValue)) { | ||
return stringValue; | ||
} | ||
|
||
const formulaInjectionCharacters = "=+-@|"; | ||
const characterToBreakFormula = "'"; | ||
if (formulaInjectionCharacters.includes(stringValue[0])) { | ||
stringValue = `${characterToBreakFormula}${stringValue}`; | ||
} | ||
|
||
const patternForCharactersToQuote = /[",\r\n]/; | ||
if (patternForCharactersToQuote.test(stringValue)) { | ||
stringValue = `"${stringValue.replace(/"/g, '""')}"`; | ||
} | ||
|
||
return stringValue; | ||
}; | ||
|
||
export const convertToCsv = <T>({ | ||
data, | ||
headers, | ||
}: { | ||
data: T[]; | ||
headers: { id: keyof T }[]; | ||
}) => { | ||
if (headers.length === 0) return ""; | ||
|
||
const sanitizedHeaders = headers | ||
.map(({ id }) => id) | ||
.map((header) => escapeCsvValue(header)); | ||
const csvRows = [sanitizedHeaders.join(",")]; | ||
|
||
for (const row of data) { | ||
const values = headers.map((header) => escapeCsvValue(row[header.id])); | ||
csvRows.push(values.join(",")); | ||
} | ||
|
||
return csvRows.join("\n"); | ||
}; |
101 changes: 101 additions & 0 deletions
101
frontend/src/tests/lib/utils/export-to-csv.utils.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { convertToCsv } from "$lib/utils/export-to-csv.utils"; | ||
|
||
describe("Export to Csv", () => { | ||
describe("convertToCSV", () => { | ||
it("should return an empty string when empty headers are provided", () => { | ||
const data = []; | ||
const headers = []; | ||
const expected = ""; | ||
expect(convertToCsv({ data, headers })).toBe(expected); | ||
}); | ||
|
||
it("should return a string with headers and no content when empty data is provided", () => { | ||
const data = []; | ||
const headers = [{ id: "name" }]; | ||
const expected = "name"; | ||
expect(convertToCsv({ data, headers })).toBe(expected); | ||
}); | ||
|
||
it("should apply order defined by the headers argument", () => { | ||
const data = [ | ||
{ name: "Peter", age: 25 }, | ||
{ name: "John", age: 30 }, | ||
]; | ||
const headers: { id: "age" | "name" }[] = [{ id: "age" }, { id: "name" }]; | ||
const expected = "age,name\n25,Peter\n30,John"; | ||
expect(convertToCsv({ data, headers })).toBe(expected); | ||
}); | ||
|
||
it("should handle null, undefined and empty strings ", () => { | ||
const data = [ | ||
{ name: "Peter", age: undefined }, | ||
{ name: null, age: 25 }, | ||
{ name: "", age: 22 }, | ||
]; | ||
const headers: { id: "age" | "name" }[] = [{ id: "name" }, { id: "age" }]; | ||
const expected = "name,age\nPeter,\n,25\n,22"; | ||
expect(convertToCsv({ data, headers })).toBe(expected); | ||
}); | ||
|
||
it("should handle values containing commas by wrapping them in quotes", () => { | ||
const data = [ | ||
{ name: "John, Jr.", age: 30 }, | ||
{ name: "Jane", age: 25 }, | ||
]; | ||
const headers: { id: "age" | "name" }[] = [{ id: "name" }, { id: "age" }]; | ||
const expected = 'name,age\n"John, Jr.",30\nJane,25'; | ||
expect(convertToCsv({ data, headers })).toBe(expected); | ||
}); | ||
|
||
it("should escape double quotes by doubling them", () => { | ||
const data = [ | ||
{ name: 'John "Johnny" Doe', age: 30 }, | ||
{ name: "Jane", age: 25 }, | ||
]; | ||
const headers: { id: "age" | "name" }[] = [{ id: "name" }, { id: "age" }]; | ||
const expected = 'name,age\n"John ""Johnny"" Doe",30\nJane,25'; | ||
expect(convertToCsv({ data, headers })).toBe(expected); | ||
}); | ||
|
||
it("should prevent formula injection by prefixing with single quote", () => { | ||
const data = [ | ||
{ formula: "=SUM(A1:A10)", value: 100 }, | ||
{ formula: "+1234567", value: 200 }, | ||
{ formula: "-1234567", value: 300 }, | ||
{ formula: "@SUM(A1)", value: 400 }, | ||
{ formula: "|MACRO", value: 500 }, | ||
]; | ||
const headers: { id: "formula" | "value" }[] = [ | ||
{ id: "formula" }, | ||
{ id: "value" }, | ||
]; | ||
const expected = | ||
"formula,value\n'=SUM(A1:A10),100\n'+1234567,200\n'-1234567,300\n'@SUM(A1),400\n'|MACRO,500"; | ||
expect(convertToCsv({ data, headers })).toBe(expected); | ||
}); | ||
|
||
it("should handle formula injection and special characters in values", () => { | ||
const data = [ | ||
{ formula: "=SUM(A1:A10)", value: 100 }, | ||
{ formula: "+1234567,12", value: 200 }, | ||
]; | ||
const headers: { id: "formula" | "value" }[] = [ | ||
{ id: "formula" }, | ||
{ id: "value" }, | ||
]; | ||
const expected = "formula,value\n'=SUM(A1:A10),100\n\"'+1234567,12\",200"; | ||
expect(convertToCsv({ data, headers })).toBe(expected); | ||
}); | ||
|
||
it("should handle values containing newlines by wrapping them in quotes", () => { | ||
const data = [ | ||
{ note: "Line 1\nLine 2", id: 1 }, | ||
{ note: "Single Line", id: 2 }, | ||
]; | ||
``; | ||
const headers: { id: "note" | "id" }[] = [{ id: "note" }, { id: "id" }]; | ||
const expected = 'note,id\n"Line 1\nLine 2",1\nSingle Line,2'; | ||
expect(convertToCsv({ data, headers })).toBe(expected); | ||
}); | ||
}); | ||
}); |