@diia-inhouse/utils 4.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENCE.md +59 -59
  2. package/dist/address.js +41 -0
  3. package/dist/address.js.map +1 -0
  4. package/dist/applicationUtils.js +319 -23
  5. package/dist/applicationUtils.js.map +1 -1
  6. package/dist/dictionaries/phoneCodes.js +2443 -0
  7. package/dist/dictionaries/phoneCodes.js.map +1 -0
  8. package/dist/guards.js +25 -0
  9. package/dist/guards.js.map +1 -1
  10. package/dist/index.js +41 -15
  11. package/dist/index.js.map +1 -1
  12. package/dist/interfaces/applicationUtils.js +3 -0
  13. package/dist/interfaces/applicationUtils.js.map +1 -0
  14. package/dist/interfaces/error.js +3 -0
  15. package/dist/interfaces/error.js.map +1 -0
  16. package/dist/interfaces/slack.js +3 -0
  17. package/dist/interfaces/slack.js.map +1 -0
  18. package/dist/phoneticChecker/index.js +6 -4
  19. package/dist/phoneticChecker/index.js.map +1 -1
  20. package/dist/phoneticChecker/metaphone.js +0 -1
  21. package/dist/phoneticChecker/metaphone.js.map +1 -1
  22. package/dist/publicService.js +118 -12
  23. package/dist/publicService.js.map +1 -1
  24. package/dist/random.js +1 -0
  25. package/dist/random.js.map +1 -1
  26. package/dist/session.js.map +1 -1
  27. package/dist/slackFormatting.js +24 -0
  28. package/dist/slackFormatting.js.map +1 -0
  29. package/dist/typeUtils.js +1 -0
  30. package/dist/typeUtils.js.map +1 -1
  31. package/dist/types/address.d.ts +4 -0
  32. package/dist/types/applicationUtils.d.ts +122 -5
  33. package/dist/types/dictionaries/phoneCodes.d.ts +2 -0
  34. package/dist/types/guards.d.ts +4 -1
  35. package/dist/types/index.d.ts +59 -1
  36. package/dist/types/interfaces/applicationUtils.d.ts +1 -0
  37. package/dist/types/interfaces/error.d.ts +7 -0
  38. package/dist/types/interfaces/publicService.d.ts +8 -0
  39. package/dist/types/interfaces/slack.d.ts +3 -0
  40. package/dist/types/phoneticChecker/index.d.ts +3 -2
  41. package/dist/types/publicService.d.ts +51 -3
  42. package/dist/types/random.d.ts +2 -0
  43. package/dist/types/slackFormatting.d.ts +7 -0
  44. package/package.json +34 -51
package/LICENCE.md CHANGED
@@ -18,40 +18,40 @@ or has expressed by any other means his willingness to license under the EUPL.
18
18
 
19
19
  In this Licence, the following terms have the following meaning:
20
20
 
21
- - ‘The Licence’: this Licence.
21
+ - ‘The Licence’: this Licence.
22
22
 
23
- - ‘The Original Work’: the work or software distributed or communicated by the
24
- Licensor under this Licence, available as Source Code and also as Executable
25
- Code as the case may be.
23
+ - ‘The Original Work’: the work or software distributed or communicated by the
24
+ Licensor under this Licence, available as Source Code and also as Executable
25
+ Code as the case may be.
26
26
 
27
- - ‘Derivative Works’: the works or software that could be created by the
28
- Licensee, based upon the Original Work or modifications thereof. This Licence
29
- does not define the extent of modification or dependence on the Original Work
30
- required in order to classify a work as a Derivative Work; this extent is
31
- determined by copyright law applicable in the country mentioned in Article 15.
27
+ - ‘Derivative Works’: the works or software that could be created by the
28
+ Licensee, based upon the Original Work or modifications thereof. This Licence
29
+ does not define the extent of modification or dependence on the Original Work
30
+ required in order to classify a work as a Derivative Work; this extent is
31
+ determined by copyright law applicable in the country mentioned in Article 15.
32
32
 
33
- - ‘The Work’: the Original Work or its Derivative Works.
33
+ - ‘The Work’: the Original Work or its Derivative Works.
34
34
 
35
- - ‘The Source Code’: the human-readable form of the Work which is the most
36
- convenient for people to study and modify.
35
+ - ‘The Source Code’: the human-readable form of the Work which is the most
36
+ convenient for people to study and modify.
37
37
 
