@revolugo/common 7.13.1-alpha.1 → 7.13.1-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revolugo/common",
3
- "version": "7.13.1-alpha.1",
3
+ "version": "7.13.1-alpha.10",
4
4
  "private": false,
5
5
  "description": "Revolugo common",
6
6
  "author": "Revolugo",
@@ -0,0 +1,72 @@
1
+ import {
2
+ camelCase,
3
+ capitalCase,
4
+ kebabCase,
5
+ pascalCase,
6
+ snakeCase,
7
+ } from 'change-case'
8
+ import slugify from 'slugify'
9
+
10
+ function slugCase(input: string): string {
11
+ return slugify(kebabCase(input), {
12
+ lower: true,
13
+ strict: true,
14
+ })
15
+ }
16
+
17
+ // eslint-disable-next-line @typescript-eslint/naming-convention -- public API constant object
18
+ export const CaseTransformer = {
19
+ Camel: 'camelCase',
20
+ Capital: 'capitalCase',
21
+ Param: 'paramCase',
22
+ Pascal: 'pascalCase',
23
+ Slug: 'slugCase',
24
+ Snake: 'snakeCase',
25
+ } as const
26
+
27
+ export type CaseTransformer =
28
+ (typeof CaseTransformer)[keyof typeof CaseTransformer]
29
+
30
+ export const CASE_TRANSFORMERS_MAPPING = {
31
+ [CaseTransformer.Camel]: camelCase,
32
+ [CaseTransformer.Capital]: capitalCase,
33
+ [CaseTransformer.Param]: kebabCase,
34
+ [CaseTransformer.Pascal]: pascalCase,
35
+ [CaseTransformer.Slug]: slugCase,
36
+ [CaseTransformer.Snake]: snakeCase,
37
+ } as const
38
+
39
+ export function changeCase<T extends string[] | string>(
40
+ input: T,
41
+ toCase: CaseTransformer,
42
+ ): T extends string ? string : string[] {
43
+ if (Array.isArray(input)) {
44
+ return input.map(item =>
45
+ CASE_TRANSFORMERS_MAPPING[toCase](item),
46
+ ) as T extends string ? string : string[]
47
+ }
48
+
49
+ return CASE_TRANSFORMERS_MAPPING[toCase](input) as T extends string
50
+ ? string
51
+ : string[]
52
+ }
53
+
54
+ export type PlainObject = Record<string, unknown>
55
+
56
+ export function isPlainObject(value: unknown): value is PlainObject {
57
+ return (
58
+ value === Object(value) &&
59
+ !Array.isArray(value) &&
60
+ typeof value !== 'function'
61
+ )
62
+ }
63
+
64
+ export function matches(patterns: (RegExp | string)[], value: string): boolean {
65
+ return patterns.some(pattern =>
66
+ typeof pattern === 'string' ? pattern === value : pattern.test(value),
67
+ )
68
+ }
69
+
70
+ export function capitalize(value: string): string {
71
+ return value.replace(/^\w/u, character => character.toUpperCase())
72
+ }
@@ -0,0 +1,73 @@
1
+ /* eslint-disable camelcase -- fixture keys intentionally use snake_case */
2
+ import { describe, expect, test } from 'vitest'
3
+
4
+ import { CaseTransformer } from './case-transformer-core.ts'
5
+ import { keysCaseTransformer, keysChangeCase } from './keys-case-transformer.ts'
6
+
7
+ describe('keysCaseTransformer', () => {
8
+ test('transforms keys shallowly by default', () => {
9
+ const input = { nested_object: { inner_key: 1 }, top_level: true }
10
+
11
+ expect(keysCaseTransformer(input, CaseTransformer.Camel)).toEqual({
12
+ nestedObject: { inner_key: 1 },
13
+ topLevel: true,
14
+ })
15
+ })
16
+
17
+ test('transforms keys deeply when requested', () => {
18
+ const input = { nested_object: { inner_key: 1 } }
19
+
20
+ expect(
21
+ keysCaseTransformer(input, CaseTransformer.Camel, { deep: true }),
22
+ ).toEqual({
23
+ nestedObject: { innerKey: 1 },
24
+ })
25
+ })
26
+
27
+ test('keeps excluded keys unchanged', () => {
28
+ const input = { keep_me: 1, transform_me: 2 }
29
+
30
+ expect(
31
+ keysCaseTransformer(input, CaseTransformer.Camel, {
32
+ deep: true,
33
+ exclude: [/^keep_me$/u],
34
+ }),
35
+ ).toEqual({
36
+ keep_me: 1,
37
+ transformMe: 2,
38
+ })
39
+ })
40
+
41
+ test('skips Date instances', () => {
42
+ const date = new Date('2026-01-01T00:00:00.000Z')
43
+ const input = { created_at: date }
44
+
45
+ expect(
46
+ keysCaseTransformer(input, CaseTransformer.Camel, { deep: true }),
47
+ ).toEqual({
48
+ createdAt: date,
49
+ })
50
+ })
51
+ })
52
+
53
+ describe('keysChangeCase', () => {
54
+ test('defaults to deep transformation for backward compatibility', () => {
55
+ const input = { nested_object: { inner_key: 1 } }
56
+
57
+ expect(keysChangeCase(input, CaseTransformer.Camel)).toEqual({
58
+ nestedObject: { innerKey: 1 },
59
+ })
60
+ })
61
+
62
+ test('matches keysCaseTransformer for deep snake_case transforms', () => {
63
+ const input = {
64
+ hotel_room_offer: {
65
+ booking_policy: { cancel_until: '2026-06-10' },
66
+ },
67
+ }
68
+
69
+ expect(keysChangeCase(input, CaseTransformer.Snake)).toEqual(
70
+ keysCaseTransformer(input, CaseTransformer.Snake, { deep: true }),
71
+ )
72
+ })
73
+ })
@@ -1,108 +1,11 @@
1
- import {
2
- camelCase,
3
- capitalCase,
4
- kebabCase,
5
- pascalCase,
6
- snakeCase,
7
- } from 'change-case'
8
- import slugify from 'slugify'
9
-
10
- function slugCase(input: string): string {
11
- return slugify(kebabCase(input), {
12
- lower: true,
13
- strict: true,
14
- })
15
- }
16
-
17
- export enum CaseTransformer {
18
- Camel = 'camelCase',
19
- Capital = 'capitalCase',
20
- Param = 'paramCase',
21
- Pascal = 'pascalCase',
22
- Slug = 'slugCase',
23
- Snake = 'snakeCase',
24
- }
25
-
26
- export const CASE_TRANSFORMERS_MAPPING = {
27
- [CaseTransformer.Camel]: camelCase,
28
- [CaseTransformer.Capital]: capitalCase,
29
- [CaseTransformer.Param]: kebabCase,
30
- [CaseTransformer.Pascal]: pascalCase,
31
- [CaseTransformer.Slug]: slugCase,
32
- [CaseTransformer.Snake]: snakeCase,
33
- }
34
-
35
- export function changeCase<T extends string[] | string>(
36
- input: T,
37
- toCase: CaseTransformer,
38
- ): T extends string ? string : string[] {
39
- if (Array.isArray(input)) {
40
- return input.map(item =>
41
- CASE_TRANSFORMERS_MAPPING[toCase](item),
42
- ) as T extends string ? string : string[]
43
- }
44
-
45
- return CASE_TRANSFORMERS_MAPPING[toCase](input) as T extends string
46
- ? string
47
- : string[]
48
- }
49
-
50
- function matches(patterns: (RegExp | string)[], value: string) {
51
- return patterns.some(pattern =>
52
- typeof pattern === 'string' ? pattern === value : pattern.test(value),
53
- )
54
- }
55
-
56
- type PlainObject = Record<string, unknown>
57
-
58
- function isPlainObject(value: unknown): value is PlainObject {
59
- return (
60
- value === Object(value) &&
61
- !Array.isArray(value) &&
62
- typeof value !== 'function'
63
- )
64
- }
65
-
66
- /** Infer `T` from input, or set `T` explicitly when output type differs (e.g. fixture → domain). */
67
- export function keysChangeCase<T extends object | readonly unknown[]>(
68
- obj: T | object | readonly unknown[],
69
- toCase: CaseTransformer,
70
- options?: { deep?: boolean; exclude?: (RegExp | string)[] },
71
- ): T
72
-
73
- /** Unknown input — `Record<string, unknown>` / `unknown[]` instead of `unknown`. */
74
- export function keysChangeCase(
75
- obj: unknown,
76
- toCase: CaseTransformer,
77
- options?: { deep?: boolean; exclude?: (RegExp | string)[] },
78
- ): PlainObject | unknown[]
79
-
80
- export function keysChangeCase(
81
- obj: unknown,
82
- toCase: CaseTransformer,
83
- options: { deep?: boolean; exclude?: (RegExp | string)[] } = { deep: true },
84
- ): PlainObject | unknown[] | unknown {
85
- if (isPlainObject(obj) && !(obj instanceof Date)) {
86
- return Object.keys(obj).reduce<PlainObject>((result, key) => {
87
- // Check if key should be excluded from the transformation
88
- const transformedKey =
89
- options?.exclude && matches(options.exclude, key)
90
- ? key
91
- : CASE_TRANSFORMERS_MAPPING[toCase](key)
92
-
93
- result[transformedKey] = options.deep
94
- ? keysChangeCase(obj[key], toCase, options)
95
- : obj[key]
96
-
97
- return result
98
- }, {})
99
- } else if (Array.isArray(obj)) {
100
- return obj.map(item => keysChangeCase(item, toCase, options))
101
- }
102
-
103
- return obj
104
- }
105
-
106
- export function capitalize(value: string): string {
107
- return value.replace(/^\w/u, c => c.toUpperCase())
108
- }
1
+ export {
2
+ CASE_TRANSFORMERS_MAPPING,
3
+ CaseTransformer,
4
+ capitalize,
5
+ changeCase,
6
+ isPlainObject,
7
+ matches,
8
+ type PlainObject,
9
+ } from './case-transformer-core.ts'
10
+
11
+ export { keysChangeCase, keysCaseTransformer } from './keys-case-transformer.ts'
@@ -35,7 +35,6 @@ export * from './is-equal.ts'
35
35
  export * from './is-nil.ts'
