@homer0/object-utils 5.0.3 → 5.0.4

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.
@@ -1,270 +0,0 @@
1
- /**
2
- * The callback that will be used to replace a key.
3
- *
4
- * @param substring The captured string to replace.
5
- * @param args The list of groups captured by the expression.
6
- */
7
- export type FormatKeysReplaceFn = (substring: string, ...args: string[]) => string;
8
-
9
- export type FormatKeysOptions = {
10
- /**
11
- * The object to transform.
12
- */
13
- target: unknown;
14
- /**
15
- * A regular expression to match the keys to transform.
16
- */
17
- search: RegExp;
18
- /**
19
- * The callback that will be used to replace the matched substring. Think of `search` and
20
- * `replace` as the parameters of `String.replace`.
21
- */
22
- replace: FormatKeysReplaceFn;
23
- /**
24
- * A list of keys or paths where the transformation will be made. If not specified, the
25
- * function will use all the keys from the object.
26
- */
27
- include?: string[];
28
- /**
29
- * A list of keys or paths where the transformation won't be made.
30
- */
31
- exclude?: string[];
32
- /**
33
- * The delimiter that will separate the path components for both `include` and `exclude`.
34
- */
35
- pathDelimiter?: string;
36
- };
37
-
38
- export type FormatKeysExtensionOptions = Omit<FormatKeysOptions, 'search' | 'replace'>;
39
-
40
- /**
41
- * Formats all the keys on an object using a way similar to `.replace(regexp, ...)` but
42
- * that also works recursively and with _"object paths"_.
43
- *
44
- * @param options The set of options to customize the transformation.
45
- * @template T The type of the returned object.
46
- * @example
47
- *
48
- * const target = {
49
- * prop_one: 'Charito!',
50
- * };
51
- * console.log(
52
- * formatKeys({
53
- * target,
54
- * // Find all the keys with snake case.
55
- * search: /([a-z])_([a-z])/g,
56
- * // Using the same .replace style callback, replace it with lower camel case.
57
- * replace: (_, firstLetter, secondLetter) => {
58
- * const newSecondLetter = secondLetter.toUpperCase();
59
- * return `${firstLetter}${newSecondLetter}`;
60
- * },
61
- * }),
62
- * );
63
- * // Will output { propOne: 'Charito!}.
64
- *
65
- */
66
- export const formatKeys = <T = Record<string, unknown>>(
67
- options: FormatKeysOptions,
68
- ): T => {
69
- const {
70
- target,
71
- search,
72
- replace,
73
- include = [],
74
- exclude = [],
75
- pathDelimiter = '.',
76
- } = options;
77
- // Re-case target.
78
- const useTarget = target as Record<string, unknown>;
79
- // First of all, get all the keys from the target.
80
- const keys = Object.keys(target as Record<string, unknown>);
81
- /**
82
- * Then, check which keys are parent to other objects.
83
- * This is saved on a dictionary not only because it makes it easier to check if the
84
- * method should make a recursive call for a key, but also because when parsing the
85
- * `exclude`
86
- * parameter, if one of items is a key (and not an specific path), the method won't make
87
- * the recursive call.
88
- *
89
- * @ignore
90
- */
91
- const hasChildrenByKey: Record<string, unknown> = {};
92
- keys.forEach((key) => {
93
- const value = useTarget[key];
94
- hasChildrenByKey[key] = !!(
95
- value &&
96
- typeof value === 'object' &&
97
- value !== null &&
98
- !Array.isArray(value) &&
99
- Object.keys(value)
100
- );
101
- });
102
- /**
103
- * Escape the path delimiter and create two regular expression: One that removes the path
104
- * delimiter from the start of a path and one that removes it from the end.
105
- * They are later used to normalize paths in order to avoid "incomplete paths" (paths that
106
- * end or start with the delimiter).
107
- */
108
- const escapedPathDelimiter = pathDelimiter.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
109
- const cleanPathStartExpression = new RegExp(`^${escapedPathDelimiter}`, 'i');
110
- const cleanPathEndExpression = new RegExp(`${escapedPathDelimiter}$`, 'i');
111
- /**
112
- * This dictionary will be used to save the `include` parameter that will be sent for specific
113
- * keys on recursive calls.
114
- * If `include` has a path like `myKey.mySubKey`, `myKey` is not transformed, but `mySubKey`
115
- * is saved on this dictionary (`{ myKey: ['mySubKey']}`) and when the method applies the
116
- * formatting to the object, if `myKey` has an object, it will make a recursive all and
117
- * send `['mySubKey]` as its `include` parameter.
118
- */
119
- const subIncludeByKey: Record<string, string[]> = {};
120
- /**
121
- * This will be an array containing the final list of `keys` that should be tranformed.
122
- * To be clear, these keys will be from the top level, so, they won't be paths.
123
- * Thd following blocks will parse `include` and `exclude` in order to extract the real keys,
124
- * prepare the `include` and `exclude` for recursive calls, and save the actual keys
125
- * from the object "at the current level of this call" (no, it's not thinking about the
126
- * children :P).
127
- */
128
- let keysToFormat: string[];
129
- // If the `include` parameter has paths/keys...
130
- if (include.length) {
131
- keysToFormat = include
132
- .map((includePath) => {
133
- // Normalize the path/key.
134
- const useIncludePath = includePath
135
- .replace(cleanPathStartExpression, '')
136
- .replace(cleanPathEndExpression, '');
137
- // Define the variable that will, eventually, have the real key.
138
- let key;
139
- // If the value is a path...
140
- if (useIncludePath.includes(pathDelimiter)) {
141
- // Split all its components.
142
- const pathParts = useIncludePath.split(pathDelimiter);
143
- // Get the first component, a.k.a. the real key.
144
- const pathKey = pathParts.shift()!;
145
- /**
146
- * This is very important: Since the path was specified with sub components
147
- * (like `myProp.mySubProp`), the method won't format the key, but the sub
148
- * key(s)
149
- * (`mySubProp`).
150
- * The `key` is set to `''` so it will be later removed using `.filter`.
151
- *
152
- * @ignore
153
- */
154
- key = '';
155
- /**
156
- * If there's no array for the key on the "`include` dictionary for recursive calls",
157
- * create an empty one.
158
- */
159
- if (!subIncludeByKey[pathKey]) {
160
- subIncludeByKey[pathKey] = [];
161
- }
162
- // Save the rest of the path to be sent on the recursive call as `include`.
163
- subIncludeByKey[pathKey]!.push(pathParts.join(pathDelimiter));
164
- } else {
165
- // If the value wasn't a path, assume it's a key, and set it to be returned.
166
- key = useIncludePath;
167
- }
168
-
169
- return key;
170
- })
171
- // Remove any _empty keys_.
172
- .filter((key) => key);
173
- } else {
174
- // There's nothing on the `include` parameter, so use all the keys.
175
- keysToFormat = keys;
176
- }
177
- /**
178
- * Similar to `subIncludeByKey`, this dictionary will be used to save the `exclude` parameter
179
- * that will be sent for specific keys on recursive calls.
180
- * If `exclude` has a path like `myKey.mySubKey`, `myKey` will be transformed, but `mySubKey`
181
- * is saved on this dictionary (`{ myKey: ['mySubKey']}`) and when the method applies the
182
- * formatting to the object, if `myKey` has an object, it will make a recursive all and
183
- * send `['mySubKey]` as its `exclude` parameter.
184
- */
185
- const subExcludeByKey: Record<string, string[]> = {};
186
- // If the `include` parameter has paths/keys...
187
- if (exclude.length) {
188
- /**
189
- * Create a dictionary of keys that should be removed from `keysToFormat`.
190
- * It's easier to have them on a list and use `.filter` than actually call `.splice` for
191
- * every key that should be removed.
192
- */
193
- const keysToRemove: string[] = [];
194
- exclude.forEach((excludePath) => {
195
- // Normalize the path/key.
196
- const useExcludePath = excludePath
197
- .replace(cleanPathStartExpression, '')
198
- .replace(cleanPathEndExpression, '');
199
- // If the value is a path...
200
- if (useExcludePath.includes(pathDelimiter)) {
201
- // Split all its components.
202
- const pathParts = useExcludePath.split(pathDelimiter);
203
- // Get the first component, a.k.a. the real key.
204
- const pathKey = pathParts.shift()!;
205
- /**
206
- * If there's no array for the key on the "`exclude` dictionary for recursive calls",
207
- * create an empty one.
208
- */
209
- if (!subExcludeByKey[pathKey]) {
210
- subExcludeByKey[pathKey] = [];
211
- }
212
- // Save the rest of the path to be sent on the recursive call as `exclude`.
213
- subExcludeByKey[pathKey]!.push(pathParts.join(pathDelimiter));
214
- } else {
215
- /**
216
- * If the value wasn't a path, assume it's a key, turn the flag on the "children
217
- * dictionary" to `false`, to prevent recursive calls, and add the key to the list
218
- * of keys that will be removed from `keysToFormat`.
219
- * Basically: If it's a key, don't format it and don't make recursive calls for
220
- * it.
221
- *
222
- * @ignore
223
- */
224
- hasChildrenByKey[useExcludePath] = false;
225
- keysToRemove.push(useExcludePath);
226
- }
227
- });
228
- // Remove keys that should be excluded.
229
- keysToFormat = keysToFormat.filter((key) => key && !keysToRemove.includes(key));
230
- }
231
- // "Finally", reduce all the keys from the object and create the new one...
232
- return keys.reduce<Record<string, unknown>>((newObj, key) => {
233
- /**
234
- * Define the new key and value for the object property. Depending on the validations,
235
- * they may be replaced with formatted ones.
236
- */
237
- let newKey;
238
- let newValue;
239
- /**
240
- * Get the current value for the key, in case it's needed for a recursive call or just
241
- * to send it back because it didn't need any change.
242
- */
243
- const value = useTarget[key];
244
- // If the key should be formatted, apply the formatting; otherwise, keep the original.
245
- if (keysToFormat.includes(key)) {
246
- newKey = key.replace(search, replace);
247
- } else {
248
- newKey = key;
249
- }
250
- /**
251
- * If the paths/keys on `exclude` didn't modify the "children dictionary" for the key and
252
- * the value is another object, make a recursive call; otherwise, just use the original
253
- * value.
254
- */
255
- if (hasChildrenByKey[key]) {
256
- newValue = formatKeys({
257
- target: value,
258
- search,
259
- replace,
260
- include: subIncludeByKey[key] || [],
261
- exclude: subExcludeByKey[key] || [],
262
- pathDelimiter,
263
- });
264
- } else {
265
- newValue = value;
266
- }
267
- // "Done", return the new object with the "new key" and the "new value".
268
- return { ...newObj, [newKey]: newValue };
269
- }, {}) as T;
270
- };
package/src/fns/get.ts DELETED
@@ -1,93 +0,0 @@
1
- export type GetOptions = {
2
- /**
3
- * The object from where the property will be read.
4
- */
5
- target: unknown;
6
- /**
7
- * The path to the property.
8
- */
9
- path: string;
10
- /**
11
- * The delimiter that will separate the path components.
12
- */
13
- pathDelimiter?: string;
14
- /**
15
- * Whether or not to throw an error when the path is invalid. If this is `false`,
16
- * the method will silently fail and return `undefined`.
17
- */
18
- failWithError?: boolean;
19
- };
20
-
21
- /**
22
- * Returns the value of an object property using a path.
23
- *
24
- * @param options The options to use.
25
- * @template T The type of the returned object.
26
- * @throws {Error} If the path is invalid and `failWithError` is set to `true`.
27
- * @example
28
- *
29
- * const obj = {
30
- * propOne: {
31
- * propOneSub: 'Charito!',
32
- * },
33
- * propTwo: '!!!',
34
- * };
35
- * console.log(get({ target: obj, path: 'propOne.propOneSub' }));
36
- * // Will output 'Charito!'
37
- *
38
- */
39
- function get<T>(options: GetOptions): T | undefined;
40
- function get<T>(options: unknown, path: string): T | undefined;
41
- function get<T>(options: GetOptions | unknown, path?: string): T | undefined {
42
- let useOptions: GetOptions;
43
- if (typeof path === 'string') {
44
- useOptions = {
45
- target: options,
46
- path,
47
- };
48
- } else {
49
- useOptions = options as GetOptions;
50
- }
51
-
52
- const {
53
- target,
54
- path: usePath,
55
- pathDelimiter = '.',
56
- failWithError = false,
57
- } = useOptions;
58
- const useTarget = target as Record<string, unknown>;
59
- const parts = usePath.split(pathDelimiter);
60
- const first = parts.shift()!;
61
- let currentElement = useTarget[first] as Record<string, unknown>;
62
- if (typeof currentElement === 'undefined') {
63
- if (failWithError) {
64
- throw new Error(`There's nothing on '${usePath}'`);
65
- }
66
-
67
- return undefined;
68
- }
69
-
70
- if (!parts.length) {
71
- return currentElement as T;
72
- }
73
-
74
- let currentPath = first;
75
- parts.some((currentPart) => {
76
- let breakLoop = false;
77
- currentPath += `${pathDelimiter}${currentPart}`;
78
- currentElement = currentElement[currentPart] as Record<string, unknown>;
79
- if (typeof currentElement === 'undefined') {
80
- if (failWithError) {
81
- throw new Error(`There's nothing on '${currentPath}'`);
82
- } else {
83
- breakLoop = true;
84
- }
85
- }
86
-
87
- return breakLoop;
88
- });
89
-
90
- return currentElement as T;
91
- }
92
-
93
- export { get };
@@ -1,18 +0,0 @@
1
- import { formatKeys, type FormatKeysExtensionOptions } from './formatKeys.js';
2
-
3
- /**
4
- * A shorthand method for {@link formatKeys} that transforms the keys from
5
- * `lowerCamelCase` to `dash-case`.
6
- *
7
- * @param options The options to use.
8
- * @template T The type of the returned object.
9
- */
10
- export const lowerCamelToDashKeys = <T>(options: FormatKeysExtensionOptions): T =>
11
- formatKeys<T>({
12
- ...options,
13
- search: /([a-z])([A-Z])/g,
14
- replace: (_, firstLetter, secondLetter) => {
15
- const newSecondLetter = secondLetter.toLowerCase();
16
- return `${firstLetter}-${newSecondLetter}`;
17
- },
18
- });
@@ -1,18 +0,0 @@
1
- import { formatKeys, type FormatKeysExtensionOptions } from './formatKeys.js';
2
-
3
- /**
4
- * A shorthand method for {@link formatKeys} that transforms the keys from
5
- * `lowerCamelCase` to `snake_case`.
6
- *
7
- * @param options The options to use.
8
- * @template T The type of the returned object.
9
- */
10
- export const lowerCamelToSnakeKeys = <T>(options: FormatKeysExtensionOptions): T =>
11
- formatKeys<T>({
12
- ...options,
13
- search: /([a-z])([A-Z])/g,
14
- replace: (_, firstLetter, secondLetter) => {
15
- const newSecondLetter = secondLetter.toLowerCase();
16
- return `${firstLetter}_${newSecondLetter}`;
17
- },
18
- });
package/src/fns/merge.ts DELETED
@@ -1,29 +0,0 @@
1
- import extend from 'extend';
2
-
3
- /**
4
- * This method makes a deep merge of a list of objects into a new one. The method also
5
- * supports arrays: if the first element is an array, it will be merged into an array,
6
- * otherwise, it will be an object.
7
- *
8
- * @param targets The list of objects to merge.
9
- * @template T The type of the merged object that will be returned.
10
- * @example
11
- *
12
- * const objA = { a: 'first' };
13
- * const objB = { b: 'second' };
14
- * console.log(merge(objA, objB));
15
- * // Will output { a: 'first', b: 'second' }
16
- *
17
- * @example
18
- *
19
- * const arrA = [{ a: 'first' }];
20
- * const arrB = [{ b: 'second' }];
21
- * console.log(merge(objA, objB));
22
- * // Will output [{ a: 'first', b: 'second' }]
23
- *
24
- */
25
- export const merge = <T = unknown>(...targets: unknown[]): T => {
26
- const [firstTarget] = targets;
27
- const base = Array.isArray(firstTarget) ? [] : {};
28
- return extend(true, base, ...targets) as T;
29
- };
package/src/fns/remove.ts DELETED
@@ -1,102 +0,0 @@
1
- import { copy } from './copy.js';
2
- import { get } from './get.js';
3
-
4
- export type RemoveOptions = {
5
- /**
6
- * The object from where the property will be removed.
7
- */
8
- target: unknown;
9
- /**
10
- * The path to the property.
11
- */
12
- path: string;
13
- /**
14
- * The delimiter that will separate the path components.
15
- */
16
- pathDelimiter?: string;
17
- /**
18
- * Whether or not to throw an error when the path is invalid. If this is `false`, the
19
- * function will silently fail.
20
- */
21
- failWithError?: boolean;
22
- /**
23
- * If this flag is `true` and after removing the property the parent object is empty,
24
- * it will remove it recursively until a non empty parent object is found.
25
- */
26
- cleanEmptyProperties?: boolean;
27
- };
28
- /**
29
- * Deletes a property of an object using a path.
30
- *
31
- * @param options The options to use.
32
- * @template T The type of the returned object.
33
- * @example
34
- *
35
- * const target = {
36
- * propOne: {
37
- * propOneSub: 'Charito!',
38
- * },
39
- * propTwo: '!!!',
40
- * };
41
- * console.log(remove({ target, path: 'propOne.propOneSub' }));
42
- * // Will output { propTwo: '!!!' }
43
- *
44
- */
45
- function remove<T = unknown>(options: RemoveOptions): T | undefined;
46
- function remove<T = unknown>(options: unknown, path: string): T | undefined;
47
- function remove<T = unknown>(
48
- options: RemoveOptions | unknown,
49
- path?: string,
50
- ): T | undefined {
51
- let useOptions: RemoveOptions;
52
- if (typeof path === 'string') {
53
- useOptions = {
54
- target: options,
55
- path,
56
- };
57
- } else {
58
- useOptions = options as RemoveOptions;
59
- }
60
-
61
- const {
62
- target,
63
- path: usePath,
64
- pathDelimiter = '.',
65
- cleanEmptyProperties = true,
66
- failWithError = false,
67
- } = useOptions;
68
-
69
- const parts = usePath.split(pathDelimiter);
70
- const last = parts.pop()!;
71
- const result = copy(target) as Record<string, unknown>;
72
- if (!parts.length) {
73
- delete result[last];
74
- return result as T;
75
- }
76
- const parentPath = parts.join(pathDelimiter);
77
- const parentObj = get<Record<string, unknown>>({
78
- target: result,
79
- path: parentPath,
80
- pathDelimiter,
81
- failWithError,
82
- });
83
-
84
- if (typeof parentObj === 'undefined') {
85
- return undefined;
86
- }
87
-
88
- delete parentObj[last];
89
- if (cleanEmptyProperties && !Object.keys(parentObj).length) {
90
- return remove({
91
- target: result,
92
- path: parentPath,
93
- pathDelimiter,
94
- failWithError,
95
- cleanEmptyProperties,
96
- }) as T;
97
- }
98
-
99
- return result as T;
100
- }
101
-
102
- export { remove };
package/src/fns/set.ts DELETED
@@ -1,131 +0,0 @@
1
- import { copy } from './copy.js';
2
-
3
- /**
4
- * List of paths that are forbidden to use on the `set` function, as they could lead to
5
- * security issues.
6
- *
7
- * @access protected
8
- */
9
- const FORBIDDEN_PATHS = ['__proto__', 'prototype', 'constructor'];
10
-
11
- export type SetOptions = {
12
- /**
13
- * The object where the property will be set.
14
- */
15
- target: unknown;
16
- /**
17
- * The path for the property.
18
- */
19
- path: string;
20
- /**
21
- * The value to set on the property.
22
- */
23
- value: unknown;
24
- /**
25
- * The delimiter that will separate the path components.
26
- */
27
- pathDelimiter?: string;
28
- /**
29
- * Whether or not to throw an error when the path is invalid. If this is `false`, the method
30
- * will silently fail and return `undefined`.
31
- */
32
- failWithError?: boolean;
33
- };
34
- /**
35
- * Sets a property on an object using a path. If the path doesn't exist, it will be
36
- * created.
37
- *
38
- * @param options The options to use.
39
- * @template T The type of the returned object.
40
- * @throws {Error} If one of the path components is for a non-object property and
41
- * `failWithError` is set to `true`.
42
- * @example
43
- *
44
- * const target = {};
45
- * console.log(set({ target, path: 'some.prop.path', value: 'some-value' }));
46
- * // Will output { some: { prop: { path: 'some-value' } } }
47
- *
48
- */
49
- function set<T = Record<string, unknown>>(options: SetOptions): T | undefined;
50
- function set<T = Record<string, unknown>>(
51
- options: SetOptions | unknown,
52
- path: string,
53
- value: unknown,
54
- ): T | undefined;
55
- function set<T = Record<string, unknown>>(
56
- options: SetOptions | unknown,
57
- path?: string,
58
- value?: unknown,
59
- ): T | undefined {
60
- let useOptions: SetOptions;
61
- if (typeof path === 'string') {
62
- useOptions = {
63
- target: options,
64
- path,
65
- value,
66
- };
67
- } else {
68
- useOptions = options as SetOptions;
69
- }
70
-
71
- const {
72
- target,
73
- path: usePath,
74
- value: useValue,
75
- pathDelimiter = '.',
76
- failWithError = false,
77
- } = useOptions;
78
- const result = copy(target) as Record<string, unknown>;
79
- if (!usePath.includes(pathDelimiter)) {
80
- if (!usePath || FORBIDDEN_PATHS.includes(usePath.toLowerCase())) {
81
- return undefined;
82
- }
83
-
84
- result[usePath] = useValue;
85
- return result as T;
86
- }
87
-
88
- const parts = usePath.split(pathDelimiter);
89
- const isInvalid = parts.some(
90
- (part) => !part || FORBIDDEN_PATHS.includes(part.toLowerCase()),
91
- );
92
-
93
- if (isInvalid) {
94
- return undefined;
95
- }
96
-
97
- const last = parts.pop();
98
- let currentElement = result;
99
- let currentPath = '';
100
- let foundInvalid = false;
101
- parts.forEach((part) => {
102
- currentPath += `${pathDelimiter}${part}`;
103
- const element = currentElement[part];
104
- const elementType = typeof element;
105
- if (elementType === 'undefined') {
106
- currentElement[part] = {};
107
- currentElement = currentElement[part] as Record<string, unknown>;
108
- } else if (elementType === 'object') {
109
- currentElement = currentElement[part] as Record<string, unknown>;
110
- } else {
111
- const errorPath = currentPath.substring(pathDelimiter.length);
112
- if (failWithError) {
113
- throw new Error(
114
- `There's already an element of type '${elementType}' on '${errorPath}'`,
115
- );
116
- } else {
117
- foundInvalid = true;
118
- }
119
- }
120
- });
121
-
122
- if (foundInvalid) return undefined;
123
-
124
- if (result && last) {
125
- currentElement[last] = useValue;
126
- }
127
-
128
- return result as T | undefined;
129
- }
130
-
131
- export { set };
@@ -1,15 +0,0 @@
1
- import { formatKeys, type FormatKeysExtensionOptions } from './formatKeys.js';
2
-
3
- /**
4
- * A shorthand method for {@link formatKeys} that transforms the keys from `snake_case` to
5
- * `dash-case`.
6
- *
7
- * @param options The options to use.
8
- * @template T The type of the returned object.
9
- */
10
- export const snakeToDashKeys = <T>(options: FormatKeysExtensionOptions): T =>
11
- formatKeys<T>({
12
- ...options,
13
- search: /([a-z])_([a-z])/g,
14
- replace: (_, firstLetter, secondLetter) => `${firstLetter}-${secondLetter}`,
15
- });
@@ -1,18 +0,0 @@
1
- import { formatKeys, type FormatKeysExtensionOptions } from './formatKeys.js';
2
-
3
- /**
4
- * A shorthand method for {@link formatKeys} that transforms the keys from `snake_case` to
5
- * `lowerCamelCase`.
6
- *
7
- * @param options The options to use.
8
- * @template T The type of the returned object.
9
- */
10
- export const snakeToLowerCamelKeys = <T>(options: FormatKeysExtensionOptions): T =>
11
- formatKeys<T>({
12
- ...options,
13
- search: /([a-z])_([a-z])/g,
14
- replace: (_, firstLetter, secondLetter) => {
15
- const newSecondLetter = secondLetter.toUpperCase();
16
- return `${firstLetter}${newSecondLetter}`;
17
- },
18
- });