@diia-inhouse/utils 5.3.30 → 6.0.9

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 (85) hide show
  1. package/dist/address.d.ts +7 -0
  2. package/dist/address.js +38 -41
  3. package/dist/applicationUtils.d.ts +225 -0
  4. package/dist/applicationUtils.js +576 -678
  5. package/dist/asserts.d.ts +6 -0
  6. package/dist/asserts.js +12 -19
  7. package/dist/dictionaries/phoneCodes.js +2435 -2442
  8. package/dist/guards.d.ts +22 -0
  9. package/dist/guards.js +58 -74
  10. package/dist/index.d.ts +36 -0
  11. package/dist/index.js +34 -70
  12. package/dist/integration.d.ts +6 -0
  13. package/dist/integration.js +7 -10
  14. package/dist/interfaces/asserts.d.ts +9 -0
  15. package/dist/interfaces/publicService.d.ts +11 -0
  16. package/dist/interfaces/slack.d.ts +6 -0
  17. package/dist/network.d.ts +17 -0
  18. package/dist/network.js +64 -72
  19. package/dist/payment.d.ts +8 -0
  20. package/dist/payment.js +19 -22
  21. package/dist/pdfUtils.d.ts +6 -0
  22. package/dist/pdfUtils.js +7 -10
  23. package/dist/phoneticChecker/index.d.ts +9 -0
  24. package/dist/phoneticChecker/index.js +18 -23
  25. package/dist/phoneticChecker/metaphone.js +38 -41
  26. package/dist/publicService.d.ts +59 -0
  27. package/dist/publicService.js +137 -150
  28. package/dist/random.d.ts +7 -0
  29. package/dist/random.js +11 -10
  30. package/dist/session.d.ts +7 -0
  31. package/dist/session.js +10 -19
  32. package/dist/slackFormatting.d.ts +13 -0
  33. package/dist/slackFormatting.js +41 -24
  34. package/dist/typeUtils.d.ts +8 -0
  35. package/dist/typeUtils.js +13 -15
  36. package/package.json +42 -27
  37. package/dist/address.js.map +0 -1
  38. package/dist/applicationUtils.js.map +0 -1
  39. package/dist/asserts.js.map +0 -1
  40. package/dist/dictionaries/phoneCodes.js.map +0 -1
  41. package/dist/guards.js.map +0 -1
  42. package/dist/index.js.map +0 -1
  43. package/dist/integration.js.map +0 -1
  44. package/dist/interfaces/applicationUtils.js +0 -3
  45. package/dist/interfaces/applicationUtils.js.map +0 -1
  46. package/dist/interfaces/asserts.js +0 -3
  47. package/dist/interfaces/asserts.js.map +0 -1
  48. package/dist/interfaces/error.js +0 -3
  49. package/dist/interfaces/error.js.map +0 -1
  50. package/dist/interfaces/publicService.js +0 -12
  51. package/dist/interfaces/publicService.js.map +0 -1
  52. package/dist/interfaces/slack.js +0 -3
  53. package/dist/interfaces/slack.js.map +0 -1
  54. package/dist/network.js.map +0 -1
  55. package/dist/payment.js.map +0 -1
  56. package/dist/pdfUtils.js.map +0 -1
  57. package/dist/phoneticChecker/index.js.map +0 -1
  58. package/dist/phoneticChecker/metaphone.js.map +0 -1
  59. package/dist/publicService.js.map +0 -1
  60. package/dist/random.js.map +0 -1
  61. package/dist/session.js.map +0 -1
  62. package/dist/slackFormatting.js.map +0 -1
  63. package/dist/typeUtils.js.map +0 -1
  64. package/dist/types/address.d.ts +0 -4
  65. package/dist/types/applicationUtils.d.ts +0 -212
  66. package/dist/types/asserts.d.ts +0 -2
  67. package/dist/types/dictionaries/phoneCodes.d.ts +0 -2
  68. package/dist/types/guards.d.ts +0 -18
  69. package/dist/types/index.d.ts +0 -86
  70. package/dist/types/integration.d.ts +0 -3
  71. package/dist/types/interfaces/applicationUtils.d.ts +0 -1
  72. package/dist/types/interfaces/asserts.d.ts +0 -5
  73. package/dist/types/interfaces/error.d.ts +0 -7
  74. package/dist/types/interfaces/publicService.d.ts +0 -15
  75. package/dist/types/interfaces/slack.d.ts +0 -3
  76. package/dist/types/network.d.ts +0 -13
  77. package/dist/types/payment.d.ts +0 -5
  78. package/dist/types/pdfUtils.d.ts +0 -3
  79. package/dist/types/phoneticChecker/index.d.ts +0 -7
  80. package/dist/types/phoneticChecker/metaphone.d.ts +0 -6
  81. package/dist/types/publicService.d.ts +0 -55
  82. package/dist/types/random.d.ts +0 -5
  83. package/dist/types/session.d.ts +0 -3
  84. package/dist/types/slackFormatting.d.ts +0 -7
  85. package/dist/types/typeUtils.d.ts +0 -5
