diff --git a/README.md b/README.md index 50026bb..20d0f10 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ The following items will NOT be removed: ### `preserveEmptyArray` -Optional boolean. -If provided, empty arrays `[]` will not get removed +Optional boolean or context map. +Controls whether empty arrays will not get removed. ```js import removeUndefinedObjects from 'remove-undefined-objects'; @@ -54,10 +54,28 @@ console.log( // { key1: [], key2: [], nested: { key3: 'a', key4: [] } } ``` +To preserve empty arrays only in certain locations, provide a context map: + +```js +console.log(removeUndefinedObjects([], { preserveEmptyArray: { root: true } })); +// [] + +console.log( + removeUndefinedObjects( + { key1: [], key2: [undefined], key3: [[], ['value']] }, + { preserveEmptyArray: { objectProperty: true } }, + ), +); +// { key1: [], key2: [], key3: [['value']] } + +console.log(removeUndefinedObjects([[], [undefined], ['value']], { preserveEmptyArray: { arrayItem: true } })); +// [[], [], ['value']] +``` + ### `preserveEmptyObject` -Optional boolean. -If provided, empty objects will not be removed. +Optional boolean or context map. +Controls whether empty objects will not be removed. ```js import removeUndefinedObjects from 'remove-undefined-objects'; @@ -71,6 +89,35 @@ console.log( // { key1: {}, key2: {}, key3: 123 } ``` +To preserve empty objects only in certain locations, provide a context map: + +```js +console.log(removeUndefinedObjects({}, { preserveEmptyObject: { root: true } })); +// {} + +console.log(removeUndefinedObjects({ key1: [{}, { nested: undefined }, { key: 'value' }] })); +// { key1: [{ key: 'value' }] } + +console.log( + removeUndefinedObjects( + { key1: [{}, { nested: undefined }, { key: 'value' }], key2: {}, key3: { nested: undefined } }, + { preserveEmptyObject: { objectProperty: true } }, + ), +); +// { key1: [{ key: 'value' }], key2: {}, key3: {} } + +console.log( + removeUndefinedObjects([{}, { nested: undefined }, { key: 'value' }], { preserveEmptyObject: { arrayItem: true } }), +); +// [{}, {}, { key: 'value' }] +``` + +Supported preservation contexts are: + +- `root`: the cleaned input value itself. +- `objectProperty`: a value assigned to an object property. +- `arrayItem`: a value contained directly in an array. + ### `preserveNullishArrays` Optional boolean. diff --git a/src/index.ts b/src/index.ts index b7ad7e5..fe6ea9d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,13 +10,25 @@ function isEmptyArray(arr: unknown) { return Array.isArray(arr) && arr.length === 0; } +type PreservationContext = 'arrayItem' | 'objectProperty' | 'root'; + +type PreservationRule = boolean | Partial>; + interface RemovalOptions { - preserveEmptyArray?: boolean; - preserveEmptyObject?: boolean; + preserveEmptyArray?: PreservationRule; + preserveEmptyObject?: PreservationRule; preserveNullishArrays?: boolean; removeAllFalsy?: boolean; } +function shouldPreserve(rule: PreservationRule | undefined, context: PreservationContext) { + if (typeof rule === 'boolean') { + return rule; + } + + return rule?.[context] ?? false; +} + // Remove objects that has undefined value or recursively contain undefined values function removeUndefined(obj: any): any { if (obj === undefined) { @@ -72,9 +84,9 @@ function stripEmptyObjects(obj: any, options: RemovalOptions = {}) { value = stripEmptyObjects(value, options); - if (isEmptyObject(value) && !options.preserveEmptyObject) { + if (isEmptyObject(value) && !shouldPreserve(options.preserveEmptyObject, 'objectProperty')) { delete cleanObj[key]; - } else if (isEmptyArray(value) && !options.preserveEmptyArray) { + } else if (isEmptyArray(value) && !shouldPreserve(options.preserveEmptyArray, 'objectProperty')) { delete cleanObj[key]; } else { cleanObj[key] = value; @@ -89,9 +101,9 @@ function stripEmptyObjects(obj: any, options: RemovalOptions = {}) { if (typeof value === 'object' && value !== null) { value = stripEmptyObjects(value, options); - if (isEmptyObject(value) && !options.preserveEmptyObject) { + if (isEmptyObject(value) && !shouldPreserve(options.preserveEmptyObject, 'arrayItem')) { delete cleanObj[idx]; - } else if (isEmptyArray(value) && !options.preserveEmptyArray) { + } else if (isEmptyArray(value) && !shouldPreserve(options.preserveEmptyArray, 'arrayItem')) { delete cleanObj[idx]; } else { cleanObj[idx] = value; @@ -123,8 +135,8 @@ export default function removeUndefinedObjects(obj?: T, options?: RemovalOpti // If the only thing that's leftover is an empty object or empty array then return nothing. if ( - (isEmptyObject(withoutUndefined) && !options?.preserveEmptyObject) || - (isEmptyArray(withoutUndefined) && !options?.preserveEmptyArray) + (isEmptyObject(withoutUndefined) && !shouldPreserve(options?.preserveEmptyObject, 'root')) || + (isEmptyArray(withoutUndefined) && !shouldPreserve(options?.preserveEmptyArray, 'root')) ) { return; } diff --git a/test/index.test.ts b/test/index.test.ts index 1fb0d1b..77e8012 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -181,6 +181,125 @@ it('should not remove empty object values from arrays when preserveEmptyObject i }); }); +it('should preserve empty object values by context', () => { + expect(removeUndefinedObjects({}, { preserveEmptyObject: { root: true } })).toStrictEqual({}); + expect(removeUndefinedObjects({ value: {} }, { preserveEmptyObject: { root: true } })).toStrictEqual({}); + expect(removeUndefinedObjects({}, { preserveEmptyObject: { objectProperty: true } })).toBeUndefined(); + + expect( + removeUndefinedObjects( + { value: {}, nested: { value: undefined } }, + { preserveEmptyObject: { objectProperty: true } }, + ), + ).toStrictEqual({ value: {}, nested: {} }); + + expect( + removeUndefinedObjects( + { + topLevelEmptyObject: {}, + nested: { + emptyObject: {}, + }, + array: [{}, { emptyObject: {} }, { value: 'value' }], + }, + { + preserveEmptyObject: { + objectProperty: true, + root: true, + }, + }, + ), + ).toStrictEqual({ + topLevelEmptyObject: {}, + nested: { + emptyObject: {}, + }, + array: [{ emptyObject: {} }, { value: 'value' }], + }); + + expect( + removeUndefinedObjects( + { value: [{}, { a: undefined }] }, + { + preserveEmptyArray: true, + preserveEmptyObject: { + objectProperty: true, + root: true, + }, + }, + ), + ).toStrictEqual({ value: [] }); + + expect( + removeUndefinedObjects([{}, { a: undefined }, { b: 'b' }], { + preserveEmptyObject: { arrayItem: true }, + }), + ).toStrictEqual([{}, {}, { b: 'b' }]); +}); + +it('should preserve empty arrays by context', () => { + expect(removeUndefinedObjects([], { preserveEmptyArray: { root: true } })).toStrictEqual([]); + expect(removeUndefinedObjects({ value: [] }, { preserveEmptyArray: { root: true } })).toBeUndefined(); + expect(removeUndefinedObjects([], { preserveEmptyArray: { objectProperty: true } })).toBeUndefined(); + + expect( + removeUndefinedObjects( + { value: [], nested: { value: [undefined] } }, + { preserveEmptyArray: { objectProperty: true } }, + ), + ).toStrictEqual({ value: [], nested: { value: [] } }); + + expect( + removeUndefinedObjects([[], [undefined], ['value']], { + preserveEmptyArray: { arrayItem: true }, + }), + ).toStrictEqual([[], [], ['value']]); +}); + +it('should preserve empty objects and arrays by context when both maps are supplied', () => { + expect( + removeUndefinedObjects( + { + emptyArrayProperty: [], + emptyObjectProperty: {}, + nested: { + emptyArrayProperty: [undefined], + emptyObjectProperty: { value: undefined }, + }, + array: [ + {}, + [], + { + emptyArrayProperty: [], + emptyObjectProperty: {}, + }, + ['value'], + { value: 'value' }, + ], + }, + { + preserveEmptyArray: { objectProperty: true }, + preserveEmptyObject: { objectProperty: true }, + }, + ), + ).toStrictEqual({ + emptyArrayProperty: [], + emptyObjectProperty: {}, + nested: { + emptyArrayProperty: [], + emptyObjectProperty: {}, + }, + array: [ + { + emptyArrayProperty: [], + emptyObjectProperty: {}, + }, + ['value'], + { value: 'value' }, + ], + }); +}); + it('should not remove null values from arrays when preserveArrayNulls is true', () => { expect(removeUndefinedObjects([null], { preserveNullishArrays: true })).toStrictEqual([null]); expect(removeUndefinedObjects([undefined], { preserveNullishArrays: true })).toBeUndefined();