@excofy/utils 1.0.0 → 1.0.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.
package/dist/index.cjs CHANGED
@@ -311,6 +311,21 @@ function createValidator() {
311
311
  }
312
312
  return validator;
313
313
  },
314
+ url(message) {
315
+ if (shouldSkipValidation()) {
316
+ return validator;
317
+ }
318
+ if (typeof current.value === "string") {
319
+ try {
320
+ new URL(current.value);
321
+ } catch {
322
+ current.pushError(message);
323
+ }
324
+ } else {
325
+ current.pushError(message);
326
+ }
327
+ return validator;
328
+ },
314
329
  uuid(message) {
315
330
  if (shouldSkipValidation()) {
316
331
  return validator;
@@ -341,6 +356,53 @@ function createValidator() {
341
356
  }
342
357
  return validator;
343
358
  }
359
+ },
360
+ asNumber(message) {
361
+ if (shouldSkipValidation()) return validator;
362
+ const value = current.value;
363
+ const num = typeof value === "number" ? value : Number(value);
364
+ if (Number.isNaN(num)) {
365
+ current.pushError(message);
366
+ } else {
367
+ current.value = num;
368
+ current.inputs[current.field] = num;
369
+ }
370
+ return validator;
371
+ },
372
+ asBoolean(message) {
373
+ if (shouldSkipValidation()) return validator;
374
+ const value = current.value;
375
+ let bool;
376
+ if (typeof value === "boolean") {
377
+ bool = value;
378
+ } else if (typeof value === "string") {
379
+ if (value.toLowerCase() === "true") bool = true;
380
+ else if (value.toLowerCase() === "false") bool = false;
381
+ }
382
+ if (typeof bool !== "boolean") {
383
+ current.pushError(message);
384
+ } else {
385
+ current.value = bool;
386
+ current.inputs[current.field] = bool;
387
+ }
388
+ return validator;
389
+ },
390
+ asStringArray(message) {
391
+ if (shouldSkipValidation()) return validator;
392
+ const value = current.value;
393
+ let arr;
394
+ if (Array.isArray(value)) {
395
+ arr = value.map((v) => String(v).trim());
396
+ } else if (typeof value === "string") {
397
+ arr = value.split(",").map((s) => s.trim()).filter(Boolean);
398
+ }
399
+ if (!arr || arr.some((item) => item === "")) {
400
+ current.pushError(message);
401
+ } else {
402
+ current.value = arr;
403
+ current.inputs[current.field] = arr;
404
+ }
405
+ return validator;
344
406
  }
345
407
  };
346
408
  return {
package/dist/index.d.cts CHANGED
@@ -55,11 +55,15 @@ declare function createValidator<TRaw extends Record<string, TInputValue>, TPars
55
55
  slug(message: string): /*elided*/ any;
56
56
  sanitize(): /*elided*/ any;
57
57
  sanitizeHTML(tags?: AllowedTag[]): /*elided*/ any;
58
+ url(message: string): /*elided*/ any;
58
59
  uuid(message: string): /*elided*/ any;
59
60
  oneOf: (types: string[], message: string) => /*elided*/ any;
60
61
  videoUrl: {
61
62
  youtube(message: string): /*elided*/ any;
62
63
  };
64
+ asNumber(message: string): /*elided*/ any;
65
+ asBoolean(message: string): /*elided*/ any;
66
+ asStringArray(message: string): /*elided*/ any;
63
67
  };
64
68
  };
65
69
  isNotRequired(): {
@@ -75,11 +79,15 @@ declare function createValidator<TRaw extends Record<string, TInputValue>, TPars
75
79
  slug(message: string): /*elided*/ any;
76
80
  sanitize(): /*elided*/ any;
77
81
  sanitizeHTML(tags?: AllowedTag[]): /*elided*/ any;
82
+ url(message: string): /*elided*/ any;
78
83
  uuid(message: string): /*elided*/ any;
79
84
  oneOf: (types: string[], message: string) => /*elided*/ any;
80
85
  videoUrl: {
81
86
  youtube(message: string): /*elided*/ any;
82
87
  };
88
+ asNumber(message: string): /*elided*/ any;
89
+ asBoolean(message: string): /*elided*/ any;
90
+ asStringArray(message: string): /*elided*/ any;
83
91
  };