@@ -1,679 +1,577 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { compare } from "compare-versions";
4
+ import lodash from "lodash";
5
+ import { DateTime } from "luxon";
6
+ import mapAgeCleaner from "map-age-cleaner";
7
+ import { ApiError, BadRequestError } from "@diia-inhouse/errors";
8
+ import { ActionVersion, Gender, HttpStatusCode, SessionType } from "@diia-inhouse/types";
9
+ //#region src/applicationUtils.ts
10
+ const { camelCase, cloneDeep, kebabCase, startCase } = lodash;
11
+ var ApplicationUtils = class ApplicationUtils {
12
+ static documentTypeToCamelCaseExceptions = {
13
+ ["student-id-card"]: "studentCard",
14
+ ["internal-passport"]: "idCard"
15
+ };
16
+ static camelCaseToDocumentTypeExceptions = {
17
+ ["studentCard"]: "student-id-card",
18
+ ["idCard"]: "internal-passport"
19
+ };
20
+ static defaultItnDate = "31.12.1899";
21
+ static defaultFormat = "dd.MM.yyyy";
22
+ static cyrillicToLatin = new Map([
23
+ ["і", "i"],
24
+ ["е", "e"],
25
+ ["у", "y"],
26
+ ["и", "u"],
27
+ ["о", "o"],
28
+ ["р", "p"],
29
+ ["а", "a"],
30
+ ["к", "k"],
31
+ ["х", "x"],
32
+ ["с", "c"],
33
+ ["І", "I"],
34
+ ["Е", "E"],
35
+ ["Т", "T"],
36
+ ["О", "O"],
37
+ ["Р", "P"],
38
+ ["А", "A"],
39
+ ["Н", "H"],
40
+ ["К", "K"],
41
+ ["Х", "X"],
42
+ ["С", "C"],
43
+ ["В", "B"],
44
+ ["М", "M"],
45
+ ["У", "Y"]
46
+ ]);
47
+ static nameComponentsToLowerCase = [
48
+ "огли",
49
+ "оглу",
50
+ "заде",
51
+ "кизи",
52
+ "бей",
53
+ "бек",
54
+ "паша",
55
+ "хан",
56
+ "шах",
57
+ "мелік",
58
+ "зуль",
59
+ "ібн",
60
+ "тер",
61
+ "аль",
62
+ "ель",
63
+ "аш",
64
+ "ас",
65
+ "ад",
66
+ "ель",
67
+ "ес"
68
+ ];
69
+ static documentTypeToCamelCase(documentType) {
70
+ return ApplicationUtils.documentTypeToCamelCaseExceptions[documentType] || camelCase(documentType);
71
+ }
72
+ static camelCaseToDocumentType(camelCaseDocumentType) {
73
+ return ApplicationUtils.camelCaseToDocumentTypeExceptions[camelCaseDocumentType] || kebabCase(camelCaseDocumentType);
74
+ }
75
+ static isItnChecksumValid(itn) {
76
+ if (itn.length !== 10) return false;
77
+ const ITN_POLY = [
78
+ -1,
79
+ 5,
80
+ 7,
81
+ 9,
82
+ 4,
83
+ 6,
84
+ 10,
85
+ 5,
86
+ 7
87
+ ];
88
+ const digits = Array.from(itn, Number);
89
+ const checksum = ITN_POLY.reduce((a, x, i) => a + x * digits[i], 0);
90
+ const controlDigit = (checksum - 11 * Math.floor(checksum / 11)) % 10;
91
+ return digits.at(-1) === controlDigit;
92
+ }
93
+ static getBirthDayFromItn(itn) {
94
+ if (itn.length !== 10) return "";
95
+ const days = Number.parseInt(itn.slice(0, 5), 10);
96
+ return DateTime.fromFormat(this.defaultItnDate, this.defaultFormat).plus({ days }).toFormat(this.defaultFormat);
97
+ }
98
+ static getGenderFromItn(itn) {
99
+ return Number(itn.at(-2)) % 2 === 0 ? Gender.female : Gender.male;
100
+ }
101
+ static isItnFormatValid(itn) {
102
+ if (/^0{1,20}$/.test(itn) || itn === "null" || !/^\d{10}$/.test(itn)) return false;
103
+ return true;
104
+ }
105
+ static capitalizeName(name) {
106
+ if (!name) return "";
107
+ const loweredNameParts = name.toLowerCase().split(" ");
108
+ if (loweredNameParts.length === 1) {
109
+ const [namePart] = loweredNameParts;
110
+ if (this.nameComponentsToLowerCase.includes(namePart)) return this.capitalizeFirstLetter(namePart);
111
+ }
112
+ return loweredNameParts.map((word) => word.split("-").map((namePart) => this.nameComponentsToLowerCase.includes(namePart) ? namePart : this.capitalizeFirstLetter(namePart)).join("-")).join(" ");
113
+ }
114
+ static capitalizeFirstLetter(str) {
115
+ if (typeof str !== "string") return "";
116
+ return `${str.charAt(0).toUpperCase()}${str.slice(1)}`;
117
+ }
118
+ static lowerFirstLetter(str) {
119
+ if (typeof str !== "string") return "";
120
+ return `${str.charAt(0).toLowerCase()}${str.slice(1)}`;
121
+ }
122
+ static mapCyrillic(value) {
123
+ return value.split("").map((char) => this.cyrillicToLatin.get(char) || char).join("");
124
+ }
125
+ static mapLatin(value) {
126
+ const latinToCyrillic = new Map(Array.from(this.cyrillicToLatin).map(([cyr, lat]) => [lat, cyr]));
127
+ return value.split("").map((char) => latinToCyrillic.get(char) || char).join("");
128
+ }
129
+ /**
130
+ * Validates the Ukrainian IBAN number by format and checksum.
131
+ */
132
+ static isIbanNumberValid(iban) {
133
+ const CODE_LENGTHS = { UA: 29 };
134
+ const code = iban.match(/^([A-Z]{2})(\d{2})([\dA-Z]+)$/);
135
+ if (!code || iban.length !== CODE_LENGTHS[code[1]]) return false;
136
+ const digits = (code[3] + code[1] + code[2]).replaceAll(/[A-Z]/g, (letter) => {
137
+ const letterCode = letter.codePointAt(0);
138
+ return letterCode ? (letterCode - 55).toString() : NaN.toString();
139
+ });
140
+ return this.mod97(digits) === 1;
141
+ }
142
+ /**
143
+ * Extracts the bank code (МФО) from Ukrainian IBAN (4-10 digits). Does not validate the IBAN.
144
+ *
145
+ * @link https://en.wikipedia.org/wiki/International_Bank_Account_Number#IBAN_formats_by_country
146
+ * @example
147
+ * ```typescript
148
+ * const bankCode = extractBankCodeFromIban('UA833052991234567890123456789') // '305299'
149
+ * ```
150
+ */
151
+ static extractBankCodeFromIban(iban) {
152
+ return iban.slice(4, 10);
153
+ }
154
+ static getStreetName(street, streetType) {
155
+ if (!street) return "";
156
+ return streetType ? `${streetType} ${street}`.trim() : street;
157
+ }
158
+ static toHashedFilesWithSignatures(hashedFiles, signedItems) {
159
+ if (!hashedFiles?.length) throw new BadRequestError("Hashed files array is empty");
160
+ if (hashedFiles.length !== signedItems.length) throw new Error("Number of hashed files and signed elements are not matched");
161
+ const fileNames = new Set(hashedFiles.map(({ fileName }) => fileName));
162
+ const signatureByFilename = /* @__PURE__ */ new Map();
163
+ const signatures = [];
164
+ for (const { name, signature } of signedItems) {
165
+ if (!fileNames.has(name)) throw new Error(`Provided name doesn't match with a hashed file: ${name}`);
166
+ signatureByFilename.set(name, signature);
167
+ signatures.push(signature);
168
+ }
169
+ return [hashedFiles.map(({ fileName, fileHash }) => ({
170
+ name: fileName,
171
+ hash: fileHash,
172
+ signature: signatureByFilename.get(fileName)
173
+ })), signatures];
174
+ }
175
+ static handleError(err, cb) {
176
+ if (err instanceof ApiError) return cb(err);
177
+ if (err instanceof Error) return cb(this.toApiError(err));
178
+ const message = `Unexpected error caused: ${String(err)}`;
179
+ const wrappedError = new Error(message);
180
+ return cb(this.toApiError(wrappedError));
181
+ }
182
+ static makeSession(data) {
183
+ const { sessionType } = data;
184
+ switch (sessionType) {
185
+ case SessionType.User: return {
186
+ sessionType,
187
+ user: data
188
+ };
189
+ case SessionType.EResident: return {
190
+ sessionType,
191
+ user: data
192
+ };
193
+ case SessionType.EResidentApplicant: return {
194
+ sessionType,
195
+ user: data
196
+ };
197
+ case SessionType.Acquirer: return {
198
+ sessionType,
199
+ acquirer: data
200
+ };
201
+ case SessionType.PortalUser: return {
202
+ sessionType,
203
+ user: data
204
+ };
205
+ case SessionType.Partner: return {
206
+ sessionType,
207
+ partner: data
208
+ };
209
+ case SessionType.Temporary: return {
210
+ sessionType,
211
+ temporary: data
212
+ };
213
+ case SessionType.ServiceEntrance: return {
214
+ sessionType,
215
+ entrance: data
216
+ };
217
+ case SessionType.ServiceUser: return {
218
+ sessionType,
219
+ serviceUser: data
220
+ };
221
+ default: throw new TypeError(`Unhandled sessionType: ${String(sessionType)}`);
222
+ }
223
+ }
224
+ static getActionNameWithVersion(actionName, actionVersion = ActionVersion.V0) {
225
+ return `${actionName}.${actionVersion}`;
226
+ }
227
+ static getGreeting(fName, skipEmogi) {
228
+ const text = `Вітаємо, ${fName}`;
229
+ return skipEmogi ? `${text}!` : `${text} 👋`;
230
+ }
231
+ /**
232
+ * Get age from birth date. Powered by {@link https://moment.github.io/luxon/#/ luxon}
233
+ *
234
+ * @param birthDay Input birth date.
235
+ * @param params Additional parameters. Default: `{ format: 'dd.MM.yyyy', unitOfTime: 'years' }`
236
+ * @param params.format Input date format (https://moment.github.io/luxon/#/formatting?id=table-of-tokens). Default - 'dd.MM.yyyy'
237
+ * @param params.unitOfTime Unit of time to get the difference in. Default - 'years'
238
+ * @param params.relativeTo Date to compare the input date to. Default - current date
239
+ *
240
+ * @returns Age in years if other `unitOfTime` is not specified
241
+ * @throws If input date is invalid
242
+ */
243
+ static getAge(birthDay, params = {}) {
244
+ const { format = "dd.MM.yyyy", unitOfTime = "years", relativeTo } = params;
245
+ const birthdayDate = DateTime.fromFormat(birthDay, format);
246
+ if (!birthdayDate.isValid) throw new Error("Invalid user birthday");
247
+ return (relativeTo ? DateTime.fromISO(relativeTo) : DateTime.now()).diff(birthdayDate, [unitOfTime, "milliseconds"])[unitOfTime];
248
+ }
249
+ static getFullName(lastName, firstName, middleName, separator = " ") {
250
+ return [
251
+ lastName,
252
+ firstName,
253
+ middleName
254
+ ].filter(Boolean).join(separator).trim();
255
+ }
256
+ static getShortName(lastName, firstName, middleName) {
257
+ const shortMiddleName = middleName ? ` ${middleName.charAt(0).toUpperCase()}.` : "";
258
+ return `${lastName} ${firstName.charAt(0).toUpperCase()}.${shortMiddleName}`.trim();
259
+ }
260
+ static getUserFullName(user) {
261
+ const { lName, fName, mName } = user;
262
+ return `${lName} ${fName} ${mName || ""}`.trim();
263
+ }
264
+ static getPassportFullName({ lastNameUA, firstNameUA, middleNameUA }) {
265
+ return `${lastNameUA} ${firstNameUA} ${middleNameUA || ""}`.trim();
266
+ }
267
+ static getChildFullName({ child: { lastName, firstName, middleName } }) {
268
+ return `${lastName} ${firstName} ${middleName || ""}`.trim();
269
+ }
270
+ static encodeObjectToBase64(object) {
271
+ return Buffer.from(JSON.stringify(object)).toString("base64");
272
+ }
273
+ static decodeObjectFromBase64(encodedObject) {
274
+ return JSON.parse(Buffer.from(encodedObject, "base64").toString());
275
+ }
276
+ /**
277
+ * Format string or Date to the specified string format. Powered by {@link https://moment.github.io/luxon/#/ luxon}
278
+ *
279
+ * @param date Input date
280
+ * @param format Output date format (https://moment.github.io/luxon/#/formatting?id=table-of-tokens)
281
+ * @param fromFormat Input date format (https://moment.github.io/luxon/#/formatting?id=table-of-tokens). Used for string `date`. Default - 'dd.MM.yyyy'
282
+ *
283
+ * @throws If input date is invalid
284
+ */
285
+ static formatDate(date, format, fromFormat = "dd.MM.yyyy") {
286
+ const convertedDate = date instanceof Date ? DateTime.fromJSDate(date) : DateTime.fromFormat(date, fromFormat);
287
+ const { isValid, invalidReason, invalidExplanation } = convertedDate;
288
+ if (!isValid) throw new Error(`Invalid date -> ${invalidReason}: ${invalidExplanation}`);
289
+ return convertedDate.setLocale("uk-UA").toFormat(format);
290
+ }
291
+ /**
292
+ * Check if the input date is valid. Powered by {@link https://moment.github.io/luxon/#/ luxon}
293
+ *
294
+ * @param date Input date.
295
+ * @param format `date` format (https://moment.github.io/luxon/#/formatting?id=table-of-tokens). Default - 'yyyy-MM-dd'
296
+ *
297
+ * @throws If input date is invalid
298
+ */
299
+ static validateDate(date, format = "yyyy-MM-dd") {
300
+ const parsedDate = DateTime.fromFormat(date, format);
301
+ if (!parsedDate.isValid) throw new BadRequestError(`Invalid date: ${parsedDate.invalidReason} ${parsedDate.invalidExplanation}`);
302
+ }
303
+ static formatPhoneNumber(rawPhone, separator = " ", prefix = "+") {
304
+ return prefix + rawPhone.replace(/^\+?(\d{2})(\d{3})(\d{3})(\d{2})(\d+)$/, [
305
+ "$1",
306
+ "$2",
307
+ "$3",
308
+ "$4",
309
+ "$5"
310
+ ].join(separator));
311
+ }
312
+ /**
313
+ * Format number as: "XX XXX,XX {units}".
314
+ *
315
+ * @param amount Input number
316
+ * @param units Unit to be append at the end of the formatted number
317
+ * @param minimumFractionDigits Minimum number of fraction digits
318
+ */
319
+ static formatAmountWithThousandsSeparator(amount, units, minimumFractionDigits) {
320
+ const result = amount.toLocaleString("uk-UA", {
321
+ useGrouping: true,
322
+ minimumFractionDigits
323
+ }).replaceAll("\xA0", " ");
324
+ return units ? `${result} ${units}` : result;
325
+ }
326
+ static convertToPennies(amount) {
327
+ return this.toDecimalPlaces(amount * 100, 0);
328
+ }
329
+ static convertFromPennies(amount) {
330
+ return this.toDecimalPlaces(amount / 100, 2);
331
+ }
332
+ static multiplySum(amount, multiplier, decimalPlaces) {
333
+ const multipliedSum = amount * 100 * multiplier / 100;
334
+ return this.toDecimalPlaces(multipliedSum, decimalPlaces);
335
+ }
336
+ static toDecimalPlaces(amount, decimalPlaces = 2) {
337
+ return Number.parseFloat(amount.toFixed(decimalPlaces));
338
+ }
339
+ static filterByAppVersions(items, headers) {
340
+ const { appVersion, platformType } = headers || {};
341
+ if (!appVersion || !platformType) return items;
342
+ return items.filter(({ appVersions }) => {
343
+ if (!appVersions) return true;
344
+ return ApplicationUtils.isAppVersionMatch(appVersion, platformType, appVersions);
345
+ });
346
+ }
347
+ static isAppVersionMatch(appVersion, platformType, appVersions) {
348
+ if (appVersions.versions?.[platformType]?.some((version) => compare(appVersion, version, "="))) return true;
349
+ const minVersion = appVersions.minVersion?.[platformType];
350
+ const maxVersion = appVersions.maxVersion?.[platformType];
351
+ if (minVersion && maxVersion) {
352
+ if (compare(appVersion, minVersion, ">=") && compare(appVersion, maxVersion, "<=")) return true;
353
+ return false;
354
+ }
355
+ if (minVersion && compare(appVersion, minVersion, ">=")) return true;
356
+ if (maxVersion && compare(appVersion, maxVersion, "<=")) return true;
357
+ return false;
358
+ }
359
+ static getFileName(name, id, requestDateTime, postFix) {
360
+ return [
361
+ name.trim(),
362
+ id.trim(),
363
+ requestDateTime?.trim(),
364
+ postFix?.trim()
365
+ ].filter(Boolean).join("-");
366
+ }
367
+ static sanitizeString(input) {
368
+ return input.replaceAll(/[^\da-zа-яєіїґ]/gi, "");
369
+ }
370
+ /**
371
+ * Returns the appropriate plural form of a string based on the specified numeric value and locale.
372
+ * Uses the pluralization rules for the provided locale to determine the correct form among the options.
373
+ *
374
+ * @param value - The number used to determine the plural form.
375
+ * @param one - The form for singular (e.g., "one item").
376
+ * @param few - The form for a few items (e.g., "few items").
377
+ * @param many - The form for many items (e.g., "many items").
378
+ * @param other - Optional. The fallback form if none of the specific plural rules match.
379
+ * Defaults to the 'many' form if not provided.
380
+ * @param locale - The locale used for pluralization rules. Defaults to 'uk' (Ukrainian).
381
+ * @returns The appropriate pluralized string based on the value.
382
+ *
383
+ * @example
384
+ * getPluralForm(1, 'яблуко', 'яблука', 'яблук'); // 'яблуко'
385
+ * getPluralForm(2, 'яблуко', 'яблука', 'яблук'); // 'яблука'
386
+ * getPluralForm(0, 'яблуко', 'яблука', 'яблук', 'плоди'); // 'плоди'
387
+ * getPluralForm(0, 'яблуко', 'яблука', 'яблук'); // 'яблук' (uses 'many' as default fallback)
388
+ */
389
+ static pluralizeString(value, one, few, many, other, locale = "uk") {
390
+ switch (new Intl.PluralRules(locale).select(value)) {
391
+ case "one": return one;
392
+ case "few": return few;
393
+ case "many": return many;
394
+ default: return other || many;
395
+ }
396
+ }
397
+ /**
398
+ * Returns the name of a service from a package.json in the current working directory (process.cwd()) converted to PascalCase.
399
+ * If the name is scoped, it will be returned without the scope.
400
+ * If the name is not found, an empty string is returned. Also, all whitespace characters are removed.
401
+ *
402
+ * @returns The name of the service in PascalCase.
403
+ *
404
+ * @example
405
+ * getServiceName() // 'my-service' -> 'MyService'
406
+ * getServiceName() // '@scope/my-service' -> 'MyService'
407
+ */
408
+ static getServiceName() {
409
+ try {
410
+ const packageName = this.getPackageJson().name || "";
411
+ let nameWithoutScope = packageName;
412
+ if (packageName.startsWith("@")) {
413
+ const parts = packageName.split("/");
414
+ if (parts.length > 1) nameWithoutScope = parts[1];
415
+ }
416
+ return startCase(camelCase(nameWithoutScope)).replaceAll(/\s/g, "");
417
+ } catch {
418
+ return "";
419
+ }
420
+ }
421
+ /**
422
+ * Returns the version of a service from a package.json in the current working directory (process.cwd()).
423
+ * If the version is not found, an empty string is returned.
424
+ *
425
+ * @returns The version of the service.
426
+ *
427
+ * @example
428
+ * getServiceVersion() // '1.143.0-rc.2'
429
+ */
430
+ static getServiceVersion() {
431
+ try {
432
+ return this.getPackageJson().version || "";
433
+ } catch {
434
+ return "";
435
+ }
436
+ }
437
+ /**
438
+ * Format string according to mask pattern
439
+ *
440
+ * Basic Examples:
441
+ * '12345' with '##-###' -> '12-345'
442
+ * 'AB345' with '##-###' -> 'AB-345'
443
+ * '123456' with '##.####' -> '12.3456'
444
+ * '12345' with '### ##' -> '123 45'
445
+ * 'ABCDEF' with '##-##-##' -> 'AB-CD-EF'
446
+ */
447
+ static formatMask(rawData, mask) {
448
+ if (!rawData || !mask) return rawData;
449
+ let result = "";
450
+ let dataIndex = 0;
451
+ for (const element of mask) {
452
+ if (dataIndex >= rawData.length) break;
453
+ if (element === "#") {
454
+ result += rawData[dataIndex];
455
+ dataIndex++;
456
+ } else result += element;
457
+ }
458
+ result += rawData.slice(dataIndex);
459
+ return result;
460
+ }
461
+ /**
462
+ * Memoize a function.
463
+ * The function is memoized by the arguments (`JSON.stringify(args)`) and the result is cached for the specified time to live in milliseconds.
464
+ * If the function is called with the same arguments again, the cached result is returned.
465
+ * If the function is called with the same arguments again after the time to live has expired, the function is called again and the result is cached again.
466
+ *
467
+ * @param fn - The function to memoize.
468
+ * @param ttl - The time to live for the cached value in milliseconds.
469
+ * @returns The memoized function.
470
+ */
471
+ static memoize(fn, ttl = Infinity) {
472
+ const promiseCache = /* @__PURE__ */ new Map();
473
+ const cache = /* @__PURE__ */ new Map();
474
+ mapAgeCleaner(cache);
475
+ const memoized = (async (...args) => {
476
+ const key = JSON.stringify(args);
477
+ const cachedPromise = promiseCache.get(key);
478
+ if (cachedPromise) return cachedPromise;
479
+ const cachedValue = cache.get(key);
480
+ if (cachedValue) return cachedValue.data;
481
+ try {
482
+ const promise = fn(...args);
483
+ promiseCache.set(key, promise);
484
+ const result = await promise;
485
+ cache.set(key, {
486
+ data: result,
487
+ maxAge: Date.now() + ttl
488
+ });
489
+ return result;
490
+ } finally {
491
+ promiseCache.delete(key);
492
+ }
493
+ });
494
+ return memoized;
495
+ }
496
+ /**
497
+ * Recursively encodes all string values in an object or string using encodeURIComponent.
498
+ * For objects, it creates a deep clone and processes all string values.
499
+ * For strings, it directly applies the encoding.
500
+ *
501
+ * @param data Input data to encode - can be an object or string
502
+ * @returns Deep cloned object with encoded string values, or encoded string if input was string
503
+ * @example
504
+ * ```typescript
505
+ * const data = { name: 'John Doe', email: 'john@example.com' }
506
+ * const encoded = ApplicationUtils.encodeValuesWithIterator(data)
507
+ * // Result: { name: 'John%20Doe', email: 'john%40example.com' }
508
+ * ```
509
+ */
510
+ static encodeValuesWithIterator(data) {
511
+ return this.processValues(data, encodeURIComponent);
512
+ }
513
+ /**
514
+ * Recursively decodes all string values in an object or string using decodeURIComponent.
515
+ * For objects, it creates a deep clone and processes all string values.
516
+ * For strings, it directly applies the decoding.
517
+ *
518
+ * @param data Input data to decode - can be an object or string
519
+ * @returns Deep cloned object with decoded string values, or decoded string if input was string
520
+ * @example
521
+ * ```typescript
522
+ * const data = { name: 'John%20Doe', email: 'john%40example.com' }
523
+ * const decoded = ApplicationUtils.decodeValuesWithIterator(data)
524
+ * // Result: { name: 'John Doe', email: 'john@example.com' }
525
+ * ```
526
+ */
527
+ static decodeValuesWithIterator(data) {
528
+ return this.processValues(data, decodeURIComponent);
529
+ }
530
+ static mod97(string) {
531
+ let checksum = string.slice(0, 2);
532
+ let fragment;
533
+ for (let offset = 2; offset < string.length; offset += 7) {
534
+ fragment = String(checksum) + string.slice(offset, offset + 7);
535
+ checksum = Number.parseInt(fragment, 10) % 97;
536
+ }
537
+ return checksum;
538
+ }
539
+ static toApiError(err) {
540
+ const { type, code, message, data = {}, keyValue } = err;
541
+ const processCode = data.processCode;
542
+ if (code === 11e3 && keyValue) data.keyValue = keyValue;
543
+ return new ApiError(message, code || HttpStatusCode.INTERNAL_SERVER_ERROR, data, processCode, type);
544
+ }
545
+ static getPackageJson() {
546
+ const packageJsonPath = path.resolve(process.cwd(), "package.json");
547
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
548
+ }
549
+ static processObject(obj, processor) {
550
+ if (typeof obj === "string") try {
551
+ return processor(obj);
552
+ } catch {
553
+ return obj;
554
+ }
555
+ if (Array.isArray(obj)) return obj.map((item) => {
556
+ return this.processObject(item, processor);
557
+ });
558
+ if (obj && typeof obj === "object") {
559
+ if (obj instanceof Date || obj instanceof RegExp || obj instanceof Map || obj instanceof Set || obj instanceof Error || obj instanceof URL || obj instanceof URLSearchParams) return obj;
560
+ const result = {};
561
+ for (const [key, value] of Object.entries(obj)) result[key] = this.processObject(value, processor);
562
+ return result;
563
+ }
564
+ return obj;
565
+ }
566
+ static processValues(data, processor) {
567
+ if (typeof data === "string") try {
568
+ return processor(data);
569
+ } catch {
570
+ return data;
571
+ }
572
+ const clonedData = cloneDeep(data);
573
+ return this.processObject(clonedData, processor);
574
+ }
4
575
  };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ApplicationUtils = void 0;