36
36
  export * from './is-object.ts'
37
37
  export * from './key-by.ts'
38
- export * from './keys-case-transformer.ts'
39
38
  export * from './lang-default-fallbacks.ts'
40
39
  export * from './map-keys.ts'
41
40
  export * from './map-values.ts'
@@ -43,6 +42,7 @@ export * from './merge.ts'
43
42
  export * from './omit-by.ts'
44
43
  export * from './omit.ts'
45
44
  export * from './parse-children.ts'
45
+ export * from './pick-by.ts'
46
46
  export * from './pick.ts'
47
47
  export * from './poller.ts'
48
48
  export * from './prepare-ts-query.ts'
@@ -1,7 +1,6 @@
1
- import { type CaseTransformer, changeCase } from './case-transformers.ts'
2
- import { isObject } from './is-object.ts'
3
- import { mapKeys } from './map-keys.ts'
1
+ import { changeCase, isPlainObject, matches } from './case-transformer-core.ts'
4
2
 
3
+ import type { CaseTransformer, PlainObject } from './case-transformer-core.ts'
5
4
  import type {
6
5
  CamelCasedProperties,
7
6
  CamelCasedPropertiesDeep,
@@ -13,46 +12,32 @@ import type {
13
12
  SnakeCasedPropertiesDeep,
14
13
  } from 'type-fest'
15
14
 
16
- /**
17
- * Maps CaseTransformer enum to the corresponding type-fest property transformation (shallow).
18
- * Capital and Slug cases don't have type-fest equivalents, so they preserve the original type.
19
- */
20
15
  type TransformProperties<
21
16
  T,
22
17
  C extends CaseTransformer,
23
- > = C extends CaseTransformer.Camel
18
+ > = C extends typeof CaseTransformer.Camel
24
19
  ? CamelCasedProperties<T>
25
- : C extends CaseTransformer.Snake
20
+ : C extends typeof CaseTransformer.Snake
26
21
  ? SnakeCasedProperties<T>
27
- : C extends CaseTransformer.Pascal
22
+ : C extends typeof CaseTransformer.Pascal
28
23
  ? PascalCasedProperties<T>
29
- : C extends CaseTransformer.Param
24
+ : C extends typeof CaseTransformer.Param
30
25
  ? KebabCasedProperties<T>
31
26
  : T
32
27
 
33
- /**
34
- * Maps CaseTransformer enum to the corresponding type-fest deep property transformation.
35
- * Capital and Slug cases don't have type-fest equivalents, so they preserve the original type.
36
- */
37
28
  type TransformPropertiesDeep<
38
29
  T,
39
30
  C extends CaseTransformer,
40
- > = C extends CaseTransformer.Camel
31
+ > = C extends typeof CaseTransformer.Camel
41
32
  ? CamelCasedPropertiesDeep<T>
42
- : C extends CaseTransformer.Snake
33
+ : C extends typeof CaseTransformer.Snake
43
34
  ? SnakeCasedPropertiesDeep<T>
44
- : C extends CaseTransformer.Pascal
35
+ : C extends typeof CaseTransformer.Pascal
45
36
  ? PascalCasedPropertiesDeep<T>
46
- : C extends CaseTransformer.Param
37
+ : C extends typeof CaseTransformer.Param
47
38
  ? KebabCasedPropertiesDeep<T>
48
39
  : T
49
40
 
50
- /**
51
- * Transforms object keys based on the specified case transformer.
52
- * - Arrays: recursively applies transformation to each element
53
- * - Objects: applies property transformation (shallow by default, deep if Deep=true)
54
- * - Primitives: returns as-is
55
- */
56
41
  export type KeysCaseTransformed<
57
42
  T,
58
43
  C extends CaseTransformer,
@@ -71,48 +56,86 @@ export type KeysCaseTransformed<
71
56
 
72
57
  export interface KeysCaseTransformerOptions {
73
58
  deep?: boolean
59
+ exclude?: (RegExp | string)[]
60
+ }
61
+
62
+ function keysCaseTransformerImpl(
63
+ obj: unknown,
64
+ toCase: CaseTransformer,
65
+ options: KeysCaseTransformerOptions,
66
+ ): unknown {
67
+ const deep = options.deep ?? false
68
+
69
+ if (Array.isArray(obj)) {
70
+ return obj.map(item => keysCaseTransformerImpl(item, toCase, options))
71
+ }
72
+
73
+ if (isPlainObject(obj) && !(obj instanceof Date)) {
74
+ return Object.keys(obj).reduce<PlainObject>((accumulator, key) => {
75
+ const transformedKey =
76
+ options.exclude && matches(options.exclude, key)
77
+ ? key
78
+ : changeCase(key, toCase)
79
+
80
+ accumulator[transformedKey] = deep
81
+ ? keysCaseTransformerImpl(obj[key], toCase, {
82
+ ...options,
83
+ deep: true,
84
+ })
85
+ : obj[key]
86
+
87
+ return accumulator
88
+ }, {})
89
+ }
90
+
91
+ return obj
74
92
  }
75
93
 
76
- // Overload: deep transformation
77
94
  export function keysCaseTransformer<T, C extends CaseTransformer>(
78
95
  obj: T,
79
96
  toCase: C,
80
- options: { deep: true },
97
+ options: KeysCaseTransformerOptions & { deep: true },
81
98
  ): KeysCaseTransformed<T, C, true>
82
99
 
83
- // Overload: shallow transformation (default)
84
100
  export function keysCaseTransformer<T, C extends CaseTransformer>(
85
101
  obj: T,
86
102
  toCase: C,
87
- options?: { deep?: false },
103
+ options?: KeysCaseTransformerOptions & { deep?: false },
88
104
  ): KeysCaseTransformed<T, C>
89
105
 
90
- // Implementation
91
106
  export function keysCaseTransformer<T, C extends CaseTransformer>(
92
107
  obj: T,
93
108
  toCase: C,
94
- options?: KeysCaseTransformerOptions,
109
+ options: KeysCaseTransformerOptions = {},
95
110
  ): KeysCaseTransformed<T, C, boolean> {
96
- if (Array.isArray(obj)) {
97
- return obj.map(item =>
98
- keysCaseTransformer(item, toCase, options as { deep: true }),
99
- ) as KeysCaseTransformed<T, C, boolean>
100
- }
111
+ return keysCaseTransformerImpl(obj, toCase, options) as KeysCaseTransformed<
112
+ T,
113
+ C,
114
+ boolean
115
+ >
116
+ }
101
117
 
102
- if (isObject(obj)) {
103
- const transformed = mapKeys(obj, key => changeCase(key, toCase))
104
- if (options?.deep) {
105
- // Recursively transform nested values
106
- return Object.fromEntries(
107
- Object.entries(transformed).map(([k, v]) => [
108
- k,
109
- keysCaseTransformer(v, toCase, options as { deep: true }),
110
- ]),
111
- ) as KeysCaseTransformed<T, C, boolean>
112
- }
113
-
114
- return transformed as KeysCaseTransformed<T, C, boolean>
115
- }
118
+ /** Backward-compatible wrapper defaulting to deep transforms. */
119
+ export function keysChangeCase<T extends object | readonly unknown[]>(
120
+ obj: T | object | readonly unknown[],
121
+ toCase: CaseTransformer,
122
+ options?: KeysCaseTransformerOptions,
123
+ ): T
124
+
125
+ /** Unknown input — `Record<string, unknown>` / `unknown[]` instead of `unknown`. */
126
+ export function keysChangeCase(
127
+ obj: unknown,
128
+ toCase: CaseTransformer,
129
+ options?: KeysCaseTransformerOptions,
130
+ ): PlainObject | unknown[]
116
131
 
117
- return obj as KeysCaseTransformed<T, C, boolean>
132
+ export function keysChangeCase(
133
+ obj: unknown,
134
+ toCase: CaseTransformer,
135
+ options: KeysCaseTransformerOptions = {},
136
+ ): PlainObject | unknown[] | unknown {
137
+ return keysCaseTransformerImpl(obj, toCase, {
138
+ deep: options.deep ?? true,
139
+ exclude: options.exclude,
140
+ })
118
141
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Creates an object composed of the own enumerable properties of object that predicate returns truthy for.
3
+ * The predicate is invoked with two arguments: (value, key).
4
+ * If predicate is a string, it picks the property with that key name.
5
+ *
6
+ * @param object - The source object
7
+ * @param predicate - The function invoked per property or a string key to pick
8
+ * @returns Returns the new object
9
+ */
10
+ export function pickBy<T extends object>(
11
+ object: T,
12
+ predicate: string | ((value: T[keyof T], key: keyof T) => boolean),
13
+ ): Partial<T> {
14
+ const result = {} as Partial<T>
15
+
16
+ for (const key in object) {
17
+ if (Object.hasOwn(object, key)) {
18
+ const value = object[key]
19
+
20
+ // If predicate is a string, check if key matches
21
+ if (typeof predicate === 'string') {
22
+ // eslint-disable-next-line max-depth
23
+ if (key === predicate) {
24
+ result[key] = value
25
+ }
26
+ } else if (typeof predicate === 'function') {
27
+ // If predicate is a function, use it
28
+ // eslint-disable-next-line max-depth
29
+ if (predicate(value, key)) {
30
+ result[key] = value
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ return result
37
+ }
@@ -6,10 +6,13 @@ export function sumBy<T>(
6
6
  return 0
7
7
  }
8
8
 
9
- const getValue =
9
+ const getValue: (item: T) => number | undefined =
10
10
  typeof iteratee === 'function'
11
11
  ? iteratee
12
- : (item: T) => item[iteratee] as unknown as number
12
+ : (item: T) => {
13
+ const value = item[iteratee]
14
+ return typeof value === 'number' ? value : undefined
15
+ }
13
16
 
14
17
  let result: number | undefined = undefined
15
18
  for (const item of array) {