38
- - ‘The Executable Code’: any code which has generally been compiled and which is
39
- meant to be interpreted by a computer as a program.
38
+ - ‘The Executable Code’: any code which has generally been compiled and which is
39
+ meant to be interpreted by a computer as a program.
40
40
 
41
- - ‘The Licensor’: the natural or legal person that distributes or communicates
42
- the Work under the Licence.
41
+ - ‘The Licensor’: the natural or legal person that distributes or communicates
42
+ the Work under the Licence.
43
43
 
44
- - ‘Contributor(s)’: any natural or legal person who modifies the Work under the
45
- Licence, or otherwise contributes to the creation of a Derivative Work.
44
+ - ‘Contributor(s)’: any natural or legal person who modifies the Work under the
45
+ Licence, or otherwise contributes to the creation of a Derivative Work.
46
46
 
47
- - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
48
- the Work under the terms of the Licence.
47
+ - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
48
+ the Work under the terms of the Licence.
49
49
 
50
- - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
51
- renting, distributing, communicating, transmitting, or otherwise making
52
- available, online or offline, copies of the Work or providing access to its
53
- essential functionalities at the disposal of any other natural or legal
54
- person.
50
+ - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
51
+ renting, distributing, communicating, transmitting, or otherwise making
52
+ available, online or offline, copies of the Work or providing access to its
53
+ essential functionalities at the disposal of any other natural or legal
54
+ person.
55
55
 
56
56
  2. Scope of the rights granted by the Licence
57
57
 
@@ -59,15 +59,15 @@ The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
59
59
  sublicensable licence to do the following, for the duration of copyright vested
60
60
  in the Original Work:
61
61
 
62
- - use the Work in any circumstance and for all usage,
63
- - reproduce the Work,
64
- - modify the Work, and make Derivative Works based upon the Work,
65
- - communicate to the public, including the right to make available or display
66
- the Work or copies thereof to the public and perform publicly, as the case may
67
- be, the Work,
68
- - distribute the Work or copies thereof,
69
- - lend and rent the Work or copies thereof,
70
- - sublicense rights in the Work or copies thereof.
62
+ - use the Work in any circumstance and for all usage,
63
+ - reproduce the Work,
64
+ - modify the Work, and make Derivative Works based upon the Work,
65
+ - communicate to the public, including the right to make available or display
66
+ the Work or copies thereof to the public and perform publicly, as the case may
67
+ be, the Work,
68
+ - distribute the Work or copies thereof,
69
+ - lend and rent the Work or copies thereof,
70
+ - sublicense rights in the Work or copies thereof.
71
71
 
72
72
  Those rights can be exercised on any media, supports and formats, whether now
73
73
  known or later invented, as far as the applicable law permits so.
@@ -240,43 +240,43 @@ their choice.
240
240
 
241
241
  Without prejudice to specific agreement between parties,
242
242
 
243
- - any litigation resulting from the interpretation of this License, arising
244
- between the European Union institutions, bodies, offices or agencies, as a
245
- Licensor, and any Licensee, will be subject to the jurisdiction of the Court
246
- of Justice of the European Union, as laid down in article 272 of the Treaty on
247
- the Functioning of the European Union,
243
+ - any litigation resulting from the interpretation of this License, arising
244
+ between the European Union institutions, bodies, offices or agencies, as a
245
+ Licensor, and any Licensee, will be subject to the jurisdiction of the Court
246
+ of Justice of the European Union, as laid down in article 272 of the Treaty on
247
+ the Functioning of the European Union,
248
248
 
249
- - any litigation arising between other parties and resulting from the
250
- interpretation of this License, will be subject to the exclusive jurisdiction
251
- of the competent court where the Licensor resides or conducts its primary
252
- business.
249
+ - any litigation arising between other parties and resulting from the
250
+ interpretation of this License, will be subject to the exclusive jurisdiction
251
+ of the competent court where the Licensor resides or conducts its primary
252
+ business.
253
253
 
254
254
  15. Applicable Law
255
255
 
256
256
  Without prejudice to specific agreement between parties,
257
257
 
258
- - this Licence shall be governed by the law of the European Union Member State
259
- where the Licensor has his seat, resides or has his registered office,
258
+ - this Licence shall be governed by the law of the European Union Member State
259
+ where the Licensor has his seat, resides or has his registered office,
260
260
 
