From 80bbf22405974522b4a33f3d12336cc5f5fad81b Mon Sep 17 00:00:00 2001 From: Sergio LR Date: Mon, 3 Mar 2025 11:12:31 +0100 Subject: [PATCH 1/7] feat: add transformKeystoSnake + isPlainObject fix --- CHANGELOG | 5 +++++ README.md | 1 + index.d.ts | 1 + index.js | 2 ++ package.json | 2 +- src/isPlainObject.js | 3 ++- src/transformKeysToSnake.js | 25 ++++++++++++++++++++++ test/isPlainObject.js | 8 +++++++ test/transformKeystoSnake.js | 41 ++++++++++++++++++++++++++++++++++++ 9 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/transformKeysToSnake.js create mode 100644 test/transformKeystoSnake.js diff --git a/CHANGELOG b/CHANGELOG index 0f12971..9935704 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ +#1.20.0 +- feat: add transformKeysToSnake +- fix: fix isPlainObject to check that the constructor is an object + #1.19.0 - feat: add unset + #1.18.1 - feat: Add URL validation diff --git a/README.md b/README.md index 491b941..c017750 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Currently supported utils: - `snakeCase` - convert camel case and string/dash separated strings to snake case - `sum` - calculate sum of array items - `sumBy` - calculate sum of array items using iteratee function or string shortcut +- `transfoormKeystoSnake` - transform every object key to snake case - `union` - Returns the union of the given arrays - `uniqBy` - get unique values of array by the iteratee function or property path - `uniqWith` - Returns a new array with unique values, using a comparator function diff --git a/index.d.ts b/index.d.ts index adb3c52..8fee829 100644 --- a/index.d.ts +++ b/index.d.ts @@ -40,6 +40,7 @@ export function shuffle (array: Array): Array export function snakeCase (str: string): string export function sum (values: Array): number export function sumBy (values: Array, iteratee: Function | string): number +export function transformKeysToSnake (obj: Object) : Object export function union (...arrays: Array[]): Array export function uniqBy (array: Array, iteratee: Function | string): Array export function uniqWith (array: Array, comparator: Function): Array diff --git a/index.js b/index.js index 2b00365..db46d74 100644 --- a/index.js +++ b/index.js @@ -42,6 +42,7 @@ const shuffle = require('./src/shuffle') const snakeCase = require('./src/snakeCase') const sum = require('./src/sum') const sumBy = require('./src/sumBy') +const transformKeysToSnake = require('./src/transformKeysToSnake') const union = require('./src/union') const uniqBy = require('./src/uniqBy') const uniqWith = require('./src/uniqWith') @@ -93,6 +94,7 @@ module.exports = { snakeCase, sum, sumBy, + transformKeysToSnake, union, uniqBy, uniqWith, diff --git a/package.json b/package.json index 46ba1d3..bde5b2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitfinexcom/lib-js-util-base", - "version": "1.19.0", + "version": "1.20.0", "description": "general utils", "main": "index.js", "scripts": { diff --git a/src/isPlainObject.js b/src/isPlainObject.js index e166e90..f8e4618 100644 --- a/src/isPlainObject.js +++ b/src/isPlainObject.js @@ -1,5 +1,6 @@ 'use strict' -const isPlainObject = (val) => val !== null && typeof val === 'object' && !Array.isArray(val) +const isUsingObjConstructor = (val) => Object.getPrototypeOf(val) === Object.prototype || Object.getPrototypeOf(val) === null +const isPlainObject = (val) => val !== null && typeof val === 'object' && isUsingObjConstructor(val) module.exports = isPlainObject diff --git a/src/transformKeysToSnake.js b/src/transformKeysToSnake.js new file mode 100644 index 0000000..ee8ad57 --- /dev/null +++ b/src/transformKeysToSnake.js @@ -0,0 +1,25 @@ +'use strict' + +const isPlainObject = require('./isPlainObject') +const snakeCase = require('./snakeCase') + +/** + * Transform object keys to snake case + * e.g. { fooBar: 1 } => { foo_bar: 1 } + * @param {Object} obj + * @returns {Object} + */ +const transformKeysToSnakeCase = (obj) => { + if (!isPlainObject(obj)) { + return obj + } + + return Object.fromEntries( + Object.entries(obj).map(([k, v]) => [ + snakeCase(k), + Array.isArray(v) ? v.map(transformKeysToSnakeCase) : transformKeysToSnakeCase(v) + ]) + ) +} + +module.exports = transformKeysToSnakeCase diff --git a/test/isPlainObject.js b/test/isPlainObject.js index 9adb1c4..c386ac5 100644 --- a/test/isPlainObject.js +++ b/test/isPlainObject.js @@ -9,6 +9,10 @@ describe('isPlainObject', () => { it('should return true for a plain object', () => { assert.strictEqual(isPlainObject({}), true) assert.strictEqual(isPlainObject({ key: 'value' }), true) + // eslint-disable-next-line no-new-object + assert.strictEqual(isPlainObject(new Object()), true) + // eslint-disable-next-line no-new-object + assert.strictEqual(isPlainObject(new Object({ key: 'value ' })), true) }) it('should return false for an array', () => { @@ -19,6 +23,10 @@ describe('isPlainObject', () => { assert.strictEqual(isPlainObject(() => { }), false) }) + it('should return false for a date', () => { + assert.strictEqual(isPlainObject(new Date()), false) + }) + it('should return false for a number', () => { assert.strictEqual(isPlainObject(42), false) }) diff --git a/test/transformKeystoSnake.js b/test/transformKeystoSnake.js new file mode 100644 index 0000000..5988a23 --- /dev/null +++ b/test/transformKeystoSnake.js @@ -0,0 +1,41 @@ +'use strict' + +/* eslint-env mocha */ + +const assert = require('assert') +const { transformKeysToSnake } = require('../index') +const { itEach } = require('mocha-it-each') + +describe('transformKeystoSnake', () => { + itEach('should return input when it is not an object', [new Date(), null, false], (input) => { + assert.equal(transformKeysToSnake(input), input) + }) + + it('should return input with snake case props recursively', () => { + const input = { + camelProp: 'true', + nestedProp: { + first_name: 'john', + lastName: 'doe', + tags: ['keepCamel'] + }, + someObjs: [ + { someProp: 'value' }, + { foo_bar: 'value', foo: 'value' } + ] + } + const output = { + camel_prop: 'true', + nested_prop: { + first_name: 'john', + last_name: 'doe', + tags: ['keepCamel'] + }, + some_objs: [ + { some_prop: 'value' }, + { foo_bar: 'value', foo: 'value' } + ] + } + assert.deepEqual(transformKeysToSnake(input), output) + }) +}) From 0167588b963e6b0976e47a76a5d0e15e0a71960d Mon Sep 17 00:00:00 2001 From: Sergio LR Date: Tue, 4 Mar 2025 16:19:46 +0100 Subject: [PATCH 2/7] refactor: fix typo --- README.md | 2 +- test/transformKeystoSnake.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c017750..0d5f940 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Currently supported utils: - `snakeCase` - convert camel case and string/dash separated strings to snake case - `sum` - calculate sum of array items - `sumBy` - calculate sum of array items using iteratee function or string shortcut -- `transfoormKeystoSnake` - transform every object key to snake case +- `transformKeysToSnake` - transform every object key to snake case - `union` - Returns the union of the given arrays - `uniqBy` - get unique values of array by the iteratee function or property path - `uniqWith` - Returns a new array with unique values, using a comparator function diff --git a/test/transformKeystoSnake.js b/test/transformKeystoSnake.js index 5988a23..a9b41fd 100644 --- a/test/transformKeystoSnake.js +++ b/test/transformKeystoSnake.js @@ -6,7 +6,7 @@ const assert = require('assert') const { transformKeysToSnake } = require('../index') const { itEach } = require('mocha-it-each') -describe('transformKeystoSnake', () => { +describe('transformKeysToSnake', () => { itEach('should return input when it is not an object', [new Date(), null, false], (input) => { assert.equal(transformKeysToSnake(input), input) }) From 0cd794cbc9234edb74c0c46a7e6a637aae71b55d Mon Sep 17 00:00:00 2001 From: Sergio LR Date: Fri, 7 Mar 2025 15:14:50 +0100 Subject: [PATCH 3/7] fix: make it work with array of objects --- src/transformKeysToSnake.js | 4 ++++ test/transformKeystoSnake.js | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/transformKeysToSnake.js b/src/transformKeysToSnake.js index ee8ad57..05a7b81 100644 --- a/src/transformKeysToSnake.js +++ b/src/transformKeysToSnake.js @@ -10,6 +10,10 @@ const snakeCase = require('./snakeCase') * @returns {Object} */ const transformKeysToSnakeCase = (obj) => { + if (Array.isArray(obj)) { + return obj.map(item => transformKeysToSnakeCase(item)) + } + if (!isPlainObject(obj)) { return obj } diff --git a/test/transformKeystoSnake.js b/test/transformKeystoSnake.js index a9b41fd..3268d9e 100644 --- a/test/transformKeystoSnake.js +++ b/test/transformKeystoSnake.js @@ -38,4 +38,14 @@ describe('transformKeysToSnake', () => { } assert.deepEqual(transformKeysToSnake(input), output) }) + + it('should work with', () => { + const input = { + mixedArray: [123, { someKey: 'value' }, [{ nestedArrayItem: 'test' }]] + } + const output = { + mixed_array: [123, { some_key: 'value' }, [{ nested_array_item: 'test' }]] + } + assert.deepEqual(transformKeysToSnake(input), output) + }) }) From e1dd42fa7cbc0626ecc83b92753674cc541df3ba Mon Sep 17 00:00:00 2001 From: Sergio LR Date: Fri, 7 Mar 2025 19:49:39 +0100 Subject: [PATCH 4/7] refactor: remove useless check --- src/transformKeysToSnake.js | 2 +- test/transformKeystoSnake.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transformKeysToSnake.js b/src/transformKeysToSnake.js index 05a7b81..028ecf8 100644 --- a/src/transformKeysToSnake.js +++ b/src/transformKeysToSnake.js @@ -21,7 +21,7 @@ const transformKeysToSnakeCase = (obj) => { return Object.fromEntries( Object.entries(obj).map(([k, v]) => [ snakeCase(k), - Array.isArray(v) ? v.map(transformKeysToSnakeCase) : transformKeysToSnakeCase(v) + transformKeysToSnakeCase(v) ]) ) } diff --git a/test/transformKeystoSnake.js b/test/transformKeystoSnake.js index 3268d9e..a234d11 100644 --- a/test/transformKeystoSnake.js +++ b/test/transformKeystoSnake.js @@ -6,7 +6,7 @@ const assert = require('assert') const { transformKeysToSnake } = require('../index') const { itEach } = require('mocha-it-each') -describe('transformKeysToSnake', () => { +describe.only('transformKeysToSnake', () => { itEach('should return input when it is not an object', [new Date(), null, false], (input) => { assert.equal(transformKeysToSnake(input), input) }) From 35de227217aecd09368d706787ce1fdf99e46c47 Mon Sep 17 00:00:00 2001 From: Sergio LR Date: Mon, 10 Mar 2025 09:36:34 +0100 Subject: [PATCH 5/7] test: remove .only --- test/transformKeystoSnake.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/transformKeystoSnake.js b/test/transformKeystoSnake.js index a234d11..3268d9e 100644 --- a/test/transformKeystoSnake.js +++ b/test/transformKeystoSnake.js @@ -6,7 +6,7 @@ const assert = require('assert') const { transformKeysToSnake } = require('../index') const { itEach } = require('mocha-it-each') -describe.only('transformKeysToSnake', () => { +describe('transformKeysToSnake', () => { itEach('should return input when it is not an object', [new Date(), null, false], (input) => { assert.equal(transformKeysToSnake(input), input) }) From 0336090de9215b2e30e49e26967b118688eb1160 Mon Sep 17 00:00:00 2001 From: Sergio LR Date: Thu, 27 Mar 2025 16:59:35 +0100 Subject: [PATCH 6/7] feat: add recursive option --- README.md | 2 +- index.d.ts | 2 +- src/transformKeysToSnake.js | 8 +-- test/transformKeystoSnake.js | 121 ++++++++++++++++++++++++----------- 4 files changed, 89 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 0d5f940..7e47d2a 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Currently supported utils: - `snakeCase` - convert camel case and string/dash separated strings to snake case - `sum` - calculate sum of array items - `sumBy` - calculate sum of array items using iteratee function or string shortcut -- `transformKeysToSnake` - transform every object key to snake case +- `transformKeysToSnake` - transform every object key to snake case with option to be recursive - `union` - Returns the union of the given arrays - `uniqBy` - get unique values of array by the iteratee function or property path - `uniqWith` - Returns a new array with unique values, using a comparator function diff --git a/index.d.ts b/index.d.ts index 8fee829..3a3c119 100644 --- a/index.d.ts +++ b/index.d.ts @@ -40,7 +40,7 @@ export function shuffle (array: Array): Array export function snakeCase (str: string): string export function sum (values: Array): number export function sumBy (values: Array, iteratee: Function | string): number -export function transformKeysToSnake (obj: Object) : Object +export function transformKeysToSnake (obj: Object, opts?: {recursive?: boolean}) : Object export function union (...arrays: Array[]): Array export function uniqBy (array: Array, iteratee: Function | string): Array export function uniqWith (array: Array, comparator: Function): Array diff --git a/src/transformKeysToSnake.js b/src/transformKeysToSnake.js index 028ecf8..0eea97c 100644 --- a/src/transformKeysToSnake.js +++ b/src/transformKeysToSnake.js @@ -9,9 +9,9 @@ const snakeCase = require('./snakeCase') * @param {Object} obj * @returns {Object} */ -const transformKeysToSnakeCase = (obj) => { +const transformKeysToSnake = (obj, { recursive } = {}) => { if (Array.isArray(obj)) { - return obj.map(item => transformKeysToSnakeCase(item)) + return obj.map(item => transformKeysToSnake(item)) } if (!isPlainObject(obj)) { @@ -21,9 +21,9 @@ const transformKeysToSnakeCase = (obj) => { return Object.fromEntries( Object.entries(obj).map(([k, v]) => [ snakeCase(k), - transformKeysToSnakeCase(v) + recursive ? transformKeysToSnake(v) : v ]) ) } -module.exports = transformKeysToSnakeCase +module.exports = transformKeysToSnake diff --git a/test/transformKeystoSnake.js b/test/transformKeystoSnake.js index 3268d9e..5a49c84 100644 --- a/test/transformKeystoSnake.js +++ b/test/transformKeystoSnake.js @@ -6,46 +6,91 @@ const assert = require('assert') const { transformKeysToSnake } = require('../index') const { itEach } = require('mocha-it-each') -describe('transformKeysToSnake', () => { - itEach('should return input when it is not an object', [new Date(), null, false], (input) => { - assert.equal(transformKeysToSnake(input), input) - }) +describe.only('transformKeysToSnake', () => { + describe('non recursive (default)', () => { + itEach('should return input when it is not an object', [new Date(), null, false], (input) => { + assert.equal(transformKeysToSnake(input), input) + }) + + it('should return input with snake case props recursively', () => { + const input = { + camelProp: 'true', + nestedProp: { + first_name: 'john', + lastName: 'doe', + tags: ['keepCamel'] + }, + someObjs: [ + { someProp: 'value' }, + { foo_bar: 'value', foo: 'value' } + ] + } + const output = { + camel_prop: 'true', + nested_prop: { + first_name: 'john', + lastName: 'doe', + tags: ['keepCamel'] + }, + some_objs: [ + { someProp: 'value' }, + { foo_bar: 'value', foo: 'value' } + ] + } + assert.deepEqual(transformKeysToSnake(input), output) + }) - it('should return input with snake case props recursively', () => { - const input = { - camelProp: 'true', - nestedProp: { - first_name: 'john', - lastName: 'doe', - tags: ['keepCamel'] - }, - someObjs: [ - { someProp: 'value' }, - { foo_bar: 'value', foo: 'value' } - ] - } - const output = { - camel_prop: 'true', - nested_prop: { - first_name: 'john', - last_name: 'doe', - tags: ['keepCamel'] - }, - some_objs: [ - { some_prop: 'value' }, - { foo_bar: 'value', foo: 'value' } - ] - } - assert.deepEqual(transformKeysToSnake(input), output) + it('should work with array mix of object and nested arrays', () => { + const input = { + mixedArray: [123, { someKey: 'value' }, [{ nestedArrayItem: 'test' }]] + } + const output = { + mixed_array: [123, { someKey: 'value' }, [{ nestedArrayItem: 'test' }]] + } + assert.deepEqual(transformKeysToSnake(input), output) + }) }) + describe('recursive', () => { + itEach('should return input when it is not an object', [new Date(), null, false], (input) => { + assert.equal(transformKeysToSnake(input, { recursive: true }), input) + }) + + it('should return input with snake case props recursively', () => { + const input = { + camelProp: 'true', + nestedProp: { + first_name: 'john', + lastName: 'doe', + tags: ['keepCamel'] + }, + someObjs: [ + { someProp: 'value' }, + { foo_bar: 'value', foo: 'value' } + ] + } + const output = { + camel_prop: 'true', + nested_prop: { + first_name: 'john', + last_name: 'doe', + tags: ['keepCamel'] + }, + some_objs: [ + { some_prop: 'value' }, + { foo_bar: 'value', foo: 'value' } + ] + } + assert.deepEqual(transformKeysToSnake(input, { recursive: true }), output) + }) - it('should work with', () => { - const input = { - mixedArray: [123, { someKey: 'value' }, [{ nestedArrayItem: 'test' }]] - } - const output = { - mixed_array: [123, { some_key: 'value' }, [{ nested_array_item: 'test' }]] - } - assert.deepEqual(transformKeysToSnake(input), output) + it('should work with array mix of object and nested arrays', () => { + const input = { + mixedArray: [123, { someKey: 'value' }, [{ nestedArrayItem: 'test' }]] + } + const output = { + mixed_array: [123, { some_key: 'value' }, [{ nested_array_item: 'test' }]] + } + assert.deepEqual(transformKeysToSnake(input, { recursive: true }), output) + }) }) }) From f1781af5d548b2f7fff99d7e5975ffc8257990a1 Mon Sep 17 00:00:00 2001 From: Sergio LR Date: Thu, 27 Mar 2025 17:06:50 +0100 Subject: [PATCH 7/7] fix: propagate options in recursions --- src/transformKeysToSnake.js | 8 +++++--- test/transformKeystoSnake.js | 8 +++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/transformKeysToSnake.js b/src/transformKeysToSnake.js index 0eea97c..d125d8d 100644 --- a/src/transformKeysToSnake.js +++ b/src/transformKeysToSnake.js @@ -7,11 +7,13 @@ const snakeCase = require('./snakeCase') * Transform object keys to snake case * e.g. { fooBar: 1 } => { foo_bar: 1 } * @param {Object} obj + * @param {Object} [opts] + * @param {boolean} [opts.recursive] * @returns {Object} */ -const transformKeysToSnake = (obj, { recursive } = {}) => { +const transformKeysToSnake = (obj, opts) => { if (Array.isArray(obj)) { - return obj.map(item => transformKeysToSnake(item)) + return obj.map(item => transformKeysToSnake(item, opts)) } if (!isPlainObject(obj)) { @@ -21,7 +23,7 @@ const transformKeysToSnake = (obj, { recursive } = {}) => { return Object.fromEntries( Object.entries(obj).map(([k, v]) => [ snakeCase(k), - recursive ? transformKeysToSnake(v) : v + opts?.recursive ? transformKeysToSnake(v, opts) : v ]) ) } diff --git a/test/transformKeystoSnake.js b/test/transformKeystoSnake.js index 5a49c84..fd29304 100644 --- a/test/transformKeystoSnake.js +++ b/test/transformKeystoSnake.js @@ -6,7 +6,7 @@ const assert = require('assert') const { transformKeysToSnake } = require('../index') const { itEach } = require('mocha-it-each') -describe.only('transformKeysToSnake', () => { +describe('transformKeysToSnake', () => { describe('non recursive (default)', () => { itEach('should return input when it is not an object', [new Date(), null, false], (input) => { assert.equal(transformKeysToSnake(input), input) @@ -61,6 +61,9 @@ describe.only('transformKeysToSnake', () => { nestedProp: { first_name: 'john', lastName: 'doe', + level2: { + fooBar: 'x' + }, tags: ['keepCamel'] }, someObjs: [ @@ -73,6 +76,9 @@ describe.only('transformKeysToSnake', () => { nested_prop: { first_name: 'john', last_name: 'doe', + level2: { + foo_bar: 'x' + }, tags: ['keepCamel'] }, some_objs: [