topkat-utils 1.0.60 → 1.1.2

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.
Files changed (102) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/index.d.ts +34 -0
  3. package/dist/index.js +56 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/src/array-utils.d.ts +56 -0
  6. package/dist/src/array-utils.js +138 -0
  7. package/dist/src/array-utils.js.map +1 -0
  8. package/dist/src/date-utils.d.ts +100 -0
  9. package/dist/src/date-utils.js +357 -0
  10. package/dist/src/date-utils.js.map +1 -0
  11. package/dist/src/env-utils.d.ts +8 -0
  12. package/dist/src/env-utils.js +38 -0
  13. package/dist/src/env-utils.js.map +1 -0
  14. package/dist/src/error-utils.d.ts +8 -0
  15. package/dist/src/error-utils.js +99 -0
  16. package/dist/src/error-utils.js.map +1 -0
  17. package/dist/src/is-empty.d.ts +1 -0
  18. package/dist/src/is-empty.js +13 -0
  19. package/dist/src/is-empty.js.map +1 -0
  20. package/dist/src/is-object.d.ts +2 -0
  21. package/dist/src/is-object.js +7 -0
  22. package/dist/src/is-object.js.map +1 -0
  23. package/dist/src/isset.d.ts +1 -0
  24. package/dist/src/isset.js +8 -0
  25. package/dist/src/isset.js.map +1 -0
  26. package/dist/src/logger-utils.d.ts +76 -0
  27. package/dist/src/logger-utils.js +355 -0
  28. package/dist/src/logger-utils.js.map +1 -0
  29. package/dist/src/loop-utils.d.ts +37 -0
  30. package/dist/src/loop-utils.js +105 -0
  31. package/dist/src/loop-utils.js.map +1 -0
  32. package/dist/src/math-utils.d.ts +23 -0
  33. package/dist/src/math-utils.js +43 -0
  34. package/dist/src/math-utils.js.map +1 -0
  35. package/dist/src/mongo-utils.d.ts +11 -0
  36. package/dist/src/mongo-utils.js +49 -0
  37. package/dist/src/mongo-utils.js.map +1 -0
  38. package/dist/src/object-utils.d.ts +96 -0
  39. package/dist/src/object-utils.js +369 -0
  40. package/dist/src/object-utils.js.map +1 -0
  41. package/dist/src/private/config.d.ts +44 -0
  42. package/dist/src/private/config.js +55 -0
  43. package/dist/src/private/config.js.map +1 -0
  44. package/dist/src/private/error-handler.d.ts +10 -0
  45. package/dist/src/private/error-handler.js +18 -0
  46. package/dist/src/private/error-handler.js.map +1 -0
  47. package/dist/src/private/types.d.ts +4 -0
  48. package/dist/src/private/types.js +3 -0
  49. package/dist/src/private/types.js.map +1 -0
  50. package/dist/src/regexp-utils.d.ts +12 -0
  51. package/dist/src/regexp-utils.js +44 -0
  52. package/dist/src/regexp-utils.js.map +1 -0
  53. package/dist/src/remove-circular-json-stringify.d.ts +1 -0
  54. package/dist/src/remove-circular-json-stringify.js +20 -0
  55. package/dist/src/remove-circular-json-stringify.js.map +1 -0
  56. package/dist/src/string-utils.d.ts +77 -0
  57. package/dist/src/string-utils.js +209 -0
  58. package/dist/src/string-utils.js.map +1 -0
  59. package/dist/src/tests-utils.js +77 -0
  60. package/dist/src/tests-utils.js.map +1 -0
  61. package/dist/src/timer-utils.d.ts +16 -0
  62. package/dist/src/timer-utils.js +79 -0
  63. package/dist/src/timer-utils.js.map +1 -0
  64. package/dist/src/transaction-utils.d.ts +14 -0
  65. package/dist/src/transaction-utils.js +87 -0
  66. package/dist/src/transaction-utils.js.map +1 -0
  67. package/dist/src/validation-utils.d.ts +89 -0
  68. package/dist/src/validation-utils.js +192 -0
  69. package/dist/src/validation-utils.js.map +1 -0
  70. package/dist/src/wtf-utils.d.ts +7 -0
  71. package/dist/src/wtf-utils.js +83 -0
  72. package/dist/src/wtf-utils.js.map +1 -0
  73. package/index.ts +38 -0
  74. package/package.json +2 -2
  75. package/src/array-utils.ts +128 -0
  76. package/src/date-utils.ts +377 -0
  77. package/src/env-utils.ts +29 -0
  78. package/src/error-utils.ts +77 -0
  79. package/src/is-empty.ts +5 -0
  80. package/src/is-object.ts +3 -0
  81. package/src/isset.ts +3 -0
  82. package/src/logger-utils.ts +349 -0
  83. package/src/loop-utils.ts +101 -0
  84. package/src/math-utils.ts +38 -0
  85. package/src/mongo-utils.ts +38 -0
  86. package/src/object-utils.ts +356 -0
  87. package/src/private/config.ts +85 -0
  88. package/src/private/error-handler.ts +21 -0
  89. package/src/private/types.ts +6 -0
  90. package/src/regexp-utils.ts +37 -0
  91. package/src/remove-circular-json-stringify.ts +17 -0
  92. package/src/string-utils.ts +212 -0
  93. package/src/tests-utils.ts +70 -0
  94. package/src/timer-utils.ts +58 -0
  95. package/src/transaction-utils.ts +63 -0
  96. package/src/validation-utils.ts +253 -0
  97. package/src/wtf-utils.ts +88 -0
  98. package/tsconfig.json +11 -4
  99. package/utils.d.ts +0 -694
  100. package/utils.js +0 -2227
  101. package/utils.js.map +0 -1
  102. package/utils.ts +0 -2304