261
- - this licence shall be governed by Belgian law if the Licensor has no seat,
262
- residence or registered office inside a European Union Member State.
261
+ - this licence shall be governed by Belgian law if the Licensor has no seat,
262
+ residence or registered office inside a European Union Member State.
263
263
 
264
264
  Appendix
265
265
 
266
266
  ‘Compatible Licences’ according to Article 5 EUPL are:
267
267
 
268
- - GNU General Public License (GPL) v. 2, v. 3
269
- - GNU Affero General Public License (AGPL) v. 3
270
- - Open Software License (OSL) v. 2.1, v. 3.0
271
- - Eclipse Public License (EPL) v. 1.0
272
- - CeCILL v. 2.0, v. 2.1
273
- - Mozilla Public Licence (MPL) v. 2
274
- - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
275
- - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
276
- works other than software
277
- - European Union Public Licence (EUPL) v. 1.1, v. 1.2
278
- - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
279
- Reciprocity (LiLiQ-R+).
268
+ - GNU General Public License (GPL) v. 2, v. 3
269
+ - GNU Affero General Public License (AGPL) v. 3
270
+ - Open Software License (OSL) v. 2.1, v. 3.0
271
+ - Eclipse Public License (EPL) v. 1.0
272
+ - CeCILL v. 2.0, v. 2.1
273
+ - Mozilla Public Licence (MPL) v. 2
274
+ - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
275
+ - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
276
+ works other than software
277
+ - European Union Public Licence (EUPL) v. 1.1, v. 1.2
278
+ - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
279
+ Reciprocity (LiLiQ-R+).
280
280
 
281
281
  The European Commission may update this Appendix to later versions of the above
282
282
  licences without producing a new version of the EUPL, as long as they provide
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AddressUtils = void 0;
4
+ class AddressUtils {
5
+ // For details see https://uk.wikipedia.org/wiki/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D0%BA%D0%BE%D0%B4%D1%96%D0%B2_%D0%9A%D0%9E%D0%90%D0%A2%D0%A3%D0%A3_%D0%B4%D0%BB%D1%8F_%D0%BE%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D0%B5%D0%B9
6
+ regionAtuIdByCode = {
7
+ '01': 1, // Автономна Республіка Крим
8
+ '05': 2, // Вінницька область
9
+ '07': 3, // Волинська область
10
+ '12': 4, // Дніпропетровська область
11
+ '14': 5, // Донецька область
12
+ '18': 6, // Житомирська область
13
+ '21': 7, // Закарпатська область
14
+ '23': 8, // Запорізька область
15
+ '26': 9, // Івано-Франківська область
16
+ '32': 10, // Київська область
17
+ '35': 11, // Кіровоградська область
18
+ '44': 12, // Луганська область
19
+ '46': 13, // Львівська область
20
+ '48': 14, // Миколаївська область
21
+ '51': 15, // Одеська область
22
+ '53': 16, // Полтавська область
23
+ '56': 17, // Рівненська область
24
+ '59': 18, // Сумська область
25
+ '61': 19, // Тернопільська область
26
+ '63': 20, // Харківська область
27
+ '65': 21, // Херсонська область
28
+ '68': 22, // Хмельницька область
29
+ '71': 23, // Черкаська область
30
+ '73': 24, // Чернівецька область
31
+ '74': 25, // Чернігівська область
32
+ '80': 26, // Київ
33
+ '85': 27, // Севастополь
34
+ };
35
+ getRegionAtuIdByKatottgCode(katottgCode) {
36
+ const code = katottgCode.slice(2, 4);
37
+ return this.regionAtuIdByCode[code] ?? null;
38
+ }
39
+ }
40
+ exports.AddressUtils = AddressUtils;
41
+ //# sourceMappingURL=address.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.js","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":";;;AAAA,MAAa,YAAY;IACrB,6NAA6N;IAC5M,iBAAiB,GAA2B;QACzD,IAAI,EAAE,CAAC,EAAE,4BAA4B;QACrC,IAAI,EAAE,CAAC,EAAE,oBAAoB;QAC7B,IAAI,EAAE,CAAC,EAAE,oBAAoB;QAC7B,IAAI,EAAE,CAAC,EAAE,2BAA2B;QACpC,IAAI,EAAE,CAAC,EAAE,mBAAmB;QAC5B,IAAI,EAAE,CAAC,EAAE,sBAAsB;QAC/B,IAAI,EAAE,CAAC,EAAE,uBAAuB;QAChC,IAAI,EAAE,CAAC,EAAE,qBAAqB;QAC9B,IAAI,EAAE,CAAC,EAAE,4BAA4B;QACrC,IAAI,EAAE,EAAE,EAAE,mBAAmB;QAC7B,IAAI,EAAE,EAAE,EAAE,yBAAyB;QACnC,IAAI,EAAE,EAAE,EAAE,oBAAoB;QAC9B,IAAI,EAAE,EAAE,EAAE,oBAAoB;QAC9B,IAAI,EAAE,EAAE,EAAE,uBAAuB;QACjC,IAAI,EAAE,EAAE,EAAE,kBAAkB;QAC5B,IAAI,EAAE,EAAE,EAAE,qBAAqB;QAC/B,IAAI,EAAE,EAAE,EAAE,qBAAqB;QAC/B,IAAI,EAAE,EAAE,EAAE,kBAAkB;QAC5B,IAAI,EAAE,EAAE,EAAE,wBAAwB;QAClC,IAAI,EAAE,EAAE,EAAE,qBAAqB;QAC/B,IAAI,EAAE,EAAE,EAAE,qBAAqB;QAC/B,IAAI,EAAE,EAAE,EAAE,sBAAsB;QAChC,IAAI,EAAE,EAAE,EAAE,oBAAoB;QAC9B,IAAI,EAAE,EAAE,EAAE,sBAAsB;QAChC,IAAI,EAAE,EAAE,EAAE,uBAAuB;QACjC,IAAI,EAAE,EAAE,EAAE,OAAO;QACjB,IAAI,EAAE,EAAE,EAAE,cAAc;KAC3B,CAAA;IAED,2BAA2B,CAAC,WAAmB;QAC3C,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAEpC,OAAO,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,IAAI,CAAA;IAC/C,CAAC;CACJ;AArCD,oCAqCC"}
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.ApplicationUtils = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
4
9
  const compare_versions_1 = require("compare-versions");
