From 8b8ba695321d5907cae6525ad1c79e1f24907796 Mon Sep 17 00:00:00 2001 From: Juanjo Diaz Date: Thu, 12 Oct 2023 21:00:43 +0200 Subject: [PATCH] refactor: remove lodash --- build-cdn.js | 3 +- dist/cdn/plainjs/BaseParser.js | 5 +- dist/cdn/plainjs/StreamParser.js | 2 +- dist/cdn/plainjs/utils.js | 28 +++- dist/cdn/transforms/unwind.js | 5 +- dist/cdn/transforms/utils.js | 29 +++++ package-lock.json | 31 +---- package.json | 1 - packages/cli/test/CLI.ts | 10 ++ packages/node/test/AsyncParser.ts | 34 ++++- packages/node/test/AsyncParserInMemory.ts | 34 ++++- packages/node/test/Transform.ts | 34 ++++- packages/plainjs/package.json | 3 +- packages/plainjs/src/BaseParser.ts | 11 +- packages/plainjs/src/utils.ts | 65 ++++++++- packages/plainjs/test/Parser.ts | 34 ++++- packages/plainjs/test/StreamParser.ts | 34 ++++- .../test-helpers/fixtures/fields/nested.json | 2 +- .../fixtures/fields/nestedWithBrackets.json | 18 +++ .../test-helpers/fixtures/json/nested.json | 8 +- packages/transforms/package.json | 3 - packages/transforms/src/unwind.ts | 5 +- packages/transforms/src/utils.ts | 123 ++++++++++++++++++ packages/whatwg/test/AsyncParser.ts | 34 ++++- packages/whatwg/test/AsyncParserInMemory.ts | 34 ++++- packages/whatwg/test/TransformStream.ts | 34 ++++- 26 files changed, 551 insertions(+), 73 deletions(-) create mode 100644 packages/test-helpers/fixtures/fields/nestedWithBrackets.json diff --git a/build-cdn.js b/build-cdn.js index cd5522a..a42f6c8 100644 --- a/build-cdn.js +++ b/build-cdn.js @@ -28,8 +28,7 @@ const replaceDependenciesByJsdelivr = { }); const dependencies = { - '@streamparser/json': `https://cdn.jsdelivr.net/npm/@streamparser/json@${pkg.dependencies['@streamparser/json']}/dist/mjs/index.js`, - 'lodash.get': 'https://cdn.jsdelivr.net/gh/lodash/lodash@master/get.js' + '@streamparser/json': `https://cdn.jsdelivr.net/npm/@streamparser/json@${pkg.dependencies['@streamparser/json']}/dist/mjs/index.js` }; build.onResolve({ namespace: 'file', filter: new RegExp(`(?:${Object.keys(dependencies).join('|')})`) }, (args) => { diff --git a/dist/cdn/plainjs/BaseParser.js b/dist/cdn/plainjs/BaseParser.js index 9b9f9d7..0c01eca 100644 --- a/dist/cdn/plainjs/BaseParser.js +++ b/dist/cdn/plainjs/BaseParser.js @@ -1,5 +1,4 @@ // packages/plainjs/src/BaseParser.ts -import lodashGet from "https://cdn.jsdelivr.net/gh/lodash/lodash@master/get.js"; import { default as defaultFormatter, number as numberFormatterCtor, @@ -78,7 +77,7 @@ var JSON2CSVBase = class { if (typeof fieldInfo === "string") { return { label: fieldInfo, - value: fieldInfo.includes(".") || fieldInfo.includes("[") ? (row) => lodashGet(row, fieldInfo, globalDefaultValue) : (row) => getProp(row, fieldInfo, globalDefaultValue) + value: (row) => getProp(row, fieldInfo, globalDefaultValue) }; } if (typeof fieldInfo === "object") { @@ -87,7 +86,7 @@ var JSON2CSVBase = class { const fieldPath = fieldInfo.value; return { label: fieldInfo.label || fieldInfo.value, - value: fieldInfo.value.includes(".") || fieldInfo.value.includes("[") ? (row) => lodashGet(row, fieldPath, defaultValue) : (row) => getProp(row, fieldPath, defaultValue) + value: (row) => getProp(row, fieldPath, defaultValue) }; } if (typeof fieldInfo.value === "function") { diff --git a/dist/cdn/plainjs/StreamParser.js b/dist/cdn/plainjs/StreamParser.js index 8d7396b..bc187c9 100644 --- a/dist/cdn/plainjs/StreamParser.js +++ b/dist/cdn/plainjs/StreamParser.js @@ -4,7 +4,7 @@ import { TokenParser, TokenType, TokenizerError -} from "https://cdn.jsdelivr.net/npm/@streamparser/json@^0.0.16/dist/mjs/index.js"; +} from "https://cdn.jsdelivr.net/npm/@streamparser/json@^0.0.17/dist/mjs/index.js"; import JSON2CSVBase from "./BaseParser.js"; var JSON2CSVStreamParser = class extends JSON2CSVBase { constructor(opts, asyncOpts) { diff --git a/dist/cdn/plainjs/utils.js b/dist/cdn/plainjs/utils.js index 0f88952..7771522 100644 --- a/dist/cdn/plainjs/utils.js +++ b/dist/cdn/plainjs/utils.js @@ -1,7 +1,31 @@ // packages/plainjs/src/utils.ts +var rePropName = RegExp( + // Match anything that isn't a dot or bracket. + `[^.[\\]]+|\\[(?:([^"'][^[]*)|(["'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))`, + "g" +); +function castPath(value, object) { + var _a, _b, _c; + if (Array.isArray(value)) + return value; + if (value in object) + return [value]; + const result = []; + let match; + while (match = rePropName.exec(value)) { + result.push((_c = (_b = match[3]) != null ? _b : (_a = match[1]) == null ? void 0 : _a.trim()) != null ? _c : match[0]); + } + return result; +} function getProp(obj, path, defaultValue) { - const value = obj[path]; - return value === void 0 ? defaultValue : value; + const processedPath = castPath(path, obj); + let currentValue = obj; + for (const key of processedPath) { + currentValue = currentValue == null ? void 0 : currentValue[key]; + if (currentValue === void 0) + return defaultValue; + } + return currentValue; } function flattenReducer(acc, arr) { try { diff --git a/dist/cdn/transforms/unwind.js b/dist/cdn/transforms/unwind.js index 39fc5e5..5c3093f 100644 --- a/dist/cdn/transforms/unwind.js +++ b/dist/cdn/transforms/unwind.js @@ -1,6 +1,5 @@ // packages/transforms/src/unwind.ts -import lodashGet from "https://cdn.jsdelivr.net/gh/lodash/lodash@master/get.js"; -import { setProp, unsetProp, flattenReducer } from "./utils.js"; +import { getProp, setProp, unsetProp, flattenReducer } from "./utils.js"; function getUnwindablePaths(obj, currentPath) { return Object.keys(obj).reduce( (unwindablePaths, key) => { @@ -24,7 +23,7 @@ function getUnwindablePaths(obj, currentPath) { function unwind(opts = {}) { function unwindReducer(rows, unwindPath) { return rows.flatMap((row) => { - const unwindArray = lodashGet(row, unwindPath); + const unwindArray = getProp(row, unwindPath); if (!Array.isArray(unwindArray)) { return row; } diff --git a/dist/cdn/transforms/utils.js b/dist/cdn/transforms/utils.js index 6fa2c4c..6731d29 100644 --- a/dist/cdn/transforms/utils.js +++ b/dist/cdn/transforms/utils.js @@ -1,4 +1,32 @@ // packages/transforms/src/utils.ts +var rePropName = RegExp( + // Match anything that isn't a dot or bracket. + `[^.[\\]]+|\\[(?:([^"'][^[]*)|(["'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))`, + "g" +); +function castPath(value, object) { + var _a, _b, _c; + if (Array.isArray(value)) + return value; + if (value in object) + return [value]; + const result = []; + let match; + while (match = rePropName.exec(value)) { + result.push((_c = (_b = match[3]) != null ? _b : (_a = match[1]) == null ? void 0 : _a.trim()) != null ? _c : match[0]); + } + return result; +} +function getProp(obj, path, defaultValue) { + const processedPath = castPath(path, obj); + let currentValue = obj; + for (const key of processedPath) { + currentValue = currentValue == null ? void 0 : currentValue[key]; + if (currentValue === void 0) + return defaultValue; + } + return currentValue; +} function propertyPathToString(path) { if (typeof path === "string") return path.split("."); @@ -42,6 +70,7 @@ function flattenReducer(acc, arr) { } export { flattenReducer, + getProp, setProp, unsetProp }; diff --git a/package-lock.json b/package-lock.json index 80f82db..9f77b3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,6 @@ "packages/test-performance" ], "devDependencies": { - "@types/lodash.get": "^4.4.7", "@types/tape": "^5.6.0", "@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/parser": "^6.2.0", @@ -37,7 +36,7 @@ "tape": "^5.2.2", "tiny-glob": "^0.2.9", "ts-node": "^10.9.1", - "typescript": "^5.0.4" + "typescript": "~5.1.0" }, "engines": { "node": ">=14.17.0", @@ -812,21 +811,6 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, - "node_modules/@types/lodash": { - "version": "4.14.197", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.197.tgz", - "integrity": "sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==", - "dev": true - }, - "node_modules/@types/lodash.get": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@types/lodash.get/-/lodash.get-4.4.7.tgz", - "integrity": "sha512-af34Mj+KdDeuzsJBxc/XeTtOx0SZHZNLd+hdrn+PcKGQs0EG2TJTzQAOTCZTgDJCArahlCzLWSy8c2w59JRz7Q==", - "dev": true, - "dependencies": { - "@types/lodash": "*" - } - }, "node_modules/@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", @@ -6257,11 +6241,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, "node_modules/lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", @@ -10509,8 +10488,7 @@ "license": "MIT", "dependencies": { "@json2csv/formatters": "^7.0.3", - "@streamparser/json": "^0.0.17", - "lodash.get": "^4.4.2" + "@streamparser/json": "^0.0.17" } }, "packages/test-helpers": { @@ -10531,10 +10509,7 @@ "packages/transforms": { "name": "@json2csv/transforms", "version": "7.0.3", - "license": "MIT", - "dependencies": { - "lodash.get": "^4.4.2" - } + "license": "MIT" }, "packages/whatwg": { "name": "@json2csv/whatwg", diff --git a/package.json b/package.json index 7eda32b..affad1b 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "changelog:update": "conventional-changelog-cli -p conventionalcommits -i CHANGELOG.md -s && cp CHANGELOG.md docs/others/CHANGELOG.md" }, "devDependencies": { - "@types/lodash.get": "^4.4.7", "@types/tape": "^5.6.0", "@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/parser": "^6.2.0", diff --git a/packages/cli/test/CLI.ts b/packages/cli/test/CLI.ts index 3f80fe7..023d6be 100644 --- a/packages/cli/test/CLI.ts +++ b/packages/cli/test/CLI.ts @@ -301,6 +301,16 @@ export default function ( t.equal(csv, csvFixtures.nested); }); + testRunner.add('should support nested properties selectors using braket notation', async (t) => { + const opts = `--config "${getFixturePath('/fields/nestedWithBrackets.json')}"`; + + const { stdout: csv } = await execAsync( + `${cli} -i "${getFixturePath('/json/nested.json')}" ${opts}`, + ); + + t.equal(csv, csvFixtures.nested); + }); + testRunner.add( 'field.value function should receive a valid field object', async (t) => { diff --git a/packages/node/test/AsyncParser.ts b/packages/node/test/AsyncParser.ts index bf12d7c..f6d33b6 100644 --- a/packages/node/test/AsyncParser.ts +++ b/packages/node/test/AsyncParser.ts @@ -422,7 +422,7 @@ export default function ( }, { label: 'Price', - value: 'price', + value: 'prices[0]', }, { label: 'Color', @@ -441,6 +441,38 @@ export default function ( t.equal(csv, csvFixtures.nested); }); + testRunner.add('should support nested properties selectors using braket notation', async (t) => { + const opts: ParserOptions = { + fields: [ + { + label: 'Make', + value: 'car[make]', + }, + { + label: 'Model', + value: 'car["model"]', + }, + { + label: 'Price', + value: 'prices[0]', + }, + { + label: 'Color', + value: 'color', + }, + { + label: 'Year', + value: 'car[\'ye\'][ar]', + }, + ], + }; + + const parser = new Parser(opts); + const csv = await parseInput(parser, jsonFixtures.nested()); + + t.equal(csv, csvFixtures.nested); + }); + testRunner.add( 'field.value function should receive a valid field object', async (t) => { diff --git a/packages/node/test/AsyncParserInMemory.ts b/packages/node/test/AsyncParserInMemory.ts index bf12d7c..f6d33b6 100644 --- a/packages/node/test/AsyncParserInMemory.ts +++ b/packages/node/test/AsyncParserInMemory.ts @@ -422,7 +422,7 @@ export default function ( }, { label: 'Price', - value: 'price', + value: 'prices[0]', }, { label: 'Color', @@ -441,6 +441,38 @@ export default function ( t.equal(csv, csvFixtures.nested); }); + testRunner.add('should support nested properties selectors using braket notation', async (t) => { + const opts: ParserOptions = { + fields: [ + { + label: 'Make', + value: 'car[make]', + }, + { + label: 'Model', + value: 'car["model"]', + }, + { + label: 'Price', + value: 'prices[0]', + }, + { + label: 'Color', + value: 'color', + }, + { + label: 'Year', + value: 'car[\'ye\'][ar]', + }, + ], + }; + + const parser = new Parser(opts); + const csv = await parseInput(parser, jsonFixtures.nested()); + + t.equal(csv, csvFixtures.nested); + }); + testRunner.add( 'field.value function should receive a valid field object', async (t) => { diff --git a/packages/node/test/Transform.ts b/packages/node/test/Transform.ts index b9eac35..67f49d2 100644 --- a/packages/node/test/Transform.ts +++ b/packages/node/test/Transform.ts @@ -408,7 +408,7 @@ export default function ( }, { label: 'Price', - value: 'price', + value: 'prices[0]', }, { label: 'Color', @@ -427,6 +427,38 @@ export default function ( t.equal(csv, csvFixtures.nested); }); + testRunner.add('should support nested properties selectors using braket notation', async (t) => { + const opts: ParserOptions = { + fields: [ + { + label: 'Make', + value: 'car[make]', + }, + { + label: 'Model', + value: 'car["model"]', + }, + { + label: 'Price', + value: 'prices[0]', + }, + { + label: 'Color', + value: 'color', + }, + { + label: 'Year', + value: 'car[\'ye\'][ar]', + }, + ], + }; + + const parser = new Parser(opts); + const csv = await parseInput(parser, jsonFixtures.nested()); + + t.equal(csv, csvFixtures.nested); + }); + testRunner.add( 'field.value function should receive a valid field object', async (t) => { diff --git a/packages/plainjs/package.json b/packages/plainjs/package.json index b0f1664..7d3d73f 100644 --- a/packages/plainjs/package.json +++ b/packages/plainjs/package.json @@ -49,7 +49,6 @@ }, "dependencies": { "@json2csv/formatters": "^7.0.3", - "@streamparser/json": "^0.0.17", - "lodash.get": "^4.4.2" + "@streamparser/json": "^0.0.17" } } diff --git a/packages/plainjs/src/BaseParser.ts b/packages/plainjs/src/BaseParser.ts index e61a6c5..0ec5988 100644 --- a/packages/plainjs/src/BaseParser.ts +++ b/packages/plainjs/src/BaseParser.ts @@ -1,4 +1,3 @@ -import lodashGet from 'lodash.get'; import { type Formatter, default as defaultFormatter, @@ -167,10 +166,7 @@ export default abstract class JSON2CSVBase< if (typeof fieldInfo === 'string') { return { label: fieldInfo, - value: - fieldInfo.includes('.') || fieldInfo.includes('[') - ? (row) => lodashGet(row, fieldInfo, globalDefaultValue) - : (row) => getProp(row, fieldInfo, globalDefaultValue as unknown), + value: (row) => getProp(row, fieldInfo, globalDefaultValue), }; } @@ -182,10 +178,7 @@ export default abstract class JSON2CSVBase< const fieldPath: string = fieldInfo.value; return { label: fieldInfo.label || fieldInfo.value, - value: - fieldInfo.value.includes('.') || fieldInfo.value.includes('[') - ? (row) => lodashGet(row, fieldPath, defaultValue) - : (row) => getProp(row, fieldPath, defaultValue), + value: (row) => getProp(row, fieldPath, defaultValue), }; } diff --git a/packages/plainjs/src/utils.ts b/packages/plainjs/src/utils.ts index 8d1787b..90919bd 100644 --- a/packages/plainjs/src/utils.ts +++ b/packages/plainjs/src/utils.ts @@ -41,11 +41,60 @@ type GetFieldType = P extends `${infer Left}.${infer Right}` type PropertyName = string | number | symbol; +const rePropName = RegExp( + // Match anything that isn't a dot or bracket. + '[^.[\\]]+' + '|' + + // Or match property names within brackets. + '\\[(?:' + + // Match a non-string expression. + '([^"\'][^[]*)' + '|' + + // Or match strings (supports escaping characters). + '(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' + + ')\\]'+ '|' + + // Or match "" as the space between consecutive dots or empty brackets. + '(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))' + , 'g'); + +/** + * Casts `value` to a path array if it's not one. + * + * @private + * @param {*} value The value to inspect. + * @param {Object} [object] The object to query keys on. + * @returns {Array} Returns the cast property path array. + */ +function castPath( + path: TKey, + obj: TObject, +): [TKey]; +function castPath< + TPath extends string, + TObject, +>( + path: TPath, + obj: TObject, +): Exclude, null | undefined>; +function castPath(value: string): string[] { + const result: string[] = []; + let match: RegExpExecArray | null; + while (match = rePropName.exec(value)) { + result.push(match[3] ?? match[1]?.trim() ?? match[0]); + } + return result; +} + export function getProp( obj: TObject, path: TKey, defaultValue: TObject[TKey], ): TObject[TKey]; +export function getProp< + TObject, + TPath extends string, +>( + obj: TObject, + path: TPath, +): Exclude, null | undefined>; export function getProp< TObject, TPath extends string, @@ -55,9 +104,19 @@ export function getProp< path: TPath, defaultValue: TDefault, ): Exclude, null | undefined> | TDefault; -export function getProp(obj: any, path: PropertyName, defaultValue?: T): T { - const value = obj[path]; - return value === undefined ? defaultValue : value; +export function getProp(obj: any, path: PropertyName, defaultValue?: T): T | undefined { + if (path in obj) { + const value = obj[path]; + return value === undefined ? defaultValue : value; + } + + const processedPath = Array.isArray(path) ? path : castPath(path, obj); + let currentValue = obj; + for (const key of processedPath) { + currentValue = currentValue?.[key]; + if (currentValue === undefined) return defaultValue; + } + return currentValue; } export function flattenReducer(acc: Array, arr: Array | T): Array { diff --git a/packages/plainjs/test/Parser.ts b/packages/plainjs/test/Parser.ts index 43e30e8..4989151 100644 --- a/packages/plainjs/test/Parser.ts +++ b/packages/plainjs/test/Parser.ts @@ -299,7 +299,7 @@ export default function ( }, { label: 'Price', - value: 'price', + value: 'prices[0]', }, { label: 'Color', @@ -318,6 +318,38 @@ export default function ( t.equal(csv, csvFixtures.nested); }); + testRunner.add('should support nested properties selectors using braket notation', async (t) => { + const opts: ParserOptions = { + fields: [ + { + label: 'Make', + value: 'car[make]', + }, + { + label: 'Model', + value: 'car["model"]', + }, + { + label: 'Price', + value: 'prices[0]', + }, + { + label: 'Color', + value: 'color', + }, + { + label: 'Year', + value: 'car[\'ye\'][ar]', + }, + ], + }; + + const parser = new Parser(opts); + const csv = await parseInput(parser, jsonFixtures.nested()); + + t.equal(csv, csvFixtures.nested); + }); + testRunner.add( 'field.value function should receive a valid field object', async (t) => { diff --git a/packages/plainjs/test/StreamParser.ts b/packages/plainjs/test/StreamParser.ts index ad201ac..ff176bd 100644 --- a/packages/plainjs/test/StreamParser.ts +++ b/packages/plainjs/test/StreamParser.ts @@ -380,7 +380,7 @@ export default function ( }, { label: 'Price', - value: 'price', + value: 'prices[0]', }, { label: 'Color', @@ -399,6 +399,38 @@ export default function ( t.equal(csv, csvFixtures.nested); }); + testRunner.add('should support nested properties selectors using braket notation', async (t) => { + const opts: ParserOptions = { + fields: [ + { + label: 'Make', + value: 'car[make]', + }, + { + label: 'Model', + value: 'car["model"]', + }, + { + label: 'Price', + value: 'prices[0]', + }, + { + label: 'Color', + value: 'color', + }, + { + label: 'Year', + value: 'car[\'ye\'][ar]', + }, + ], + }; + + const parser = new Parser(opts); + const csv = await parseInput(parser, jsonFixtures['nested']()); + + t.equal(csv, csvFixtures.nested); + }); + testRunner.add( 'field.value function should receive a valid field object', async (t) => { diff --git a/packages/test-helpers/fixtures/fields/nested.json b/packages/test-helpers/fixtures/fields/nested.json index 550b2d8..64ff8db 100644 --- a/packages/test-helpers/fixtures/fields/nested.json +++ b/packages/test-helpers/fixtures/fields/nested.json @@ -7,7 +7,7 @@ "value": "car.model" },{ "label": "Price", - "value": "price" + "value": "prices[0]" },{ "label": "Color", "value": "color" diff --git a/packages/test-helpers/fixtures/fields/nestedWithBrackets.json b/packages/test-helpers/fixtures/fields/nestedWithBrackets.json new file mode 100644 index 0000000..64ff8db --- /dev/null +++ b/packages/test-helpers/fixtures/fields/nestedWithBrackets.json @@ -0,0 +1,18 @@ +{ + "fields": [{ + "label": "Make", + "value": "car.make" + },{ + "label": "Model", + "value": "car.model" + },{ + "label": "Price", + "value": "prices[0]" + },{ + "label": "Color", + "value": "color" + },{ + "label": "Year", + "value": "car.ye.ar" + }] +} \ No newline at end of file diff --git a/packages/test-helpers/fixtures/json/nested.json b/packages/test-helpers/fixtures/json/nested.json index 7d17e7e..847cd15 100644 --- a/packages/test-helpers/fixtures/json/nested.json +++ b/packages/test-helpers/fixtures/json/nested.json @@ -1,6 +1,6 @@ [ - { "car" : { "make" : "Audi", "model" : "A3", "ye": {"ar": "2001"}}, "price" : 10000, "color" : "blue" } - , { "car" : { "make" : "BMW", "model" : "F20", "ye": {"ar": "2002"}}, "price" : 15000, "color" : "red" } - , { "car" : { "make" : "Mercedes", "model" : "SLS", "ye": {"ar": "2003"}}, "price" : 20000, "color" : "yellow" } - , { "car" : { "make" : "Porsche", "model" : "9PA AF1", "ye": {"ar": "2004"}}, "price" : 30000, "color" : "green" } + { "car" : { "make" : "Audi", "model" : "A3", "ye": {"ar": "2001"}}, "prices" : [10000], "color" : "blue" }, + { "car" : { "make" : "BMW", "model" : "F20", "ye": {"ar": "2002"}}, "prices" : [15000], "color" : "red" }, + { "car" : { "make" : "Mercedes", "model" : "SLS", "ye": {"ar": "2003"}}, "prices" : [20000], "color" : "yellow" }, + { "car" : { "make" : "Porsche", "model" : "9PA AF1", "ye": {"ar": "2004"}}, "prices" : [30000], "color" : "green" } ] diff --git a/packages/transforms/package.json b/packages/transforms/package.json index 6380b7c..ae218f4 100644 --- a/packages/transforms/package.json +++ b/packages/transforms/package.json @@ -42,8 +42,5 @@ "build": "npm run build:cjs && npm run build:mjs", "test": "echo \"Error: no test specified\" && exit 1", "prepublishOnly": "npm run build" - }, - "dependencies": { - "lodash.get": "^4.4.2" } } diff --git a/packages/transforms/src/unwind.ts b/packages/transforms/src/unwind.ts index 8a815b7..abcc210 100644 --- a/packages/transforms/src/unwind.ts +++ b/packages/transforms/src/unwind.ts @@ -1,6 +1,5 @@ -import lodashGet from 'lodash.get'; import type Transform from './Transform.js'; -import { setProp, unsetProp, flattenReducer } from './utils.js'; +import { getProp, setProp, unsetProp, flattenReducer } from './utils.js'; function getUnwindablePaths( obj: T, @@ -57,7 +56,7 @@ export default function unwind< unwindPath: string, ): Array { return rows.flatMap((row) => { - const unwindArray = lodashGet(row, unwindPath); + const unwindArray = getProp(row, unwindPath); if (!Array.isArray(unwindArray)) { return row; diff --git a/packages/transforms/src/utils.ts b/packages/transforms/src/utils.ts index 00da66b..f2356c3 100644 --- a/packages/transforms/src/utils.ts +++ b/packages/transforms/src/utils.ts @@ -1,3 +1,126 @@ +type GetIndexedField = K extends keyof T + ? T[K] + : K extends `${number}` + ? 'length' extends keyof T + ? number extends T['length'] + ? number extends keyof T + ? T[number] + : undefined + : undefined + : undefined + : undefined; + +type FieldWithPossiblyUndefined = + | GetFieldType, Key> + | Extract; + +type IndexedFieldWithPossiblyUndefined = + | GetIndexedField, Key> + | Extract; + +type GetFieldType = P extends `${infer Left}.${infer Right}` + ? Left extends keyof Exclude + ? + | FieldWithPossiblyUndefined[Left], Right> + | Extract + : Left extends `${infer FieldKey}[${infer IndexKey}]` + ? FieldKey extends keyof T + ? FieldWithPossiblyUndefined< + IndexedFieldWithPossiblyUndefined, + Right + > + : undefined + : undefined + : P extends keyof T + ? T[P] + : P extends `${infer FieldKey}[${infer IndexKey}]` + ? FieldKey extends keyof T + ? IndexedFieldWithPossiblyUndefined + : undefined + : IndexedFieldWithPossiblyUndefined; + +type PropertyName = string | number | symbol; + +const rePropName = RegExp( + // Match anything that isn't a dot or bracket. + '[^.[\\]]+' + '|' + + // Or match property names within brackets. + '\\[(?:' + + // Match a non-string expression. + '([^"\'][^[]*)' + '|' + + // Or match strings (supports escaping characters). + '(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' + + ')\\]'+ '|' + + // Or match "" as the space between consecutive dots or empty brackets. + '(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))' + , 'g'); + +/** + * Casts `value` to a path array if it's not one. + * + * @private + * @param {*} value The value to inspect. + * @param {Object} [object] The object to query keys on. + * @returns {Array} Returns the cast property path array. + */ +function castPath( + path: TKey, + obj: TObject, +): [TKey]; +function castPath< + TPath extends string, + TObject, +>( + path: TPath, + obj: TObject, +): Exclude, null | undefined>; +function castPath(value: string): string[] { + const result: string[] = []; + let match: RegExpExecArray | null; + while (match = rePropName.exec(value)) { + result.push(match[3] ?? match[1]?.trim() ?? match[0]); + } + return result; +} + +export function getProp( + obj: TObject, + path: TKey, + defaultValue: TObject[TKey], +): TObject[TKey]; +export function getProp< + TObject, + TPath extends string, +>( + obj: TObject, + path: TPath, +): Exclude, null | undefined>; +export function getProp< + TObject, + TPath extends string, + TDefault = GetFieldType, +>( + obj: TObject, + path: TPath, + defaultValue: TDefault, +): Exclude, null | undefined> | TDefault; +export function getProp(obj: any, path: PropertyName, defaultValue?: T): T | undefined { + if (path in obj) { + const value = obj[path]; + return value === undefined ? defaultValue : value; + } + + const processedPath = Array.isArray(path) ? path : castPath(path, obj); + let currentValue = obj; + for (const key of processedPath) { + currentValue = currentValue?.[key]; + if (currentValue === undefined) return defaultValue; + } + return currentValue; + // const value = obj[path]; + // return value === undefined ? defaultValue : value; +} + type PropertyPath = string | ReadonlyArray; function propertyPathToString(path: PropertyPath): ReadonlyArray { diff --git a/packages/whatwg/test/AsyncParser.ts b/packages/whatwg/test/AsyncParser.ts index 239a76c..ba47d43 100644 --- a/packages/whatwg/test/AsyncParser.ts +++ b/packages/whatwg/test/AsyncParser.ts @@ -412,7 +412,7 @@ export default function ( }, { label: 'Price', - value: 'price', + value: 'prices[0]', }, { label: 'Color', @@ -431,6 +431,38 @@ export default function ( t.equal(csv, csvFixtures.nested); }); + testRunner.add('should support nested properties selectors using braket notation', async (t) => { + const opts: ParserOptions = { + fields: [ + { + label: 'Make', + value: 'car[make]', + }, + { + label: 'Model', + value: 'car["model"]', + }, + { + label: 'Price', + value: 'prices[0]', + }, + { + label: 'Color', + value: 'color', + }, + { + label: 'Year', + value: 'car[\'ye\'][ar]', + }, + ], + }; + + const parser = new Parser(opts); + const csv = await parseInput(parser, jsonFixtures.nested()); + + t.equal(csv, csvFixtures.nested); + }); + testRunner.add( 'field.value function should receive a valid field object', async (t) => { diff --git a/packages/whatwg/test/AsyncParserInMemory.ts b/packages/whatwg/test/AsyncParserInMemory.ts index 239a76c..ba47d43 100644 --- a/packages/whatwg/test/AsyncParserInMemory.ts +++ b/packages/whatwg/test/AsyncParserInMemory.ts @@ -412,7 +412,7 @@ export default function ( }, { label: 'Price', - value: 'price', + value: 'prices[0]', }, { label: 'Color', @@ -431,6 +431,38 @@ export default function ( t.equal(csv, csvFixtures.nested); }); + testRunner.add('should support nested properties selectors using braket notation', async (t) => { + const opts: ParserOptions = { + fields: [ + { + label: 'Make', + value: 'car[make]', + }, + { + label: 'Model', + value: 'car["model"]', + }, + { + label: 'Price', + value: 'prices[0]', + }, + { + label: 'Color', + value: 'color', + }, + { + label: 'Year', + value: 'car[\'ye\'][ar]', + }, + ], + }; + + const parser = new Parser(opts); + const csv = await parseInput(parser, jsonFixtures.nested()); + + t.equal(csv, csvFixtures.nested); + }); + testRunner.add( 'field.value function should receive a valid field object', async (t) => { diff --git a/packages/whatwg/test/TransformStream.ts b/packages/whatwg/test/TransformStream.ts index 1f31a2a..0a063c2 100644 --- a/packages/whatwg/test/TransformStream.ts +++ b/packages/whatwg/test/TransformStream.ts @@ -391,7 +391,7 @@ export default function ( }, { label: 'Price', - value: 'price', + value: 'prices[0]', }, { label: 'Color', @@ -410,6 +410,38 @@ export default function ( t.equal(csv, csvFixtures.nested); }); + testRunner.add('should support nested properties selectors using braket notation', async (t) => { + const opts: ParserOptions = { + fields: [ + { + label: 'Make', + value: 'car[make]', + }, + { + label: 'Model', + value: 'car["model"]', + }, + { + label: 'Price', + value: 'prices[0]', + }, + { + label: 'Color', + value: 'color', + }, + { + label: 'Year', + value: 'car[\'ye\'][ar]', + }, + ], + }; + + const parser = new Parser(opts); + const csv = await parseInput(parser, jsonFixtures.nested()); + + t.equal(csv, csvFixtures.nested); + }); + testRunner.add( 'field.value function should receive a valid field object', async (t) => {