84
92
  };
85
93
  };
package/dist/index.d.ts CHANGED
@@ -55,11 +55,15 @@ declare function createValidator<TRaw extends Record<string, TInputValue>, TPars
55
55
  slug(message: string): /*elided*/ any;
56
56
  sanitize(): /*elided*/ any;
57
57
  sanitizeHTML(tags?: AllowedTag[]): /*elided*/ any;
58
+ url(message: string): /*elided*/ any;
58
59
  uuid(message: string): /*elided*/ any;
59
60
  oneOf: (types: string[], message: string) => /*elided*/ any;
60
61
  videoUrl: {
61
62
  youtube(message: string): /*elided*/ any;
62
63
  };
64
+ asNumber(message: string): /*elided*/ any;
65
+ asBoolean(message: string): /*elided*/ any;
66
+ asStringArray(message: string): /*elided*/ any;
63
67
  };
64
68
  };
65
69
  isNotRequired(): {
@@ -75,11 +79,15 @@ declare function createValidator<TRaw extends Record<string, TInputValue>, TPars
75
79
  slug(message: string): /*elided*/ any;
76
80
  sanitize(): /*elided*/ any;
77
81
  sanitizeHTML(tags?: AllowedTag[]): /*elided*/ any;
82
+ url(message: string): /*elided*/ any;
78
83
  uuid(message: string): /*elided*/ any;
79
84
  oneOf: (types: string[], message: string) => /*elided*/ any;
80
85
  videoUrl: {
81
86
  youtube(message: string): /*elided*/ any;
82
87
  };
88
+ asNumber(message: string): /*elided*/ any;
89
+ asBoolean(message: string): /*elided*/ any;
90
+ asStringArray(message: string): /*elided*/ any;
83
91
  };
84
92
  };
85
93
  };
package/dist/index.js CHANGED
@@ -284,6 +284,21 @@ function createValidator() {
284
284
  }
285
285
  return validator;
286
286
  },
287
+ url(message) {
288
+ if (shouldSkipValidation()) {
289
+ return validator;
290
+ }
291
+ if (typeof current.value === "string") {
292
+ try {
293
+ new URL(current.value);
294
+ } catch {
295
+ current.pushError(message);
296
+ }
297
+ } else {
298
+ current.pushError(message);
299
+ }
300
+ return validator;
301
+ },
287
302
  uuid(message) {
288
303
  if (shouldSkipValidation()) {
289
304
  return validator;
@@ -314,6 +329,53 @@ function createValidator() {
314
329
  }
315
330
  return validator;
316
331
  }
332
+ },
333
+ asNumber(message) {
334
+ if (shouldSkipValidation()) return validator;
335
+ const value = current.value;
336
+ const num = typeof value === "number" ? value : Number(value);
337
+ if (Number.isNaN(num)) {
338
+ current.pushError(message);
339
+ } else {
340
+ current.value = num;
341
+ current.inputs[current.field] = num;
342
+ }
343
+ return validator;
344
+ },
345
+ asBoolean(message) {
346
+ if (shouldSkipValidation()) return validator;
347
+ const value = current.value;
348
+ let bool;
349
+ if (typeof value === "boolean") {
350
+ bool = value;
351
+ } else if (typeof value === "string") {
352
+ if (value.toLowerCase() === "true") bool = true;
353
+ else if (value.toLowerCase() === "false") bool = false;
354
+ }
355
+ if (typeof bool !== "boolean") {
356
+ current.pushError(message);
357
+ } else {
358
+ current.value = bool;
359
+ current.inputs[current.field] = bool;
360
+ }
361
+ return validator;
362
+ },
363
+ asStringArray(message) {
364
+ if (shouldSkipValidation()) return validator;
365
+ const value = current.value;
366
+ let arr;
367
+ if (Array.isArray(value)) {
368
+ arr = value.map((v) => String(v).trim());
369
+ } else if (typeof value === "string") {
370
+ arr = value.split(",").map((s) => s.trim()).filter(Boolean);
371
+ }
372
+ if (!arr || arr.some((item) => item === "")) {
373
+ current.pushError(message);
374
+ } else {
375
+ current.value = arr;
376
+ current.inputs[current.field] = arr;
377
+ }
378
+ return validator;
317
379
  }