5
10
  const lodash_1 = require("lodash");
6
11
  const luxon_1 = require("luxon");
12
+ const map_age_cleaner_1 = __importDefault(require("map-age-cleaner"));
7
13
  const errors_1 = require("@diia-inhouse/errors");
8
14
  const types_1 = require("@diia-inhouse/types");
9
15
  class ApplicationUtils {
@@ -17,7 +23,6 @@ class ApplicationUtils {
17
23
  };
18
24
  static defaultItnDate = '31.12.1899';
19
25
  static defaultFormat = 'dd.MM.yyyy';
20
- static nameDelimiters = [' ', '-'];
21
26
  static cyrillicToLatin = new Map([
22
27
  ['і', 'i'],
23
28
  ['е', 'e'],
@@ -43,6 +48,28 @@ class ApplicationUtils {
43
48
  ['М', 'M'],
44
49
  ['У', 'Y'],
45
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
+ ];
46
73
  static documentTypeToCamelCase(documentType) {
47
74
  return ApplicationUtils.documentTypeToCamelCaseExceptions[documentType] || (0, lodash_1.camelCase)(documentType);
48
75
  }
@@ -77,21 +104,22 @@ class ApplicationUtils {
77
104
  return true;
78
105
  }
79
106
  static capitalizeName(name) {
80
- const foundDelimiters = this.nameDelimiters.filter((delimiter) => {
81
- const regex = new RegExp(delimiter);
82
- return regex.test(name);
83
- });
84
- let resultName = name?.toLowerCase();
85
- if (foundDelimiters.length > 0) {
86
- for (const foundDelimiter of foundDelimiters) {
87
- resultName = resultName
88
- .split(foundDelimiter)
89
- .map((partName) => ApplicationUtils.capitalizeFirstLetter(partName))
90
- .join(foundDelimiter);
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);
91
115
  }
92
- return resultName;
93
116
  }
94
- return ApplicationUtils.capitalizeFirstLetter(resultName);
117
+ return loweredNameParts
118
+ .map((word) => word
119
+ .split('-')
120
+ .map((namePart) => this.nameComponentsToLowerCase.includes(namePart) ? namePart : this.capitalizeFirstLetter(namePart))
121
+ .join('-'))
122
+ .join(' ');
95
123
  }
96
124
  static capitalizeFirstLetter(str) {
97
125
  if (typeof str !== 'string') {
@@ -118,6 +146,9 @@ class ApplicationUtils {
118
146
  .map((char) => latinToCyrillic.get(char) || char)
119
147
  .join('');
120
148
  }
149
+ /**
150
+ * Validates the Ukrainian IBAN number by format and checksum.
151
+ */
121
152
  static isIbanNumberValid(iban) {
122
153
  const CODE_LENGTHS = {
123
154
  UA: 29,
@@ -132,6 +163,18 @@ class ApplicationUtils {
132
163
  });
133
164
  return this.mod97(digits) === 1;
134
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
+ }
135
178
  static getStreetName(street, streetType) {
136
179
  if (!street) {
137
180
  return '';
@@ -185,9 +228,6 @@ class ApplicationUtils {
185
228
  case types_1.SessionType.EResidentApplicant: {
186
229
  return { sessionType, user: data };
187
230
  }
188
- case types_1.SessionType.CabinetUser: {
189
- return { sessionType, user: data };
190
- }
191
231
  case types_1.SessionType.Acquirer: {
192
232
  return { sessionType, acquirer: data };
193
233
  }
@@ -223,17 +263,21 @@ class ApplicationUtils {
223
263
  * Get age from birth date. Powered by {@link https://moment.github.io/luxon/#/ luxon}
224
264
  *
225
265
  * @param birthDay Input birth date.
226
- * @param format `birthDay` format (https://moment.github.io/luxon/#/formatting?id=table-of-tokens). Default - 'dd.MM.yyyy'
227
- * @param unitOfTime Unit of time to return. Default - 'years'
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
228
270
  *
229
271
  * @returns Age in years if other `unitOfTime` is not specified
230
272
  * @throws If input date is invalid
231
273
  */
232
- static getAge(birthDay, format = 'dd.MM.yyyy', unitOfTime = 'years') {
274
+ static getAge(birthDay, params = {}) {
275
+ const { format = 'dd.MM.yyyy', unitOfTime = 'years', relativeTo } = params;
233
276
  const birthdayDate = luxon_1.DateTime.fromFormat(birthDay, format);
234
277
  if (!birthdayDate.isValid) {
235
278
  throw new Error('Invalid user birthday');
236
279
  }
280
+ const fromDate = relativeTo ? luxon_1.DateTime.fromISO(relativeTo) : luxon_1.DateTime.now();
237
281
  /* Added an array of "units" with milliseconds to ensure that all larger unit values are integers.
238
282
  For instance:
239
283
  birthDay = '24.03.2009'
@@ -243,7 +287,7 @@ class ApplicationUtils {
243
287
  DateTime.now().diff(birthdayDate, ['years', 'milliseconds'])['years'] => 14
244
288
  DateTime.now().diff(birthdayDate, ['months', 'milliseconds'])['months'] => 168
245
289
  */
246
- return luxon_1.DateTime.now().diff(birthdayDate, [unitOfTime, 'milliseconds'])[unitOfTime];
290
+ return fromDate.diff(birthdayDate, [unitOfTime, 'milliseconds'])[unitOfTime];
247
291
  }
248
292
  static getFullName(lastName, firstName, middleName, separator = ' ') {
249
293
  return [lastName, firstName, middleName].filter(Boolean).join(separator).trim();
@@ -365,6 +409,205 @@ class ApplicationUtils {
365
409
  static sanitizeString(input) {
366
410
  return input.replaceAll(/[^\da-zа-яєіїґ]/gi, '');
367
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
+ }
368
611
  static mod97(string) {
369
612
  let checksum = string.slice(0, 2);
370
613
  let fragment;
@@ -375,8 +618,61 @@ class ApplicationUtils {
375
618
  return checksum;
376
619
  }
377
620
  static toApiError(err) {
378
- const { type, code, message, data } = err;
379
- return new errors_1.ApiError(message, code || types_1.HttpStatusCode.INTERNAL_SERVER_ERROR, data, data?.processCode, type);
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);
380
676
  }
381
677
  }
382
678
  exports.ApplicationUtils = ApplicationUtils;