-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Universal-ts-utils migrating node-core object utils (#400)
* Adding convertDateFieldsToIsoString * transformToKebabCase added * Improving doc * Adding deepClone * Adding pick * Adding isEmpty * Adding groupByPath * Adding copyWithoutNullish * Improving copy wwithoutNullish * Adding copyWithoutFalsy * Improving doc * Lint fix * Fixing tests * Lint fix * Increasing coverage * Improving pick type * Pick return type base on options
- Loading branch information
1 parent
1b5f900
commit 722daff
Showing
18 changed files
with
1,404 additions
and
1 deletion.
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
10 changes: 10 additions & 0 deletions
10
...es/app/universal-ts-utils/src/public/object/__snapshots__/copyWithoutNullish.spec.ts.snap
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,10 @@ | ||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html | ||
|
||
exports[`copyWithoutNullish > Does nothing when there are no undefined fields 1`] = ` | ||
{ | ||
"a": "a", | ||
"b": "", | ||
"c": " ", | ||
"e": {}, | ||
} | ||
`; |
215 changes: 215 additions & 0 deletions
215
packages/app/universal-ts-utils/src/public/object/convertDateFieldsToIsoString.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,215 @@ | ||
import { describe, expect, it } from 'vitest' | ||
import { convertDateFieldsToIsoString } from './convertDateFieldsToIsoString.js' | ||
|
||
type TestInputType = { | ||
id: number | ||
value: string | ||
date: Date | ||
code: number | ||
reason?: string | null | ||
other?: TestInputType | ||
array?: { | ||
id: number | ||
createdAt: Date | ||
}[] | ||
} | ||
|
||
type TestExpectedType = { | ||
id: number | ||
value: string | ||
date: string | ||
code: number | ||
other?: TestExpectedType | ||
array?: { | ||
id: number | ||
createdAt: string | ||
}[] | ||
} | ||
|
||
describe('convertDateFieldsToIsoString', () => { | ||
it('empty object', () => { | ||
expect(convertDateFieldsToIsoString({})).toStrictEqual({}) | ||
}) | ||
|
||
it('simple object', () => { | ||
const date = new Date() | ||
const input: TestInputType = { | ||
id: 1, | ||
date, | ||
value: 'test', | ||
reason: 'reason', | ||
code: 100, | ||
} | ||
|
||
const output: TestExpectedType = convertDateFieldsToIsoString(input) | ||
|
||
expect(output).toStrictEqual({ | ||
id: 1, | ||
date: date.toISOString(), | ||
value: 'test', | ||
code: 100, | ||
reason: 'reason', | ||
}) | ||
}) | ||
|
||
it('simple array', () => { | ||
const date1 = new Date() | ||
const date2 = new Date() | ||
const input: TestInputType[] = [ | ||
{ | ||
id: 1, | ||
date: date1, | ||
value: 'test', | ||
reason: 'reason', | ||
code: 100, | ||
}, | ||
{ | ||
id: 2, | ||
date: date2, | ||
value: 'test 2', | ||
reason: 'reason 2', | ||
code: 200, | ||
}, | ||
] | ||
|
||
const output: TestExpectedType[] = convertDateFieldsToIsoString(input) | ||
|
||
expect(output).toStrictEqual([ | ||
{ | ||
id: 1, | ||
date: date1.toISOString(), | ||
value: 'test', | ||
code: 100, | ||
reason: 'reason', | ||
}, | ||
{ | ||
id: 2, | ||
date: date2.toISOString(), | ||
value: 'test 2', | ||
code: 200, | ||
reason: 'reason 2', | ||
}, | ||
]) | ||
}) | ||
|
||
it('handles undefined and null', () => { | ||
const date = new Date() | ||
const input: TestInputType = { | ||
id: 1, | ||
date, | ||
value: 'test', | ||
code: 100, | ||
reason: null, | ||
other: undefined, | ||
} | ||
|
||
const output: TestExpectedType = convertDateFieldsToIsoString(input) | ||
|
||
expect(output).toStrictEqual({ | ||
id: 1, | ||
date: date.toISOString(), | ||
value: 'test', | ||
code: 100, | ||
reason: null, | ||
other: undefined, | ||
}) | ||
}) | ||
|
||
it('properly handles all types of arrays', () => { | ||
const date = new Date() | ||
const input = { | ||
array1: [date, date], | ||
array2: [1, 2], | ||
array3: ['a', 'b'], | ||
array4: [ | ||
{ id: 1, value: 'value', date, code: 100 } satisfies TestInputType, | ||
{ id: 2, value: 'value2', date, code: 200 } satisfies TestInputType, | ||
], | ||
array5: [1, date, 'a', { id: 1, value: 'value', date, code: 100 } satisfies TestInputType], | ||
} | ||
|
||
type Expected = { | ||
array1: string[] | ||
array2: number[] | ||
array3: string[] | ||
array4: TestExpectedType[] | ||
array5: (number | string | TestExpectedType)[] | ||
} | ||
const output: Expected = convertDateFieldsToIsoString(input) | ||
|
||
expect(output).toStrictEqual({ | ||
array1: [date.toISOString(), date.toISOString()], | ||
array2: [1, 2], | ||
array3: ['a', 'b'], | ||
array4: [ | ||
{ id: 1, value: 'value', date: date.toISOString(), code: 100 }, | ||
{ id: 2, value: 'value2', date: date.toISOString(), code: 200 }, | ||
], | ||
array5: [ | ||
1, | ||
date.toISOString(), | ||
'a', | ||
{ id: 1, value: 'value', date: date.toISOString(), code: 100 }, | ||
], | ||
}) | ||
}) | ||
|
||
it('nested objects and array', () => { | ||
const date1 = new Date() | ||
const date2 = new Date() | ||
date2.setFullYear(1990) | ||
const input: TestInputType = { | ||
id: 1, | ||
date: date1, | ||
value: 'test', | ||
code: 100, | ||
reason: 'reason', | ||
other: { | ||
id: 2, | ||
value: 'test 2', | ||
date: date2, | ||
code: 200, | ||
reason: null, | ||
other: undefined, | ||
}, | ||
array: [ | ||
{ | ||
id: 1, | ||
createdAt: date1, | ||
}, | ||
{ | ||
id: 2, | ||
createdAt: date2, | ||
}, | ||
], | ||
} | ||
|
||
const output: TestExpectedType = convertDateFieldsToIsoString(input) | ||
|
||
expect(output).toMatchObject({ | ||
id: 1, | ||
date: date1.toISOString(), | ||
value: 'test', | ||
code: 100, | ||
reason: 'reason', | ||
other: { | ||
id: 2, | ||
value: 'test 2', | ||
date: date2.toISOString(), | ||
code: 200, | ||
reason: null, | ||
other: undefined, | ||
}, | ||
array: [ | ||
{ | ||
id: 1, | ||
createdAt: date1.toISOString(), | ||
}, | ||
{ | ||
id: 2, | ||
createdAt: date2.toISOString(), | ||
}, | ||
], | ||
}) | ||
}) | ||
}) |
51 changes: 51 additions & 0 deletions
51
packages/app/universal-ts-utils/src/public/object/convertDateFieldsToIsoString.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,51 @@ | ||
type DatesAsString<T> = T extends Date ? string : ExactlyLikeWithDateAsString<T> | ||
|
||
type ExactlyLikeWithDateAsString<T> = T extends object ? { [K in keyof T]: DatesAsString<T[K]> } : T | ||
|
||
/** | ||
* Recursively converts all Date fields in an object or array of objects to ISO string format. | ||
* This function retains the structure of the input, ensuring non-Date fields remain unchanged, | ||
* while Date fields are replaced with their ISO string representations. | ||
* | ||
* @param {object | object[]} object - The object or array of objects to convert. | ||
* @returns {object | object[]} A new object or array of objects with Date fields as ISO strings. | ||
* | ||
* @example | ||
*```typescript | ||
* const obj = { id: 1, created: new Date(), meta: { updated: new Date() } } | ||
* const result = convertDateFieldsToIsoString(obj) | ||
* console.log(result) // { id: 1, created: '2024-01-01T00:00:00.000Z', meta: { updated: '2024-01-01T00:00:00.000Z' } } | ||
* ``` | ||
*/ | ||
export function convertDateFieldsToIsoString<Input extends object>( | ||
object: Input, | ||
): ExactlyLikeWithDateAsString<Input> | ||
export function convertDateFieldsToIsoString<Input extends object>( | ||
object: Input[], | ||
): ExactlyLikeWithDateAsString<Input>[] | ||
export function convertDateFieldsToIsoString<Input extends object>( | ||
object: Input | Input[], | ||
): ExactlyLikeWithDateAsString<Input> | ExactlyLikeWithDateAsString<Input>[] { | ||
if (Array.isArray(object)) { | ||
return object.map(internalConvert) as ExactlyLikeWithDateAsString<Input>[] | ||
} | ||
|
||
return Object.entries(object).reduce( | ||
(result, [key, value]) => { | ||
// @ts-expect-error | ||
result[key] = internalConvert(value) | ||
return result | ||
}, | ||
{} as ExactlyLikeWithDateAsString<Input>, | ||
) | ||
} | ||
|
||
const internalConvert = <T>(item: T): DatesAsString<T> => { | ||
// @ts-expect-error | ||
if (item instanceof Date) return item.toISOString() | ||
// @ts-expect-error | ||
if (item && typeof item === 'object') return convertDateFieldsToIsoString(item) | ||
|
||
// @ts-expect-error | ||
return item | ||
} |
70 changes: 70 additions & 0 deletions
70
packages/app/universal-ts-utils/src/public/object/copyWithoutEmpty.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,70 @@ | ||
import { describe, expect, it } from 'vitest' | ||
import { copyWithoutEmpty } from './copyWithoutEmpty.js' | ||
|
||
describe('copyWithoutEmpty', () => { | ||
it('Does nothing when there are no empty fields', () => { | ||
const result = copyWithoutEmpty({ | ||
a: 'a', | ||
b: ' t ', | ||
c: ' tt', | ||
d: 'tt ', | ||
e: {}, | ||
y: 88, | ||
z: 0, | ||
}) | ||
|
||
expect(result).toMatchInlineSnapshot(` | ||
{ | ||
"a": "a", | ||
"b": " t ", | ||
"c": " tt", | ||
"d": "tt ", | ||
"e": {}, | ||
"y": 88, | ||
"z": 0, | ||
} | ||
`) | ||
}) | ||
|
||
it('Removes empty fields', () => { | ||
const result = copyWithoutEmpty({ | ||
a: undefined, | ||
b: 'a', | ||
c: '', | ||
d: undefined, | ||
e: ' ', | ||
f: null, | ||
g: { | ||
someParam: 12, | ||
}, | ||
h: undefined, | ||
y: 88, | ||
z: 0, | ||
}) | ||
|
||
const varWithNarrowedType = result satisfies Record< | ||
string, | ||
string | Record<string, unknown> | null | number | ||
> | ||
const bValue: string = varWithNarrowedType.b | ||
const gValue: { | ||
someParam: number | ||
} = varWithNarrowedType.g | ||
|
||
expect(bValue).toBe('a') | ||
expect(gValue).toEqual({ | ||
someParam: 12, | ||
}) | ||
|
||
expect(result).toMatchInlineSnapshot(` | ||
{ | ||
"b": "a", | ||
"g": { | ||
"someParam": 12, | ||
}, | ||
"y": 88, | ||
"z": 0, | ||
} | ||
`) | ||
}) | ||
}) |
32 changes: 32 additions & 0 deletions
32
packages/app/universal-ts-utils/src/public/object/copyWithoutEmpty.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,32 @@ | ||
import type { RecordKeyType } from '../../internal/types.js' | ||
|
||
type Output<T extends Record<RecordKeyType, unknown>> = Pick< | ||
T, | ||
{ | ||
[Prop in keyof T]: T[Prop] extends null | undefined | '' ? never : Prop | ||
}[keyof T] | ||
> | ||
|
||
/** | ||
* Creates a shallow copy of an object, excluding properties with "empty" values. | ||
* | ||
* A "empty" value includes `null`, `undefined`, empty strings (`''`). | ||
* | ||
* @param {Record} object - The source object from which to copy properties. | ||
* @returns {Record} A new object containing only the properties from the source object | ||
* that do not have "falsy" values. | ||
*/ | ||
export const copyWithoutEmpty = <T extends Record<RecordKeyType, unknown>>(object: T): Output<T> => | ||
Object.keys(object).reduce( | ||
(acc, key) => { | ||
const value = object[key] as unknown | ||
if (value === undefined) return acc | ||
if (value === null) return acc | ||
if (typeof value === 'string' && value.trim().length === 0) return acc | ||
|
||
// TODO: handle nested objects | ||
acc[key] = object[key] | ||
return acc | ||
}, | ||
{} as Record<RecordKeyType, unknown>, | ||
) as Output<T> |
Oops, something went wrong.