package/utils.ts DELETED
@@ -1,2304 +0,0 @@
1
-
2
-
3
- type Color = [number, number, number]
4
- type ObjectGeneric = { [k: string]: any }
5
-
6
- export type BaseTypes = 'objectId' | 'dateInt6' | 'dateInt' | 'dateInt8' | 'dateInt12' | 'time' | 'humanReadableTimestamp' | 'date' | 'dateObject' | 'array' | 'object' | 'buffer' | 'string' | 'function' | 'boolean' | 'number' | 'bigint' | 'year' | 'email' | 'any'
7
-
8
- /** Round with custom number of decimals (default:0) */
9
- export function round(number: number | string, decimals = 0) { return Math.round((typeof number === 'number' ? number : parseFloat(number)) * Math.pow(10, decimals)) / Math.pow(10, decimals); }
10
- /** Round with custom number of decimals (default:2) */
11
- export function round2(number: number | string, decimals = 2) { return round(number, decimals) }
12
-
13
- /** Is number between two numbers (including those numbers) */
14
- export function isBetween(number: number, min: number, max: number, inclusive = true) { return inclusive ? number <= max && number >= min : number < max && number > min; }
15
-
16
- /** Random number between two values with 0 decimals by default */
17
- export function random(nb1: number, nb2: number, nbOfDecimals = 0) { return round(Math.random() * (nb2 - nb1) + nb1, nbOfDecimals); }
18
-
19
- /** Sum all values of an array, all values MUST be numbers */
20
- export function sumArray(array: number[]) {
21
- return array.filter(item => typeof item === 'number').reduce((sum, val) => isset(val) ? val + sum : sum, 0);
22
- }
23
-
24
- /** Moyenne / average between array of values
25
- * @param {Number} round number of decimals to keep. Default:2
26
- */
27
- export function moyenne(array: number[], nbOfDecimals = 2) {
28
- return round(sumArray(array) / array.length, nbOfDecimals);
29
- }
30
-
31
- /** Clean output for outside world. All undefined / null / NaN / Infinity values are changed to '-' */
32
- export function cln(val, replacerInCaseItIsUndefinNaN = '-') { return ['undefined', undefined, 'indéfini', 'NaN', NaN, Infinity, null].includes(val) ? replacerInCaseItIsUndefinNaN : val; }
33
-
34
- /** length default 2, shortcut for 1 to 01 */
35
- export function pad(numberOrStr: number | string, length = 2) { return ('' + numberOrStr).padStart(length, '0'); }
36
-
37
- /** return the number or the closest number of the range
38
- * * nb min max => returns
39
- * * 7 5 10 => 7 // in the range
40
- * * 2 5 10 => 5 // below the min value
41
- * * 99 5 10 => 10// above the max value
42
- */
43
- export function minMax(nb: number, min: number, max: number) { return Math.max(min, Math.min(nb, max)); }
44
-
45
- export async function tryCatch(callback: Function, onErr: Function = () => { }) {
46
- try {
47
- return await callback()
48
- } catch (err) {
49
- return await onErr(err)
50
- }
51
- }
52
-
53
-
54
- let generatedTokens = []; // cache to avoid collision
55
- let lastTs = new Date().getTime();
56
- /** minLength 8 if unique
57
- * @param {Number} length default: 20
58
- * @param {Boolean} unique default: true. Generate a real unique token base on the date. min length will be min 8 in this case
59
- * @param {string} mode one of ['alphanumeric', 'hexadecimal']
60
- * NOTE: to generate a mongoDB Random Id, use the params: 24, true, 'hexadecimal'
61
- */
62
- export function generateToken(length = 20, unique = true, mode: 'alphanumeric' | 'hexadecimal' = 'alphanumeric') {
63
- let charConvNumeric = mode === 'alphanumeric' ? 36 : 16;
64
- if (unique && length < 8) length = 8;
65
- let token
66
- let tokenTs
67
- do {
68
- tokenTs = (new Date()).getTime()
69
- token = unique ? tokenTs.toString(charConvNumeric) : '';
70
- while (token.length < length) token += Math.random().toString(charConvNumeric).substr(2, 1); // char alphaNumeric aléatoire
71
- } while (generatedTokens.includes(token))
72
- if (lastTs < tokenTs) generatedTokens = [] // reset generated token on new timestamp because cannot collide
73
- generatedTokens.push(token)
74
- return token;
75
- }
76
-
77
- export function generateObjectId() {
78
- return generateToken(24, true, 'hexadecimal')
79
- }
80
-
81
-
82
- /** Useful to join differents bits of url with normalizing slashes
83
- * * urlPathJoin('https://', 'www.kikou.lol/', '/user', '//2//') => https://www.kikou.lol/user/2/
84
- * * urlPathJoin('http:/', 'kikou.lol') => https://www.kikou.lol
85
- */
86
- export function urlPathJoin(...bits: string[]) {
87
- return bits.join('/').replace(/\/+/g, '/').replace(/(https?:)\/\/?/, '$1//');
88
- }
89
-
90
- /** path shall always be sorted before using in express
91
- * to avoid a generic route like /* to catch a specific one like /bonjour
92
- *
93
- * @param {Object[]} urlObjOrArr
94
- * @param {String} propInObjectOrIndexInArray
95
- * @return {Array} urls modified
96
- */
97
- export function sortUrlsByDeepnessInArrayOrObject(urlObjOrArr, propInObjectOrIndexInArray) {
98
- // SORTING BY
99
- // Deepness => /user vs /user/contract
100
- // simply count the / occurence to sort deepest path first to avoid concurrence or routes mislead
101
- const deepness = route => (route.match(/\//g) || []).length;
102
- // AND params deepness
103
- // avoid `/user/:param` to take precedence over `/user/field` as `user/fields` will be catched by the first route
104
- // Eg: /user/:rr/name/:id => 14pts
105
- // /user/:id/:name/id => 15pts take precedence
106
- const paramPrecedencePts = route => route.split('/').reduce((pts, routeChunk, i) => /^:/.test(routeChunk) ? pts + (10 - i) : pts, 0);
107
-
108
- return urlObjOrArr.sort((a, b) => {
109
- const aUrl = a[propInObjectOrIndexInArray] || a;
110
- const bUrl = b[propInObjectOrIndexInArray] || b;
111
- if (!aUrl) return -1;
112
- if (!bUrl) return 1;
113
- return deepness(bUrl) - deepness(aUrl) || // /a/b vs /a
114
- paramPrecedencePts(aUrl) - paramPrecedencePts(bUrl) || // /:dynamicparam vs /param
115
- bUrl.length - aUrl.length; // help separating / vs /blah
116
- });
117
- }
118
-
119
-
120
- export type MiniTemplaterOptions = {
121
- valueWhenNotSet?: string
122
- regexp?: RegExp
123
- valueWhenContentUndefined?: string
124
- }
125
- /** Replace variables in a string like: `Hello {{userName}}!`
126
- * @param {String} content
127
- * @param {Object} varz object with key => value === toReplace => replacer
128
- * @param {Object} options
129
- * * valueWhenNotSet => replacer for undefined values. Default: ''
130
- * * regexp => must be 'g' and first capturing group matching the value to replace. Default: /{{\s*([^}]*)\s*}}/g
131
- */
132
- export function miniTemplater(content: string, varz: ObjectGeneric, options: MiniTemplaterOptions = {}): string {
133
- options = {
134
- valueWhenNotSet: '',
135
- regexp: /{{\s*([^}]*)\s*}}/g,
136
- valueWhenContentUndefined: '',
137
- ...options,
138
- };
139
- return isset(content) ? content.replace(options.regexp, (m, $1) => isset(varz[$1]) ? varz[$1] : options.valueWhenNotSet) : options.valueWhenContentUndefined
140
- }
141
-
142
- /**
143
- *
144
- * @param {Object} object main object
145
- * @param {String[]} maskedOrSelectedFields array of fields
146
- * @param {Boolean} isMask default: true; determine the behavior of the function. If is mask, selected fields will not appear in the resulting object. If it's a select, only selected fields will appear.
147
- * @param {Boolean} deleteKeysInsteadOfReturningAnewObject default:false; modify the existing object instead of creating a new instance
148
- */
149
- export function simpleObjectMaskOrSelect(object: ObjectGeneric, maskedOrSelectedFields: string[], isMask = true, deleteKeysInsteadOfReturningAnewObject = false) {
150
- const allKeys = Object.keys(object);
151
- const keysToMask = allKeys.filter(keyName => {
152
- if (isMask) return maskedOrSelectedFields.includes(keyName);
153
- else return !maskedOrSelectedFields.includes(keyName);
154
- });
155
- if (deleteKeysInsteadOfReturningAnewObject) {
156
- keysToMask.forEach(keyNameToDelete => delete object[keyNameToDelete]);
157
- return object;
158
- } else {
159
- return allKeys.reduce((newObject, key) => {
160
- if (!keysToMask.includes(key)) newObject[key] = object[key];
161
- return newObject;
162
- }, {});
163
- }
164
- }
165
-
166
- /** Parse one dimention object undefined, true, false, null represented as string will be converted to primitives */
167
- export function parseEnv(env) {
168
- const newEnv = {};
169
- for (const k in env) {
170
- const val = env[k];
171
- if (val === 'undefined') newEnv[k] = undefined;
172
- else if (val === 'true') newEnv[k] = true;
173
- else if (val === 'false') newEnv[k] = false;
174
- else if (val === 'null') newEnv[k] = null;
175
- else newEnv[k] = env[k];
176
- }
177
- return newEnv;
178
- }
179
-
180
- /** READ ONLY, output a parsed version of process.env
181
- * use it like ENV().myVar
182
- */
183
- export function ENV(): { [key: string]: any } {
184
- const throwErr = () => { throw new Error('Please use process.env to write to env'); };
185
- return new Proxy(parseEnv(process.env), {
186
- set: throwErr,
187
- defineProperty: throwErr,
188
- deleteProperty: throwErr,
189
- });
190
- }
191
-
192
- /**
193
- * @param {any} mayBeAstring
194
- * @return !!value
195
- */
196
- export function parseBool(mayBeAstring: string | boolean | number): boolean {
197
- if (typeof mayBeAstring === 'boolean') return mayBeAstring;
198
- else return mayBeAstring === 'true' ? true : mayBeAstring === 'false' ? false : !!mayBeAstring;
199
- }
200
-
201
-
202
- export function dim(str = '') {
203
- return configFn().terminal.noColor ? str : `\x1b[2m${str.toString().split('\n').join('\x1b[0m\n\x1b[2m')}\x1b[0m`;
204
- }
205
-
206
- export type Config = {
207
- env: string;
208
- isProd: boolean;
209
- nbOfLogsToKeep: number
210
- customTypes: object,
211
- preprocessLog?: Function,
212
- terminal: {
213
- noColor: boolean;
214
- theme: {
215
- primary: Color, // blue theme
216
- shade1: Color,
217
- shade2: Color,
218
- bgColor?: Color;
219
- paddingX: number;
220
- paddingY: number;
221
- fontColor?: Color;
222
- pageWidth: number;
223
- debugModeColor: Color,
224
- }
225
- },
226
- };
227
-
228
- let config: Config = {
229
- env: 'development',
230
- isProd: false,
231
- nbOfLogsToKeep: 25,
232
- customTypes: {},
233
- terminal: {
234
- noColor: false,
235
- theme: {
236
- primary: [0, 149, 250], // blue theme
237
- shade1: [0, 90, 250],
238
- shade2: [0, 208, 245],
239
- paddingX: 2,
240
- paddingY: 2,
241
- pageWidth: 53,
242
- debugModeColor: [201, 27, 169],
243
- }
244
- },
245
- };
246
-
247
- /** Allow dynamic changing of config */
248
- export function configFn() { return config; }
249
-
250
-
251
- /** Register custom config
252
- * @param {object} customConfig { 'email': email => /.+@.+/.test(email), type2 : myTestFunction() }
253
- * * env: 'development',
254
- * * customTypes: {},
255
- * * terminal: {
256
- * * noColor: false, // disable colored escape sequences like /mOO35...etc
257
- * * theme: {
258
- * * primary: [61, 167, 32], // main color (title font)
259
- * * shade1: [127, 192, 39], // gradient shade 1
260
- * * shade2: [194, 218, 47], // gradient shade 2
261
- * * bgColor: false, // background color
262
- * * paddingX: 2, // nb spaces added before an outputted str
263
- * * paddingY: 2, //
264
- * * fontColor: false, // default font color
265
- * * pageWidth: 53, // page size in character
266
- * * debugModeColor: [147, 212, 6], // usually orange
267
- * * }
268
- * * },
269
- */
270
- export function registerConfig(customConfig) {
271
- if (!isset(customConfig.terminal)) customConfig.terminal = {};
272
- const newconfig = {
273
- ...config,
274
- ...customConfig
275
- };
276
- newconfig.terminal = {
277
- ...config.terminal,
278
- ...customConfig.terminal
279
- };
280
- newconfig.terminal.theme = {
281
- ...config.terminal.theme,
282
- ...(customConfig.terminal.theme || {})
283
- };
284
- config = newconfig;
285
-
286
- config.isProd = config.env.includes('prod');
287
- }
288
-
289
- //----------------------------------------
290
- // ERROR HANDLER
291
- //----------------------------------------
292
-
293
- class dataValidationUtilErrorHandler extends Error {
294
- fromDataValidation: boolean
295
- code: number
296
- extraInfos: object
297
- msg: string
298
- errorDescription: { [k: string]: any }
299
- constructor(msg, code, extraInfos?) {
300
- super(msg);
301
- this.message = msg;
302
- this.msg = msg;
303
- this.name = `${code} ${msg}`;
304
- this.fromDataValidation = true; // will be catched by express error handler
305
- this.code = code;
306
- this.extraInfos = extraInfos;
307
- this.errorDescription = {
308
- msg,
309
- code,
310
- ...extraInfos,
311
- }
312
- }
313
- }
314
-
315
- //----------------------------------------
316
- // Object utils shared
317
- //----------------------------------------
318
-
319
- /**
320
- * check if **object OR array** has property Safely (avoid cannot read property x of null and such)
321
- * @param {Object} obj object to test against
322
- * @param {string} addr `a.b.c.0.1` will test if myObject has props a that has prop b. Work wit arrays as well (like `arr.0`)
323
- */
324
- export function has(obj: ObjectGeneric, addr: string) {
325
- if (!isset(obj) || typeof obj !== 'object') return;
326
- let propsArr = addr.replace(/\.?\[(\d+)\]/g, '.$1').split('.'); // replace a[3] => a.3;
327
- let objChain = obj;
328
- return propsArr.every(prop => {
329
- objChain = objChain[prop];
330
- return isset(objChain);
331
- });
332
- }
333
-
334
- /** Find address in an object "a.b.c" IN { a : { b : {c : 'blah' }}} RETURNS 'blah'
335
- * @param obj
336
- * @param addr accept syntax like "obj.subItem.[0].sub2" OR "obj.subItem.0.sub2" OR "obj.subItem[0].sub2"
337
- * @returns the last item of the chain OR undefined if not found
338
- */
339
- export function findByAddress(obj: ObjectGeneric, addr: string): any | undefined {
340
- if (addr === '') return obj;
341
- if (!isset(obj) || typeof obj !== 'object') return console.warn('Main object in `findByAddress` function is undefined or has the wrong type');
342
- const propsArr = addr.replace(/\.?\[(\d+)\]/g, '.$1').split('.'); // replace .[4] AND [4] TO .4
343
- const objRef = propsArr.reduce((objChain, prop) => {
344
- if (!isset(objChain) || typeof objChain !== 'object' || !isset(objChain[prop])) return;
345
- else return objChain[prop];
346
- }, obj);
347
- return objRef
348
- }
349
-
350
-
351
- /** Will return all objects matching that path. Eg: user.*.myVar */
352
- export function findByAddressAll(obj, addr, returnAddresses?: true): Array<[string, any]>
353
- export function findByAddressAll(obj, addr, returnAddresses?: false): Array<any>
354
- export function findByAddressAll(obj, addr, returnAddresses = false) {
355
- err500IfNotSet({ obj, addr });
356
- if (addr === '') return obj;
357
- const addrRegexp = new RegExp('^' + addr
358
- .replace(/\.?\[(\d+)\]/g, '.$1') // replace .[4] AND [4] TO .4
359
- .replace(/\./g, '\\.')
360
- .replace(/\.\*/g, '.[^.]+') // replace * by [^. (all but a point)]
361
- + '$');
362
-
363
- const matchingItems = []
364
-
365
- recursiveGenericFunctionSync(obj, (item, address) => {
366
- if (addrRegexp.test(address)) matchingItems.push(returnAddresses ? [address, item] : item)
367
- })
368
- return matchingItems;
369
- }
370
-
371
- /** Enforce writing subItems. Eg: user.name.blah will ensure all are set until the writing of the last item
372
- * NOTE: doesn't work with arrays
373
- */
374
- export function objForceWrite(obj: ObjectGeneric, addr: string, item) {
375
- const chunks = addr.replace(/\.?\[(\d+)\]/g, '.[$1').split('.');
376
- let lastItem = obj;
377
- chunks.forEach((chunkRaw, i) => {
378
- const chunk = chunkRaw.replace(/^\[/, '')
379
- if (i === chunks.length - 1) lastItem[chunk] = item;
380
- else if (!isset(lastItem[chunk])) {
381
- const nextChunk = chunks[i + 1]
382
- if (isset(nextChunk) && nextChunk.startsWith('[')) lastItem[chunk] = [];
383
- else lastItem[chunk] = {};
384
- } else if (typeof lastItem[chunk] !== 'object') throw new dataValidationUtilErrorHandler(`itemNotTypeObjectOrArrayInAddrChainForObjForceWrite`, 500, { origin: 'Validator', chunks, actualValueOfItem: lastItem[chunk], actualChunk: chunk });
385
- lastItem = lastItem[chunk];
386
- });
387
- }
388
-
389
- /** Enforce writing subItems, only if obj.addr is empty.
390
- * Eg: user.name.blah will ensure all are set until the writing of the last item
391
- * if user.name.blah has a value it will not change it.
392
- * NOTE: doesn't work with arrays
393
- */
394
- export function objForceWriteIfNotSet(obj: ObjectGeneric, addr: string, item) {
395
- if (!isset(findByAddress(obj, addr))) return objForceWrite(obj, addr, item);
396
- }
397
-
398
- /** Merge mixins into class. Use it in the constructor like: mergeMixins(this, {myMixin: true}) */
399
- export function mergeMixins(that, ...mixins) {
400
- mixins.forEach(mixin => {
401
- for (const method in mixin) {
402
- that[method] = mixin[method];
403
- }
404
- });
405
- }
406
-
407
-
408
-
409
-
410
- /** If a string is provided, return it as array else return the value */
411
- export function strAsArray(arrOrStr) {
412
- return typeof arrOrStr === 'string' ? [arrOrStr] : arrOrStr;
413
- }
414
-
415
- /** If not an array provided, return the array with the value
416
- * /!\ NOTE /!\ In case the value is null or undefined, it will return that value
417
- */
418
- export function asArray<T extends any[] | any>(item: T): T extends undefined ? undefined : T extends any[] ? T : T[] {
419
- return ((typeof item === 'undefined' ? item : Array.isArray(item) ? item : [item]) as T extends undefined ? undefined : T extends any[] ? T : T[])
420
- }
421
-
422
- /** Array comparison
423
- * @return {object} { inCommon, notInB, notInA }
424
- */
425
- export function compareArrays(arrayA: any[], arrayB: any[], compare = (a, b) => a === b) {
426
- return {
427
- inCommon: getArrayInCommon(arrayA, arrayB, compare),
428
- notInB: getNotInArrayA(arrayB, arrayA, compare),
429
- notInA: getNotInArrayA(arrayA, arrayB, compare),
430
- };
431
- }
432
-
433
- /**
434
- * @return [] only elements that are both in arrayA and arrayB
435
- */
436
- export function getArrayInCommon(arrayA = [], arrayB = [], compare = (a, b) => a === b): any[] {
437
- if (!Array.isArray(arrayA) || !Array.isArray(arrayB)) return [];
438
- else return arrayA.filter(a => arrayB.some(b => compare(a, b)));
439
- }
440
-
441
- /**
442
- * @return [] only elements that are in arrayB and not in arrayA
443
- */
444
- export function getNotInArrayA(arrayA = [], arrayB = [], compare = (a, b) => a === b): any[] {
445
- if (!Array.isArray(arrayA) && Array.isArray(arrayB)) return arrayB;
446
- else if (!Array.isArray(arrayB)) return [];
447
- else return arrayB.filter(b => !arrayA.some(a => compare(a, b)));
448
- }
449
-
450
- /**
451
- * @return [] only elements that are in neither arrayA and arrayB
452
- */
453
- export function getArrayDiff(arrayA = [], arrayB = [], compare = (a, b) => a === b): any[] {
454
- return [...getNotInArrayA(arrayA, arrayB, compare), ...getNotInArrayA(arrayB, arrayA, compare)];
455
- }
456
-
457
- /** filter duplicate values in an array
458
- * @param {function} comparisonFn default:(a, b) => a === b. A function that shall return true if two values are considered equal
459
- * @return {array|function}
460
- */
461
- export function noDuplicateFilter(arr, comparisonFn = (a, b) => a === b): any[] {
462
- return arr.filter((a, i, arr) => arr.findIndex(b => comparisonFn(a, b)) === i);
463
- }
464
-
465
- /** Count number of occurence of item in array */
466
- export function arrayCount(item: any, arr: any[]): number {
467
- return arr.reduce((total, item2) => item === item2 ? total + 1 : total, 0)
468
- }
469
-
470
- /**
471
- * Sort an array in an object of subArrays, no duplicate.
472
- * @param {Array} array
473
- * @param {function} getFieldFromItem (itemOfArray) => field[String|Number]
474
- * tell me how you want to sort your Array
475
- */
476
- export function arrayToObjectSorted(array, getFieldFromItem) {
477
- const res = {};
478
-
479
- array.forEach(item => {
480
- objForceWriteIfNotSet(res, getFieldFromItem(item), []);
481
- res[getFieldFromItem(item)].push(item);
482
- });
483
-
484
- return res;
485
- }
486
-
487
- /**
488
- * @param {Function} comparisonFunction default: (itemToPush, itemAlreadyInArray) => itemToPush === itemAlreadyInArray; comparison function to consider the added item duplicate
489
- */
490
- export function pushIfNotExist(arrayToPushInto, valueOrArrayOfValuesToBePushed, comparisonFunction = (a, b) => a === b): any[] {
491
- const valuesToPush = asArray(valueOrArrayOfValuesToBePushed).filter(a => !arrayToPushInto.some(b => comparisonFunction(a, b)));
492
- arrayToPushInto.push(...valuesToPush);
493
- return arrayToPushInto;
494
- }
495
-
496
- export function isNotEmptyArray(arr): boolean {
497
- return Array.isArray(arr) && !!arr.length;
498
- }
499
-
500
- export function randomItemInArray<T>(array: T[]): T {
501
- return array[Math.floor(Math.random() * array.length)];
502
- }
503
-
504
-
505
-
506
- export function cloneObject(o) {
507
- return JSON.parse(JSON.stringify(o));
508
- }
509
-
510
- /** Deep clone. WILL REMOVE circular references */
511
- export function deepClone<T>(obj: T, cache = []): T {
512
-
513
- let copy;
514
- // usefull to not modify 1st level objet by lower levels
515
- // this is required for the same object to be referenced not in a redundant way
516
- const newCache = [...cache];
517
- if (obj instanceof Date) return new Date(obj) as any
518
-
519
- // Handle Array
520
- if (Array.isArray(obj)) {
521
- if (newCache.includes(obj)) return [] as any
522
- newCache.push(obj);
523
- copy = [];
524
- for (var i = 0, len = obj.length; i < len; i++) {
525
- copy[i] = deepClone(obj[i], newCache);
526
- }
527
- return copy;
528
- }
529
-
530
- if (typeof obj === 'object' && obj !== null && Object.getPrototypeOf(obj) === Object.prototype) {
531
- if (newCache.includes(obj)) return {} as any
532
- newCache.push(obj);
533
- copy = {};
534
- for (var key in obj) {
535
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
536
- copy[key] = deepClone(obj[key], newCache);
537
- }
538
- }
539
- return copy;
540
- }
541
-
542
- return obj; // number, string...
543
- }
544
-
545
- /** test if object but not array and not null (null is an object in Js) */
546
- export function isObject(o: any): boolean { return o instanceof Object && [Object, Error].includes(o.constructor); }
547
-
548
- /** object and array merge
549
- * @warn /!\ Array will be merged and duplicate values will be deleted /!\
550
- * @return {Object} new object result from merge
551
- * NOTE: objects in params will NOT be modified*/
552
- export function mergeDeep(...objects) {
553
- return mergeDeepConfigurable(
554
- (previousVal, currentVal) => [...previousVal, ...currentVal].filter((elm, i, arr) => arr.indexOf(elm) === i),
555
- (previousVal, currentVal) => mergeDeep(previousVal, currentVal),
556
- undefined,
557
- ...objects
558
- );
559
- }
560
-
561
- /** object and array merge
562
- * @warn /!\ Array will be replaced by the latest object /!\
563
- * @return {Object} new object result from merge
564
- * NOTE: objects in params will NOT be modified */
565
- export function mergeDeepOverrideArrays(...objects) {
566
- return mergeDeepConfigurable(
567
- undefined,
568
- (previousVal, currentVal) => mergeDeepOverrideArrays(previousVal, currentVal),
569
- undefined,
570
- ...objects
571
- );
572
- }
573
-
574
- /** object and array merge
575
- * @param {Function} replacerForArrays item[key] = (prevValue, currentVal) => () When 2 values are arrays,
576
- * @param {Function} replacerForObjects item[key] = (prevValue, currentVal) => () When 2 values are objects,
577
- * @param {Function} replacerDefault item[key] = (prevValue, currentVal) => () For all other values
578
- * @param {...Object} objects
579
- * @return {Object} new object result from merge
580
- * NOTE: objects in params will NOT be modified
581
- */
582
- export function mergeDeepConfigurable(replacerForArrays = (prev, curr) => curr, replacerForObjects, replacerDefault = (prev, curr) => curr, ...objects) {
583
- return objects.reduce((actuallyMerged, obj) => {
584
- Object.keys(obj).forEach(key => {
585
- const previousVal = actuallyMerged[key];
586
- const currentVal = obj[key];
587
-
588
- if (Array.isArray(previousVal) && Array.isArray(currentVal)) {
589
- actuallyMerged[key] = replacerForArrays(previousVal, currentVal);
590
- } else if (isObject(previousVal) && isObject(currentVal)) {
591
- actuallyMerged[key] = replacerForObjects(previousVal, currentVal);
592
- } else {
593
- actuallyMerged[key] = replacerDefault(previousVal, currentVal);
594
- }
595
- });
596
-
597
- return actuallyMerged;
598
- }, {});
599
- }
600
-
601
- /** { a: {b:2}} => {'a.b':2} useful for translations
602
- * NOTE: will remove circular references
603
- */
604
- export function flattenObject(data, config: { withoutArraySyntax?: boolean, withArraySyntaxMinified?: boolean } = {}) {
605
- const { withoutArraySyntax = false, withArraySyntaxMinified = false } = config
606
- const result = {};
607
- const seenObjects = []; // avoidCircular reference to infinite loop
608
- const recurse = (cur, prop) => {
609
- if (Array.isArray(cur)) {
610
- let l = cur.length
611
- let i = 0;
612
- if (withoutArraySyntax) recurse(cur[0], prop)
613
- else {
614
- for (; i < l; i++) recurse(cur[i], prop + (withArraySyntaxMinified ? `.${i}` : `[${i}]`))
615
- if (l == 0) result[prop] = [];
616
- }
617
- } else if (isObject(cur)) { // is object
618
- try {
619
- if (seenObjects.includes(cur)) cur = deepClone(cur); // avoid circular ref but allow duplicate objects
620
- else seenObjects.push(cur);
621
-
622
- const isEmpty = Object.keys(cur).length === 0;
623
-
624
- for (const p in cur) recurse(cur[p], (prop ? prop + '.' : '') + p.replace(/\./g, '%')); // allow prop to contain special chars like points);
625
-
626
- if (isEmpty && prop) result[prop] = {};
627
- } catch (error) {
628
- C.warning('Circular reference in flattenObject, impossible to parse');
629
- }
630
- } else result[prop] = cur;
631
- };
632
- recurse(data, '');
633
- return result;
634
- }
635
-
636
- /** {'a.b':2} => { a: {b:2}} */
637
- export function unflattenObject(data) {
638
- const newO = {}
639
- for (const [addr, value] of Object.entries(data)) objForceWrite(newO, addr, value)
640
- return newO;
641
- }
642
-
643
- export type RecursiveCallback = (item: any, addr: string, lastElementKey: string, parent: ObjectGeneric | any[]) => false | any
644
- /**
645
- * @param {any} item the first array or object or whatever you want to recursively browse
646
- * @param {function} callback the callback you want to apply on items including the main one
647
- * * this callback has 2 arguments: (item, address) =>
648
- * * `item` => the actual item
649
- * * `addr` => the address of the item, not including root (Eg: subItem1.sub2.[3].[2]) array indexes are juste written as numbers
650
- * * `lastElementKey` => the key of last item. May be a number if last item is an array
651
- * * `parent` => reference the parent object as this is the only way of reassigning a value for the item. Eg: parent[lastElementKey] = myNewItem
652
- * * **NOTE** => if a key of an item contains dots, they will be replaced by '%' in `addr`
653
- * * **NOTE2** => if false is returned by the callback it will stop all other iterations (but not in an array)
654
- * @param {string} addr$ optional, the base address for the callback function
655
- * @param lastElementKey technical field
656
- * NOTE: will remove circular references
657
- * /!\ check return values
658
- */
659
- export async function recursiveGenericFunction(item: ObjectGeneric | any[], callback: RecursiveCallback, addr$ = '', lastElementKey = '', parent?, techFieldToAvoidCircularDependency = []) {
660
- err500IfNotSet({ callback });
661
-
662
- if (!techFieldToAvoidCircularDependency.includes(item)) {
663
- const result = addr$ === '' ? true : await callback(item, addr$, lastElementKey, parent);
664
-
665
- if (result !== false) {
666
- const addr = addr$ ? addr$ + '.' : '';
667
- if (isType(item, 'array')) {
668
- techFieldToAvoidCircularDependency.push(item);
669
- await Promise.all(item.map(
670
- (e, i) => recursiveGenericFunction(e, callback, addr + '[' + i + ']', i, item, techFieldToAvoidCircularDependency)
671
- ));
672
- } else if (isObject(item)) {
673
- techFieldToAvoidCircularDependency.push(item);
674
- await Promise.all(Object.entries(item).map(
675
- ([key, val]) => recursiveGenericFunction(val, callback, addr + key.replace(/\./g, '%'), key, item, techFieldToAvoidCircularDependency)
676
- ));
677
- }
678
- }
679
- }
680
- return item;
681
- }
682
-
683
- /**
684
- * @param {any} item the first array or object or whatever you want to recursively browse
685
- * @param {function} callback the callback you want to apply on items including the main one
686
- * * this callback has 2 arguments: (item, address) =>
687
- * * `item` => the actual item
688
- * * `addr` => the address of the item, not including root (Eg: subItem1.sub2.[3].[2]) array indexes are juste written as numbers
689
- * * `lastElementKey` => the key of last item. May be a number if last item is an array
690
- * * `parent` => reference the parent object as this is the only way of reassigning a value for the item. Eg: parent[lastElementKey] = myNewItem
691
- * * **NOTE** => if a key of an item contains dots, they will be replaced by '%' in `addr`
692
- * * **NOTE2** => if false is returned by the callback it will stop all other iterations (but not in an array)
693
- * * **NOTE3** => to reassign a key use => parent[lastElementKey] = myNewItem
694
- * @param {string} addr$ optional, the base address for the callback function
695
- * @param lastElementKey technical field
696
- * NOTE: will remove circular references
697
- * /!\ check return values
698
- */
699
- export function recursiveGenericFunctionSync(item: ObjectGeneric | any[], callback: RecursiveCallback, addr$ = '', lastElementKey = '', parent?, techFieldToAvoidCircularDependency = []) {
700
- err500IfNotSet({ callback });
701
-
702
- if (!techFieldToAvoidCircularDependency.includes(item)) {
703
- const result = addr$ === '' ? true : callback(item, addr$, lastElementKey, parent);
704
-
705
- if (result !== false) {
706
- const addr = addr$ ? addr$ + '.' : '';
707
- if (isType(item, 'array')) {
708
- techFieldToAvoidCircularDependency.push(item); // do not up one level
709
- item.forEach((e, i) => recursiveGenericFunctionSync(e, callback, addr + '[' + i + ']', i, item, techFieldToAvoidCircularDependency));
710
- } else if (isObject(item)) {
711
- techFieldToAvoidCircularDependency.push(item);
712
- Object.entries(item).forEach(([key, val]) => recursiveGenericFunctionSync(val, callback, addr + key.replace(/\./g, '%'), key, item, techFieldToAvoidCircularDependency));
713
- }
714
- }
715
- }
716
- return item;
717
- }
718
-
719
- /** Remove all key/values pair if value is undefined */
720
- export function objFilterUndefined(o) {
721
- Object.keys(o).forEach(k => !isset(o[k]) && delete o[k]);
722
- return o;
723
- }
724
-
725
- /** Lock all 1st level props of an object to read only */
726
- export function readOnly(o) {
727
- const throwErr = () => { throw new dataValidationUtilErrorHandler('Cannot modify object that is read only', 500); };
728
- return new Proxy(o, {
729
- set: throwErr,
730
- defineProperty: throwErr,
731
- deleteProperty: throwErr,
732
- });
733
- }
734
-
735
- /** Fields of the object can be created BUT NOT reassignated */
736
- export function reassignForbidden(o) {
737
- return new Proxy(o, {
738
- defineProperty: function (that, key, value) {
739
- if (key in that) throw new dataValidationUtilErrorHandler(`Cannot reassign the property ${key.toString()} of this object`, 500);
740
- else {
741
- that[key] = value
742
- return true
743
- }
744
- },
745
- deleteProperty: function (_, key) {
746
- throw new dataValidationUtilErrorHandler(`Cannot delete the property ${key.toString()} of this object`, 500);
747
- }
748
- });
749
- }
750
-
751
- /** All fileds and subFields of the object will become readOnly */
752
- export function readOnlyForAll(object) {
753
- recursiveGenericFunctionSync(object, (item, _, lastElementKey, parent) => {
754
- if (typeof item === 'object') parent[lastElementKey] = readOnly(item);
755
- });
756
- return object;
757
- }
758
-
759
- export function objFilterUndefinedRecursive(obj) {
760
- if (obj) {
761
- const flattenedObj = flattenObject(obj);
762
- Object.keys(flattenedObj).forEach(key => {
763
- if (!isset(flattenedObj[key])) {
764
- delete flattenedObj[key];
765
- }
766
- });
767
- return unflattenObject(flattenedObj);
768
- } else return obj;
769
- }
770
-
771
- export function sortObjKeyAccordingToValue(unorderedObj, ascending = true) {
772
- const orderedObj = {};
773
- const sortingConst = ascending ? 1 : -1;
774
- Object.keys(unorderedObj)
775
- .sort((keyA, keyB) => unorderedObj[keyA] < unorderedObj[keyB] ? sortingConst : -sortingConst)
776
- .forEach(key => { orderedObj[key] = unorderedObj[key]; });
777
- return orderedObj;
778
- }
779
-
780
- /**
781
- * Make default value if object key do not exist
782
- * @param {object} obj
783
- * @param {string} addr
784
- * @param {any} defaultValue
785
- * @param {function} callback (obj[addr]) => processValue. Eg: myObjAddr => myObjAddr.push('bikou')
786
- * @return obj[addr] eventually processed by the callback
787
- */
788
- export function ensureObjectProp(obj: object, addr: string, defaultValue, callback = o => o) {
789
- err500IfNotSet({ obj, addr, defaultValue, callback });
790
- if (!isset(obj[addr])) obj[addr] = defaultValue;
791
- callback(obj[addr]);
792
- return obj[addr];
793
- }
794
-
795
- /**
796
- * Maye sure obj[addr] is an array and push a value to it
797
- * @param {Object} obj parent object
798
- * @param {String} addr field name in parent
799
- * @param {Any} valToPush
800
- * @param {Boolean} onlyUniqueValues default:false; may be true or a comparision function; (a,b) => return true if they are the same like (a, b) => a.name === b.name
801
- * @return obj[addr] eventually processed by the callback
802
- */
803
- export function ensureIsArrayAndPush(obj: object, addr: string, valToPush, onlyUniqueValues: Function) {
804
- return ensureObjectProp(obj, addr, [], objValue => {
805
- if (isset(onlyUniqueValues)) {
806
- let duplicateFound = false;
807
- if (typeof onlyUniqueValues === 'function') duplicateFound = objValue.some(a => onlyUniqueValues(a, valToPush));
808
- else duplicateFound = objValue.includes(valToPush);
809
- if (!duplicateFound) objValue.push(valToPush);
810
- } else objValue.push(valToPush);
811
- });
812
- }
813
-
814
- /**
815
- * @param {Object} obj the object on which we want to filter the keys
816
- * @param {function} filterFunc function that returns true if the key match the wanted criteria
817
- */
818
- export function filterKeys(obj: object, filter) {
819
- const clone = cloneObject(obj)
820
- recursiveGenericFunctionSync(obj, (item, addr, lastElementKey) => {
821
- if (!filter(lastElementKey)) deleteByAddress(clone, addr.split('.'))
822
- });
823
- return clone
824
- }
825
- /**
826
- * @param {Object} obj the object on which we want to delete a property
827
- * @param {Array} addr addressArray on which to delete the property
828
- */
829
- export function deleteByAddress(obj: object, addr: string[]) {
830
- let current = obj
831
- for (let i = 0; i < addr.length - 2; i++) current = current[addr[i]]
832
- delete current[addr[addr.length - 1]]
833
- }
834
-
835
- /** @return undefined if cannot find _id */
836
- export function getId(obj: any = {}): string {
837
- if (!obj) return; // null case
838
- if (obj._id) return obj._id.toString();
839
- else if (isType(obj, 'objectId')) return obj.toString();
840
- }
841
-
842
- export function forI<T extends any[] | any>(nbIterations: number, callback: (number: number, previousValue, arrayOfPreviousValues: any[]) => T): T[] {
843
- const results = []
844
- for (let i = 0; i < nbIterations; i++) {
845
- const prevValue = results[results.length - 1]
846
- results.push(callback(i, prevValue, results))
847
- }
848
- return results
849
- }
850
-
851
- export async function forIasync<T extends any[] | any>(nbIterations: number, callback: (number) => T): Promise<T[]> {
852
- const results = []
853
- for (let i = 0; i < nbIterations; i++) {
854
- results.push(await callback(i))
855
- }
856
- return results
857
- }
858
-
859
-
860
-
861
- export function cleanStackTrace(stack) {
862
- if (typeof stack !== 'string') return '';
863
- stack.replace(/home\/[^/]+\/[^/]+\//g, '');
864
- const lines = stack.split('\n');
865
- const removeIfContain = [
866
- 'logger-utils.js',
867
- 'TCP.onread',
868
- 'readableAddChunk',
869
- 'Socket.EventEmitter.emit (domain.js',
870
- 'Socket.emit (events.js',
871
- 'Connection.EventEmitter.emit (domain.js',
872
- 'Connection.emit (events.js',
873
- 'Socket.Readable.push (_stream_readable',
874
- 'model.Query',
875
- 'Object.promiseOrCallback',
876
- 'Connection.<anonymous>',
877
- 'process.topLevelDomainCallback',
878
- // internal
879
- 'internal/process',
880
- 'internal/timers',
881
- 'internal/modules',
882
- 'internal/main',
883
- 'DefaultError.throw',
884
- 'Object.throw',
885
- 'mongoose/lib/utils',
886
- 'at Array.forEach (<anonymous>)',
887
- ];
888
- const linesClean = lines
889
- .filter(l => !removeIfContain.some(text => l.includes(text)))
890
- .map((line, i) => {
891
- if (i === 0) return '';
892
- else {
893
- const [, start, fileName, end] = line.match(/(^.+\/)([^/]+:\d+:\d+)(.{0,3})/) || [];
894
- return fileName ? `\x1b[2m${start}\x1b[0m${fileName}\x1b[2m${end}\x1b[0m` : `\x1b[2m${line}\x1b[0m`;
895
- }
896
- })
897
- .join('\n');
898
- return linesClean;
899
- }
900
-
901
-
902
- export function isset(...elms) {
903
- return elms.every(elm => typeof elm !== 'undefined' && elm !== null);
904
- }
905
- export function removeCircularJSONstringify(object, indent = 2) {
906
- const getCircularReplacer = () => {
907
- const seen = new WeakSet();
908
- return (key, value) => {
909
- if (typeof value === 'object' && value !== null) {
910
- if (seen.has(value)) {
911
- return;
912
- }
913
- seen.add(value);
914
- }
915
- return value;
916
- };
917
- };
918
-
919
- return JSON.stringify(object, getCircularReplacer(), indent);
920
- }
921
-
922
-
923
- export function shuffleArray(array) {
924
- for (let i = array.length - 1; i > 0; i--) {
925
- const j = Math.floor(Math.random() * (i + 1));
926
- [array[i], array[j]] = [array[j], array[i]];
927
- }
928
- return array;
929
- }
930
-
931
-
932
- /**Eg: camelCase */
933
- export function camelCase(...wordBits) {
934
- return wordBits.filter(e => e).map((w, i) => i === 0 ? w : capitalize1st(w)).join('');
935
- }
936
-
937
- /**Eg: snake_case
938
- * trimmed but not lowerCased
939
- */
940
- export function snakeCase(...wordBits) {
941
- return wordBits.filter(e => e).map(w => w.trim()).join('_');
942
- }
943
- /**Eg: kebab-case
944
- * trimmed AND lowerCased
945
- * undefined, null... => ''
946
- */
947
- export function kebabCase(...wordBits) {
948
- return wordBits.filter(e => e).map(w => w.trim().toLowerCase()).join('-');
949
- }
950
- /**Eg: PascalCase undefined, null... => '' */
951
- export function pascalCase(...wordBits) {
952
- return wordBits.filter(e => e).map((w, i) => capitalize1st(w)).join('');
953
- }
954
-
955
- /**Eg: Titlecase undefined, null... => '' */
956
- export function titleCase(...wordBits) {
957
- return capitalize1st(wordBits.filter(e => e).map(w => w.trim()).join(''));
958
- }
959
-
960
- /**Eg: UPPERCASE undefined, null... => '' */
961
- export function upperCase(...wordBits) {
962
- return wordBits.filter(e => e).map(w => w.trim().toUpperCase()).join('');
963
- }
964
-
965
- /**Eg: lowercase undefined, null... => '' */
966
- export function lowerCase(...wordBits) {
967
- return wordBits.filter(e => e).map(w => w.trim().toLowerCase()).join('');
968
- }
969
-
970
- export function capitalize1st(str = '') { return str[0].toUpperCase() + str.slice(1); }
971
-
972
- export function camelCaseToWords(str) {
973
- return str ? str.trim().replace(/([A-Z])/g, '-$1').toLowerCase().split('-') : [];
974
- }
975
-
976
-
977
-
978
- export function escapeRegexp(str: string, config: { parseStarChar?: boolean } = {}): string {
979
- const { parseStarChar = false } = config
980
- if (parseStarChar) return str.replace(/[-[\]{}()+?.,\\^$|#\s]/g, '\\$&').replace(/\*/g, '.*')
981
- else return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
982
- }
983
-
984
- /** Get first match of the first capturing group of regexp
985
- * Eg: const basePath = firstMatch(apiFile, /basePath = '(.*?)'/); will get what is inside quotes
986
- */
987
- export function firstMatch(str: string, regExp: RegExp): string | undefined { return (str.match(regExp) || [])[1]; }
988
-
989
- /** Get all matches from regexp with g flag
990
- * Eg: [ [full, match1, m2], [f, m1, m2]... ]
991
- * NOTE: the G flag will be appended to regexp
992
- */
993
- export function allMatches(str: string, reg: RegExp): string[] {
994
- let i = 0;
995
- let matches;
996
- const arr = [];
997
- if (typeof str !== 'string') C.error('Not a string provided as first argument for allMatches()');
998
- else {
999
- reg = new RegExp(reg, 'g');
1000
- while ((matches = reg.exec(str))) {
1001
- arr.push(matches);
1002
- if (i++ > 99) {
1003
- C.error('error', 'Please provide the G flag in regexp for allMatches');
1004
- break;
1005
- }
1006
- }
1007
- }
1008
- return arr;
1009
- }
1010
-
1011
- /** GIVEN A STRING '{ blah;2}, ['nested,(what,ever)']' AND A SEPARATOR ",""
1012
- * This will return the content separated by first level of separators
1013
- * @return ["{ blah;2}", "['nested,(what,ever)']"]
1014
- */
1015
- export function getValuesBetweenSeparator(str: string, separator: string, removeTrailingSpaces = true) {
1016
- err500IfEmptyOrNotSet({ separator, str });
1017
- const { outer } = getValuesBetweenStrings(str, separator, undefined, undefined, undefined, removeTrailingSpaces);
1018
- return outer;
1019
- }
1020
-
1021
-
1022
- /** GIVEN A STRING "a: [ 'str', /[^]]/, '[aa]]]str', () => [ nestedArray ] ], b: ['arr']"
1023
- * @return matching: [ "'str', /[^]]/, '[aa]]]str', () => [ nestedArray ]", "'arr'" ], between: [ "a:", ", b: " ]
1024
- * @param str base string
1025
- * @param openingOrSeparator opening character OR separator if closing not set
1026
- * @param closing
1027
- * @param ignoreBetweenOpen default ['\'', '`', '"', '/'], when reaching an opening char, it will ignore all until it find the corresponding closing char
1028
- * @param ignoreBetweenClose default ['\'', '`', '"', '/'] list of corresponding closing chars
1029
- */
1030
- export function getValuesBetweenStrings(str: string, openingOrSeparator, closing, ignoreBetweenOpen = ['\'', '`', '"', '/'], ignoreBetweenClose = ['\'', '`', '"', '/'], removeTrailingSpaces = true) {
1031
- err500IfEmptyOrNotSet({ openingOrSeparator, str });
1032
-
1033
- str = str.replace(/<</g, '§§"').replace(/>>/g, '"§§');
1034
-
1035
- const arrayValues = [];
1036
- const betweenArray = [];
1037
- let level = 0;
1038
- let ignoreUntil: boolean | string = false;
1039
- let actualValue = '';
1040
- let precedingChar = '';
1041
-
1042
- let separatorMode = false;
1043
- if (!closing) separatorMode = true;
1044
-
1045
- const separator = separatorMode ? openingOrSeparator : false;
1046
- const openingChars = separatorMode ? ['(', '{', '['] : [openingOrSeparator];
1047
- const closingChars = separatorMode ? [')', '}', ']'] : [closing];
1048
-
1049
- const pushActualValue = () => {
1050
- if (level === 0) betweenArray.push(removeTrailingSpaces ? actualValue.replace(/(?:^\s+|\s+$)/g, '') : actualValue);
1051
- else arrayValues.push(removeTrailingSpaces ? actualValue.replace(/(?:^\s+|\s+$)/g, '') : actualValue);
1052
- actualValue = '';
1053
- };
1054
-
1055
- str.split('').forEach(char => {
1056
- // handle unwanted nested structure like characters in a strings that may be a unmatched closing / opening character
1057
- // Eg: {'azer}aze'}
1058
- if (ignoreUntil && char === ignoreUntil && precedingChar !== '\\') ignoreUntil = false;
1059
- else if (ignoreUntil && char !== ignoreUntil) true;
1060
- else if (ignoreBetweenOpen.includes(char)) {
1061
- const indexChar = ignoreBetweenOpen.findIndex(char2 => char2 === char);
1062
- ignoreUntil = ignoreBetweenClose[indexChar];
1063
- } else if (openingChars.includes(char)) {
1064
- // handle nested structures
1065
- if (!separatorMode && level === 0) pushActualValue();
1066
- level++;
1067
- if (!separatorMode) return;
1068
- } else if (closingChars.includes(char)) {
1069
- // handle nested structures
1070
- if (!separatorMode && level === 1) pushActualValue();
1071
- level--;
1072
- } else if (separatorMode && level === 0 && char === separator) {
1073
- // SEPARATOR MODE
1074
- pushActualValue();
1075
- return;
1076
- }
1077
- actualValue += char;
1078
- precedingChar = char;
1079
- });
1080
-
1081
- pushActualValue();
1082
-
1083
- const replaceValz = arr => arr.map(v => v.replace(/§§"/g, '<<').replace(/"§§/g, '>>')).filter(v => v);
1084
-
1085
- return { inner: replaceValz(arrayValues), outer: replaceValz(betweenArray) };
1086
- }
1087
-
1088
-
1089
-
1090
-
1091
-
1092
-
1093
-
1094
- export function issetOr(...elms) { return elms.some(elm => typeof elm !== 'undefined' && elm !== null); }
1095
-
1096
- export function isEmptyOrNotSet(...elms) { return elms.some(elm => !isset(elm) || isEmpty(elm)); }
1097
-
1098
- export function errIfNotSet(objOfVarNamesWithValues, additionalMessage) { return errXXXIfNotSet(422, false, objOfVarNamesWithValues); }
1099
-
1100
- export function err500IfNotSet(objOfVarNamesWithValues) { return errXXXIfNotSet(500, false, objOfVarNamesWithValues); }
1101
-
1102
- export function errIfEmptyOrNotSet(objOfVarNamesWithValues) { return errXXXIfNotSet(422, true, objOfVarNamesWithValues); }
1103
-
1104
- export function err500IfEmptyOrNotSet(objOfVarNamesWithValues) { return errXXXIfNotSet(500, true, objOfVarNamesWithValues); }
1105
-
1106
- export function errXXXIfNotSet(errCode, checkEmpty, objOfVarNamesWithValues) {
1107
- let missingVars = [];
1108
- for (let prop in objOfVarNamesWithValues) {
1109
- if (!isset(objOfVarNamesWithValues[prop]) || (checkEmpty && isEmpty(objOfVarNamesWithValues[prop]))) missingVars.push(prop);
1110
- }
1111
- if (missingVars.length) throw new dataValidationUtilErrorHandler(`requiredVariableEmptyOrNotSet`, errCode, { origin: 'Validator', varNames: missingVars.join(', ') });
1112
- }
1113
-
1114
-
1115
- export function isDateObject(variable) { return variable instanceof Date; }
1116
-
1117
- /** Check all values are set */
1118
- export function checkAllObjectValuesAreEmpty(o) { return Object.values(o).every(value => !isset(value)); }
1119
-
1120
- /** Throw an error in case data passed is not a valid ctx */
1121
- export function checkCtxIntegrity(ctx) {
1122
- if (!isset(ctx) || !isset(ctx.user)) throw new dataValidationUtilErrorHandler('ctxNotSet', 500);
1123
- }
1124
-
1125
- /**
1126
- ## VALIDATOR
1127
-
1128
- @name validator
1129
-
1130
- @description support multiple names, multiple values and multiple type check
1131
- @option if nameString ends by $ sign it is considered optional
1132
-
1133
- @function validator([Objects])
1134
- @return {error|true/false|testMode} depend on mode (see prop mode)
1135
- @param {} mode normal (default) | test (TODO) | boolean
1136
- @param {} name 'myName' || [{myVar1: 'blah, myvar2: myvar2}], support multiple names / values
1137
- @param {} value myVar,
1138
- @param {string} myVar myVar, instead of name / value
1139
- @param {array} in ['blah', 'otherPossibleValue', true], equal ONE OF THESE values
1140
- @param {any} eq exactly equal to in, both support string or array of values
1141
- @param {any} neq not in, both support string or array of values
1142
- @param {number} lte 3, less than or equal
1143
- @param {number} gte 1, greater or equal
1144
- @param {number} lt 3, less than
1145
- @param {number} gt 1, greater
1146
- @param {string|string[]} type
1147
- * possibleTypes: object, number, string, boolean, array, date, dateInt8, dateInt12, dateInt6, time, objectId (mongo), humanReadableTimestamp, buffer
1148
- * Notes: multiples value is an OR, /!\ Array is type 'array' and not 'object' like in real JS /!\
1149
- @param {regExp} regexp /regexp/, test against regexp
1150
- @param {number} minLength for string, array or number length
1151
- @param {number} maxLength
1152
- @param {number} length
1153
- @param {boolean} optional default false
1154
- @param {boolean} emptyAllowed default false (to use if must be set but can be empty)
1155
- @param {boolean} mustNotBeSet this one must not be set
1156
- @param {any} includes check if array or string includes value (like js .includes())
1157
-
1158
- @example
1159
- validator(
1160
- { myNumber : 3, type: 'number', gte: 1, lte: 3 }, // use the name directly as a param
1161
- { name: 'email', value: 'nameATsite.com', regexp: /[^\sAT]+AT[^\sAT]+\.[^\sAT]/},
1162
- { name: [{'blahVar': blahVarValue, 'myOtherVar': myOtherVarValue}], type: 'string'} // multiple names for same check
1163
- )
1164
- ----------------------------------------*/
1165
-
1166
- export type ValidatorObject = {
1167
- name?: string
1168
- value?: any
1169
- type?: BaseTypes
1170
- eq?: any
1171
- neq?: any
1172
- in?: any[]
1173
- lt?: number
1174
- gt?: number
1175
- lte?: number
1176
- gte?: number
1177
- length?: number
1178
- minLength?: number
1179
- maxLength?: number
1180
- emptyAllowed?: boolean
1181
- regexp?: RegExp
1182
- mustNotBeSet?: boolean
1183
- optional?: boolean
1184
- isArray?: boolean
1185
- [k: string]: any
1186
- }
1187
- export function validator(...paramsToValidate: ValidatorObject[]) {
1188
- const errArray = validatorReturnErrArray(...paramsToValidate);
1189
- if (errArray.length) throw new dataValidationUtilErrorHandler(...errArray);
1190
- }
1191
-
1192
- export const restTestMini = {
1193
- throwOnErr: false,
1194
- reset(throwOnErr = false) {
1195
- restTestMini.nbSuccess = 0
1196
- restTestMini.nbError = 0
1197
- restTestMini.lastErrors = []
1198
- restTestMini.throwOnErr = throwOnErr
1199
- },
1200
- newErr(err) {
1201
- restTestMini.nbError++
1202
- restTestMini.lastErrors.push(err)
1203
- if (restTestMini.throwOnErr) throw new Error(err)
1204
- else C.error(false, err)
1205
- },
1206
- printStats() {
1207
- // TODO print last errz
1208
- C.info(`ERRORS RESUME =========`)
1209
- if (restTestMini.lastErrors.length) C.log('\n\n\n')
1210
- for (const lastErr of restTestMini.lastErrors) C.error(false, lastErr)
1211
- C.log('\n\n\n')
1212
- C.info(`STATS =========`)
1213
- C.info(`Total: ${restTestMini.nbSuccess + restTestMini.nbError}`)
1214
- C.success(`Success: ${restTestMini.nbSuccess}`)
1215
- C.error(false, ` Errors: ${restTestMini.nbError}`)
1216
- },
1217
- nbSuccess: 0,
1218
- nbError: 0,
1219
- lastErrors: []
1220
- }
1221
-
1222
- export function assert(msg: string, value: any, validatorObject: ValidatorObject | number | boolean | string = {}) {
1223
- try {
1224
- if (typeof validatorObject !== 'object') validatorObject = { eq: validatorObject }
1225
- const issetCheck = isEmpty(validatorObject)
1226
- validatorObject.value = value
1227
- validatorObject.name = msg
1228
- const [errMsg, , extraInfos] = validatorReturnErrArray(validatorObject)
1229
- const msg2 = msg + ` ${issetCheck ? 'isset' : `${JSON.stringify({ ...validatorObject, name: undefined, value: undefined })}`}`
1230
- if (!isset(errMsg)) {
1231
- restTestMini.nbSuccess++
1232
- C.success(msg2)
1233
- } else {
1234
- const err = msg2 + `\n ${errMsg}\n ${JSON.stringify(extraInfos)}`
1235
- restTestMini.newErr(err)
1236
- }
1237
- } catch (err) {
1238
- restTestMini.newErr(err)
1239
- }
1240
- }
1241
-
1242
- /** Same as validator but return a boolean
1243
- * See {@link validator}
1244
- */
1245
- export function isValid(...paramsToValidate) {
1246
- const errArray = validatorReturnErrArray(...paramsToValidate);
1247
- return errArray.length ? false : true;
1248
- }
1249
-
1250
- /** Default types + custom types
1251
- * 'objectId','dateInt6','dateInt','dateInt8','dateInt12','time','humanReadableTimestamp','date','array','object','buffer','string','function','boolean','number','bigint',
1252
- */
1253
- export function isType(value, type: BaseTypes) { return isValid({ name: 'Is type check', value, type }); }
1254
-
1255
- export function validatorReturnErrArray(...paramsToValidate: ValidatorObject[]): [string?, number?, object?] {
1256
- let paramsFormatted = [];
1257
-
1258
- // support for multiple names with multiple values for one rule. Eg: {name: [{startDate:'20180101'}, {endDate:'20180101'}], type: 'dateInt8'}
1259
- paramsToValidate.forEach(param => {
1260
- if (typeof param !== 'object' || Array.isArray(param))
1261
- throw new dataValidationUtilErrorHandler(`wrongTypeForDataValidatorArgument`, 500, { origin: 'Generic validator', expected: 'object', actualType: Array.isArray(param) ? 'array' : typeof param });
1262
-
1263
- // parse => name: {myVar1: 'blah, myvar2: myvar2}
1264
- if (typeof param.name === 'object' && !Array.isArray(param.name))
1265
- Object.keys(param.name).forEach(name => paramsFormatted.push(Object.assign({}, param, { name: name, value: param.name[name] })));
1266
- else paramsFormatted.push(param);
1267
- });
1268
-
1269
- for (const paramObj of paramsFormatted) {
1270
- let name = paramObj.name;
1271
- let value = paramObj.value;
1272
- let optional = paramObj.optional || false;
1273
- let emptyAllowed = optional || paramObj.emptyAllowed || false;
1274
- const errMess = (msg, extraInfos = {}, errCode = 422): [string, number, object] => [msg, errCode, { origin: 'Generic validator', varName: name, gotValue: isset(value) && isset(value.data) && isset(value.data.data) ? { ...value, data: 'Buffer' } : value, ...extraInfos }];
1275
-
1276
- // accept syntax { 'myVar.var2': myVar.var2, ... }
1277
- if (!isset(name)) {
1278
- name = Object.keys(paramObj).find(param => !['in', 'eq', 'lte', 'gte', 'name', 'value', 'type', 'regexp', 'minLength', 'maxLength', 'optional', 'emptyAllowed', 'mustNotBeSet', 'includes', 'length'].includes(param));
1279
- if (isset(name)) value = paramObj[name]; // throw new dataValidationUtilErrorHandler('noNameProvidedForDataValidator', 500, { origin: 'Generic validator', });
1280
- }
1281
- // if nameString ends by $ sign it is optional
1282
- if (isset(name) && /.*\$$/.test(name)) {
1283
- name = name.substr(0, name.length - 1);
1284
- optional = true;
1285
- }
1286
-
1287
- // DEFINED AND NOT EMPTY
1288
- if (!isset(value) && optional) continue;
1289
- if (isset(value) && paramObj.mustNotBeSet) return errMess('variableMustNotBeSet')
1290
- if (paramObj.mustNotBeSet) continue; // exit
1291
- if (!isset(value)) return errMess('requiredVariableEmptyOrNotSet')
1292
- if (!emptyAllowed && value === '') return errMess('requiredVariableEmpty')
1293
-
1294
- const isArray = paramObj.isArray
1295
- if (isArray && !Array.isArray(value)) return errMess('wrongTypeForVar', { expectedTypes: 'array', gotType: typeof value })
1296
-
1297
- // TYPE
1298
- if (isset(paramObj.type)) {
1299
- const types = asArray(paramObj.type) // support for multiple type
1300
- const areSomeTypeValid = types.some(type => {
1301
- if (type.endsWith('[]')) {
1302
- if (!Array.isArray(value)) errMess('wrongTypeForVar', { expectedTypes: 'array', gotType: typeof value })
1303
- type = type.replace('[]', '')
1304
- }
1305
-
1306
- const allTypes: Array<BaseTypes> = [
1307
- 'objectId',
1308
- 'dateInt6',
1309
- 'dateInt', // alias for dateInt8
1310
- 'dateInt8',
1311
- 'dateInt12',
1312
- 'time',
1313
- 'humanReadableTimestamp',
1314
- 'date',
1315
- 'dateObject', // alias
1316
- 'array',
1317
- 'object',
1318
- 'buffer',
1319
- 'string',
1320
- 'function',
1321
- 'boolean',
1322
- 'number',
1323
- 'bigint',
1324
- 'year',
1325
- 'any',
1326
- 'email',
1327
- //...Object.keys(configFn().customTypes)
1328
- ];
1329
-
1330
- if (!allTypes.includes(type)) throw new dataValidationUtilErrorHandler('typeDoNotExist', 500, { type });
1331
-
1332
- const basicTypeCheck = {
1333
- objectId: val => /^[0-9a-fA-F-]{24,}$/.test(val), // "0c65940b-6b0c-4dd8-9c7a-7c5fe1ba907a"
1334
- dateInt6: val => isDateIntOrStringValid(parseInt(val + '01'), true, 8),
1335
- dateInt: val => isDateIntOrStringValid(val, true, 8),
1336
- dateInt8: val => isDateIntOrStringValid(val, true, 8),
1337
- dateInt12: val => isDateIntOrStringValid(val, true, 12),
1338
- time: val => /^\d\d:\d\d$/.test(val) && isTimeStringValid(val),
1339
- humanReadableTimestamp: val => (val + '').length === 17,
1340
- date: val => isDateIsoOrObjectValid(val, true),
1341
- dateObject: val => isDateIsoOrObjectValid(val, true),
1342
- array: val => Array.isArray(val),
1343
- object: val => !Array.isArray(val) && val !== null && typeof val === type,
1344
- buffer: val => Buffer.isBuffer(val),
1345
- year: val => /^\d\d\d\d$/.test(val),
1346
- email: val => /^[^\s@]+@([^\s@.,]+\.)+[^\s@.,]+$/.test(val),
1347
- any: () => true,
1348
- };
1349
-
1350
- return isset(basicTypeCheck[type]) && basicTypeCheck[type](value) ||
1351
- typeof value === type && type !== 'object' || // for string, number, boolean...
1352
- isset(configFn().customTypes[type]) && configFn().customTypes[type].test(value);
1353
- });
1354
- if (!areSomeTypeValid) return errMess(`wrongTypeForVar`, { expectedTypes: types.join(', '), gotType: Object.prototype.toString.call(value) });
1355
- }
1356
-
1357
- // GREATER / LESS
1358
- if (isset(paramObj.gte) && value < paramObj.gte) return errMess(`valueShouldBeSuperiorOrEqualForVar`, { shouldBeSupOrEqTo: paramObj.gte });
1359
- if (isset(paramObj.lte) && value > paramObj.lte) return errMess(`valueShouldBeInferiorOrEqualForVar`, { shouldBeInfOrEqTo: paramObj.lte });
1360
- if (isset(paramObj.gt) && value <= paramObj.gt) return errMess(`valueShouldBeSuperiorForVar`, { shouldBeSupOrEqTo: paramObj.gt });
1361
- if (isset(paramObj.lt) && value >= paramObj.lt) return errMess(`valueShouldBeInferiorForVar`, { shouldBeInfOrEqTo: paramObj.lt });
1362
-
1363
- // IN VALUES
1364
- if (isset(paramObj.in)) {
1365
- const equals = Array.isArray(paramObj.in) ? paramObj.in : [paramObj.in];
1366
- if (!equals.some(equalVal => equalVal === value)) return errMess(`wrongValueForVar`, { supportedValues: JSON.stringify(equals) });
1367
- }
1368
-
1369
- // EQUAL (exact copy of .in)
1370
- if (paramObj.hasOwnProperty('eq')) {
1371
- const equals = Array.isArray(paramObj.eq) ? paramObj.eq : [paramObj.eq];
1372
- if (!equals.some(equalVal => equalVal === value)) return errMess(`wrongValueForVar`, { supportedValues: equals.join(', '), });
1373
- }
1374
-
1375
- // NOT EQUAL
1376
- if (paramObj.hasOwnProperty('neq')) {
1377
- const notEquals = Array.isArray(paramObj.neq) ? paramObj.neq : [paramObj.neq];
1378
- if (notEquals.some(equalVal => equalVal === value)) return errMess(`wrongValueForVar`, { NOTsupportedValues: notEquals.join(', ') });
1379
- }
1380
-
1381
- // INCLUDES
1382
- if (isset(paramObj.includes) && !value.includes(paramObj.includes))
1383
- return errMess(`wrongValueForVar`, { shouldIncludes: paramObj.includes });
1384
-
1385
- // REGEXP
1386
- if (isset(paramObj.regexp) && !paramObj.regexp.test(value))
1387
- return errMess(`wrongValueForVar`, { shouldMatchRegexp: paramObj.regexp.toString() });
1388
-
1389
- // MIN / MAX LENGTH works for number length. Eg: 20180101.length == 8
1390
- if (isset(paramObj.minLength) && paramObj.minLength > (typeof value == 'number' ? value + '' : value).length)
1391
- return errMess(`wrongLengthForVar`, { minLength: paramObj.minLength });
1392
- if (isset(paramObj.maxLength) && paramObj.maxLength < (typeof value == 'number' ? value + '' : value).length)
1393
- return errMess(`wrongLengthForVar`, { maxLength: paramObj.maxLength });
1394
- if (isset(paramObj.length) && paramObj.length !== (typeof value == 'number' ? value + '' : value).length)
1395
- return errMess(`wrongLengthForVar`, { length: paramObj.length });
1396
- }
1397
-
1398
- return [];
1399
- }
1400
-
1401
-
1402
- export function isEmpty(objOrArr: object | any[] | string | null | undefined) {
1403
- if (Array.isArray(objOrArr) || typeof objOrArr === 'string') return objOrArr.length === 0
1404
- else if (typeof objOrArr == 'object' && objOrArr !== null && !(objOrArr instanceof Date)) return Object.keys(objOrArr).length === 0
1405
- else false
1406
- }
1407
-
1408
- export function err422IfNotSet(o) {
1409
- let m = []
1410
- for (let p in o) if (!isset(o[p])) m.push(p)
1411
- if (m.length) throw new dataValidationUtilErrorHandler(`requiredVariableEmptyOrNotSet`, 422, { origin: 'Validator', varNames: m.join(', ') })
1412
- }
1413
-
1414
- export function getDateAsInt12(dateAllFormat?: Date | string | number, errIfNotValid?): string { return getDateAsInt(dateAllFormat, errIfNotValid, true); } // alias
1415
-
1416
- export function humanReadableTimestamp(dateAllFormat: any): number {
1417
- if (isset(dateAllFormat)) dateAllFormat = getDateAsObject(dateAllFormat);
1418
- return parseInt(getDateAsInt12(dateAllFormat) + pad((dateAllFormat || new Date()).getUTCSeconds()) + pad((dateAllFormat || new Date()).getUTCMilliseconds(), 3));
1419
- }
1420
-
1421
- /** format for 6/8/2018 => 20180806
1422
- * @param dateAllFormat multiple format allowed 2012, 20120101, 201201011200, new Date(), "2019-12-08T16:19:10.341Z" and all string that new Date() can parse
1423
- */
1424
- export function getDateAsInt(dateAllFormat: Date | string | number = new Date(), errIfNotValid$ = false, withHoursAndMinutes$ = false): string { // optional
1425
- let dateInt;
1426
- if (typeof dateAllFormat === 'string' && dateAllFormat.includes('/')) {
1427
- // 01/01/2020 format
1428
- const [d, m, y] = dateAllFormat.split('/')
1429
- return y + m.toString().padStart(2, '0') + d.toString().padStart(2, '0')
1430
- } else if (isDateIntOrStringValid(dateAllFormat)) {
1431
- // we can pass an int or string format (20180106)
1432
- dateInt = (dateAllFormat + '00000000').substr(0, 12); // add default 000000 for "month days minutes:sec" if not set
1433
- } else {
1434
- let date: any = dateAllFormat;
1435
- if (typeof date === 'string') date = new Date(date);
1436
- const realDate: Date = date
1437
- //let dateArr = dateAllFormat.toString().split(); // we cannot use ISOString
1438
- dateInt = '' + realDate.getUTCFullYear() + pad(realDate.getUTCMonth() + 1) + pad(realDate.getUTCDate()) + pad(realDate.getUTCHours()) + pad(realDate.getUTCMinutes());
1439
- }
1440
- isDateIntOrStringValid(dateInt, errIfNotValid$);
1441
- return (withHoursAndMinutes$ ? dateInt : dateInt.substr(0, 8));
1442
- }
1443
-
1444
-
1445
- export function getMonthAsInt(dateAllFormat: Date | string | number = new Date()): number {
1446
- let dateInt;
1447
- if (isDateIntOrStringValid(dateAllFormat)) {
1448
- // we can pass an int or string format (20180106)
1449
- dateInt = (dateAllFormat + '').substr(0, 6);
1450
- } else {
1451
- let date: any = dateAllFormat;
1452
- if (typeof date === 'string') date = new Date(date);
1453
- //let dateArr = dateAllFormat.toString().split(); // we cannot use ISOString
1454
- dateInt = '' + date.getUTCFullYear() + pad(date.getUTCMonth() + 1);
1455
- }
1456
- return int(dateInt);
1457
- }
1458
- /**
1459
- * @param dateAllFormat multiple format allowed 2012, 20120101, 201201011200, new Date(), "2019-12-08T16:19:10.341Z" and all string that new Date() can parse
1460
- */
1461
- export function getDateAsObject(dateAllFormat: any = new Date(), errIfNotValid$ = true): Date {
1462
- let dateObj = dateAllFormat;
1463
- if (isDateIntOrStringValid(dateAllFormat)) {
1464
- const [y, M, d, h, m] = dateStringToArray(dateAllFormat);
1465
- dateObj = new Date(`${y}-${M}-${d}T${h}:${m}`);
1466
- } else if (typeof dateAllFormat === 'string') {
1467
- dateObj = new Date(dateAllFormat);
1468
- } else {
1469
- dateObj = new Date(dateAllFormat.getTime()); // clone
1470
- }
1471
- isDateIsoOrObjectValid(dateObj, errIfNotValid$);
1472
- return dateObj;
1473
- }
1474
-
1475
- export function isDateIntOrStringValid(dateStringOrInt, outputAnError = false, length?): boolean {
1476
- if (!isset(dateStringOrInt)) return false
1477
- const dateStr = dateStringOrInt.toString();
1478
-
1479
- if (length && dateStr.length !== length) throw new dataValidationUtilErrorHandler(`wrongLengthForDateInt`, 422, { origin: 'Date Int validator', dateStringOrInt: dateStringOrInt, extraInfo: `${dateStringOrInt} length !== ${length}` });
1480
-
1481
- if ((typeof dateStringOrInt === 'object' && isNaN(int(dateStr))) || ![4, 6, 8, 10, 12, 17].includes(dateStr.length)) return false;
1482
-
1483
- const dateArr = dateStringToArray(dateStringOrInt);
1484
- const [y, M, d, h, m] = dateArr;
1485
-
1486
- const test1 = dateArr.length >= 3 && int(y) >= 1000; // Y
1487
- const test2 = int(M) <= 12 && int(M) > 0; // M
1488
- const test3 = !isset(d) || int(d) <= 31 && int(d) > 0; // D
1489
- const test4 = !isset(h) || (int(h) <= 23 && int(h) >= 0); // H
1490
- const test5 = !isset(m) || (int(m) <= 59 && int(m) >= 0); // M
1491
-
1492
- if (outputAnError && !(test1 && test2 && test3 && test4 && test5)) throw new dataValidationUtilErrorHandler(`dateStringOrIntFormatInvalid`, 422, { origin: 'Date Int validator', dateStringOrInt: dateStringOrInt, extraInfo: 'Needs YYYYMMDD[HHMM] between 100001010000 and 999912312359', dateArr, isYearValid: test1, isMonthValid: test2, isDayValid: test3, isHourValid: test4, isMinutesValid: test5 });
1493
- return true;
1494
- }
1495
-
1496
- export function isDateIsoOrObjectValid(dateIsoOrObj, outputAnError = false) {
1497
- let dateObj: Date | number | string = dateIsoOrObj;
1498
- if (typeof dateIsoOrObj === 'string') dateObj = new Date(dateIsoOrObj);
1499
- let valid = dateObj instanceof Date;
1500
- if (outputAnError && !valid) throw new dataValidationUtilErrorHandler('dateIsoStringOrObjectIsNotValid', 422, { origin: 'Date Object validator', isoDate: dateIsoOrObj });
1501
- return valid;
1502
- }
1503
-
1504
- /** [2018,01,06] */
1505
- export function dateStringToArray(strOrInt) {
1506
- err422IfNotSet({ strOrInt });
1507
-
1508
- const dateStr = strOrInt.toString();
1509
- return [
1510
- dateStr.substr(0, 4), // Y
1511
- dateStr.substr(4, 2) || '01', // M
1512
- dateStr.substr(6, 2) || '01', // D
1513
- dateStr.substr(8, 2) || '12', // H 12 default to avoid time shift when converting to dateObj
1514
- dateStr.substr(10, 2) || '00', // M
1515
- dateStr.substr(12, 2) || '00', // S
1516
- dateStr.substr(14, 3) || '000', // MS
1517
- ];
1518
- }
1519
-
1520
- /**
1521
- * @param dateAllFormat default: actualDate
1522
- * @returns ['01', '01', '2019'] OR **string** if separator is provided */
1523
- export function dateArray(dateAllFormat: Date | string | number = getDateAsInt()): [string, string, string] {
1524
- const dateStr = getDateAsInt(dateAllFormat).toString();
1525
- return [
1526
- dateStr.substr(6, 2), // D
1527
- dateStr.substr(4, 2), // M
1528
- dateStr.substr(0, 4), // Y
1529
- ];
1530
- }
1531
-
1532
- /**
1533
- * @param dateAllFormat default: actualDate
1534
- * @returns ['01', '01', '2019'] OR **string** if separator is provided */
1535
- export function dateArrayInt(dateAllFormat: Date | string | number = getDateAsInt()): [number, number, number] {
1536
- const dateStr = getDateAsInt(dateAllFormat).toString();
1537
- return [
1538
- int(dateStr.substr(6, 2)), // D
1539
- int(dateStr.substr(4, 2)), // M
1540
- int(dateStr.substr(0, 4)), // Y
1541
- ];
1542
- }
1543
-
1544
-
1545
- /**
1546
- * @return 01/01/2012 (alias of dateArrayFormatted(date, '/'))
1547
- */
1548
- export function dateFormatted(dateAllFormat: Date | string | number, separator = '/') { return dateArray(dateAllFormat).join(separator); }
1549
-
1550
- /** Date with custom offset (Ex: +2 for France) */
1551
- export function dateOffset(offsetHours, dateObj = new Date()) {
1552
-
1553
- var utc = Date.UTC(dateObj.getUTCFullYear(), dateObj.getUTCMonth(), dateObj.getUTCDate(),
1554
- dateObj.getUTCHours(), dateObj.getUTCMinutes(), dateObj.getUTCSeconds());
1555
-
1556
- return new Date(utc + (3600000 * offsetHours));
1557
- }
1558
-
1559
- //----------------------------------------
1560
- // TIME UTILS
1561
- //----------------------------------------
1562
-
1563
- /** */
1564
- export function getTimeAsInt(timeOrDateInt: any = getDateAsInt12()) {
1565
- if (isDateIntOrStringValid(timeOrDateInt)) {
1566
- const tl = timeOrDateInt.toString().length;
1567
- return int(timeOrDateInt.toString().substring(tl - 4, tl));
1568
- } else if (typeof timeOrDateInt === 'string' && timeOrDateInt.length === 5 && timeOrDateInt.includes(':'))
1569
- return int(timeOrDateInt.replace(':', ''));
1570
- else return 'dateInvalid';
1571
- }
1572
-
1573
- /**
1574
- * @param {timeInt|dateInt12} Eg: 2222 OR 201201012222. Default, actual dateInt12
1575
- * @param {String} separator default: ":"
1576
- */
1577
- export function getIntAsTime(intOrDateTimeInt = getDateAsInt12(), separator = ':') {
1578
- const time = intOrDateTimeInt.toString().padStart(4, '0');
1579
- const tl = time.length;
1580
- return time.substring(tl - 4, tl - 2) + separator + time.substring(tl - 2, tl);
1581
- }
1582
-
1583
- export function isTimeStringValid(timeStr, outputAnError$ = false) {
1584
- let timeArr = timeStr.split(':');
1585
- let h = int(timeArr[0]);
1586
- let m = int(timeArr[1]);
1587
- let test1 = h >= 0 && h < 24;
1588
- let test2 = m >= 0 && m < 60;
1589
- if (outputAnError$ && !(test1 && test2)) throw new dataValidationUtilErrorHandler('timeStringOutOfRange', 422, { origin: 'Time validator' });
1590
- else return test1 && test2;
1591
- }
1592
-
1593
- //----------------------------------------
1594
- // DURATIONS
1595
- //----------------------------------------
1596
-
1597
- export function getDuration(startDate, endDate, inMinutes = false) {
1598
- const startDateO = getDateAsObject(startDate);
1599
- const endDateO = getDateAsObject(endDate);
1600
- let diffInSec = Math.floor(endDateO.getTime() / 1000) - Math.floor(startDateO.getTime() / 1000);
1601
-
1602
- if (inMinutes) return Math.floor(diffInSec / 60);
1603
- else return [
1604
- Math.floor(diffInSec / (24 * 3600)), // D
1605
- Math.floor((diffInSec % (24 * 3600)) / 3600), // H
1606
- Math.floor(((diffInSec % (24 * 3600)) % 3600) / 60), // M
1607
- ];
1608
- }
1609
-
1610
- /** compare two object with DATE INT, if they overlap return true
1611
- * @param {Object} event1 {startDate, endDate}
1612
- * @param {Object} event2 {startDate, endDate}
1613
- * @param {String} fieldNameForStartDate$ replace startDate with this string
1614
- * @param {String} fieldNameForEndDate$ replace endDate with this string
1615
- * @param {Boolean} allowNull$ if false, retrun false if any of the startdates or enddates are not set
1616
- * @param {Boolean} strict$ if true,
1617
- */
1618
- export function doDateOverlap(event1, event2, fieldNameForStartDate$ = 'startDate', fieldNameForEndDate$ = 'endDate', allowNull$ = true, strict$ = false) {
1619
- if (!allowNull$ && !isset(event1[fieldNameForStartDate$], event1[fieldNameForEndDate$], event2[fieldNameForStartDate$], event2[fieldNameForEndDate$])) return false;
1620
-
1621
- if (strict$)
1622
- return (!event2[fieldNameForEndDate$] || event1[fieldNameForStartDate$] < event2[fieldNameForEndDate$]) && (!event1[fieldNameForEndDate$] || event1[fieldNameForEndDate$] > event2[fieldNameForStartDate$]);
1623
-
1624
- return (!event2[fieldNameForEndDate$] || event1[fieldNameForStartDate$] <= event2[fieldNameForEndDate$]) && (!event1[fieldNameForEndDate$] || event1[fieldNameForEndDate$] >= event2[fieldNameForStartDate$]);
1625
- }
1626
-
1627
- type DateAllFormat = DateObjectFormat | DateStringFormats
1628
- type DateStringFormats = 'dateInt8' | 'dateInt12' | 'humanReadableTimestamp'
1629
- type DateObjectFormat = 'date'
1630
-
1631
- export function nextWeekDay(fromDate, weekDayInt?: 0 | 1 | 2 | 3 | 4 | 5 | 6, outputFormat?: DateStringFormats, sameDayAllowed?: boolean): number
1632
- export function nextWeekDay(fromDate, weekDayInt?: 0 | 1 | 2 | 3 | 4 | 5 | 6, outputFormat?: DateObjectFormat, sameDayAllowed?: boolean): Date
1633
- export function nextWeekDay(fromDate, weekDayInt?: 0 | 1 | 2 | 3 | 4 | 5 | 6, outputFormat = 'date', sameDayAllowed = false): any {
1634
- const date = getDateAsObject(fromDate);
1635
- if (!isset(weekDayInt)) weekDayInt = (date.getDay() as 0 | 1 | 2 | 3 | 4 | 5 | 6)
1636
- const toAdd = !sameDayAllowed && date.getDay() === weekDayInt ? 7 : 0
1637
- date.setUTCDate(date.getUTCDate() + toAdd + (7 + weekDayInt - date.getUTCDay()) % 7);
1638
- return getDateAs(date, outputFormat as any);
1639
- }
1640
-
1641
- /**
1642
- * @param {String} outputFormat dateInt, dateInt8, dateInt12, date, humanReadableTimestamp, int (dateInt8)
1643
- */
1644
- export function addDays(dateAllFormat?: Date | string | number, numberOfDays?: number, outputFormat?: 'dateInt8' | 'dateInt12' | 'humanReadableTimestamp'): string
1645
- export function addDays(dateAllFormat?: Date | string | number, numberOfDays?: number, outputFormat?: 'date'): Date
1646
- export function addDays(dateAllFormat: Date | string | number = getDateAsInt(), numberOfDays = 1, outputFormat: DateAllFormat = 'date'): any {
1647
- let date = getDateAsObject(dateAllFormat);
1648
- date.setTime(date.getTime() + numberOfDays * 24 * 60 * 60 * 1000);
1649
- return getDateAs(date, outputFormat as any);
1650
- }
1651
-
1652
- /**
1653
- * @param {String} outputFormat dateInt, dateInt8, dateInt12, date, humanReadableTimestamp, int (dateInt8)
1654
- */
1655
- export function addMinutes(dateAllFormat?: Date | string | number, numberOfMinutes?: number, outputFormat?: DateStringFormats): string
1656
- export function addMinutes(dateAllFormat?: Date | string | number, numberOfMinutes?: number, outputFormat?: DateObjectFormat): Date
1657
- export function addMinutes(dateAllFormat: Date | string | number = getDateAsInt(), numberOfMinutes = 1, outputFormat: DateAllFormat = 'date'): any {
1658
- let date = getDateAsObject(dateAllFormat);
1659
- date.setTime(date.getTime() + 1 * numberOfMinutes * 60 * 1000);
1660
- return getDateAs(date, outputFormat as any);
1661
- }
1662
- /**
1663
- * @param {String} outputFormat dateInt, dateInt8, dateInt12, date, humanReadableTimestamp, int (dateInt8)
1664
- */
1665
- export function addHours(dateAllFormat?: Date | string | number, numberOfHours?: number, outputFormat?: 'dateInt8' | 'dateInt12' | 'humanReadableTimestamp'): string
1666
- export function addHours(dateAllFormat?: Date | string | number, numberOfHours?: number, outputFormat?: 'date'): Date
1667
- export function addHours(dateAllFormat: Date | string | number = getDateAsInt(), numberOfHours = 1, outputFormat: DateAllFormat = 'date'): any {
1668
- let date = getDateAsObject(dateAllFormat);
1669
- date.setTime(date.getTime() + 1 * numberOfHours * 60 * 60 * 1000);
1670
- return getDateAs(date, outputFormat as any);
1671
- }
1672
- /**
1673
- * @param {String} outputFormat dateInt, dateInt8, dateInt12, date, humanReadableTimestamp, int (dateInt8)
1674
- */
1675
- export function addMonths(dateAllFormat?: Date | string | number, numberOfMonths?: number, outputFormat?: 'dateInt8' | 'dateInt12' | 'humanReadableTimestamp'): string
1676
- export function addMonths(dateAllFormat?: Date | string | number, numberOfMonths?: number, outputFormat?: 'date'): Date
1677
- export function addMonths(dateAllFormat: Date | string | number = getDateAsInt(), numberOfMonths = 1, outputFormat: DateAllFormat = 'date'): any {
1678
- let date = getDateAsObject(dateAllFormat);
1679
- date.setUTCMonth(date.getUTCMonth() + numberOfMonths);
1680
- return getDateAs(date, outputFormat as any);
1681
- }
1682
- /**
1683
- * @param {String} outputFormat dateInt, dateInt8, dateInt12, date, humanReadableTimestamp, int (dateInt8)
1684
- */
1685
- export function addYears(dateAllFormat: Date | string | number = getDateAsInt(), numberOfYears = 1, outputFormat: DateAllFormat = 'date') {
1686
- let date = getDateAsObject(dateAllFormat);
1687
- date.setUTCFullYear(date.getUTCFullYear() + numberOfYears);
1688
- return getDateAs(date, outputFormat as any);
1689
- }
1690
-
1691
- export function getDayOfMonth(dateAllFormat: Date | string | number = getDateAsInt()) {
1692
- let dateAsInt = getDateAsInt(dateAllFormat);
1693
- const [, , d] = dateStringToArray(dateAsInt);
1694
- return d;
1695
- }
1696
-
1697
- export function getYear(dateAllFormat: Date | string | number = getDateAsInt()) {
1698
- let dateAsInt = getDateAsInt(dateAllFormat);
1699
- const [y] = dateStringToArray(dateAsInt);
1700
- return y;
1701
- }
1702
-
1703
-
1704
- export function getHours(dateAllFormat: Date | string | number = getDateAsInt()) {
1705
- let dateAsInt = getDateAsInt(dateAllFormat);
1706
- const [, , , h,] = dateStringToArray(dateAsInt);
1707
- return h;
1708
- }
1709
-
1710
- export function getMinutes(dateAllFormat: Date | string | number = getDateAsInt()) {
1711
- let dateAsInt = getDateAsInt(dateAllFormat);
1712
- const [, , , , m] = dateStringToArray(dateAsInt);
1713
- return m;
1714
- }
1715
-
1716
- /**
1717
- * @param {String} outputFormat dateInt, dateInt8, dateInt12, date, humanReadableTimestamp, int (dateInt8)
1718
- */
1719
- export function lastDayOfMonth(dateAllFormat: Date | string | number = getDateAsInt(), outputFormat: DateAllFormat = 'date') {
1720
- let date = getDateAsObject(dateAllFormat);
1721
- const lastDay = new Date(date.getUTCFullYear(), date.getUTCMonth() + 1, 0);
1722
- lastDay.setUTCHours(date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds());
1723
- return getDateAs(lastDay, outputFormat as any);
1724
- }
1725
-
1726
- /**
1727
- * @param {String} outputFormat dateInt, dateInt8, dateInt12, date, humanReadableTimestamp, int (dateInt8)
1728
- */
1729
- export function firstDayOfMonth(dateAllFormat: Date | string | number = getDateAsInt(), outputFormat: DateAllFormat = 'date') {
1730
- let date = getDateAsObject(dateAllFormat);
1731
- const firstDay = new Date(date.getUTCFullYear(), date.getUTCMonth(), 1);
1732
- firstDay.setUTCHours(date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds());
1733
- return getDateAs(firstDay, outputFormat as any);
1734
- }
1735
-
1736
- export function differenceInMilliseconds(startDateAllFormat, endDateAllFormat) {
1737
- const startDate = getDateAsObject(startDateAllFormat);
1738
- const endDate = getDateAsObject(endDateAllFormat);
1739
- return endDate.getTime() - startDate.getTime();
1740
- }
1741
-
1742
- export function differenceInSeconds(startDateAllFormat, endDateAllFormat) {
1743
- return differenceInMilliseconds(startDateAllFormat, endDateAllFormat) / 1000;
1744
- }
1745
-
1746
- export function differenceInMinutes(startDateAllFormat, endDateAllFormat) {
1747
- return differenceInSeconds(startDateAllFormat, endDateAllFormat) / 60;
1748
- }
1749
-
1750
- export function differenceInHours(startDateAllFormat, endDateAllFormat) {
1751
- return differenceInMinutes(startDateAllFormat, endDateAllFormat) / 60;
1752
- }
1753
-
1754
- export function differenceInDays(startDateAllFormat, endDateAllFormat) {
1755
- return differenceInHours(startDateAllFormat, endDateAllFormat) / 24;
1756
- }
1757
-
1758
- export function differenceInWeeks(startDateAllFormat, endDateAllFormat) {
1759
- return differenceInDays(startDateAllFormat, endDateAllFormat) / 7;
1760
- }
1761
-
1762
- /**
1763
- * @param {String} outputDateFormat dateInt, dateInt8, dateInt12, date, humanReadableTimestamp, int (dateInt8)
1764
- */
1765
- export function getDateAs(dateAllFormat?: Date | string | number, outputFormat?: 'dateInt8' | 'dateInt12' | 'humanReadableTimestamp'): string
1766
- export function getDateAs(dateAllFormat?: Date | string | number, outputFormat?: 'date'): Date
1767
- export function getDateAs(dateAllFormat: Date | string | number = new Date(), outputDateFormat: DateAllFormat = 'date') {
1768
- switch (outputDateFormat) {
1769
- case 'dateInt8':
1770
- return getDateAsInt(dateAllFormat);
1771
- case 'dateInt12':
1772
- return getDateAsInt12(dateAllFormat);
1773
- case 'humanReadableTimestamp':
1774
- return humanReadableTimestamp(dateAllFormat);
1775
- case 'date':
1776
- default:
1777
- return getDateAsObject(dateAllFormat);
1778
- }
1779
- }
1780
-
1781
-
1782
- //----------------------------------------
1783
- // LOGGER
1784
- //----------------------------------------
1785
-
1786
- export const logger = {
1787
- log(str, type = 'log') {
1788
- const { preprocessLog } = configFn();
1789
- if (typeof preprocessLog === 'function') str = preprocessLog(str) || str;
1790
- if (isset(console[type])) console[type](str);
1791
- else if (type === 'appError') console.warn(str);
1792
- else console.log(str);
1793
-
1794
- logger.raw.push(str + `\n`);
1795
- logger.raw = logger.raw.slice(logger.raw.length - configFn().nbOfLogsToKeep, logger.raw.length);
1796
- },
1797
- /**
1798
- * @param {String[]|String} inputLogs
1799
- */
1800
- toHtml(inputLogs = [...logger.raw]) {
1801
- if (!Array.isArray(inputLogs)) inputLogs = [inputLogs];
1802
- const code2css = {
1803
- 2: `opacity:.5`, // dim
1804
- 32: `color:#679933`, // green
1805
- 31: `color:#A8383B`, // red
1806
- 33: `color:#D7941C`, // yellow
1807
- 35: `color:#C21949`, // magenta
1808
- 36: `color:#128C6D`, // cyan
1809
- 34: `color:#1B568B`, // blue
1810
- };
1811
-
1812
- let htmlLogs = '';
1813
- inputLogs.join('<br/>').split('\x1b[').forEach((bit, i) => {
1814
- // the first doesn't have a preceding (may be undef)
1815
- if (bit) {
1816
- if (i !== 0) {
1817
- const [, code, R, G, B, content] = bit.match(/(^\d\d?)(?:;|m)(?:\d;(\d+);(\d+);(\d+)m)?(.*$)/) || [];
1818
- if (content) {
1819
- const style = !isset(R) ? isset(code) ? code2css[code] : undefined : `color:rgb(${R},${G},${B})`;
1820
- htmlLogs += isset(style) ? `<span style='${style}'>${content.replace(/\n/g, '<br>')}</span>` : content.replace(/\n/g, '<br>');
1821
- }
1822
- } else htmlLogs += bit.replace(/\n/g, '<br>');
1823
- }
1824
- });
1825
- return `<div style='color:#ccc'>${htmlLogs}<br></div>`;
1826
- },
1827
- toText(inputLogs = [...logger.raw]) {
1828
- const str = Array.isArray(inputLogs) ? inputLogs.join('\n') : inputLogs;
1829
- return str.replace(/\x1b\[.*?m/g, '');
1830
- },
1831
- raw: [],
1832
- json: [],
1833
- };
1834
-
1835
- /**
1836
- // console colored output
1837
- // * console.log(C.green(C.dim('Hey bro !')))
1838
- // * or C.log() // will use padding and color defined by themes
1839
- // * or C.line('MY TITLE', 53)
1840
- // * or C.gradientize(myLongString)
1841
- */
1842
- export const C = {
1843
- dim: str => C.output(2, str), // opacity 0.5
1844
- green: str => C.output(32, str),
1845
- red: str => C.output(31, str),
1846
- yellow: str => C.output(33, str),
1847
- grey: str => C.output(2, str),
1848
- magenta: str => C.output(35, str),
1849
- cyan: str => C.output(36, str),
1850
- blue: str => C.output(34, str),
1851
- primary: str => {
1852
- const primary: Color = configFn().terminal.theme.primary
1853
- return C.rgb(...primary) + str + C.reset
1854
- },
1855
- reset: '\x1b[0m',
1856
- output: (code, str = '') => configFn().terminal.noColor ? str : `\x1b[${code}m${str.toString().split('\n').join('\x1b[0m\n\x1b[2m')}\x1b[0m`,
1857
- // true RGB colors B-*
1858
- rgb: (r, g = 0, b = 0) => configFn().terminal.noColor || !isset(r, g, b) ? '' : `\x1b[38;2;${r};${g};${b}m`,
1859
- bg: (r?, g?, b?) => configFn().terminal.noColor || !isset(r, g, b) ? '' : `${'\x1b['}48;2;${r};${g};${b}m`,
1860
- /** Output a line of title */
1861
- line(title = '', length = configFn().terminal.theme.pageWidth, clr = configFn().terminal.theme.primary, char = '=', paddingX = configFn().terminal.theme.paddingX) {
1862
- const padX = ' '.repeat(paddingX);
1863
- this.log('\u00A0\n' + padX + this.rgb(...clr) + (title + ' ').padEnd(length, char) + this.reset + padX + '\u00A0\n');
1864
- },
1865
- /** Eg: ['cell1', 'cell2', 'cell3'], [25, 15] will start cell2 at 25 and cell 3 at 25 + 15
1866
- * @param {Array} limits default divide the viewport
1867
- */
1868
- cols(strings, limits = [], clr = configFn().terminal.theme.fontColor, paddingX = configFn().terminal.theme.paddingX) {
1869
-
1870
- if (!limits.length) {
1871
- const colWidth = Math.round(configFn().terminal.theme.pageWidth / strings.length);
1872
- limits = Array(strings.length - 1).fill(2).map((itm, i) => colWidth * i + 1);
1873
- }
1874
- const str = strings.reduce((glob, str = '', i) => {
1875
- const realCharLength = str.toString().replace(/\x1b\[.*?m/, '').length;
1876
- const charLength = str.toString().length;
1877
- const realLimit = limits[i] + charLength - realCharLength;
1878
- return glob + str.toString().substring(0, realLimit || 999).padEnd(realLimit || 0, ' ');
1879
- }, '');
1880
- this.logClr(str, clr, paddingX);
1881
- },
1882
- /** Console log alias */
1883
- log(...stringsCtxMayBeFirstParam) {
1884
- stringsCtxMayBeFirstParam.forEach(str => this.logClr(str, undefined, undefined));
1885
- },
1886
- logClr(str, clr = configFn().terminal.theme.fontColor, paddingX = configFn().terminal.theme.paddingX) {
1887
- if (!isset(str)) return;
1888
- const padX = ' '.repeat(paddingX);
1889
- str = padX + (isset(clr) ? this.rgb(...clr) : '') + str.toString().replace(/\n/g, '\n' + padX + (isset(clr) ? this.rgb(...clr) : ''));
1890
- logger.log(str + this.reset, 'log');
1891
- },
1892
- info(...str) {
1893
- str.forEach((s, i) => {
1894
- if (i === 0) this.logClr('ⓘ ' + s, configFn().terminal.theme.primary);
1895
- else this.log(this.dimStrSplit(s));
1896
- });
1897
- this.log(' ');
1898
- },
1899
- success(...str) {
1900
- str.forEach((s, i) => {
1901
- if (i === 0) this.log(this.green('✓ ' + s));
1902
- else this.log(this.dimStrSplit(s));
1903
- });
1904
- },
1905
- /** First param **false** to avoid logging stack trace */
1906
- error: (...errors) => logErrPrivate('error', [255, 0, 0], ...errors),
1907
- /** First param **false** to avoid logging stack trace */
1908
- warning: (...str) => logErrPrivate('warn', [255, 122, 0], ...str),
1909
- customError: (color, ...str) => logErrPrivate('error', color, ...str),
1910
- customWarning: (color, ...str) => logErrPrivate('warn', color, ...str),
1911
- applicationError: (color, ...str) => logErrPrivate('appError', color, ...str),
1912
- warningLight: (color, ...str) => logErrPrivate('warn', [196, 120, 52], ...str),
1913
- dimStrSplit(...logs) {
1914
- let logsStr = [];
1915
- logs.filter(isset).forEach(log => log.toString().split('\n').forEach(line => line && logsStr.push(this.dim(` ${line}`))));
1916
- return logsStr.join('\n');
1917
- },
1918
- notifShow() {
1919
- console.log('\n\u00A0');
1920
- this.notifications.forEach(fn => fn());
1921
- this.notifications = [];
1922
- console.log('\n\u00A0');
1923
- },
1924
- /** Keep in memory the logs to show when needed with C.notifShow()
1925
- * Ex: C.notification('info', str); */
1926
- notification(type, ...messages) {
1927
- this.notifications.push(() => {
1928
- if (isset(this[type])) {
1929
- this[type](...messages);
1930
- } else {
1931
- this.warning('Wrong param for C.notification');
1932
- }
1933
- });
1934
- },
1935
- notifications: [],
1936
- /** Gratientize lines of text (separated by \n) */
1937
- gradientize(str = '', rgb1 = configFn().terminal.theme.shade1, rgb2 = configFn().terminal.theme.shade2, bgRgb = configFn().terminal.theme.bgColor, paddingY = configFn().terminal.theme.paddingY, paddingX = configFn().terminal.theme.paddingX) {
1938
-
1939
- const lines = str.split('\n');
1940
- const largestLine = lines.reduce((sum, line) => sum < line.length ? line.length : sum, 0);
1941
- const rgbParts = rgb1.map((val, i) => (val - rgb2[i]) / (lines.length));
1942
-
1943
- const bg = bgRgb ? this.bg(bgRgb[0], bgRgb[1], bgRgb[2]) : '';
1944
- const padX = ' '.repeat(paddingX);
1945
- const padLine = bg + padX + ' '.padEnd(largestLine, ' ') + padX + '\x1b[0m\n';
1946
-
1947
- console.log(padLine.repeat(paddingY) +
1948
- lines.reduce((s, line, i) => {
1949
- return s + bg + padX + this.rgb(...rgb1.map((val, i2) => Math.round(val - i * rgbParts[i2]))) + line.padEnd(largestLine, ' ') + padX + '\x1b[0m\n';
1950
- }, '') +
1951
- padLine.repeat(paddingY));
1952
- },
1953
- debugModeLog(title, ...string) {
1954
- this.logClr('🐞 ' + title, configFn().terminal.theme.debugModeColor, 0);
1955
- this.log(this.dimStrSplit(...string));
1956
- },
1957
- // DEPRECATED
1958
- useTheme() { },
1959
- };
1960
-
1961
- export function logErrPrivate(type, color: Color, ...errors) {
1962
- const { isProd } = configFn();
1963
-
1964
- if (errors.length === 1 && typeof errors[0].log === 'function') return errors[0].log()
1965
-
1966
- let stackTrace = (new Error('')).stack || '';
1967
- const displayStack = errors[0] === false ? errors.shift() : true;
1968
- const symbol = type === 'error' ? '✘ ' : '⚠ ';
1969
- if (errors.length > 1 && !isset(errors[0])) errors.shift();
1970
-
1971
- const getStringFromErr = (err, i) => {
1972
- if (!isset(err)) return '';
1973
- else if (typeof err === 'string') {
1974
- if (i === 0) return C.rgb(...color) + symbol + err + C.reset;
1975
- else return err.split('\n').map(val => C.dim(val)).join('\n');
1976
- } else if (err instanceof Error) {
1977
- const { str, stackTrace: stkTrc } = stringifyInstanceOfError(err, type, color);
1978
- if (stkTrc) stackTrace = stkTrc;
1979
- return str;
1980
- } else if (typeof err === 'object') {
1981
- let msg = '';
1982
- msg += removeCircularJSONstringify(err, 2).split('\n').map(val => C.dim(val)).join('\n') + '\n';
1983
- const { str, stackTrace: stkTrc } = stringifyExtraInfos(err.extraInfo || err, type, color);
1984
- if (stkTrc) stackTrace = stkTrc;
1985
- msg += str;
1986
- return msg;
1987
- } else return '';
1988
- };
1989
-
1990
- if (errors.length && errors[0]) {
1991
- const messages = errors.map((e, i) => {
1992
- if (typeof e.log === 'function') {
1993
- e.log()
1994
- return ''
1995
- } else return getStringFromErr(e, i)
1996
- });
1997
- // Stack
1998
- if (displayStack) {
1999
- messages.push(isProd ? stackTrace : cleanStackTrace(stackTrace) + '\n');
2000
- }
2001
- logger.log(messages.join(''), type);
2002
- }
2003
- }
2004
-
2005
- export function stringifyInstanceOfError(err, type = 'error', color: Color = [255, 0, 0], level = 0) { // level = keep track of recursions
2006
- if (level > 5) return { str: '' };
2007
- let str = '';
2008
- let stackTrace;
2009
- const symbol = type === 'error' ? '✘ ' : '⚠ ';
2010
- const title = err.msg || err.message || err.id || (err.stack ? err.stack.split('\n')[0] : 'Error');
2011
- // Err mess
2012
- str += C.rgb(...color) + symbol + title + C.reset + '\n';
2013
- if (err.stack) stackTrace = err.stack; // more relevant
2014
- // ExtraInfos
2015
- if (isset(err.extraInfo)) {
2016
- const { str: str2, stackTrace: stkTrc } = stringifyExtraInfos(err.extraInfo, type, color, level++);
2017
- if (stkTrc) stackTrace = stkTrc;
2018
- str += str2;
2019
- }
2020
- return { str, stackTrace };
2021
- }
2022
-
2023
- export function stringifyExtraInfos(extraInfoOriginal, type, color, level = 0) {
2024
- let stackTrace;
2025
- const originalError = [C.dim(`ORIGINAL ERROR ${'-'.repeat(39)}\n`)];
2026
- if (extraInfoOriginal instanceof Error) { // case where error is passed directly to extraInfos
2027
- return stringifyInstanceOfError(extraInfoOriginal, type, color);
2028
- } else {
2029
- const extraInfo = { ...extraInfoOriginal };
2030
- const extraInfos = [C.dim(`EXTRA INFOS ${'-'.repeat(41)}\n`)];
2031
- if (typeof extraInfo === 'object' && Object.keys(extraInfo).length) {
2032
- for (const itemName in extraInfo) {
2033
- if (extraInfo[itemName] instanceof Error) {
2034
- const { str, stackTrace: stkTrc } = stringifyInstanceOfError(extraInfo[itemName], type, color, level++);
2035
- originalError.push(str);
2036
- stackTrace = stkTrc;
2037
- delete extraInfo[itemName];
2038
- }
2039
- }
2040
- }
2041
- if (Object.keys(extraInfo).length) {
2042
- extraInfos.push(
2043
- removeCircularJSONstringify(extraInfo, 2)
2044
- .replace(/(?:^\s*{(?:\n {2})?|}\s*$)/g, '')
2045
- .replace(/\n {2}/g, '\n')
2046
- .split('\n')
2047
- .map(val => C.dim(val)).join('\n') + '\n');
2048
- }
2049
- return {
2050
- str: (extraInfos.length > 1 ? extraInfos.join('') : '') + (originalError.length > 1 ? originalError.join('') + '\n' : ''),
2051
- stackTrace
2052
- };
2053
- }
2054
- }
2055
-
2056
- /**
2057
- * Call this at each steps of your progress and change the step value
2058
- * @param {Number} step Number of "char" to output
2059
- * @param {String} char Default: '.'
2060
- * @param {String} msg String before char. Final output will be `${str}${char.repeat(step)}`
2061
- */
2062
- export function cliProgressBar(step, char = '.', msg = `\x1b[2mⓘ Waiting response`) {
2063
- if (isset(process) && isset(process.stdout) && isset(process.stdout.clearLine)) {
2064
- process.stdout.clearLine(0);
2065
- process.stdout.cursorTo(0);
2066
- process.stdout.write(`${msg}${char.repeat(step)}\x1b[0m`); // \x1b[0m == reset color
2067
- }
2068
- }
2069
-
2070
- /** This allow an intuitive inline loading spinner with a check mark when loading as finished or a red cross for errors */
2071
- class cliLoadingSpinner {
2072
- /** Please use it like spinner.start('myStuff') then spinner.end()
2073
- * @param {String} type in: ['arrow', 'dots']
2074
- */
2075
- frameRate: number
2076
- animFrames: string[]
2077
- activeProcess: any
2078
- frameNb: number
2079
- progressMessage: string
2080
- interval: any
2081
-
2082
- constructor(type = 'dots', activeProcess = process) {
2083
- const anims = {
2084
- arrow: {
2085
- interval: 120,
2086
- frames: ['▹▹▹▹▹', '▸▹▹▹▹', '▹▸▹▹▹', '▹▹▸▹▹', '▹▹▹▸▹', '▹▹▹▹▸']
2087
- },
2088
- dots: {
2089
- interval: 80,
2090
- frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
2091
- }
2092
- };
2093
- this.frameRate = anims[type].interval;
2094
- this.animFrames = anims[type].frames;
2095
- this.activeProcess = activeProcess;
2096
- }
2097
- start(msg) {
2098
- this.frameNb = 0;
2099
- this.progressMessage = msg;
2100
- this.interval = setInterval(() => {
2101
- this.activeProcess.stdout.clearLine();
2102
- this.activeProcess.stdout.cursorTo(0);
2103
- const symbol = this.animFrames[this.frameNb++ % this.animFrames.length];
2104
- this.activeProcess.stdout.write(C.primary(symbol) + ' ' + this.progressMessage);
2105
- }, this.frameRate);
2106
- }
2107
- end(error = false) {
2108
- clearInterval(this.interval);
2109
- this.activeProcess.stdout.clearLine();
2110
- this.activeProcess.stdout.cursorTo(0);
2111
- this.activeProcess.stdout.write(
2112
- error ? C.red('✘ ' + this.progressMessage + '\n\n')
2113
- : '\x1b[32m✓ ' + this.progressMessage + '\n\n');
2114
- this.progressMessage = '';
2115
- }
2116
- error() {
2117
- return this.end(true);
2118
- }
2119
- }
2120
-
2121
- //----------------------------------------
2122
- // STRING UTILS
2123
- //----------------------------------------
2124
-
2125
- /** Remove accentued character from string and eventually special chars and numbers
2126
- * @param {String} str input string
2127
- * @param {Object} config { removeSpecialChars: false, removeNumbers: false, removeSpaces: false }
2128
- * @returns String with all accentued char replaced by their non accentued version + config formattting
2129
- */
2130
- export function convertAccentedCharacters(str, config: { removeNumbers?: boolean, removeSpecialChars?: boolean, removeSpaces?: boolean } = {}) {
2131
- let output = str
2132
- .replace(/[àáâãäå]/g, 'a')
2133
- .replace(/ç/g, 'c')
2134
- .replace(/[èéêë]/g, 'e')
2135
- .replace(/[ìíîï]/g, 'i')
2136
- .replace(/[ôö]/g, 'o')
2137
- .replace(/[ùúû]/g, 'u')
2138
- .replace(/(^\s*|\s*$)/g, '');
2139
- if (config.removeNumbers === true) output = output.replace(/\d+/g, '');
2140
- if (config.removeSpecialChars === true) output = output.replace(/[^A-Za-z0-9 ]/g, '');
2141
- if (config.removeSpaces === true) output = output.replace(/\s+/g, '');
2142
- return output;
2143
- }
2144
-
2145
- //----------------------------------------
2146
- // MONGO UTILS
2147
- //----------------------------------------
2148
-
2149
- /** Merge filter with correct handling of OR and AND
2150
- * @param {Object} filterA
2151
- * @param {Object} filterB
2152
- * @param {Boolean} assignToFilterA defualt false: if true, it will modify filterA, else it will return merged filters as a new object
2153
- */
2154
- export function mongoFilterMerger(filterA, filterB, assignToFilterA = false) {
2155
- if (isset(filterA.$and) && isset(filterB.$and)) {
2156
- filterA.$and.push(...filterB.$and);
2157
- delete filterB.$and;
2158
- }
2159
- if (isset(filterA.$or) && isset(filterB.$or)) {
2160
- if (!isset(filterA.$and)) filterA.$and = [];
2161
- filterA.$and.push({ $or: filterA.$or }, { $or: filterA.$or });
2162
- delete filterB.$or;
2163
- }
2164
- if (assignToFilterA) {
2165
- Object.assign(filterA, filterB);
2166
- return filterA;
2167
- } else return { ...filterA, ...filterB };
2168
- }
2169
-
2170
- export function mongoPush(field: string, value: any, fields: { [k: string]: any }) {
2171
- if (!isset(fields.$push)) fields.$push = {}
2172
- fields.$push[field] = value;
2173
- }
2174
-
2175
-
2176
- //----------------------------------------
2177
- // TIMEOUT UTILS
2178
- //----------------------------------------
2179
-
2180
-
2181
- export async function timeout(ms, fn = () => { }) { return new Promise(res => setTimeout(res, ms)).then(fn); }
2182
-
2183
- export async function runAsync(callback, milliseconds$ = 1) { return timeout(milliseconds$, callback); }
2184
-
2185
- /**
2186
- *
2187
- * @param {Function} callback function that shall return ===true asynchronously
2188
- * @param {Number} timeoutSec default:10; general timeout in seconds
2189
- * @param {Boolean|String} errorAfterNSeconds default:true output an error in case of timeout, can be the displayed error message
2190
- * @param {*} cliOutput write a cli progress to show that a process is running
2191
- */
2192
- export async function waitUntilTrue(callback, timeoutSec = 10, errorAfterNSeconds = true, cliOutput = true) {
2193
- let generalTimeout = true;
2194
- let step = 3;
2195
- const errMess = typeof errorAfterNSeconds === 'string' ? 'Timeout: ' + errorAfterNSeconds : 'Timeout for waitUntilTrue() callback';
2196
-
2197
- if (timeoutSec) setTimeout(() => generalTimeout = false, timeoutSec * 1000);
2198
-
2199
- while (callback() !== true && generalTimeout) {
2200
- if (cliOutput) cliProgressBar(step++);
2201
- await timeout(300);
2202
- }
2203
- if (cliOutput) process.stdout.write(`\n`);
2204
- if (!generalTimeout && errorAfterNSeconds) throw new dataValidationUtilErrorHandler(errMess, 500);
2205
- }
2206
-
2207
-
2208
- const delayedLoopParams = [];
2209
- let isExecuting = false;
2210
-
2211
- /** Allow to perform an action in a delayed loop, useful for example to avoid reaching limits on servers. This function can be securely called multiple times.
2212
- * @param {Function} callback
2213
- * @param {Number} time default: 500ms;
2214
- * @param {Function} errorCallback default: e => C.error(e)
2215
- */
2216
- export async function executeInDelayedLoop(callback, time = 500, errorCallback = e => C.error(e)) {
2217
- delayedLoopParams.push([callback, time, errorCallback]);
2218
- if (isExecuting) return;
2219
- isExecuting = true;
2220
- while (delayedLoopParams.length) {
2221
- const [callback, time, errorCallback] = delayedLoopParams.shift();
2222
- try {
2223
- await callback();
2224
- await timeout(time);
2225
- } catch (err) {
2226
- errorCallback(err);
2227
- }
2228
- }
2229
- isExecuting = false;
2230
- }
2231
-
2232
-
2233
- //----------------------------------------
2234
- // TRANSACTION
2235
- //----------------------------------------
2236
-
2237
- const transactionRunning = { __default: false };
2238
- const queue = { __default: [] };
2239
-
2240
- /** Allow to perform async functions in a defined order
2241
- * This adds the callback to a queue and is resolved when ALL previous callbacks with same name are executed
2242
- * Use it like: await transaction('nameOfTheFlow', async () => { ...myFunction })
2243
- * @param {String|Function} name name for the actions that should never happen concurrently
2244
- * @param {Function} asyncCallback
2245
- * @param {Number} timeout default: 120000 (120s) will throw an error if transaction time is higher that this amount of ms
2246
- * @returns {Promise}
2247
- */
2248
- export async function transaction(name, asyncCallback, timeout = 120000, doNotThrow = false) {
2249
- if (typeof name === 'function') {
2250
- asyncCallback = name;
2251
- name = '__default';
2252
- }
2253
- if (!isset(queue[name])) queue[name] = [];
2254
- if (!isset(transactionRunning[name])) transactionRunning[name] = false;
2255
-
2256
- return await new Promise((resolve, reject) => {
2257
- if (doNotThrow) reject = C.error;
2258
- queue[name].push(async () => {
2259
- try {
2260
- setTimeout(() => {
2261
- C.warning('Transaction Timout'); // in case not catched
2262
- reject(new Error('transactionTimeout'));
2263
- }, timeout);
2264
- const res = await asyncCallback();
2265
- resolve(res);
2266
- } catch (err) {
2267
- reject(err);
2268
- }
2269
- });
2270
- removeItemFromQueue(name);
2271
- });
2272
- }
2273
-
2274
- export async function removeItemFromQueue(name) {// meoww!
2275
- if (transactionRunning[name] === true) return; // v
2276
- transactionRunning[name] = true; // A A /\
2277
- while (queue[name].length) await queue[name].shift()(); // II
2278
- transactionRunning[name] = false; // \==/_______II
2279
- } // l v_v_v _I
2280
- // 11 11
2281
-
2282
- /** Wait for a transaction to complete without creating a new transaction
2283
- *
2284
- */
2285
- export async function waitForTransaction(transactionName, forceReleaseInSeconds = 30) {
2286
- let brk = false;
2287
- setTimeout(() => brk = true, forceReleaseInSeconds * 1000);
2288
- while (isset(transactionRunning[transactionName]) && transactionRunning[transactionName] === true) {
2289
- if (brk) break;
2290
- await timeout(15);
2291
- }
2292
- }
2293
-
2294
- // ALIASES
2295
- export const int = parseInt;
2296
- export const average = moyenne;
2297
- export const arrayUniqueValue = noDuplicateFilter;
2298
- export const JSONstringyParse = o => JSON.parse(removeCircularJSONstringify(o));
2299
- export const removeUndefinedKeys = objFilterUndefinedRecursive;
2300
- export const randomizeArray = shuffleArray
2301
- export const dashCase = kebabCase
2302
- export const underscoreCase = snakeCase
2303
- export const required = validator // alias for readability
2304
- export const orIsset = issetOr