@turndown/library 0.1.23 → 0.1.28
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/dist/index.cjs +1355 -0
- package/dist/index.d.cts +2820 -0
- package/dist/index.d.ts +2820 -2
- package/dist/index.js +1190 -2
- package/package.json +3 -2
- package/dist/helpers/date/index.d.ts +0 -113
- package/dist/helpers/date/index.js +0 -171
- package/dist/helpers/index.d.ts +0 -4
- package/dist/helpers/index.js +0 -3
- package/dist/helpers/object/index.d.ts +0 -298
- package/dist/helpers/object/index.js +0 -540
- package/dist/helpers/string/index.d.ts +0 -258
- package/dist/helpers/string/index.js +0 -496
- package/dist/types/api/index.d.ts +0 -70
- package/dist/types/api/index.js +0 -53
- package/dist/types/auth/index.d.ts +0 -107
- package/dist/types/auth/index.js +0 -8
- package/dist/types/auth/routes.d.ts +0 -160
- package/dist/types/auth/routes.js +0 -1
- package/dist/types/base/index.d.ts +0 -200
- package/dist/types/base/index.js +0 -177
- package/dist/types/base/paging.types.d.ts +0 -47
- package/dist/types/base/paging.types.js +0 -1
- package/dist/types/checklist-template/index.d.ts +0 -48
- package/dist/types/checklist-template/index.js +0 -1
- package/dist/types/checklist-template/routes.d.ts +0 -85
- package/dist/types/checklist-template/routes.js +0 -1
- package/dist/types/company/index.d.ts +0 -22
- package/dist/types/company/index.js +0 -7
- package/dist/types/company/routes.d.ts +0 -100
- package/dist/types/company/routes.js +0 -1
- package/dist/types/damage-report/index.d.ts +0 -57
- package/dist/types/damage-report/index.js +0 -16
- package/dist/types/damage-report/routes.d.ts +0 -153
- package/dist/types/damage-report/routes.js +0 -1
- package/dist/types/errors/index.d.ts +0 -66
- package/dist/types/errors/index.js +0 -41
- package/dist/types/health/index.d.ts +0 -20
- package/dist/types/health/index.js +0 -1
- package/dist/types/health/routes.d.ts +0 -10
- package/dist/types/health/routes.js +0 -1
- package/dist/types/image/index.d.ts +0 -34
- package/dist/types/image/index.js +0 -11
- package/dist/types/image/routes.d.ts +0 -32
- package/dist/types/image/routes.js +0 -1
- package/dist/types/index.d.ts +0 -21
- package/dist/types/index.js +0 -21
- package/dist/types/inventory/index.d.ts +0 -124
- package/dist/types/inventory/index.js +0 -30
- package/dist/types/inventory/routes.d.ts +0 -109
- package/dist/types/inventory/routes.js +0 -1
- package/dist/types/job/index.d.ts +0 -7
- package/dist/types/job/index.js +0 -1
- package/dist/types/property/index.d.ts +0 -113
- package/dist/types/property/index.js +0 -25
- package/dist/types/property/routes.d.ts +0 -54
- package/dist/types/property/routes.js +0 -1
- package/dist/types/room/index.d.ts +0 -28
- package/dist/types/room/index.js +0 -17
- package/dist/types/room/routes.d.ts +0 -35
- package/dist/types/room/routes.js +0 -1
- package/dist/types/room-checklist/index.d.ts +0 -43
- package/dist/types/room-checklist/index.js +0 -1
- package/dist/types/room-checklist/routes.d.ts +0 -78
- package/dist/types/room-checklist/routes.js +0 -1
- package/dist/types/user/index.d.ts +0 -59
- package/dist/types/user/index.js +0 -20
- package/dist/types/user/routes.d.ts +0 -46
- package/dist/types/user/routes.js +0 -1
- package/dist/types/work-session/index.d.ts +0 -97
- package/dist/types/work-session/index.js +0 -18
- package/dist/types/work-session/routes.d.ts +0 -89
- package/dist/types/work-session/routes.js +0 -1
|
@@ -1,540 +0,0 @@
|
|
|
1
|
-
const unsafePathSegments = new Set(["__proto__", "prototype", "constructor"]);
|
|
2
|
-
const isRecord = (value) => {
|
|
3
|
-
return (typeof value === "object" &&
|
|
4
|
-
value !== null &&
|
|
5
|
-
!Array.isArray(value) &&
|
|
6
|
-
!(value instanceof Date));
|
|
7
|
-
};
|
|
8
|
-
const isSafePathSegment = (segment) => {
|
|
9
|
-
return segment.length > 0 && !unsafePathSegments.has(segment);
|
|
10
|
-
};
|
|
11
|
-
const getPathSegments = (path) => {
|
|
12
|
-
return path.split(".").filter((segment) => segment.length > 0);
|
|
13
|
-
};
|
|
14
|
-
export function parseJSON(jsonString, fallbackValue) {
|
|
15
|
-
if (!jsonString) {
|
|
16
|
-
return fallbackValue ?? {};
|
|
17
|
-
}
|
|
18
|
-
try {
|
|
19
|
-
const parsedValue = JSON.parse(jsonString);
|
|
20
|
-
return parsedValue;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return fallbackValue ?? {};
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Stringify a value to JSON while skipping circular references.
|
|
28
|
-
*
|
|
29
|
-
* Uses an internal cache to omit repeated object references that would
|
|
30
|
-
* normally cause `JSON.stringify` to throw.
|
|
31
|
-
*
|
|
32
|
-
* @param {unknown} value - Value to stringify.
|
|
33
|
-
* @returns {string | undefined} JSON string with circulars omitted.
|
|
34
|
-
* @example
|
|
35
|
-
* const value: Record<string, unknown> = {}; value.self = value;
|
|
36
|
-
* JSONStringify(value) // => "{}"
|
|
37
|
-
*/
|
|
38
|
-
export const JSONStringify = (value) => {
|
|
39
|
-
const seenValues = new WeakSet();
|
|
40
|
-
try {
|
|
41
|
-
return JSON.stringify(value, (_key, nestedValue) => {
|
|
42
|
-
if (typeof nestedValue === "bigint") {
|
|
43
|
-
return nestedValue.toString();
|
|
44
|
-
}
|
|
45
|
-
if (typeof nestedValue === "object" && nestedValue !== null) {
|
|
46
|
-
if (seenValues.has(nestedValue)) {
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
seenValues.add(nestedValue);
|
|
50
|
-
}
|
|
51
|
-
return nestedValue;
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
/**
|
|
59
|
-
* Deep-remove `undefined` properties while preserving Dates and arrays.
|
|
60
|
-
*
|
|
61
|
-
* Object properties with `undefined` values are removed. Array items are
|
|
62
|
-
* preserved so array indexes do not shift.
|
|
63
|
-
*
|
|
64
|
-
* @typeParam TValue - Input value type.
|
|
65
|
-
* @param {TValue} value - Input value.
|
|
66
|
-
* @returns {TValue} Cleaned clone with `undefined` object properties removed.
|
|
67
|
-
*/
|
|
68
|
-
export const removeUndefined = (value) => {
|
|
69
|
-
if (Array.isArray(value)) {
|
|
70
|
-
return value.map((item) => removeUndefined(item));
|
|
71
|
-
}
|
|
72
|
-
if (isRecord(value)) {
|
|
73
|
-
const updatedValue = Object.entries(value).reduce((accumulator, [key, nestedValue]) => {
|
|
74
|
-
if (nestedValue !== undefined) {
|
|
75
|
-
accumulator[key] = removeUndefined(nestedValue);
|
|
76
|
-
}
|
|
77
|
-
return accumulator;
|
|
78
|
-
}, {});
|
|
79
|
-
return updatedValue;
|
|
80
|
-
}
|
|
81
|
-
return value;
|
|
82
|
-
};
|
|
83
|
-
/**
|
|
84
|
-
* Test whether a location object's `pathname` equals a key.
|
|
85
|
-
*
|
|
86
|
-
* @param {{ pathname?: string } | null | undefined} location - Object expected
|
|
87
|
-
* to have a `pathname`.
|
|
88
|
-
* @param {string} key - Path to compare.
|
|
89
|
-
* @returns {boolean}
|
|
90
|
-
* @example
|
|
91
|
-
* validPath({ pathname: "/home" }, "/home") // true
|
|
92
|
-
*/
|
|
93
|
-
export const validPath = (location, key) => {
|
|
94
|
-
return location?.pathname === key;
|
|
95
|
-
};
|
|
96
|
-
/**
|
|
97
|
-
* Return the first element if the input is an array; otherwise return the value itself.
|
|
98
|
-
*
|
|
99
|
-
* @typeParam T - Element type.
|
|
100
|
-
* @param {T | T[]} input - A single value or an array.
|
|
101
|
-
* @returns {T} First element or the input value.
|
|
102
|
-
* @example
|
|
103
|
-
* returnObject([1,2,3]) // 1
|
|
104
|
-
* returnObject(5) // 5
|
|
105
|
-
*/
|
|
106
|
-
export const returnObject = (input) => {
|
|
107
|
-
return Array.isArray(input) ? input[0] : input;
|
|
108
|
-
};
|
|
109
|
-
/**
|
|
110
|
-
* Filter out items from `array1` whose `id` appears in `array2`.
|
|
111
|
-
*
|
|
112
|
-
* @typeParam T - Object type with an `id` field.
|
|
113
|
-
* @param {T[]} [array1] - Source array.
|
|
114
|
-
* @param {T[]} [array2] - Items whose `id`s should be excluded.
|
|
115
|
-
* @returns {T[]} Filtered array (or `[]` on invalid input).
|
|
116
|
-
*/
|
|
117
|
-
export const filterArrayById = (array1, array2) => {
|
|
118
|
-
if (!Array.isArray(array1) || !Array.isArray(array2)) {
|
|
119
|
-
return [];
|
|
120
|
-
}
|
|
121
|
-
const idsToExclude = new Set(array2.map((item) => item.id));
|
|
122
|
-
return array1.filter((item) => !idsToExclude.has(item.id));
|
|
123
|
-
};
|
|
124
|
-
/**
|
|
125
|
-
* Sort an array of objects by a given property (ascending).
|
|
126
|
-
*
|
|
127
|
-
* Mutates the original array (uses `Array.prototype.sort`).
|
|
128
|
-
*
|
|
129
|
-
* @typeParam T - Object type.
|
|
130
|
-
* @typeParam TKey - Sortable property key.
|
|
131
|
-
* @param {T[]} array - Array to sort.
|
|
132
|
-
* @param {TKey} property - Property name to sort by.
|
|
133
|
-
* @returns {T[]} The same array instance, sorted (or empty array if input invalid).
|
|
134
|
-
*/
|
|
135
|
-
export const sortArrayByProperty = (array, property) => {
|
|
136
|
-
if (!Array.isArray(array) || array.length === 0)
|
|
137
|
-
return [];
|
|
138
|
-
return array.sort((a, b) => {
|
|
139
|
-
const firstValue = a[property];
|
|
140
|
-
const secondValue = b[property];
|
|
141
|
-
if (firstValue === secondValue)
|
|
142
|
-
return 0;
|
|
143
|
-
if (firstValue === null || firstValue === undefined)
|
|
144
|
-
return 1;
|
|
145
|
-
if (secondValue === null || secondValue === undefined)
|
|
146
|
-
return -1;
|
|
147
|
-
if (firstValue < secondValue)
|
|
148
|
-
return -1;
|
|
149
|
-
if (firstValue > secondValue)
|
|
150
|
-
return 1;
|
|
151
|
-
return 0;
|
|
152
|
-
});
|
|
153
|
-
};
|
|
154
|
-
/**
|
|
155
|
-
* Recursively replace `null` values with empty strings.
|
|
156
|
-
*
|
|
157
|
-
* Works on primitives, arrays, Dates, and plain objects.
|
|
158
|
-
*
|
|
159
|
-
* @typeParam TValue - Input value type.
|
|
160
|
-
* @param {TValue} value - Input value.
|
|
161
|
-
* @returns {TReplaceNulls<TValue>} Value with all `null` replaced by `""`.
|
|
162
|
-
*/
|
|
163
|
-
export const replaceNulls = (value) => {
|
|
164
|
-
if (value === null) {
|
|
165
|
-
return "";
|
|
166
|
-
}
|
|
167
|
-
if (Array.isArray(value)) {
|
|
168
|
-
return value.map((item) => replaceNulls(item));
|
|
169
|
-
}
|
|
170
|
-
if (isRecord(value)) {
|
|
171
|
-
const updatedValue = Object.entries(value).reduce((accumulator, [key, nestedValue]) => {
|
|
172
|
-
accumulator[key] = replaceNulls(nestedValue);
|
|
173
|
-
return accumulator;
|
|
174
|
-
}, {});
|
|
175
|
-
return updatedValue;
|
|
176
|
-
}
|
|
177
|
-
return value;
|
|
178
|
-
};
|
|
179
|
-
/**
|
|
180
|
-
* Recursively remove object keys that contain a dot (`.`).
|
|
181
|
-
*
|
|
182
|
-
* @typeParam TValue - Input value type.
|
|
183
|
-
* @param {TValue} value - Input object or array.
|
|
184
|
-
* @returns {TValue} New value with dotted keys removed at all levels.
|
|
185
|
-
*/
|
|
186
|
-
export const removeFormProperties = (value) => {
|
|
187
|
-
if (Array.isArray(value)) {
|
|
188
|
-
return value.map((item) => removeFormProperties(item));
|
|
189
|
-
}
|
|
190
|
-
if (isRecord(value)) {
|
|
191
|
-
const updatedValue = Object.entries(value).reduce((accumulator, [key, nestedValue]) => {
|
|
192
|
-
if (!key.includes(".")) {
|
|
193
|
-
accumulator[key] = removeFormProperties(nestedValue);
|
|
194
|
-
}
|
|
195
|
-
return accumulator;
|
|
196
|
-
}, {});
|
|
197
|
-
return updatedValue;
|
|
198
|
-
}
|
|
199
|
-
return value;
|
|
200
|
-
};
|
|
201
|
-
/**
|
|
202
|
-
* Recursively convert string booleans `"true"`/`"false"` to actual booleans.
|
|
203
|
-
*
|
|
204
|
-
* Leaves all other values unchanged.
|
|
205
|
-
*
|
|
206
|
-
* @typeParam TValue - Input value type.
|
|
207
|
-
* @param {TValue} value - Input object or array.
|
|
208
|
-
* @returns {TValue} New value with boolean-like strings converted.
|
|
209
|
-
*/
|
|
210
|
-
export const convertStringBooleans = (value) => {
|
|
211
|
-
if (Array.isArray(value)) {
|
|
212
|
-
return value.map((item) => convertStringBooleans(item));
|
|
213
|
-
}
|
|
214
|
-
if (isRecord(value)) {
|
|
215
|
-
const updatedValue = Object.entries(value).reduce((accumulator, [key, nestedValue]) => {
|
|
216
|
-
if (nestedValue === "true") {
|
|
217
|
-
accumulator[key] = true;
|
|
218
|
-
}
|
|
219
|
-
else if (nestedValue === "false") {
|
|
220
|
-
accumulator[key] = false;
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
accumulator[key] = convertStringBooleans(nestedValue);
|
|
224
|
-
}
|
|
225
|
-
return accumulator;
|
|
226
|
-
}, {});
|
|
227
|
-
return updatedValue;
|
|
228
|
-
}
|
|
229
|
-
return value;
|
|
230
|
-
};
|
|
231
|
-
/**
|
|
232
|
-
* Convenience helper to clean form-like data:
|
|
233
|
-
* - Removes `undefined` properties
|
|
234
|
-
* - Converts string booleans to booleans
|
|
235
|
-
* - Removes keys containing a dot (`.`)
|
|
236
|
-
*
|
|
237
|
-
* @typeParam TObject - Form data object type.
|
|
238
|
-
* @param {TObject} objectToClean - Input data.
|
|
239
|
-
* @returns {Partial<TObject>} Cleaned clone.
|
|
240
|
-
*/
|
|
241
|
-
export const cleanFormData = (objectToClean) => {
|
|
242
|
-
return removeFormProperties(convertStringBooleans(removeUndefined(objectToClean)));
|
|
243
|
-
};
|
|
244
|
-
/**
|
|
245
|
-
* Return a default pagination object, allowing optional sort and filters.
|
|
246
|
-
*
|
|
247
|
-
* @param {ISortCondition[]} [sort] - Optional sort conditions.
|
|
248
|
-
* @param {IFilterCondition[]} [filters] - Optional filter conditions.
|
|
249
|
-
* @returns {{ page: number; size: number; sort: ISortCondition[]; filters: IFilterCondition[] }}
|
|
250
|
-
* @example
|
|
251
|
-
* resetPagination() // => { page:1, size:25, sort:[], filters:[] }
|
|
252
|
-
*/
|
|
253
|
-
export const resetPagination = (sort, filters) => {
|
|
254
|
-
return {
|
|
255
|
-
page: 1,
|
|
256
|
-
size: 25,
|
|
257
|
-
sort: sort || [],
|
|
258
|
-
filters: filters || [],
|
|
259
|
-
};
|
|
260
|
-
};
|
|
261
|
-
/**
|
|
262
|
-
* Format a string of digits into a U.S. phone number.
|
|
263
|
-
*
|
|
264
|
-
* Strips non-numeric characters and formats 10 digits as `(XXX) XXX-XXXX`.
|
|
265
|
-
* Strips a leading US country code when 11 digits are provided.
|
|
266
|
-
* If a value cannot be formatted, returns the original value as a string.
|
|
267
|
-
*
|
|
268
|
-
* @param {string | number} value - Phone number digits (string or number).
|
|
269
|
-
* @returns {string} Formatted phone number, or original input if invalid length.
|
|
270
|
-
* @example
|
|
271
|
-
* formatPhoneNumber("1234567890") // "(123) 456-7890"
|
|
272
|
-
* formatPhoneNumber(9876543210) // "(987) 654-3210"
|
|
273
|
-
* formatPhoneNumber("555") // "555"
|
|
274
|
-
*/
|
|
275
|
-
export const formatPhoneNumber = (value) => {
|
|
276
|
-
const originalValue = value.toString();
|
|
277
|
-
const digits = originalValue.replace(/\D/g, "");
|
|
278
|
-
const normalizedDigits = digits.length === 11 && digits.startsWith("1") ? digits.slice(1) : digits;
|
|
279
|
-
if (normalizedDigits.length !== 10)
|
|
280
|
-
return originalValue;
|
|
281
|
-
const area = normalizedDigits.slice(0, 3);
|
|
282
|
-
const prefix = normalizedDigits.slice(3, 6);
|
|
283
|
-
const line = normalizedDigits.slice(6);
|
|
284
|
-
return `(${area}) ${prefix}-${line}`;
|
|
285
|
-
};
|
|
286
|
-
/**
|
|
287
|
-
* Format a number with thousands separators (commas).
|
|
288
|
-
*
|
|
289
|
-
* @param {number} value - Number to format.
|
|
290
|
-
* @returns {string} String with commas.
|
|
291
|
-
* @example
|
|
292
|
-
* formatNumber(1234567) // "1,234,567"
|
|
293
|
-
*/
|
|
294
|
-
export const formatNumber = (value) => {
|
|
295
|
-
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
296
|
-
};
|
|
297
|
-
/**
|
|
298
|
-
* Remove comma separators from a number-like value.
|
|
299
|
-
*
|
|
300
|
-
* @param {number | string} value - Number-like value.
|
|
301
|
-
* @returns {string} Value without comma separators.
|
|
302
|
-
*/
|
|
303
|
-
export const parseNumber = (value) => {
|
|
304
|
-
return value.toString().replace(/,/g, "");
|
|
305
|
-
};
|
|
306
|
-
/**
|
|
307
|
-
* Delete a property from an object if it exists (no-op if it doesn't).
|
|
308
|
-
*
|
|
309
|
-
* @typeParam TObject - Object type.
|
|
310
|
-
* @param {TObject} objectToUpdate - Target object (mutated).
|
|
311
|
-
* @param {keyof TObject | string} propertyName - Property to delete.
|
|
312
|
-
* @returns {void}
|
|
313
|
-
*/
|
|
314
|
-
export const deletePropertyIfExists = (objectToUpdate, propertyName) => {
|
|
315
|
-
if (Object.prototype.hasOwnProperty.call(objectToUpdate, propertyName)) {
|
|
316
|
-
delete objectToUpdate[propertyName];
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
/**
|
|
320
|
-
* Split an array into chunks of a given size.
|
|
321
|
-
*
|
|
322
|
-
* @typeParam T - Element type.
|
|
323
|
-
* @param {T[]} array - Source array.
|
|
324
|
-
* @param {number} chunkSize - Size of each chunk.
|
|
325
|
-
* @returns {T[][]} Array of chunks (last one may be smaller).
|
|
326
|
-
* @example
|
|
327
|
-
* chunkArray([1,2,3,4,5], 2) // [[1,2],[3,4],[5]]
|
|
328
|
-
*/
|
|
329
|
-
export const chunkArray = (array, chunkSize) => {
|
|
330
|
-
if (!Array.isArray(array) || chunkSize <= 0 || !Number.isFinite(chunkSize)) {
|
|
331
|
-
return [];
|
|
332
|
-
}
|
|
333
|
-
const result = [];
|
|
334
|
-
const normalizedChunkSize = Math.floor(chunkSize);
|
|
335
|
-
for (let i = 0; i < array.length; i += normalizedChunkSize) {
|
|
336
|
-
result.push(array.slice(i, i + normalizedChunkSize));
|
|
337
|
-
}
|
|
338
|
-
return result;
|
|
339
|
-
};
|
|
340
|
-
/**
|
|
341
|
-
* Return a shallow clone of `objectToOmitFrom` without the listed properties.
|
|
342
|
-
*
|
|
343
|
-
* @typeParam TObject - Source object type.
|
|
344
|
-
* @typeParam TKey - Keys to omit.
|
|
345
|
-
* @param {TObject} objectToOmitFrom - Source object.
|
|
346
|
-
* @param {readonly TKey[]} propsToOmit - Property names to omit.
|
|
347
|
-
* @returns {Omit<TObject, TKey>} New object without omitted props.
|
|
348
|
-
* @example
|
|
349
|
-
* omitProperties({a:1,b:2}, ["b"]) // { a:1 }
|
|
350
|
-
*/
|
|
351
|
-
export const omitProperties = (objectToOmitFrom, propsToOmit) => {
|
|
352
|
-
const updatedObject = { ...objectToOmitFrom };
|
|
353
|
-
propsToOmit.forEach((propertyName) => {
|
|
354
|
-
delete updatedObject[propertyName];
|
|
355
|
-
});
|
|
356
|
-
return updatedObject;
|
|
357
|
-
};
|
|
358
|
-
/**
|
|
359
|
-
* Safe `hasOwnProperty` check.
|
|
360
|
-
*
|
|
361
|
-
* @param {unknown} value - Value to test.
|
|
362
|
-
* @param {PropertyKey} key - Property name.
|
|
363
|
-
* @returns {boolean}
|
|
364
|
-
*/
|
|
365
|
-
export const hasProperty = (value, key) => {
|
|
366
|
-
if (value === null || value === undefined)
|
|
367
|
-
return false;
|
|
368
|
-
return Object.prototype.hasOwnProperty.call(value, key);
|
|
369
|
-
};
|
|
370
|
-
/**
|
|
371
|
-
* Safe `hasOwnProperty` alias from the reference utilities.
|
|
372
|
-
*
|
|
373
|
-
* @param {unknown} value - Value to test.
|
|
374
|
-
* @param {PropertyKey} key - Property name.
|
|
375
|
-
* @returns {boolean}
|
|
376
|
-
*/
|
|
377
|
-
export const hasOwnProp = (value, key) => {
|
|
378
|
-
return hasProperty(value, key);
|
|
379
|
-
};
|
|
380
|
-
/**
|
|
381
|
-
* Determine if an object has at least one own enumerable property.
|
|
382
|
-
*
|
|
383
|
-
* @param {unknown} value - Object to test.
|
|
384
|
-
* @returns {boolean} `true` if there is at least one key.
|
|
385
|
-
*/
|
|
386
|
-
export const hasProperties = (value) => {
|
|
387
|
-
return isRecord(value) && Object.keys(value).length > 0;
|
|
388
|
-
};
|
|
389
|
-
/**
|
|
390
|
-
* Get the first own enumerable property value from an object.
|
|
391
|
-
*
|
|
392
|
-
* @typeParam TObject - Source object type.
|
|
393
|
-
* @param {TObject | null | undefined} value - Source object.
|
|
394
|
-
* @returns {TObject[keyof TObject] | null} First value, or null for empty/non-object input.
|
|
395
|
-
*/
|
|
396
|
-
export const getFirstPropertyValue = (value) => {
|
|
397
|
-
if (value === null || value === undefined || !hasProperties(value)) {
|
|
398
|
-
return null;
|
|
399
|
-
}
|
|
400
|
-
const source = value;
|
|
401
|
-
const keys = Object.keys(source);
|
|
402
|
-
return source[keys[0]];
|
|
403
|
-
};
|
|
404
|
-
/**
|
|
405
|
-
* Get a nested value from an object using dot notation.
|
|
406
|
-
*
|
|
407
|
-
* @param {unknown} value - Source object.
|
|
408
|
-
* @param {string} path - Dot-delimited path.
|
|
409
|
-
* @returns {unknown} Nested value, or undefined when the path cannot be resolved.
|
|
410
|
-
* @example
|
|
411
|
-
* getNestedValue({ user: { name: "John" } }, "user.name") // "John"
|
|
412
|
-
*/
|
|
413
|
-
export const getNestedValue = (value, path) => {
|
|
414
|
-
const pathSegments = getPathSegments(path);
|
|
415
|
-
if (pathSegments.length === 0) {
|
|
416
|
-
return value;
|
|
417
|
-
}
|
|
418
|
-
let currentValue = value;
|
|
419
|
-
for (const pathSegment of pathSegments) {
|
|
420
|
-
if (!isSafePathSegment(pathSegment)) {
|
|
421
|
-
return undefined;
|
|
422
|
-
}
|
|
423
|
-
if (currentValue === null || currentValue === undefined) {
|
|
424
|
-
return undefined;
|
|
425
|
-
}
|
|
426
|
-
if (typeof currentValue !== "object" &&
|
|
427
|
-
typeof currentValue !== "function") {
|
|
428
|
-
return undefined;
|
|
429
|
-
}
|
|
430
|
-
currentValue = currentValue[pathSegment];
|
|
431
|
-
}
|
|
432
|
-
return currentValue;
|
|
433
|
-
};
|
|
434
|
-
/**
|
|
435
|
-
* Set a nested value on an object using dot notation.
|
|
436
|
-
*
|
|
437
|
-
* Mutates and returns the provided object. Unsafe path segments are ignored to
|
|
438
|
-
* prevent prototype pollution.
|
|
439
|
-
*
|
|
440
|
-
* @typeParam TObject - Target object type.
|
|
441
|
-
* @param {TObject} objectToUpdate - Target object.
|
|
442
|
-
* @param {string} path - Dot-delimited path.
|
|
443
|
-
* @param {unknown} value - Value to set.
|
|
444
|
-
* @returns {TObject} The mutated target object.
|
|
445
|
-
* @example
|
|
446
|
-
* setNestedValue({}, "user.name", "John") // { user: { name: "John" } }
|
|
447
|
-
*/
|
|
448
|
-
export const setNestedValue = (objectToUpdate, path, value) => {
|
|
449
|
-
const pathSegments = getPathSegments(path);
|
|
450
|
-
if (pathSegments.length === 0 ||
|
|
451
|
-
pathSegments.some((pathSegment) => !isSafePathSegment(pathSegment))) {
|
|
452
|
-
return objectToUpdate;
|
|
453
|
-
}
|
|
454
|
-
let currentValue = objectToUpdate;
|
|
455
|
-
for (let i = 0; i < pathSegments.length - 1; i += 1) {
|
|
456
|
-
const pathSegment = pathSegments[i];
|
|
457
|
-
const nextValue = currentValue[pathSegment];
|
|
458
|
-
if (!isRecord(nextValue)) {
|
|
459
|
-
currentValue[pathSegment] = {};
|
|
460
|
-
}
|
|
461
|
-
currentValue = currentValue[pathSegment];
|
|
462
|
-
}
|
|
463
|
-
currentValue[pathSegments[pathSegments.length - 1]] = value;
|
|
464
|
-
return objectToUpdate;
|
|
465
|
-
};
|
|
466
|
-
/**
|
|
467
|
-
* Deep clone a value while preserving Dates and circular references.
|
|
468
|
-
*
|
|
469
|
-
* @typeParam TValue - Input value type.
|
|
470
|
-
* @param {TValue} value - Value to clone.
|
|
471
|
-
* @returns {TValue} Deep clone of the input.
|
|
472
|
-
*/
|
|
473
|
-
export const deepClone = (value) => {
|
|
474
|
-
const cloneValue = (nestedValue, seenValues) => {
|
|
475
|
-
if (nestedValue === null || typeof nestedValue !== "object") {
|
|
476
|
-
return nestedValue;
|
|
477
|
-
}
|
|
478
|
-
if (nestedValue instanceof Date) {
|
|
479
|
-
return new Date(nestedValue.getTime());
|
|
480
|
-
}
|
|
481
|
-
if (seenValues.has(nestedValue)) {
|
|
482
|
-
return seenValues.get(nestedValue);
|
|
483
|
-
}
|
|
484
|
-
if (Array.isArray(nestedValue)) {
|
|
485
|
-
const clonedArray = [];
|
|
486
|
-
seenValues.set(nestedValue, clonedArray);
|
|
487
|
-
nestedValue.forEach((item) => {
|
|
488
|
-
clonedArray.push(cloneValue(item, seenValues));
|
|
489
|
-
});
|
|
490
|
-
return clonedArray;
|
|
491
|
-
}
|
|
492
|
-
const clonedObject = Object.create(Object.getPrototypeOf(nestedValue));
|
|
493
|
-
seenValues.set(nestedValue, clonedObject);
|
|
494
|
-
Reflect.ownKeys(nestedValue).forEach((key) => {
|
|
495
|
-
clonedObject[key] = cloneValue(nestedValue[key], seenValues);
|
|
496
|
-
});
|
|
497
|
-
return clonedObject;
|
|
498
|
-
};
|
|
499
|
-
return cloneValue(value, new WeakMap());
|
|
500
|
-
};
|
|
501
|
-
/**
|
|
502
|
-
* Flatten a nested object into dot notation.
|
|
503
|
-
*
|
|
504
|
-
* Arrays and Dates are treated as leaf values.
|
|
505
|
-
*
|
|
506
|
-
* @param {TRecord} value - Source object.
|
|
507
|
-
* @param {string} [prefix] - Internal prefix for recursion.
|
|
508
|
-
* @returns {TRecord} Flattened object.
|
|
509
|
-
* @example
|
|
510
|
-
* flatten({ user: { name: "John" } }) // { "user.name": "John" }
|
|
511
|
-
*/
|
|
512
|
-
export const flatten = (value, prefix = "") => {
|
|
513
|
-
const result = {};
|
|
514
|
-
Object.entries(value).forEach(([key, nestedValue]) => {
|
|
515
|
-
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
516
|
-
if (isRecord(nestedValue)) {
|
|
517
|
-
Object.assign(result, flatten(nestedValue, newKey));
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
result[newKey] = nestedValue;
|
|
521
|
-
});
|
|
522
|
-
return result;
|
|
523
|
-
};
|
|
524
|
-
/**
|
|
525
|
-
* Convert a dot-notation object into a nested object.
|
|
526
|
-
*
|
|
527
|
-
* Unsafe path segments are ignored to prevent prototype pollution.
|
|
528
|
-
*
|
|
529
|
-
* @param {TRecord} value - Dot-notation source object.
|
|
530
|
-
* @returns {TRecord} Nested object.
|
|
531
|
-
* @example
|
|
532
|
-
* unflatten({ "user.name": "John" }) // { user: { name: "John" } }
|
|
533
|
-
*/
|
|
534
|
-
export const unflatten = (value) => {
|
|
535
|
-
const result = {};
|
|
536
|
-
Object.entries(value).forEach(([key, nestedValue]) => {
|
|
537
|
-
setNestedValue(result, key, nestedValue);
|
|
538
|
-
});
|
|
539
|
-
return result;
|
|
540
|
-
};
|