@splendidlabz/utils 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @splendidlabz/utils
2
2
 
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Many many upgrades which I didn't document properly 😅. The details are in Github history, but too many details to talk about now.
8
+
3
9
  ## 1.5.1
4
10
 
5
11
  ### Patch Changes
@@ -1,11 +1,5 @@
1
- /**
2
- * Gets a cookie by name
3
- * Thanks to https://stackoverflow.com/a/21125098
4
- * @param {string} name Cookie name
5
- * @returns string Cookie value
6
- */
7
- export function getCookie(name: string): string;
1
+ export function getCookie(name: any): any;
8
2
  export namespace cookies {
9
- function get(name: any): string;
3
+ function get(name: any): any;
10
4
  function set(name: any, value: any, days: any): void;
11
5
  }
@@ -3,5 +3,5 @@
3
3
  * Ensures first character is a letter so it can be used in HTML ids and classes
4
4
  * This can actually be used everywhere, not just in the browser.
5
5
  */
6
- export function randomString(length: any): string;
6
+ export function randomString(length?: number): string;
7
7
  export function uuid(): `${string}-${string}-${string}-${string}-${string}`;
@@ -27,3 +27,4 @@ export function isArray(x: unknown): boolean;
27
27
  * notObject({}) // false
28
28
  */
29
29
  export function notObject(x: unknown): boolean;
30
+ export function getType(x: any): "object" | "string" | "integer" | "float" | "boolean" | "function" | "undefined" | "symbol" | "bigint" | "array" | "unknown";
@@ -0,0 +1 @@
1
+ export function isHashedValue(value: any, type?: string): boolean;
@@ -4,6 +4,7 @@ export * from "./checks.js";
4
4
  export * from "./date/index.js";
5
5
  export * from "./form/index.js";
6
6
  export * from "./functions/index.js";
7
+ export * from "./hash.js";
7
8
  export * from "./numbers/index.js";
8
9
  export * from "./objects/index.js";
9
10
  export * from "./promises/index.js";
@@ -1,2 +1,2 @@
1
- export function isEqual(a: any, b: any): any;
1
+ export function isEqual(a: any, b: any): boolean;
2
2
  export function notEqual(a: any, b: any): boolean;
@@ -7,4 +7,6 @@
7
7
  export function splitObject(obj: any, keys: any[]): {
8
8
  picked: {};
9
9
  omitted: any;
10
+ p: {};
11
+ o: any;
10
12
  };
@@ -1,4 +1,5 @@
1
1
  export * from "./convert-case/convert-case.js";
2
2
  export * from "./markdown.js";
3
+ export * from "./name.js";
3
4
  export * from "./pluralize.js";
4
5
  export * from "./query-string.js";
@@ -0,0 +1,5 @@
1
+ export function parseFullName(name: any): {
2
+ firstName: any;
3
+ middleName: any;
4
+ lastName: any;
5
+ };
@@ -1,2 +1 @@
1
- export function randomString(length?: number): Promise<string>;
2
- export function randomStringSync(length?: number): string;
1
+ export function randomString(length?: number): string;
package/dom/cookie.js CHANGED
@@ -8,15 +8,16 @@ export const cookies = {
8
8
  },
9
9
  }
10
10
 
11
- /**
12
- * Gets a cookie by name
13
- * Thanks to https://stackoverflow.com/a/21125098
14
- * @param {string} name Cookie name
15
- * @returns string Cookie value
16
- */
17
11
  export function getCookie(name) {
18
12
  const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
19
- if (match) return match[2]
13
+ if (!match) return null
14
+ const decoded = decodeURIComponent(match[2])
15
+
16
+ try {
17
+ return JSON.parse(decoded)
18
+ } catch (e) {
19
+ return decoded
20
+ }
20
21
  }
21
22
 
22
23
  // By Copilot. Needs testing before confirmation.
@@ -3,7 +3,7 @@
3
3
  * Ensures first character is a letter so it can be used in HTML ids and classes
4
4
  * This can actually be used everywhere, not just in the browser.
5
5
  */
