Skip to content

Commit

Permalink
Universal-ts-utils migrating node-core object utils (#400)
Browse files Browse the repository at this point in the history
* 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
CarlosGamero authored Dec 5, 2024
1 parent 1b5f900 commit 722daff
Show file tree
Hide file tree
Showing 18 changed files with 1,404 additions and 1 deletion.
9 changes: 8 additions & 1 deletion packages/app/universal-ts-utils/src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,12 @@ export * from './public/array/sortByField.js'

// object
export * from './public/object/areDeepEqual.js'
export * from './public/object/groupBy.js'
export * from './public/object/convertDateFieldsToIsoString.js'
export * from './public/object/copyWithoutEmpty.js'
export * from './public/object/copyWithoutNullish.js'
export * from './public/object/deepClone.js'
export * from './public/object/groupByPath.js'
export * from './public/object/groupByUnique.js'
export * from './public/object/isEmpty.js'
export * from './public/object/pick.js'
export * from './public/object/transformToKebabCase.js'
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": {},
}
`;
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(),
},
],
})
})
})
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
}
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,
}
`)
})
})
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>
Loading

0 comments on commit 722daff

Please sign in to comment.