318
380
  };
319
381
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@excofy/utils",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Biblioteca de utilitários para o Excofy",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -30,11 +30,13 @@
30
30
  "homepage": "https://github.com/excofy/utils#readme",
31
31
  "devDependencies": {
32
32
  "@biomejs/biome": "^1.9.4",
33
+ "@types/big.js": "^6.2.2",
33
34
  "@types/node": "^24.0.3",
34
35
  "tsup": "^8.5.0",
35
36
  "typescript": "^5.8.3"
36
37
  },
37
38
  "dependencies": {
39
+ "big.js": "^7.0.1",
38
40
  "xss": "^1.0.15"
39
41
  },
40
42
  "publishConfig": {
@@ -0,0 +1,31 @@
1
+ import Big from 'big.js';
2
+
3
+ /**
4
+ * Converts a decimal number to an integer by multiplying it by 100.
5
+ * Commonly used to convert monetary values from dollars to cents.
6
+ *
7
+ * @param {number} value - The decimal number to convert (e.g., 10.90).
8
+ * @returns {number} The integer value in cents (e.g., 1090).
9
+ */
10
+ export const toCents = (value?: number): number => {
11
+ if (!value) {
12
+ return 0;
13
+ }
14
+
15
+ return new Big(value).mul(100).round(0).toNumber();
16
+ };
17
+
18
+ /**
19
+ * Converts an integer back to a decimal by dividing it by 100.
20
+ * Commonly used to convert monetary values from cents to dollars.
21
+ *
22
+ * @param {number} value - The integer value in cents (e.g., 1090).
23
+ * @returns {number} The decimal value (e.g., 10.90).
24
+ */
25
+ export const toDecimal = (value?: number): number => {
26
+ if (!value) {
27
+ return 0;
28
+ }
29
+
30
+ return new Big(value).div(100).round(2).toNumber();
31
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Generates a unique slug by appending a numeric suffix to the base slug if necessary.
3
+ *
4
+ * If the base slug is already used (either exactly or with a numeric suffix),
5
+ * the function finds the highest existing suffix and returns the next one in sequence.
6
+ *
7
+ * @example
8
+ * generateUniqueSlug('my-product', ['my-product', 'my-product-1', 'my-product-2'])
9
+ * // returns 'my-product-3'
10
+ *
11
+ * generateUniqueSlug('item', ['item-1', 'item-2'])
12
+ * // returns 'item'
13
+ *
14
+ * @param {string} baseSlug - The initial slug to use as a base.
15
+ * @param {string[]} existingSlugs - A list of existing slugs to avoid duplicates.
16
+ * @returns {string} A unique slug not present in the existingSlugs list.
17
+ */
18
+ export const generateUniqueSlug = (
19
+ baseSlug: string,
20
+ existingSlugs: string[]
21
+ ): string => {
22
+ const suffixes = existingSlugs
23
+ .map((slug) => {
24
+ const match = slug.match(new RegExp(`^${baseSlug}-(\\d+)$`));
25
+ return match ? Number(match[1]) : slug === baseSlug ? 0 : null;
26
+ })
27
+ .filter((num): num is number => num !== null);
28
+
29
+ const nextSuffix = suffixes.length > 0 ? Math.max(...suffixes) + 1 : 0;
30
+
31
+ return nextSuffix === 0 ? baseSlug : `${baseSlug}-${nextSuffix}`;
32
+ };
33
+
34
+ /**
35
+ * Removes a trailing numeric suffix from a slug, if present.
36
+ *
37
+ * This is useful for extracting the base part of a slug before checking
38
+ * or generating new unique variations.
39
+ *
40
+ * @example
41
+ * removeTrailingNumber('product-3') // returns 'product'
42
+ * removeTrailingNumber('item') // returns 'item'
43
+ *
44
+ * @param {string} slug - The slug from which to remove the trailing number.
45
+ * @returns {string} The slug without the trailing numeric suffix.
46
+ */
47
+
48
+ export const removeTrailingNumber = (slug: string): string => {
49
+ return slug.replace(/-\d+$/, '');
50
+ };
@@ -253,7 +253,6 @@ export function createValidator<
253
253
  current.inputs[current.field] = transformed as TRaw[keyof TRaw];
254
254
  return validator;
255
255
  },
256
-
257
256
  slug(message: string) {
258
257
  if (shouldSkipValidation()) {
259
258
  return validator;
@@ -293,6 +292,24 @@ export function createValidator<
293
292
  return validator;
294
293
  },
295
294
 
295
+ url(message: string) {
296
+ if (shouldSkipValidation()) {
297
+ return validator;
298
+ }
299
+
300
+ if (typeof current.value === 'string') {
301
+ try {
302
+ new URL(current.value);
303
+ } catch {
304
+ current.pushError(message);
305
+ }
306
+ } else {
307
+ current.pushError(message);
308
+ }
309
+
310
+ return validator;
311
+ },
312
+
296
313
  uuid(message: string) {
297
314
  if (shouldSkipValidation()) {
298
315
  return validator;
@@ -335,6 +352,71 @@ export function createValidator<
335
352
  return validator;
336
353
  },
337
354
  },
355
+
356
+ asNumber(message: string) {
357
+ if (shouldSkipValidation()) return validator;
358
+
359
+ const value = current.value;
360
+ const num = typeof value === 'number' ? value : Number(value);
361
+
362
+ if (Number.isNaN(num)) {
363
+ current.pushError(message);
364
+ } else {
365
+ current.value = num;
366
+ current.inputs[current.field] = num as TRaw[keyof TRaw];
367
+ }
368
+
369
+ return validator;
370
+ },
371
+
372
+ asBoolean(message: string) {
373
+ if (shouldSkipValidation()) return validator;
374
+
375
+ const value = current.value;
376
+
377
+ let bool: boolean | undefined;
378
+ if (typeof value === 'boolean') {
379
+ bool = value;
380
+ } else if (typeof value === 'string') {
381
+ if (value.toLowerCase() === 'true') bool = true;
382
+ else if (value.toLowerCase() === 'false') bool = false;
383
+ }
384
+
385
+ if (typeof bool !== 'boolean') {
386
+ current.pushError(message);
387
+ } else {
388
+ current.value = bool;
389
+ current.inputs[current.field] = bool as TRaw[keyof TRaw];
390
+ }
391
+
392
+ return validator;
393
+ },
394
+
395
+ asStringArray(message: string) {
396
+ if (shouldSkipValidation()) return validator;
397
+
398
+ const value = current.value;
399
+
400
+ let arr: string[] | undefined;
401
+
402
+ if (Array.isArray(value)) {
403
+ arr = value.map((v) => String(v).trim());
404
+ } else if (typeof value === 'string') {
405
+ arr = value
406
+ .split(',')
407
+ .map((s) => s.trim())
408
+ .filter(Boolean);
409
+ }
410
+
411
+ if (!arr || arr.some((item) => item === '')) {
412
+ current.pushError(message);
413
+ } else {
414
+ current.value = arr;
415
+ current.inputs[current.field] = arr as TRaw[keyof TRaw];
416
+ }
417
+
418
+ return validator;
419
+ },
338
420
  };
339
421
 
340
422
  return {