6
- export function randomString(length) {
6
+ export function randomString(length = 10) {
7
7
  const firstLetter = String.fromCharCode(65 + Math.floor(Math.random() * 26))
8
8
  const others = Math.random()
9
9
  .toString(36)
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { randomString } from './random-string.js'
3
+
4
+ describe('randomString', () => {
5
+ it('should generate a random string', () => {
6
+ const str = randomString()
7
+ expect(str).toBeDefined()
8
+ expect(str.length).toBe(10)
9
+
10
+ // Ensure the first character is a letter
11
+ expect(str[0]).toMatch(/[a-zA-Z]/)
12
+ })
13
+ })
package/lib/checks.js CHANGED
@@ -35,3 +35,19 @@ export function isArray(x) {
35
35
  export function notObject(x) {
36
36
  return !isObject(x)
37
37
  }
38
+
39
+ export function getType(x) {
40
+ if (typeof x === 'string') return 'string'
41
+ if (typeof x === 'number') {
42
+ if (Number.isInteger(x)) return 'integer'
43
+ return 'float'
44
+ }
45
+ if (typeof x === 'boolean') return 'boolean'
46
+ if (typeof x === 'function') return 'function'
47
+ if (typeof x === 'undefined') return 'undefined'
48
+ if (typeof x === 'symbol') return 'symbol'
49
+ if (typeof x === 'bigint') return 'bigint'
50
+ if (Array.isArray(x)) return 'array'
51
+ if (typeof x === 'object') return 'object'
52
+ return 'unknown'
53
+ }
package/lib/hash.js ADDED
@@ -0,0 +1,18 @@
1
+ export function isHashedValue(value, type = 'sha256') {
2
+ if (typeof value !== 'string') return null
3
+ value = value.trim()
4
+ type = type.toLowerCase()
5
+
6
+ if (type === 'sha256') {
7
+ return value.length === 64 && /^[a-fA-F0-9]{64}$/.test(value)
8
+ }
9
+
10
+ if (type === 'sha1') {
11
+ return value.length === 40 && /^[a-fA-F0-9]{40}$/.test(value)
12
+ }
13
+
14
+ if (type === 'md5') {
15
+ return value.length === 32 && /^[a-fA-F0-9]{32}$/.test(value)
16
+ }
17
+ return null
18
+ }
@@ -0,0 +1,163 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { isHashedValue } from './hash.js'
3
+
4
+ describe('isHashedValue', () => {
5
+ describe('SHA256 hashes', () => {
6
+ it('returns true for valid SHA256 hash', () => {
7
+ const validSha256 =
8
+ 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
9
+ expect(isHashedValue(validSha256, 'sha256')).toBe(true)
10
+ expect(isHashedValue(validSha256)).toBe(true) // default type
11
+ })
12
+
13
+ it('returns true for valid SHA256 hash with uppercase letters', () => {
14
+ const validSha256 =
15
+ 'A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3'
16
+ expect(isHashedValue(validSha256, 'sha256')).toBe(true)
17
+ })
18
+
19
+ it('returns true for valid SHA256 hash with mixed case', () => {
20
+ const validSha256 =
21
+ 'A665a45920422F9d417E4867efdc4FB8a04a1F3fff1FA07e998E86f7f7A27ae3'
22
+ expect(isHashedValue(validSha256, 'sha256')).toBe(true)
23
+ })
24
+
25
+ it('returns false for SHA256 hash with wrong length', () => {
26
+ const shortHash =
27
+ 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae'
28
+ const longHash =
29
+ 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae33'
30
+ expect(isHashedValue(shortHash, 'sha256')).toBe(false)
31
+ expect(isHashedValue(longHash, 'sha256')).toBe(false)
32
+ })
33
+
34
+ it('returns false for SHA256 hash with invalid characters', () => {
35
+ const invalidHash =
36
+ 'g665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
37
+ expect(isHashedValue(invalidHash, 'sha256')).toBe(false)
38
+ })
39
+ })
40
+
41
+ describe('SHA1 hashes', () => {
42
+ it('returns true for valid SHA1 hash', () => {
43
+ const validSha1 = '356a192b7913b04c54574d18c28d46e6395428ab'
44
+ expect(isHashedValue(validSha1, 'sha1')).toBe(true)
45
+ })
46
+
47
+ it('returns true for valid SHA1 hash with uppercase letters', () => {
48
+ const validSha1 = '356A192B7913B04C54574D18C28D46E6395428AB'
49
+ expect(isHashedValue(validSha1, 'sha1')).toBe(true)
50
+ })
51
+
52
+ it('returns false for SHA1 hash with wrong length', () => {
53
+ const shortHash = '356a192b7913b04c54574d18c28d46e6395428a'
54
+ const longHash = '356a192b7913b04c54574d18c28d46e6395428abb'
55
+ expect(isHashedValue(shortHash, 'sha1')).toBe(false)
56
+ expect(isHashedValue(longHash, 'sha1')).toBe(false)
57
+ })
58
+
59
+ it('returns false for SHA1 hash with invalid characters', () => {
60
+ const invalidHash = '356g192b7913b04c54574d18c28d46e6395428ab'
61
+ expect(isHashedValue(invalidHash, 'sha1')).toBe(false)
62
+ })
63
+ })
64
+
65
+ describe('MD5 hashes', () => {
66
+ it('returns true for valid MD5 hash', () => {
67
+ const validMd5 = '5d41402abc4b2a76b9719d911017c592'
68
+ expect(isHashedValue(validMd5, 'md5')).toBe(true)
69
+ })
70
+
71
+ it('returns true for valid MD5 hash with uppercase letters', () => {
72
+ const validMd5 = '5D41402ABC4B2A76B9719D911017C592'
73
+ expect(isHashedValue(validMd5, 'md5')).toBe(true)
74
+ })
75
+
76
+ it('returns false for MD5 hash with wrong length', () => {
77
+ const shortHash = '5d41402abc4b2a76b9719d911017c59'
78
+ const longHash = '5d41402abc4b2a76b9719d911017c5922'
79
+ expect(isHashedValue(shortHash, 'md5')).toBe(false)
80
+ expect(isHashedValue(longHash, 'md5')).toBe(false)
81
+ })
82
+
83
+ it('returns false for MD5 hash with invalid characters', () => {
84
+ const invalidHash = '5g41402abc4b2a76b9719d911017c592'
85
+ expect(isHashedValue(invalidHash, 'md5')).toBe(false)
86
+ })
87
+ })
88
+
89
+ describe('Type parameter handling', () => {
90
+ it('handles case insensitive type parameter', () => {
91
+ const validSha256 =
92
+ 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
93
+ expect(isHashedValue(validSha256, 'SHA256')).toBe(true)
94
+ expect(isHashedValue(validSha256, 'Sha256')).toBe(true)
95
+ expect(isHashedValue(validSha256, 'sHa256')).toBe(true)
96
+ })
97
+
98
+ it('returns null for unsupported hash type', () => {
99
+ const validSha256 =
100
+ 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
101
+ expect(isHashedValue(validSha256, 'sha512')).toBe(null)
102
+ expect(isHashedValue(validSha256, 'blake2b')).toBe(null)
103
+ expect(isHashedValue(validSha256, 'unknown')).toBe(null)
104
+ })
105
+ })
106
+
107
+ describe('Input validation', () => {
108
+ it('returns null for non-string inputs', () => {
109
+ expect(isHashedValue(null)).toBe(null)
110
+ expect(isHashedValue(undefined)).toBe(null)
111
+ expect(isHashedValue(123)).toBe(null)
112
+ expect(isHashedValue([])).toBe(null)
113
+ expect(isHashedValue({})).toBe(null)
114
+ expect(isHashedValue(true)).toBe(null)
115
+ })
116
+
117
+ it('handles empty string', () => {
118
+ expect(isHashedValue('')).toBe(false)
119
+ expect(isHashedValue('', 'sha1')).toBe(false)
120
+ expect(isHashedValue('', 'md5')).toBe(false)
121
+ })
122
+
123
+ it('trims whitespace from input', () => {
124
+ const validSha256 =
125
+ ' a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3 '
126
+ expect(isHashedValue(validSha256, 'sha256')).toBe(true)
127
+ })
128
+
129
+ it('returns false for strings with only whitespace', () => {
130
+ expect(isHashedValue(' ')).toBe(false)
131
+ expect(isHashedValue('\t\n')).toBe(false)
132
+ })
133
+ })
134
+
135
+ describe('Edge cases', () => {
136
+ it('returns false for strings that are all zeros but correct length', () => {
137
+ const zeroSha256 =
138
+ '0000000000000000000000000000000000000000000000000000000000000000'
139
+ const zeroSha1 = '0000000000000000000000000000000000000000'
140
+ const zeroMd5 = '00000000000000000000000000000000'
141
+
142
+ expect(isHashedValue(zeroSha256, 'sha256')).toBe(true)
143
+ expect(isHashedValue(zeroSha1, 'sha1')).toBe(true)
144
+ expect(isHashedValue(zeroMd5, 'md5')).toBe(true)
145
+ })
146
+
147
+ it('returns false for correct length but non-hex characters', () => {
148
+ const nonHexSha256 =
149
+ 'g665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
150
+ const nonHexSha1 = '356g192b7913b04c54574d18c28d46e6395428ab'
151
+ const nonHexMd5 = '5g41402abc4b2a76b9719d911017c592'
152
+
153
+ expect(isHashedValue(nonHexSha256, 'sha256')).toBe(false)
154
+ expect(isHashedValue(nonHexSha1, 'sha1')).toBe(false)
155
+ expect(isHashedValue(nonHexMd5, 'md5')).toBe(false)
156
+ })
157
+
158
+ it('handles special characters correctly', () => {
159
+ const specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?/~`'
160
+ expect(isHashedValue(specialChars, 'sha256')).toBe(false)
161
+ })
162
+ })
163
+ })
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@ export * from './checks.js'
4
4
  export * from './date/index.js'
5
5
  export * from './form/index.js'
6
6
  export * from './functions/index.js'
7
+ export * from './hash.js'
7
8
  export * from './numbers/index.js'
8
9
  export * from './objects/index.js'
9
10
  export * from './promises/index.js'
@@ -338,3 +338,11 @@ it('should skip null and undefined sources', () => {
338
338
 
339
339
  expect(mix(null, obj1, undefined, obj2, null)).toEqual(expected)
340
340
  })
341
+
342
+ it('should not skip null and false properties', () => {
343
+ const obj1 = { a: 1, b: 2, c: 3 }
344
+ const obj2 = { a: null, b: false, c: undefined }
345
+ const expected = { a: null, b: false, c: undefined }
346
+
347
+ expect(mix(obj1, obj2)).toEqual(expected)
348
+ })
@@ -15,5 +15,5 @@ export function splitObject(obj, keys) {
15
15
  }
16
16
  })
17
17
 
18
- return { picked, omitted }
18
+ return { picked, omitted, p: picked, o: omitted }
19
19
  }
@@ -1,4 +1,5 @@
1
1
  export * from './convert-case/convert-case.js'
2
2
  export * from './markdown.js'
3
+ export * from './name.js'
3
4
  export * from './pluralize.js'
4
5
  export * from './query-string.js'
@@ -0,0 +1,24 @@
1
+ export function parseFullName(name) {
2
+ const parts = name.trim().split(/\s+/)
3
+
4
+ if (parts.length === 1) {
5
+ return { firstName: parts[0], middleName: '', lastName: '' }
6
+ }
7
+
8
+ if (parts.length === 2) {
9
+ return { firstName: parts[0], middleName: '', lastName: parts[1] }
10
+ }
11
+
12
+ if (parts.length === 3) {
13
+ const firstName = parts.slice(0, 2).join(' ')
14
+ const lastName = parts[2]
15
+ return { firstName, middleName: '', lastName }
16
+ }
17
+
18
+ // 4+ words: first 2 words = firstName, last word = lastName, middle = middleName
19
+ const firstName = parts.slice(0, 2).join(' ')
20
+ const lastName = parts[parts.length - 1]
21
+ const middleName = parts.slice(2, -1).join(' ')
22
+
23
+ return { firstName, middleName, lastName }
24
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { parseFullName } from './name.js'
3
+
4
+ describe('parseFullName', () => {
5
+ test('handles single name', () => {
6
+ const result = parseFullName('John')
7
+ expect(result).toEqual({
8
+ firstName: 'John',
9
+ middleName: '',
10
+ lastName: '',
11
+ })
12
+ })
13
+
14
+ test('handles two names', () => {
15
+ const result = parseFullName('John Doe')
16
+ expect(result).toEqual({
17
+ firstName: 'John',
18
+ middleName: '',
19
+ lastName: 'Doe',
20
+ })
21
+ })
22
+
23
+ test('handles three names (Chinese pattern)', () => {
24
+ const result = parseFullName('Wee Keat Liew')
25
+ expect(result).toEqual({
26
+ firstName: 'Wee Keat',
27
+ middleName: '',
28
+ lastName: 'Liew',
29
+ })
30
+ })
31
+
32
+ test('handles four names with middle name', () => {
33
+ const result = parseFullName('Mary Jane Michael Doe')
34
+ expect(result).toEqual({
35
+ firstName: 'Mary Jane',
36
+ middleName: 'Michael',
37
+ lastName: 'Doe',
38
+ })
39
+ })
40
+
41
+ test('handles five names with multiple middle names', () => {
42
+ const result = parseFullName('Mary Jane Catherine Elizabeth Doe')
43
+ expect(result).toEqual({
44
+ firstName: 'Mary Jane',
45
+ middleName: 'Catherine Elizabeth',
46
+ lastName: 'Doe',
47
+ })
48
+ })
49
+
50
+ test('handles extra whitespace', () => {
51
+ const result = parseFullName(' John Doe ')
52
+ expect(result).toEqual({
53
+ firstName: 'John',
54
+ middleName: '',
55
+ lastName: 'Doe',
56
+ })
57
+ })
58
+
59
+ test('handles multiple spaces between words', () => {
60
+ const result = parseFullName('Wee Keat Liew')
61
+ expect(result).toEqual({
62
+ firstName: 'Wee Keat',
63
+ middleName: '',
64
+ lastName: 'Liew',
65
+ })
66
+ })
67
+ })
@@ -1,10 +1,10 @@
1
- // For Node only
2
1
  import crypto from 'node:crypto'
3
2
 
4
- export async function randomString(length = 32) {
5
- return crypto.randomBytes(length).toString('base64')
6
- }
3
+ export function randomString(length = 32) {
4
+ // Calculate bytes needed for desired character length
5
+ // Base64: 4 chars per 3 bytes, so we need ceil(length * 3 / 4) bytes
6
+ // Add extra bytes to ensure we have enough after encoding
7
+ const bytesNeeded = Math.ceil((length * 3) / 4) + 3
7
8
 
8
- export function randomStringSync(length = 32) {
9
- return crypto.randomBytes(length).toString('hex')
9
+ return crypto.randomBytes(bytesNeeded).toString('base64').slice(0, length)
10
10
  }
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { randomString } from './random-string.js'
3
+
4
+ describe('randomString', () => {
5
+ it('should generate a random string', () => {
6
+ const str = randomString(32)
7
+ expect(str).toBeDefined()
8
+ expect(str.length).toBe(32)
9
+
10
+ const str2 = randomString(40)
11
+ expect(str2).toBeDefined()
12
+ expect(str2.length).toBe(40)
13
+ expect(str2).not.toBe(str)
14
+ })
15
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splendidlabz/utils",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -51,7 +51,7 @@
51
51
  "statuses": "^2.0.1"
52
52
  },
53
53
  "devDependencies": {
54
- "@splendidlabz/eslint-config": "2.0.0",
54
+ "@splendidlabz/eslint-config": "2.1.0",
55
55
  "jsdom": "^26.0.0",
56
56
  "np": "^10.2.0",
57
57
  "typescript": "^5.4.2",