voltjs-framework 1.0.0
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/LICENSE +21 -0
- package/README.md +1265 -0
- package/bin/volt.js +139 -0
- package/package.json +56 -0
- package/src/api/graphql.js +399 -0
- package/src/api/rest.js +204 -0
- package/src/api/websocket.js +285 -0
- package/src/cli/build.js +111 -0
- package/src/cli/create.js +371 -0
- package/src/cli/db.js +106 -0
- package/src/cli/dev.js +114 -0
- package/src/cli/generate.js +278 -0
- package/src/cli/lint.js +172 -0
- package/src/cli/routes.js +118 -0
- package/src/cli/start.js +42 -0
- package/src/cli/test.js +138 -0
- package/src/core/app.js +701 -0
- package/src/core/config.js +232 -0
- package/src/core/middleware.js +133 -0
- package/src/core/plugins.js +88 -0
- package/src/core/react-renderer.js +244 -0
- package/src/core/renderer.js +337 -0
- package/src/core/router.js +183 -0
- package/src/database/index.js +461 -0
- package/src/database/migration.js +192 -0
- package/src/database/model.js +285 -0
- package/src/database/query.js +394 -0
- package/src/database/seeder.js +89 -0
- package/src/index.js +156 -0
- package/src/security/auth.js +425 -0
- package/src/security/cors.js +80 -0
- package/src/security/csrf.js +125 -0
- package/src/security/encryption.js +110 -0
- package/src/security/helmet.js +103 -0
- package/src/security/index.js +75 -0
- package/src/security/rateLimit.js +119 -0
- package/src/security/sanitizer.js +113 -0
- package/src/security/xss.js +110 -0
- package/src/ui/component.js +224 -0
- package/src/ui/reactive.js +503 -0
- package/src/ui/template.js +448 -0
- package/src/utils/cache.js +216 -0
- package/src/utils/collection.js +772 -0
- package/src/utils/cron.js +213 -0
- package/src/utils/date.js +223 -0
- package/src/utils/events.js +181 -0
- package/src/utils/excel.js +482 -0
- package/src/utils/form.js +547 -0
- package/src/utils/hash.js +121 -0
- package/src/utils/http.js +461 -0
- package/src/utils/logger.js +186 -0
- package/src/utils/mail.js +347 -0
- package/src/utils/paginator.js +179 -0
- package/src/utils/pdf.js +417 -0
- package/src/utils/queue.js +199 -0
- package/src/utils/schema.js +985 -0
- package/src/utils/sms.js +243 -0
- package/src/utils/storage.js +348 -0
- package/src/utils/string.js +236 -0
- package/src/utils/validation.js +318 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Collection Utilities (Lodash Alternative)
|
|
3
|
+
*
|
|
4
|
+
* All the array, object, and function utilities you need — zero dependencies.
|
|
5
|
+
* Covers: chunk, debounce, throttle, cloneDeep, merge, pick, omit, get, set,
|
|
6
|
+
* groupBy, keyBy, uniq, flatten, intersection, difference, range, memoize, etc.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const { _ } = require('voltjs');
|
|
10
|
+
*
|
|
11
|
+
* _.chunk([1,2,3,4,5], 2); // [[1,2],[3,4],[5]]
|
|
12
|
+
* _.groupBy(users, 'role'); // { admin: [...], user: [...] }
|
|
13
|
+
* _.pick(obj, ['name', 'email']); // { name: 'John', email: '...' }
|
|
14
|
+
* _.debounce(fn, 300); // Debounced function
|
|
15
|
+
* _.cloneDeep(obj); // Deep clone
|
|
16
|
+
* _.get(obj, 'a.b.c', 'default'); // Deep path access
|
|
17
|
+
* _.classNames('btn', { active: true }); // 'btn active'
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const _ = {
|
|
23
|
+
|
|
24
|
+
// ==========================================
|
|
25
|
+
// ARRAY UTILITIES
|
|
26
|
+
// ==========================================
|
|
27
|
+
|
|
28
|
+
/** Split array into chunks of `size` */
|
|
29
|
+
chunk(arr, size = 1) {
|
|
30
|
+
const result = [];
|
|
31
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
32
|
+
result.push(arr.slice(i, i + size));
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/** Remove falsy values (false, null, 0, '', undefined, NaN) */
|
|
38
|
+
compact(arr) {
|
|
39
|
+
return arr.filter(Boolean);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/** Flatten one level */
|
|
43
|
+
flatten(arr) {
|
|
44
|
+
return arr.reduce((acc, val) => acc.concat(val), []);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/** Flatten deeply (all nested levels) */
|
|
48
|
+
flattenDeep(arr) {
|
|
49
|
+
return arr.reduce((acc, val) =>
|
|
50
|
+
Array.isArray(val) ? acc.concat(_.flattenDeep(val)) : acc.concat(val), []);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/** Flatten to a specific depth */
|
|
54
|
+
flattenDepth(arr, depth = 1) {
|
|
55
|
+
if (depth <= 0) return arr.slice();
|
|
56
|
+
return arr.reduce((acc, val) =>
|
|
57
|
+
Array.isArray(val)
|
|
58
|
+
? acc.concat(_.flattenDepth(val, depth - 1))
|
|
59
|
+
: acc.concat(val), []);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/** Unique values */
|
|
63
|
+
uniq(arr) {
|
|
64
|
+
return [...new Set(arr)];
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/** Unique by iteratee function or property name */
|
|
68
|
+
uniqBy(arr, iteratee) {
|
|
69
|
+
const fn = typeof iteratee === 'function' ? iteratee : (item) => item[iteratee];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
return arr.filter(item => {
|
|
72
|
+
const key = fn(item);
|
|
73
|
+
if (seen.has(key)) return false;
|
|
74
|
+
seen.add(key);
|
|
75
|
+
return true;
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/** Intersection of arrays (common elements) */
|
|
80
|
+
intersection(...arrays) {
|
|
81
|
+
if (arrays.length === 0) return [];
|
|
82
|
+
const sets = arrays.slice(1).map(a => new Set(a));
|
|
83
|
+
return arrays[0].filter(item => sets.every(s => s.has(item)));
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/** Difference: elements in first array not in others */
|
|
87
|
+
difference(arr, ...others) {
|
|
88
|
+
const otherSet = new Set(others.flat());
|
|
89
|
+
return arr.filter(item => !otherSet.has(item));
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/** Union of arrays (unique combined) */
|
|
93
|
+
union(...arrays) {
|
|
94
|
+
return [...new Set(arrays.flat())];
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/** Zip arrays together */
|
|
98
|
+
zip(...arrays) {
|
|
99
|
+
const maxLen = Math.max(...arrays.map(a => a.length));
|
|
100
|
+
const result = [];
|
|
101
|
+
for (let i = 0; i < maxLen; i++) {
|
|
102
|
+
result.push(arrays.map(a => a[i]));
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/** Unzip: opposite of zip */
|
|
108
|
+
unzip(arr) {
|
|
109
|
+
if (arr.length === 0) return [];
|
|
110
|
+
return arr[0].map((_, i) => arr.map(row => row[i]));
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/** Group by key or function */
|
|
114
|
+
groupBy(arr, iteratee) {
|
|
115
|
+
const fn = typeof iteratee === 'function' ? iteratee : (item) => item[iteratee];
|
|
116
|
+
const result = {};
|
|
117
|
+
for (const item of arr) {
|
|
118
|
+
const key = fn(item);
|
|
119
|
+
if (!result[key]) result[key] = [];
|
|
120
|
+
result[key].push(item);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/** Index by key (one item per key) */
|
|
126
|
+
keyBy(arr, iteratee) {
|
|
127
|
+
const fn = typeof iteratee === 'function' ? iteratee : (item) => item[iteratee];
|
|
128
|
+
const result = {};
|
|
129
|
+
for (const item of arr) {
|
|
130
|
+
result[fn(item)] = item;
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/** Sort by key or function (stable, returns new array) */
|
|
136
|
+
sortBy(arr, iteratee) {
|
|
137
|
+
const fn = typeof iteratee === 'function' ? iteratee : (item) => item[iteratee];
|
|
138
|
+
return [...arr].sort((a, b) => {
|
|
139
|
+
const va = fn(a);
|
|
140
|
+
const vb = fn(b);
|
|
141
|
+
if (va < vb) return -1;
|
|
142
|
+
if (va > vb) return 1;
|
|
143
|
+
return 0;
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
/** Order by multiple keys (with direction) */
|
|
148
|
+
orderBy(arr, keys, orders = []) {
|
|
149
|
+
return [...arr].sort((a, b) => {
|
|
150
|
+
for (let i = 0; i < keys.length; i++) {
|
|
151
|
+
const key = keys[i];
|
|
152
|
+
const order = (orders[i] || 'asc') === 'desc' ? -1 : 1;
|
|
153
|
+
const va = typeof key === 'function' ? key(a) : a[key];
|
|
154
|
+
const vb = typeof key === 'function' ? key(b) : b[key];
|
|
155
|
+
if (va < vb) return -1 * order;
|
|
156
|
+
if (va > vb) return 1 * order;
|
|
157
|
+
}
|
|
158
|
+
return 0;
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/** Count occurrences by key or function */
|
|
163
|
+
countBy(arr, iteratee) {
|
|
164
|
+
const fn = typeof iteratee === 'function' ? iteratee : (item) => item[iteratee];
|
|
165
|
+
const result = {};
|
|
166
|
+
for (const item of arr) {
|
|
167
|
+
const key = fn(item);
|
|
168
|
+
result[key] = (result[key] || 0) + 1;
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/** Partition array into two: [truthy, falsy] */
|
|
174
|
+
partition(arr, predicate) {
|
|
175
|
+
const truthy = [];
|
|
176
|
+
const falsy = [];
|
|
177
|
+
for (const item of arr) {
|
|
178
|
+
(predicate(item) ? truthy : falsy).push(item);
|
|
179
|
+
}
|
|
180
|
+
return [truthy, falsy];
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/** Sample: random element */
|
|
184
|
+
sample(arr) {
|
|
185
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/** Sample N random elements */
|
|
189
|
+
sampleSize(arr, n = 1) {
|
|
190
|
+
const shuffled = _.shuffle(arr);
|
|
191
|
+
return shuffled.slice(0, n);
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/** Shuffle array (Fisher-Yates) */
|
|
195
|
+
shuffle(arr) {
|
|
196
|
+
const result = [...arr];
|
|
197
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
198
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
199
|
+
[result[i], result[j]] = [result[j], result[i]];
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
/** First N elements */
|
|
205
|
+
take(arr, n = 1) {
|
|
206
|
+
return arr.slice(0, n);
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
/** Last N elements */
|
|
210
|
+
takeLast(arr, n = 1) {
|
|
211
|
+
return arr.slice(-n);
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/** Drop first N elements */
|
|
215
|
+
drop(arr, n = 1) {
|
|
216
|
+
return arr.slice(n);
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
/** Drop last N elements */
|
|
220
|
+
dropLast(arr, n = 1) {
|
|
221
|
+
return arr.slice(0, -n || undefined);
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
/** First element */
|
|
225
|
+
first(arr) {
|
|
226
|
+
return arr[0];
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
/** Last element */
|
|
230
|
+
last(arr) {
|
|
231
|
+
return arr[arr.length - 1];
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
/** Find the min value in array (by iteratee) */
|
|
235
|
+
minBy(arr, iteratee) {
|
|
236
|
+
const fn = typeof iteratee === 'function' ? iteratee : (item) => item[iteratee];
|
|
237
|
+
return arr.reduce((min, item) => fn(item) < fn(min) ? item : min);
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
/** Find the max value in array (by iteratee) */
|
|
241
|
+
maxBy(arr, iteratee) {
|
|
242
|
+
const fn = typeof iteratee === 'function' ? iteratee : (item) => item[iteratee];
|
|
243
|
+
return arr.reduce((max, item) => fn(item) > fn(max) ? item : max);
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
/** Sum of values */
|
|
247
|
+
sum(arr) {
|
|
248
|
+
return arr.reduce((s, v) => s + v, 0);
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/** Sum by key or function */
|
|
252
|
+
sumBy(arr, iteratee) {
|
|
253
|
+
const fn = typeof iteratee === 'function' ? iteratee : (item) => item[iteratee];
|
|
254
|
+
return arr.reduce((s, item) => s + fn(item), 0);
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/** Mean (average) */
|
|
258
|
+
mean(arr) {
|
|
259
|
+
return arr.length === 0 ? 0 : _.sum(arr) / arr.length;
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
/** Generate a range of numbers */
|
|
263
|
+
range(start, end, step = 1) {
|
|
264
|
+
if (end === undefined) { end = start; start = 0; }
|
|
265
|
+
const result = [];
|
|
266
|
+
if (step > 0) {
|
|
267
|
+
for (let i = start; i < end; i += step) result.push(i);
|
|
268
|
+
} else if (step < 0) {
|
|
269
|
+
for (let i = start; i > end; i += step) result.push(i);
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
/** Create an array of N items, each filled by fn */
|
|
275
|
+
times(n, fn) {
|
|
276
|
+
return Array.from({ length: n }, (_, i) => fn(i));
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// ==========================================
|
|
280
|
+
// OBJECT UTILITIES
|
|
281
|
+
// ==========================================
|
|
282
|
+
|
|
283
|
+
/** Deep get by path string: _.get(obj, 'a.b[0].c', default) */
|
|
284
|
+
get(obj, path, defaultValue = undefined) {
|
|
285
|
+
const keys = String(path).replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
|
|
286
|
+
let result = obj;
|
|
287
|
+
for (const key of keys) {
|
|
288
|
+
if (result === null || result === undefined) return defaultValue;
|
|
289
|
+
result = result[key];
|
|
290
|
+
}
|
|
291
|
+
return result === undefined ? defaultValue : result;
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
/** Deep set by path string: _.set(obj, 'a.b.c', value) */
|
|
295
|
+
set(obj, path, value) {
|
|
296
|
+
const keys = String(path).replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
|
|
297
|
+
let curr = obj;
|
|
298
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
299
|
+
const key = keys[i];
|
|
300
|
+
const nextKey = keys[i + 1];
|
|
301
|
+
if (curr[key] === undefined || curr[key] === null) {
|
|
302
|
+
curr[key] = /^\d+$/.test(nextKey) ? [] : {};
|
|
303
|
+
}
|
|
304
|
+
curr = curr[key];
|
|
305
|
+
}
|
|
306
|
+
curr[keys[keys.length - 1]] = value;
|
|
307
|
+
return obj;
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
/** Check if path exists */
|
|
311
|
+
has(obj, path) {
|
|
312
|
+
const keys = String(path).replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
|
|
313
|
+
let curr = obj;
|
|
314
|
+
for (const key of keys) {
|
|
315
|
+
if (curr === undefined || curr === null || !Object.prototype.hasOwnProperty.call(curr, key)) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
curr = curr[key];
|
|
319
|
+
}
|
|
320
|
+
return true;
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
/** Pick specific keys from object */
|
|
324
|
+
pick(obj, keys) {
|
|
325
|
+
const result = {};
|
|
326
|
+
for (const key of keys) {
|
|
327
|
+
if (key in obj) result[key] = obj[key];
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
/** Omit specific keys from object */
|
|
333
|
+
omit(obj, keys) {
|
|
334
|
+
const keySet = new Set(keys);
|
|
335
|
+
const result = {};
|
|
336
|
+
for (const key of Object.keys(obj)) {
|
|
337
|
+
if (!keySet.has(key)) result[key] = obj[key];
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
/** Pick by predicate function */
|
|
343
|
+
pickBy(obj, predicate) {
|
|
344
|
+
const result = {};
|
|
345
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
346
|
+
if (predicate(val, key)) result[key] = val;
|
|
347
|
+
}
|
|
348
|
+
return result;
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
/** Omit by predicate function */
|
|
352
|
+
omitBy(obj, predicate) {
|
|
353
|
+
return _.pickBy(obj, (val, key) => !predicate(val, key));
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
/** Map object values */
|
|
357
|
+
mapValues(obj, fn) {
|
|
358
|
+
const result = {};
|
|
359
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
360
|
+
result[key] = fn(val, key);
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/** Map object keys */
|
|
366
|
+
mapKeys(obj, fn) {
|
|
367
|
+
const result = {};
|
|
368
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
369
|
+
result[fn(key, val)] = val;
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
/** Invert keys and values */
|
|
375
|
+
invert(obj) {
|
|
376
|
+
const result = {};
|
|
377
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
378
|
+
result[val] = key;
|
|
379
|
+
}
|
|
380
|
+
return result;
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
/** Deep merge (objects are merged, arrays replaced) */
|
|
384
|
+
merge(...objects) {
|
|
385
|
+
const result = {};
|
|
386
|
+
for (const obj of objects) {
|
|
387
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
388
|
+
if (val && typeof val === 'object' && !Array.isArray(val) &&
|
|
389
|
+
result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) {
|
|
390
|
+
result[key] = _.merge(result[key], val);
|
|
391
|
+
} else {
|
|
392
|
+
result[key] = val;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
/** Deep clone (handles nested objects, arrays, dates, regexps) */
|
|
400
|
+
cloneDeep(value) {
|
|
401
|
+
if (value === null || typeof value !== 'object') return value;
|
|
402
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
403
|
+
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
|
|
404
|
+
if (value instanceof Map) {
|
|
405
|
+
const map = new Map();
|
|
406
|
+
for (const [k, v] of value) map.set(_.cloneDeep(k), _.cloneDeep(v));
|
|
407
|
+
return map;
|
|
408
|
+
}
|
|
409
|
+
if (value instanceof Set) {
|
|
410
|
+
const set = new Set();
|
|
411
|
+
for (const v of value) set.add(_.cloneDeep(v));
|
|
412
|
+
return set;
|
|
413
|
+
}
|
|
414
|
+
if (Array.isArray(value)) return value.map(v => _.cloneDeep(v));
|
|
415
|
+
const result = {};
|
|
416
|
+
for (const [k, v] of Object.entries(value)) {
|
|
417
|
+
result[k] = _.cloneDeep(v);
|
|
418
|
+
}
|
|
419
|
+
return result;
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
/** Shallow clone */
|
|
423
|
+
clone(value) {
|
|
424
|
+
if (Array.isArray(value)) return [...value];
|
|
425
|
+
if (value && typeof value === 'object') return { ...value };
|
|
426
|
+
return value;
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
/** Deep equality check */
|
|
430
|
+
isEqual(a, b) {
|
|
431
|
+
if (a === b) return true;
|
|
432
|
+
if (a === null || b === null || typeof a !== typeof b) return false;
|
|
433
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
434
|
+
if (a instanceof RegExp && b instanceof RegExp) return a.toString() === b.toString();
|
|
435
|
+
if (typeof a !== 'object') return false;
|
|
436
|
+
|
|
437
|
+
const keysA = Object.keys(a);
|
|
438
|
+
const keysB = Object.keys(b);
|
|
439
|
+
if (keysA.length !== keysB.length) return false;
|
|
440
|
+
|
|
441
|
+
return keysA.every(key => _.isEqual(a[key], b[key]));
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
/** Check if value is empty ([], {}, '', null, undefined) */
|
|
445
|
+
isEmpty(value) {
|
|
446
|
+
if (value === null || value === undefined) return true;
|
|
447
|
+
if (typeof value === 'string' || Array.isArray(value)) return value.length === 0;
|
|
448
|
+
if (value instanceof Map || value instanceof Set) return value.size === 0;
|
|
449
|
+
if (typeof value === 'object') return Object.keys(value).length === 0;
|
|
450
|
+
return false;
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
/** Freeze deeply (immutable) */
|
|
454
|
+
freezeDeep(obj) {
|
|
455
|
+
Object.freeze(obj);
|
|
456
|
+
for (const val of Object.values(obj)) {
|
|
457
|
+
if (val && typeof val === 'object' && !Object.isFrozen(val)) {
|
|
458
|
+
_.freezeDeep(val);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return obj;
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
/** Convert entries to object */
|
|
465
|
+
fromEntries(entries) {
|
|
466
|
+
return Object.fromEntries(entries);
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
// ==========================================
|
|
470
|
+
// FUNCTION UTILITIES
|
|
471
|
+
// ==========================================
|
|
472
|
+
|
|
473
|
+
/** Debounce: delay fn execution until N ms after last call */
|
|
474
|
+
debounce(fn, wait = 300, options = {}) {
|
|
475
|
+
let timer = null;
|
|
476
|
+
let lastArgs = null;
|
|
477
|
+
let lastResult;
|
|
478
|
+
const leading = options.leading || false;
|
|
479
|
+
const trailing = options.trailing !== false;
|
|
480
|
+
|
|
481
|
+
function debounced(...args) {
|
|
482
|
+
lastArgs = args;
|
|
483
|
+
const callNow = leading && !timer;
|
|
484
|
+
|
|
485
|
+
if (timer) clearTimeout(timer);
|
|
486
|
+
|
|
487
|
+
timer = setTimeout(() => {
|
|
488
|
+
timer = null;
|
|
489
|
+
if (trailing && lastArgs) {
|
|
490
|
+
lastResult = fn.apply(this, lastArgs);
|
|
491
|
+
lastArgs = null;
|
|
492
|
+
}
|
|
493
|
+
}, wait);
|
|
494
|
+
|
|
495
|
+
if (callNow) {
|
|
496
|
+
lastResult = fn.apply(this, args);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return lastResult;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
debounced.cancel = () => {
|
|
503
|
+
if (timer) clearTimeout(timer);
|
|
504
|
+
timer = null;
|
|
505
|
+
lastArgs = null;
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
debounced.flush = () => {
|
|
509
|
+
if (timer) {
|
|
510
|
+
clearTimeout(timer);
|
|
511
|
+
timer = null;
|
|
512
|
+
if (lastArgs) {
|
|
513
|
+
lastResult = fn.apply(undefined, lastArgs);
|
|
514
|
+
lastArgs = null;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return lastResult;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
return debounced;
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
/** Throttle: limit fn to at most once per N ms */
|
|
524
|
+
throttle(fn, wait = 300, options = {}) {
|
|
525
|
+
let timer = null;
|
|
526
|
+
let lastCall = 0;
|
|
527
|
+
let lastResult;
|
|
528
|
+
const leading = options.leading !== false;
|
|
529
|
+
const trailing = options.trailing !== false;
|
|
530
|
+
|
|
531
|
+
function throttled(...args) {
|
|
532
|
+
const now = Date.now();
|
|
533
|
+
|
|
534
|
+
if (!lastCall && !leading) lastCall = now;
|
|
535
|
+
|
|
536
|
+
const remaining = wait - (now - lastCall);
|
|
537
|
+
|
|
538
|
+
if (remaining <= 0) {
|
|
539
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
540
|
+
lastCall = now;
|
|
541
|
+
lastResult = fn.apply(this, args);
|
|
542
|
+
} else if (!timer && trailing) {
|
|
543
|
+
timer = setTimeout(() => {
|
|
544
|
+
lastCall = leading ? Date.now() : 0;
|
|
545
|
+
timer = null;
|
|
546
|
+
lastResult = fn.apply(this, args);
|
|
547
|
+
}, remaining);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return lastResult;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
throttled.cancel = () => {
|
|
554
|
+
if (timer) clearTimeout(timer);
|
|
555
|
+
timer = null;
|
|
556
|
+
lastCall = 0;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
return throttled;
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
/** Execute function only once */
|
|
563
|
+
once(fn) {
|
|
564
|
+
let called = false;
|
|
565
|
+
let result;
|
|
566
|
+
return function (...args) {
|
|
567
|
+
if (!called) {
|
|
568
|
+
called = true;
|
|
569
|
+
result = fn.apply(this, args);
|
|
570
|
+
}
|
|
571
|
+
return result;
|
|
572
|
+
};
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
/** Memoize function results */
|
|
576
|
+
memoize(fn, resolver) {
|
|
577
|
+
const cache = new Map();
|
|
578
|
+
const memoized = function (...args) {
|
|
579
|
+
const key = resolver ? resolver(...args) : args[0];
|
|
580
|
+
if (cache.has(key)) return cache.get(key);
|
|
581
|
+
const result = fn.apply(this, args);
|
|
582
|
+
cache.set(key, result);
|
|
583
|
+
return result;
|
|
584
|
+
};
|
|
585
|
+
memoized.cache = cache;
|
|
586
|
+
memoized.clear = () => cache.clear();
|
|
587
|
+
return memoized;
|
|
588
|
+
},
|
|
589
|
+
|
|
590
|
+
/** Execute fn after N calls */
|
|
591
|
+
after(n, fn) {
|
|
592
|
+
let count = 0;
|
|
593
|
+
return function (...args) {
|
|
594
|
+
count++;
|
|
595
|
+
if (count >= n) return fn.apply(this, args);
|
|
596
|
+
};
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
/** Execute fn at most N times */
|
|
600
|
+
before(n, fn) {
|
|
601
|
+
let count = 0;
|
|
602
|
+
let result;
|
|
603
|
+
return function (...args) {
|
|
604
|
+
count++;
|
|
605
|
+
if (count < n) result = fn.apply(this, args);
|
|
606
|
+
return result;
|
|
607
|
+
};
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
/** Negate a predicate function */
|
|
611
|
+
negate(fn) {
|
|
612
|
+
return (...args) => !fn(...args);
|
|
613
|
+
},
|
|
614
|
+
|
|
615
|
+
/** Compose functions (right to left) */
|
|
616
|
+
compose(...fns) {
|
|
617
|
+
return (input) => fns.reduceRight((acc, fn) => fn(acc), input);
|
|
618
|
+
},
|
|
619
|
+
|
|
620
|
+
/** Pipe functions (left to right) */
|
|
621
|
+
pipe(...fns) {
|
|
622
|
+
return (input) => fns.reduce((acc, fn) => fn(acc), input);
|
|
623
|
+
},
|
|
624
|
+
|
|
625
|
+
/** Curry a function */
|
|
626
|
+
curry(fn) {
|
|
627
|
+
const arity = fn.length;
|
|
628
|
+
return function curried(...args) {
|
|
629
|
+
if (args.length >= arity) return fn(...args);
|
|
630
|
+
return (...moreArgs) => curried(...args, ...moreArgs);
|
|
631
|
+
};
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
/** Delay execution by ms */
|
|
635
|
+
delay(fn, ms, ...args) {
|
|
636
|
+
return new Promise(resolve => {
|
|
637
|
+
setTimeout(() => resolve(fn(...args)), ms);
|
|
638
|
+
});
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
/** Retry a function with exponential backoff */
|
|
642
|
+
async retry(fn, options = {}) {
|
|
643
|
+
const { attempts = 3, delay: d = 1000, backoff = 2 } = options;
|
|
644
|
+
let lastErr;
|
|
645
|
+
for (let i = 0; i < attempts; i++) {
|
|
646
|
+
try {
|
|
647
|
+
return await fn(i);
|
|
648
|
+
} catch (err) {
|
|
649
|
+
lastErr = err;
|
|
650
|
+
if (i < attempts - 1) {
|
|
651
|
+
await new Promise(r => setTimeout(r, d * Math.pow(backoff, i)));
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
throw lastErr;
|
|
656
|
+
},
|
|
657
|
+
|
|
658
|
+
// ==========================================
|
|
659
|
+
// TYPE CHECKS
|
|
660
|
+
// ==========================================
|
|
661
|
+
|
|
662
|
+
isString(v) { return typeof v === 'string'; },
|
|
663
|
+
isNumber(v) { return typeof v === 'number' && !isNaN(v); },
|
|
664
|
+
isBoolean(v) { return typeof v === 'boolean'; },
|
|
665
|
+
isArray(v) { return Array.isArray(v); },
|
|
666
|
+
isObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); },
|
|
667
|
+
isFunction(v) { return typeof v === 'function'; },
|
|
668
|
+
isNull(v) { return v === null; },
|
|
669
|
+
isUndefined(v) { return v === undefined; },
|
|
670
|
+
isNil(v) { return v === null || v === undefined; },
|
|
671
|
+
isDate(v) { return v instanceof Date && !isNaN(v); },
|
|
672
|
+
isRegExp(v) { return v instanceof RegExp; },
|
|
673
|
+
isMap(v) { return v instanceof Map; },
|
|
674
|
+
isSet(v) { return v instanceof Set; },
|
|
675
|
+
isSymbol(v) { return typeof v === 'symbol'; },
|
|
676
|
+
isPromise(v) { return v && typeof v.then === 'function'; },
|
|
677
|
+
isInteger(v) { return Number.isInteger(v); },
|
|
678
|
+
isFinite(v) { return Number.isFinite(v); },
|
|
679
|
+
isPlainObject(v) {
|
|
680
|
+
if (!v || typeof v !== 'object') return false;
|
|
681
|
+
const proto = Object.getPrototypeOf(v);
|
|
682
|
+
return proto === Object.prototype || proto === null;
|
|
683
|
+
},
|
|
684
|
+
|
|
685
|
+
// ==========================================
|
|
686
|
+
// STRING / CSS UTILITIES
|
|
687
|
+
// ==========================================
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* classNames — like clsx/classnames. Combines class names conditionally.
|
|
691
|
+
*
|
|
692
|
+
* @example
|
|
693
|
+
* _.classNames('btn', { active: true, disabled: false }, 'primary');
|
|
694
|
+
* // => 'btn active primary'
|
|
695
|
+
*
|
|
696
|
+
* _.classNames(['flex', condition && 'hidden']);
|
|
697
|
+
* // => 'flex hidden' (if condition is true)
|
|
698
|
+
*/
|
|
699
|
+
classNames(...args) {
|
|
700
|
+
const classes = [];
|
|
701
|
+
for (const arg of args) {
|
|
702
|
+
if (!arg) continue;
|
|
703
|
+
if (typeof arg === 'string') {
|
|
704
|
+
classes.push(arg);
|
|
705
|
+
} else if (Array.isArray(arg)) {
|
|
706
|
+
const inner = _.classNames(...arg);
|
|
707
|
+
if (inner) classes.push(inner);
|
|
708
|
+
} else if (typeof arg === 'object') {
|
|
709
|
+
for (const [key, val] of Object.entries(arg)) {
|
|
710
|
+
if (val) classes.push(key);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return classes.join(' ');
|
|
715
|
+
},
|
|
716
|
+
|
|
717
|
+
/** Alias for classNames */
|
|
718
|
+
clsx(...args) {
|
|
719
|
+
return _.classNames(...args);
|
|
720
|
+
},
|
|
721
|
+
|
|
722
|
+
// ==========================================
|
|
723
|
+
// MISC
|
|
724
|
+
// ==========================================
|
|
725
|
+
|
|
726
|
+
/** No-op function */
|
|
727
|
+
noop() {},
|
|
728
|
+
|
|
729
|
+
/** Identity function */
|
|
730
|
+
identity(v) { return v; },
|
|
731
|
+
|
|
732
|
+
/** Constant function */
|
|
733
|
+
constant(v) { return () => v; },
|
|
734
|
+
|
|
735
|
+
/** Generate unique ID (incrementing) */
|
|
736
|
+
_idCounter: 0,
|
|
737
|
+
uniqueId(prefix = '') {
|
|
738
|
+
_._idCounter++;
|
|
739
|
+
return `${prefix}${_._idCounter}`;
|
|
740
|
+
},
|
|
741
|
+
|
|
742
|
+
/** Safe JSON parse */
|
|
743
|
+
tryParse(str, defaultValue = null) {
|
|
744
|
+
try { return JSON.parse(str); } catch { return defaultValue; }
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
/** Convert object to query string */
|
|
748
|
+
toQueryString(obj) {
|
|
749
|
+
return Object.entries(obj)
|
|
750
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
751
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
752
|
+
.join('&');
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
/** Parse query string to object */
|
|
756
|
+
parseQueryString(str) {
|
|
757
|
+
const result = {};
|
|
758
|
+
const query = str.startsWith('?') ? str.slice(1) : str;
|
|
759
|
+
for (const pair of query.split('&')) {
|
|
760
|
+
const [key, value] = pair.split('=').map(decodeURIComponent);
|
|
761
|
+
if (key) result[key] = value || '';
|
|
762
|
+
}
|
|
763
|
+
return result;
|
|
764
|
+
},
|
|
765
|
+
|
|
766
|
+
/** Sleep / wait */
|
|
767
|
+
sleep(ms) {
|
|
768
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
769
|
+
},
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
module.exports = { _, Collection: _ };
|