7
- const node_fs_1 = __importDefault(require("node:fs"));
8
- const node_path_1 = __importDefault(require("node:path"));
9
- const compare_versions_1 = require("compare-versions");
10
- const lodash_1 = require("lodash");
11
- const luxon_1 = require("luxon");
12
- const map_age_cleaner_1 = __importDefault(require("map-age-cleaner"));
13
- const errors_1 = require("@diia-inhouse/errors");
14
- const types_1 = require("@diia-inhouse/types");
15
- class ApplicationUtils {
16
- static documentTypeToCamelCaseExceptions = {
17
- ['student-id-card']: 'studentCard',
18
- ['internal-passport']: 'idCard',
19
- };
20
- static camelCaseToDocumentTypeExceptions = {
21
- ['studentCard']: 'student-id-card',
22
- ['idCard']: 'internal-passport',
23
- };
24
- static defaultItnDate = '31.12.1899';
25
- static defaultFormat = 'dd.MM.yyyy';
26
- static cyrillicToLatin = new Map([
27
- ['і', 'i'],
28
- ['е', 'e'],
29
- ['у', 'y'],
30
- ['и', 'u'],
31
- ['о', 'o'],
32
- ['р', 'p'],
33
- ['а', 'a'],
34
- ['к', 'k'],
35
- ['х', 'x'],
36
- ['с', 'c'],
37
- ['І', 'I'],
38
- ['Е', 'E'],
39
- ['Т', 'T'],
40
- ['О', 'O'],
41
- ['Р', 'P'],
42
- ['А', 'A'],
43
- ['Н', 'H'],
44
- ['К', 'K'],
45
- ['Х', 'X'],
46
- ['С', 'C'],
47
- ['В', 'B'],
48
- ['М', 'M'],
49
- ['У', 'Y'],
50
- ]);
51
- static nameComponentsToLowerCase = [
52
- 'огли',
53
- 'оглу',
54
- 'заде',
55
- 'кизи',
56
- 'бей',
57
- 'бек',
58
- 'паша',
59
- 'хан',
60
- 'шах',
61
- 'мелік',
62
- 'зуль',
63
- 'ібн',
64
- 'тер',
65
- 'аль',
66
- 'ель',
67
- 'аш',
68
- 'ас',
69
- 'ад',
70
- 'ель',
71
- 'ес',
72
- ];
73
- static documentTypeToCamelCase(documentType) {
74
- return ApplicationUtils.documentTypeToCamelCaseExceptions[documentType] || (0, lodash_1.camelCase)(documentType);
75
- }
76
- static camelCaseToDocumentType(camelCaseDocumentType) {
77
- return ApplicationUtils.camelCaseToDocumentTypeExceptions[camelCaseDocumentType] || (0, lodash_1.kebabCase)(camelCaseDocumentType);
78
- }
79
- static isItnChecksumValid(itn) {
80
- if (itn.length !== 10) {
81
- return false;
82
- }
83
- const ITN_POLY = [-1, 5, 7, 9, 4, 6, 10, 5, 7];
84
- const digits = [...itn].map(Number);
85
- const checksum = ITN_POLY.reduce((a, x, i) => a + x * digits[i], 0);
86
- const controlDigit = (checksum - 11 * Math.floor(checksum / 11)) % 10;
87
- const last = digits.at(-1);
88
- return last === controlDigit;
89
- }
90
- static getBirthDayFromItn(itn) {
91
- if (itn.length !== 10) {
92
- return '';
93
- }
94
- const days = Number.parseInt(itn.slice(0, 5), 10);
95
- return luxon_1.DateTime.fromFormat(this.defaultItnDate, this.defaultFormat).plus({ days }).toFormat(this.defaultFormat);
96
- }
97
- static getGenderFromItn(itn) {
98
- return Number(itn.at(-2)) % 2 === 0 ? types_1.Gender.female : types_1.Gender.male;
99
- }
100
- static isItnFormatValid(itn) {
101
- if (/^0{1,20}$/.test(itn) || itn === 'null' || !/^\d{10}$/.test(itn)) {
102
- return false;
103
- }
104
- return true;
105
- }
106
- static capitalizeName(name) {
107
- if (!name) {
108
- return '';
109
- }
110
- const loweredNameParts = name.toLowerCase().split(' ');
111
- if (loweredNameParts.length === 1) {
112
- const [namePart] = loweredNameParts;
113
- if (this.nameComponentsToLowerCase.includes(namePart)) {
114
- return this.capitalizeFirstLetter(namePart);
115
- }
116
- }
117
- return loweredNameParts
118
- .map((word) => word
119
- .split('-')
120
- .map((namePart) => this.nameComponentsToLowerCase.includes(namePart) ? namePart : this.capitalizeFirstLetter(namePart))
121
- .join('-'))
122
- .join(' ');
123
- }
124
- static capitalizeFirstLetter(str) {
125
- if (typeof str !== 'string') {
126
- return '';
127
- }
128
- return `${str.charAt(0).toUpperCase()}${str.slice(1)}`;
129
- }
130
- static lowerFirstLetter(str) {
131
- if (typeof str !== 'string') {
132
- return '';
133
- }
134
- return `${str.charAt(0).toLowerCase()}${str.slice(1)}`;
135
- }
136
- static mapCyrillic(value) {
137
- return value
138
- .split('')
139
- .map((char) => this.cyrillicToLatin.get(char) || char)
140
- .join('');
141
- }
142
- static mapLatin(value) {
143
- const latinToCyrillic = new Map(Array.from(this.cyrillicToLatin).map(([cyr, lat]) => [lat, cyr]));
144
- return value
145
- .split('')
146
- .map((char) => latinToCyrillic.get(char) || char)
147
- .join('');
148
- }
149
- /**
150
- * Validates the Ukrainian IBAN number by format and checksum.
151
- */
152
- static isIbanNumberValid(iban) {
153
- const CODE_LENGTHS = {
154
- UA: 29,
155
- };
156
- const code = iban.match(/^([A-Z]{2})(\d{2})([\dA-Z]+)$/);
157
- if (!code || iban.length !== CODE_LENGTHS[code[1]]) {
158
- return false;
159
- }
160
- const digits = (code[3] + code[1] + code[2]).replaceAll(/[A-Z]/g, (letter) => {
161
- const letterCode = letter.codePointAt(0);
162
- return letterCode ? (letterCode - 55).toString() : Number.NaN.toString();
163
- });
164
- return this.mod97(digits) === 1;
165
- }
166
- /**
167
- * Extracts the bank code (МФО) from Ukrainian IBAN (4-10 digits). Does not validate the IBAN.
168
- *
169
- * @link https://en.wikipedia.org/wiki/International_Bank_Account_Number#IBAN_formats_by_country
170
- * @example
171
- * ```typescript
172
- * const bankCode = extractBankCodeFromIban('UA833052991234567890123456789') // '305299'
173
- * ```
174
- */
175
- static extractBankCodeFromIban(iban) {
176
- return iban.slice(4, 10);
177
- }
178
- static getStreetName(street, streetType) {
179
- if (!street) {
180
- return '';
181
- }
182
- return streetType ? `${streetType} ${street}`.trim() : street;
183
- }
184
- static toHashedFilesWithSignatures(hashedFiles, signedItems) {
185
- if (!hashedFiles?.length) {
186
- throw new errors_1.BadRequestError('Hashed files array is empty');
187
- }
188
- if (hashedFiles.length !== signedItems.length) {
189
- throw new Error('Number of hashed files and signed elements are not matched');
190
- }
191
- const fileNames = new Set(hashedFiles.map(({ fileName }) => fileName));
192
- const signatureByFilename = new Map();
193
- const signatures = [];
194
- for (const { name, signature } of signedItems) {
195
- if (!fileNames.has(name)) {
196
- throw new Error(`Provided name doesn't match with a hashed file: ${name}`);
197
- }
198
- signatureByFilename.set(name, signature);
199
- signatures.push(signature);
200
- }
201
- const hashedFilesWithSignatures = hashedFiles.map(({ fileName, fileHash }) => ({
202
- name: fileName,
203
- hash: fileHash,
204
- signature: signatureByFilename.get(fileName),
205
- }));
206
- return [hashedFilesWithSignatures, signatures];
207
- }
208
- static handleError(err, cb) {
209
- if (err instanceof errors_1.ApiError) {
210
- return cb(err);
211
- }
212
- if (err instanceof Error) {
213
- return cb(this.toApiError(err));
214
- }
215
- const message = `Unexpected error caused: ${err}`;
216
- const wrappedError = new Error(message);
217
- return cb(this.toApiError(wrappedError));
218
- }
219
- static makeSession(data) {
220
- const { sessionType } = data;
221
- switch (sessionType) {
222
- case types_1.SessionType.User: {
223
- return { sessionType, user: data };
224
- }
225
- case types_1.SessionType.EResident: {
226
- return { sessionType, user: data };
227
- }
228
- case types_1.SessionType.EResidentApplicant: {
229
- return { sessionType, user: data };
230
- }
231
- case types_1.SessionType.Acquirer: {
232
- return { sessionType, acquirer: data };
233
- }
234
- case types_1.SessionType.PortalUser: {
235
- return { sessionType, user: data };
236
- }
237
- case types_1.SessionType.Partner: {
238
- return { sessionType, partner: data };
239
- }
240
- case types_1.SessionType.Temporary: {
241
- return { sessionType, temporary: data };
242
- }
243
- case types_1.SessionType.ServiceEntrance: {
244
- return { sessionType, entrance: data };
245
- }
246
- case types_1.SessionType.ServiceUser: {
247
- return { sessionType, serviceUser: data };
248
- }
249
- default: {
250
- const unhandledType = sessionType;
251
- throw new TypeError(`Unhandled sessionType: ${unhandledType}`);
252
- }
253
- }
254
- }
255
- static getActionNameWithVersion(actionName, actionVersion = types_1.ActionVersion.V0) {
256
- return `${actionName}.${actionVersion}`;
257
- }
258
- static getGreeting(fName, skipEmogi) {
259
- const text = `Вітаємо, ${fName}`;
260
- return skipEmogi ? `${text}!` : `${text} 👋`;
261
- }
262
- /**
263
- * Get age from birth date. Powered by {@link https://moment.github.io/luxon/#/ luxon}
264
- *
265
- * @param birthDay Input birth date.
266
- * @param params Additional parameters. Default: `{ format: 'dd.MM.yyyy', unitOfTime: 'years' }`
267
- * @param params.format Input date format (https://moment.github.io/luxon/#/formatting?id=table-of-tokens). Default - 'dd.MM.yyyy'
268
- * @param params.unitOfTime Unit of time to get the difference in. Default - 'years'
269
- * @param params.relativeTo Date to compare the input date to. Default - current date
270
- *
271
- * @returns Age in years if other `unitOfTime` is not specified
272
- * @throws If input date is invalid
273
- */
274
- static getAge(birthDay, params = {}) {
275
- const { format = 'dd.MM.yyyy', unitOfTime = 'years', relativeTo } = params;
276
- const birthdayDate = luxon_1.DateTime.fromFormat(birthDay, format);
277
- if (!birthdayDate.isValid) {
278
- throw new Error('Invalid user birthday');
279
- }
280
- const fromDate = relativeTo ? luxon_1.DateTime.fromISO(relativeTo) : luxon_1.DateTime.now();
281
- /* Added an array of "units" with milliseconds to ensure that all larger unit values are integers.
282
- For instance:
283
- birthDay = '24.03.2009'
284
- currentDate = '24.03.2023'
285
- DateTime.now().diff(birthdayDate, 'years')['years'] => 14.001228454102156
286
- // fix
287
- DateTime.now().diff(birthdayDate, ['years', 'milliseconds'])['years'] => 14
288
- DateTime.now().diff(birthdayDate, ['months', 'milliseconds'])['months'] => 168
289
- */
290
- return fromDate.diff(birthdayDate, [unitOfTime, 'milliseconds'])[unitOfTime];
291
- }
292
- static getFullName(lastName, firstName, middleName, separator = ' ') {
293
- return [lastName, firstName, middleName].filter(Boolean).join(separator).trim();
294
- }
295
- static getShortName(lastName, firstName, middleName) {
296
- const shortMiddleName = middleName ? ` ${middleName.charAt(0).toUpperCase()}.` : '';
297
- return `${lastName} ${firstName.charAt(0).toUpperCase()}.${shortMiddleName}`.trim();
298
- }
299
- static getUserFullName(user) {
300
- const { lName, fName, mName } = user;
301
- return `${lName} ${fName} ${mName || ''}`.trim();
302
- }
303
- static getPassportFullName({ lastNameUA, firstNameUA, middleNameUA, }) {
304
- return `${lastNameUA} ${firstNameUA} ${middleNameUA || ''}`.trim();
305
- }
306
- static getChildFullName({ child: { lastName, firstName, middleName }, }) {
307
- return `${lastName} ${firstName} ${middleName || ''}`.trim();
308
- }
309
- static encodeObjectToBase64(object) {
310
- return Buffer.from(JSON.stringify(object)).toString('base64');
311
- }
312
- static decodeObjectFromBase64(encodedObject) {
313
- return JSON.parse(Buffer.from(encodedObject, 'base64').toString());
314
- }
315
- /**
316
- * Format string or Date to the specified string format. Powered by {@link https://moment.github.io/luxon/#/ luxon}
317
- *
318
- * @param date Input date
319
- * @param format Output date format (https://moment.github.io/luxon/#/formatting?id=table-of-tokens)
320
- * @param fromFormat Input date format (https://moment.github.io/luxon/#/formatting?id=table-of-tokens). Used for string `date`. Default - 'dd.MM.yyyy'
321
- *
322
- * @throws If input date is invalid
323
- */
324
- static formatDate(date, format, fromFormat = 'dd.MM.yyyy') {
325
- const convertedDate = date instanceof Date ? luxon_1.DateTime.fromJSDate(date) : luxon_1.DateTime.fromFormat(date, fromFormat);
326
- const { isValid, invalidReason, invalidExplanation } = convertedDate;
327
- if (!isValid) {
328
- throw new Error(`Invalid date -> ${invalidReason}: ${invalidExplanation}`);
329
- }
330
- return convertedDate.setLocale('uk-UA').toFormat(format);
331
- }
332
- /**
333
- * Check if the input date is valid. Powered by {@link https://moment.github.io/luxon/#/ luxon}
334
- *
335
- * @param date Input date.
336
- * @param format `date` format (https://moment.github.io/luxon/#/formatting?id=table-of-tokens). Default - 'yyyy-MM-dd'
337
- *
338
- * @throws If input date is invalid
339
- */
340
- static validateDate(date, format = 'yyyy-MM-dd') {
341
- const parsedDate = luxon_1.DateTime.fromFormat(date, format);
342
- if (!parsedDate.isValid) {
343
- throw new errors_1.BadRequestError(`Invalid date: ${parsedDate.invalidReason} ${parsedDate.invalidExplanation}`);
344
- }
345
- }
346
- static formatPhoneNumber(rawPhone, separator = ' ', prefix = '+') {
347
- return prefix + rawPhone.replace(/^\+?(\d{2})(\d{3})(\d{3})(\d{2})(\d+)$/, ['$1', '$2', '$3', '$4', '$5'].join(separator));
348
- }
349
- /**
350
- * Format number as: "XX XXX,XX {units}".
351
- *
352
- * @param amount Input number
353
- * @param units Unit to be append at the end of the formatted number
354
- * @param minimumFractionDigits Minimum number of fraction digits
355
- */
356
- static formatAmountWithThousandsSeparator(amount, units, minimumFractionDigits) {
357
- // https://github.com/nodejs/help/issues/4068 (char "\u00A0" not equals with space in test asserts; reproduced in node 18.13, 18.15, 18.17)
358
- const result = amount.toLocaleString('uk-UA', { useGrouping: true, minimumFractionDigits }).replaceAll('\u00A0', ' ');
359
- return units ? `${result} ${units}` : result;
360
- }
361
- static convertToPennies(amount) {
362
- return this.toDecimalPlaces(amount * 100, 0);
363
- }
364
- static convertFromPennies(amount) {
365
- return this.toDecimalPlaces(amount / 100, 2);
366
- }
367
- static multiplySum(amount, multiplier, decimalPlaces) {
368
- const multipliedSum = (amount * 100 * multiplier) / 100;
369
- return this.toDecimalPlaces(multipliedSum, decimalPlaces);
370
- }
371
- static toDecimalPlaces(amount, decimalPlaces = 2) {
372
- return Number.parseFloat(amount.toFixed(decimalPlaces));
373
- }
374
- static filterByAppVersions(items, headers) {
375
- const { appVersion, platformType } = headers || {};
376
- if (!appVersion || !platformType) {
377
- return items;
378
- }
379
- return items.filter(({ appVersions }) => {
380
- if (!appVersions) {
381
- return true;
382
- }
383
- return ApplicationUtils.isAppVersionMatch(appVersion, platformType, appVersions);
384
- });
385
- }
386
- static isAppVersionMatch(appVersion, platformType, appVersions) {
387
- if (appVersions.versions?.[platformType]?.some((version) => (0, compare_versions_1.compare)(appVersion, version, '='))) {
388
- return true;
389
- }
390
- const minVersion = appVersions.minVersion?.[platformType];
391
- const maxVersion = appVersions.maxVersion?.[platformType];
392
- if (minVersion && maxVersion) {
393
- if ((0, compare_versions_1.compare)(appVersion, minVersion, '>=') && (0, compare_versions_1.compare)(appVersion, maxVersion, '<=')) {
394
- return true;
395
- }
396
- return false;
397
- }
398
- if (minVersion && (0, compare_versions_1.compare)(appVersion, minVersion, '>=')) {
399
- return true;
400
- }
401
- if (maxVersion && (0, compare_versions_1.compare)(appVersion, maxVersion, '<=')) {
402
- return true;
403
- }
404
- return false;
405
- }
406
- static getFileName(name, id, requestDateTime, postFix) {
407
- return [name.trim(), id.trim(), requestDateTime?.trim(), postFix?.trim()].filter(Boolean).join('-');
408
- }
409
- static sanitizeString(input) {
410
- return input.replaceAll(/[^\da-zа-яєіїґ]/gi, '');
411
- }
412
- /**
413
- * Returns the appropriate plural form of a string based on the specified numeric value and locale.
414
- * Uses the pluralization rules for the provided locale to determine the correct form among the options.
415
- *
416
- * @param value - The number used to determine the plural form.
417
- * @param one - The form for singular (e.g., "one item").
418
- * @param few - The form for a few items (e.g., "few items").
419
- * @param many - The form for many items (e.g., "many items").
420
- * @param other - Optional. The fallback form if none of the specific plural rules match.
421
- * Defaults to the 'many' form if not provided.
422
- * @param locale - The locale used for pluralization rules. Defaults to 'uk' (Ukrainian).
423
- * @returns The appropriate pluralized string based on the value.
424
- *
425
- * @example
426
- * getPluralForm(1, 'яблуко', 'яблука', 'яблук'); // 'яблуко'
427
- * getPluralForm(2, 'яблуко', 'яблука', 'яблук'); // 'яблука'
428
- * getPluralForm(0, 'яблуко', 'яблука', 'яблук', 'плоди'); // 'плоди'
429
- * getPluralForm(0, 'яблуко', 'яблука', 'яблук'); // 'яблук' (uses 'many' as default fallback)
430
- */
431
- static pluralizeString(value, one, few, many, other, locale = 'uk') {
432
- const rules = new Intl.PluralRules(locale);
433
- switch (rules.select(value)) {
434
- case 'one': {
435
- return one;
436
- }
437
- case 'few': {
438
- return few;
439
- }
440
- case 'many': {
441
- return many;
442
- }
443
- default: {
444
- return other || many;
445
- }
446
- }
447
- }
448
- /**
449
- * Returns the name of a service from a package.json in the current working directory (process.cwd()) converted to PascalCase.
450
- * If the name is scoped, it will be returned without the scope.
451
- * If the name is not found, an empty string is returned. Also, all whitespace characters are removed.
452
- *
453
- * @returns The name of the service in PascalCase.
454
- *
455
- * @example
456
- * getServiceName() // 'my-service' -> 'MyService'
457
- * getServiceName() // '@scope/my-service' -> 'MyService'
458
- */
459
- static getServiceName() {
460
- try {
461
- const packageName = this.getPackageJson().name || '';
462
- let nameWithoutScope = packageName;
463
- if (packageName.startsWith('@')) {
464
- const parts = packageName.split('/');
465
- if (parts.length > 1) {
466
- nameWithoutScope = parts[1];
467
- }
468
- }
469
- return (0, lodash_1.startCase)((0, lodash_1.camelCase)(nameWithoutScope)).replaceAll(/\s/g, '');
470
- }
471
- catch {
472
- return '';
473
- }
474
- }
475
- /**
476
- * Returns the version of a service from a package.json in the current working directory (process.cwd()).
477
- * If the version is not found, an empty string is returned.
478
- *
479
- * @returns The version of the service.
480
- *
481
- * @example
482
- * getServiceVersion() // '1.143.0-rc.2'
483
- */
484
- static getServiceVersion() {
485
- try {
486
- const packageJson = this.getPackageJson();
487
- return packageJson.version || '';
488
- }
489
- catch {
490
- return '';
491
- }
492
- }
493
- /**
494
- * Format string according to mask pattern
495
- *
496
- * Basic Examples:
497
- * '12345' with '##-###' -> '12-345'
498
- * 'AB345' with '##-###' -> 'AB-345'
499
- * '123456' with '##.####' -> '12.3456'
500
- * '12345' with '### ##' -> '123 45'
501
- * 'ABCDEF' with '##-##-##' -> 'AB-CD-EF'
502
- */
503
- static formatMask(rawData, mask) {
504
- if (!rawData || !mask) {
505
- return rawData;
506
- }
507
- let result = '';
508
- let dataIndex = 0;
509
- for (const element of mask) {
510
- if (dataIndex >= rawData.length) {
511
- break;
512
- }
513
- if (element === '#') {
514
- result += rawData[dataIndex];
515
- dataIndex++;
516
- }
517
- else {
518
- result += element;
519
- }
520
- }
521
- result += rawData.slice(dataIndex);
522
- return result;
523
- }
524
- /**
525
- * Memoize a function.
526
- * The function is memoized by the arguments (`JSON.stringify(args)`) and the result is cached for the specified time to live in milliseconds.
527
- * If the function is called with the same arguments again, the cached result is returned.
528
- * If the function is called with the same arguments again after the time to live has expired, the function is called again and the result is cached again.
529
- *
530
- * @param fn - The function to memoize.
531
- * @param ttl - The time to live for the cached value in milliseconds.
532
- * @returns The memoized function.
533
- */
534
- static memoize(fn, ttl = Infinity) {
535
- const promiseCache = new Map();
536
- const cache = new Map();
537
- (0, map_age_cleaner_1.default)(cache);
538
- const memoized = (async (...args) => {
539
- const key = JSON.stringify(args);
540
- const cachedPromise = promiseCache.get(key);
541
- if (cachedPromise) {
542
- return cachedPromise;
543
- }
544
- const cachedValue = cache.get(key);
545
- if (cachedValue) {
546
- return cachedValue.data;
547
- }
548
- try {
549
- const promise = fn(...args);
550
- promiseCache.set(key, promise);
551
- const result = await promise;
552
- cache.set(key, { data: result, maxAge: Date.now() + ttl });
553
- return result;
554
- }
555
- finally {
556
- promiseCache.delete(key);
557
- }
558
- });
559
- return memoized;
560
- }
561
- /**
562
- * Recursively encodes all string values in an object or string using encodeURIComponent.
563
- * For objects, it creates a deep clone and processes all string values.
564
- * For strings, it directly applies the encoding.
565
- *
566
- * @param data Input data to encode - can be an object or string
567
- * @returns Deep cloned object with encoded string values, or encoded string if input was string
568
- * @example
569
- * ```typescript
570
- * const data = { name: 'John Doe', email: 'john@example.com' }
571
- * const encoded = ApplicationUtils.encodeValuesWithIterator(data)
572
- * // Result: { name: 'John%20Doe', email: 'john%40example.com' }
573
- * ```
574
- */
575
- static encodeValuesWithIterator(data) {
576
- return this.processValues(data, encodeURIComponent);
577
- }
578
- /**
579
- * Recursively decodes all string values in an object or string using decodeURIComponent.
580
- * For objects, it creates a deep clone and processes all string values.
581
- * For strings, it directly applies the decoding.
582
- *
583
- * @param data Input data to decode - can be an object or string
584
- * @returns Deep cloned object with decoded string values, or decoded string if input was string
585
- * @example
586
- * ```typescript
587
- * const data = { name: 'John%20Doe', email: 'john%40example.com' }
588
- * const decoded = ApplicationUtils.decodeValuesWithIterator(data)
589
- * // Result: { name: 'John Doe', email: 'john@example.com' }
590
- * ```
591
- */
592
- static decodeValuesWithIterator(data) {
593
- return this.processValues(data, decodeURIComponent);
594
- }
595
- static removeUnderscoreFields(value) {
596
- if (Array.isArray(value)) {
597
- return value.map((item) => this.removeUnderscoreFields(item));
598
- }
599
- if (typeof value === 'object' && value !== null) {
600
- const objectField = {};
601
- for (const key in value) {
602
- if (!key.startsWith('_')) {
603
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
604
- objectField[key] = this.removeUnderscoreFields(value[key]);
605
- }
606
- }
607
- return objectField;
608
- }
609
- return value;
610
- }
611
- static mod97(string) {
612
- let checksum = string.slice(0, 2);
613
- let fragment;
614
- for (let offset = 2; offset < string.length; offset += 7) {
615
- fragment = String(checksum) + string.slice(offset, offset + 7);
616
- checksum = Number.parseInt(fragment, 10) % 97;
617
- }
618
- return checksum;
619
- }
620
- static toApiError(err) {
621
- const { type, code, message, data = {}, keyValue } = err;
622
- const processCode = data.processCode;
623
- if (code === 11000 && keyValue) {
624
- data.keyValue = keyValue;
625
- }
626
- return new errors_1.ApiError(message, code || types_1.HttpStatusCode.INTERNAL_SERVER_ERROR, data, processCode, type);
627
- }
628
- static getPackageJson() {
629
- const packageJsonPath = node_path_1.default.resolve(process.cwd(), 'package.json');
630
- // eslint-disable-next-line security/detect-non-literal-fs-filename
631
- return JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, 'utf8')); // nosemgrep: eslint.detect-non-literal-fs-filename
632
- }
633
- static processObject(obj, processor) {
634
- if (typeof obj === 'string') {
635
- try {
636
- return processor(obj);
637
- }
638
- catch {
639
- return obj;
640
- }
641
- }
642
- if (Array.isArray(obj)) {
643
- return obj.map((item) => {
644
- return this.processObject(item, processor);
645
- });
646
- }
647
- if (obj && typeof obj === 'object') {
648
- if (obj instanceof Date ||
649
- obj instanceof RegExp ||
650
- obj instanceof Map ||
651
- obj instanceof Set ||
652
- obj instanceof Error ||
653
- obj instanceof URL ||
654
- obj instanceof URLSearchParams) {
655
- return obj;
656
- }
657
- const result = {};
658
- for (const [key, value] of Object.entries(obj)) {
659
- result[key] = this.processObject(value, processor);
660
- }
661
- return result;
662
- }
663
- return obj;
664
- }
665
- static processValues(data, processor) {
666
- if (typeof data === 'string') {
667
- try {
668
- return processor(data);
669
- }
670
- catch {
671
- return data;
672
- }
673
- }
674
- const clonedData = (0, lodash_1.cloneDeep)(data);
675
- return this.processObject(clonedData, processor);
676
- }
677
- }
678
- exports.ApplicationUtils = ApplicationUtils;
679
- //# sourceMappingURL=applicationUtils.js.map
576
+ //#endregion
577
+ export { ApplicationUtils };