@vpmedia/simplify 1.74.0 → 1.75.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 (217) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/const/http_status.d.ts +66 -0
  3. package/dist/const/http_status.d.ts.map +1 -0
  4. package/dist/const/http_status.js +133 -0
  5. package/dist/const/http_status.js.map +1 -0
  6. package/dist/index.d.ts +34 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/{src → dist}/index.js +3 -20
  9. package/dist/index.js.map +1 -0
  10. package/dist/logging/AbstractLogHandler.d.ts +17 -0
  11. package/dist/logging/AbstractLogHandler.d.ts.map +1 -0
  12. package/dist/logging/AbstractLogHandler.js +16 -0
  13. package/dist/logging/AbstractLogHandler.js.map +1 -0
  14. package/dist/logging/ConsoleLogHandler.d.ts +13 -0
  15. package/dist/logging/ConsoleLogHandler.d.ts.map +1 -0
  16. package/dist/logging/ConsoleLogHandler.js +41 -0
  17. package/dist/logging/ConsoleLogHandler.js.map +1 -0
  18. package/dist/logging/Logger.d.ts +19 -0
  19. package/dist/logging/Logger.d.ts.map +1 -0
  20. package/dist/logging/Logger.js +51 -0
  21. package/dist/logging/Logger.js.map +1 -0
  22. package/dist/logging/OpenTelemetryLogHandler.d.ts +15 -0
  23. package/dist/logging/OpenTelemetryLogHandler.d.ts.map +1 -0
  24. package/dist/logging/OpenTelemetryLogHandler.js +21 -0
  25. package/dist/logging/OpenTelemetryLogHandler.js.map +1 -0
  26. package/dist/logging/SentryLogHandler.d.ts +13 -0
  27. package/dist/logging/SentryLogHandler.d.ts.map +1 -0
  28. package/dist/logging/SentryLogHandler.js +36 -0
  29. package/dist/logging/SentryLogHandler.js.map +1 -0
  30. package/dist/logging/const.d.ts +14 -0
  31. package/dist/logging/const.d.ts.map +1 -0
  32. package/dist/logging/const.js +21 -0
  33. package/dist/logging/const.js.map +1 -0
  34. package/dist/logging/util.d.ts +14 -0
  35. package/dist/logging/util.d.ts.map +1 -0
  36. package/dist/logging/util.js +34 -0
  37. package/dist/logging/util.js.map +1 -0
  38. package/dist/pagelifecycle/const.d.ts +15 -0
  39. package/dist/pagelifecycle/const.d.ts.map +1 -0
  40. package/dist/pagelifecycle/const.js +27 -0
  41. package/dist/pagelifecycle/const.js.map +1 -0
  42. package/dist/pagelifecycle/typedef.d.ts +4 -0
  43. package/dist/pagelifecycle/typedef.d.ts.map +1 -0
  44. package/dist/pagelifecycle/typedef.js +2 -0
  45. package/dist/pagelifecycle/typedef.js.map +1 -0
  46. package/dist/pagelifecycle/util.d.ts +31 -0
  47. package/dist/pagelifecycle/util.d.ts.map +1 -0
  48. package/dist/pagelifecycle/util.js +117 -0
  49. package/dist/pagelifecycle/util.js.map +1 -0
  50. package/dist/typecheck/TypeCheckError.d.ts +11 -0
  51. package/dist/typecheck/TypeCheckError.d.ts.map +1 -0
  52. package/dist/typecheck/TypeCheckError.js +12 -0
  53. package/dist/typecheck/TypeCheckError.js.map +1 -0
  54. package/dist/typecheck/TypeChecker.d.ts +27 -0
  55. package/dist/typecheck/TypeChecker.d.ts.map +1 -0
  56. package/dist/typecheck/TypeChecker.js +69 -0
  57. package/dist/typecheck/TypeChecker.js.map +1 -0
  58. package/dist/typecheck/util.d.ts +17 -0
  59. package/dist/typecheck/util.d.ts.map +1 -0
  60. package/dist/typecheck/util.js +42 -0
  61. package/dist/typecheck/util.js.map +1 -0
  62. package/dist/util/async.d.ts +13 -0
  63. package/dist/util/async.d.ts.map +1 -0
  64. package/dist/util/async.js +39 -0
  65. package/dist/util/async.js.map +1 -0
  66. package/dist/util/error.d.ts +16 -0
  67. package/dist/util/error.d.ts.map +1 -0
  68. package/dist/util/error.js +27 -0
  69. package/dist/util/error.js.map +1 -0
  70. package/dist/util/event_emitter.d.ts +42 -0
  71. package/dist/util/event_emitter.d.ts.map +1 -0
  72. package/dist/util/event_emitter.js +127 -0
  73. package/dist/util/event_emitter.js.map +1 -0
  74. package/dist/util/fetch.d.ts +22 -0
  75. package/dist/util/fetch.d.ts.map +1 -0
  76. package/dist/util/fetch.js +75 -0
  77. package/dist/util/fetch.js.map +1 -0
  78. package/dist/util/number.d.ts +23 -0
  79. package/dist/util/number.d.ts.map +1 -0
  80. package/dist/util/number.js +52 -0
  81. package/dist/util/number.js.map +1 -0
  82. package/dist/util/object.d.ts +24 -0
  83. package/dist/util/object.d.ts.map +1 -0
  84. package/dist/util/object.js +83 -0
  85. package/dist/util/object.js.map +1 -0
  86. package/dist/util/query.d.ts +11 -0
  87. package/dist/util/query.d.ts.map +1 -0
  88. package/dist/util/query.js +24 -0
  89. package/dist/util/query.js.map +1 -0
  90. package/dist/util/state.d.ts +5 -0
  91. package/dist/util/state.d.ts.map +1 -0
  92. package/dist/util/state.js +19 -0
  93. package/dist/util/state.js.map +1 -0
  94. package/dist/util/string.d.ts +25 -0
  95. package/dist/util/string.d.ts.map +1 -0
  96. package/dist/util/string.js +59 -0
  97. package/dist/util/string.js.map +1 -0
  98. package/dist/util/uuid.d.ts +13 -0
  99. package/dist/util/uuid.d.ts.map +1 -0
  100. package/dist/util/uuid.js +27 -0
  101. package/dist/util/uuid.js.map +1 -0
  102. package/dist/util/validate.d.ts +106 -0
  103. package/dist/util/validate.d.ts.map +1 -0
  104. package/dist/util/validate.js +139 -0
  105. package/dist/util/validate.js.map +1 -0
  106. package/package.json +31 -16
  107. package/src/const/http_status.test.ts +7 -0
  108. package/src/const/{http_status.js → http_status.ts} +1 -1
  109. package/src/index.ts +51 -0
  110. package/src/logging/AbstractLogHandler.ts +31 -0
  111. package/src/logging/{ConsoleLogHandler.js → ConsoleLogHandler.ts} +15 -13
  112. package/src/logging/Logger.test.ts +69 -0
  113. package/src/logging/Logger.ts +77 -0
  114. package/src/logging/OpenTelemetryLogHandler.ts +40 -0
  115. package/src/logging/SentryLogHandler.ts +44 -0
  116. package/src/logging/{const.js → const.ts} +1 -1
  117. package/src/logging/util.test.ts +33 -0
  118. package/src/logging/util.ts +36 -0
  119. package/src/pagelifecycle/{const.js → const.ts} +2 -2
  120. package/src/pagelifecycle/typedef.ts +5 -0
  121. package/src/pagelifecycle/util.test.ts +99 -0
  122. package/src/pagelifecycle/{util.js → util.ts} +14 -27
  123. package/src/typecheck/{TypeCheckError.js → TypeCheckError.ts} +7 -3
  124. package/src/typecheck/TypeChecker.test.ts +70 -0
  125. package/src/typecheck/{TypeChecker.js → TypeChecker.ts} +10 -27
  126. package/src/typecheck/util.test.ts +36 -0
  127. package/src/typecheck/util.ts +50 -0
  128. package/src/util/async.test.ts +74 -0
  129. package/src/util/{async.js → async.ts} +3 -12
  130. package/src/util/error.test.ts +32 -0
  131. package/src/util/error.ts +37 -0
  132. package/src/util/event_emitter.test.ts +228 -0
  133. package/src/util/event_emitter.ts +147 -0
  134. package/src/util/fetch.test.ts +62 -0
  135. package/src/util/{fetch.js → fetch.ts} +40 -31
  136. package/src/util/number.test.ts +124 -0
  137. package/src/util/number.ts +58 -0
  138. package/src/util/object.test.ts +203 -0
  139. package/src/util/{object.js → object.ts} +14 -21
  140. package/src/util/query.test.ts +71 -0
  141. package/src/util/query.ts +35 -0
  142. package/src/util/state.test.ts +47 -0
  143. package/src/util/{state.js → state.ts} +3 -6
  144. package/src/util/string.test.ts +64 -0
  145. package/src/util/string.ts +65 -0
  146. package/src/util/uuid.test.ts +53 -0
  147. package/src/util/uuid.ts +31 -0
  148. package/src/util/validate.test.ts +309 -0
  149. package/src/util/validate.ts +230 -0
  150. package/.vscode/extensions.json +0 -6
  151. package/.vscode/settings.json +0 -27
  152. package/src/logging/AbstractLogHandler.js +0 -23
  153. package/src/logging/Logger.js +0 -115
  154. package/src/logging/OpenTelemetryLogHandler.js +0 -30
  155. package/src/logging/SentryLogHandler.js +0 -46
  156. package/src/logging/util.js +0 -41
  157. package/src/pagelifecycle/typedef.js +0 -9
  158. package/src/typecheck/util.js +0 -60
  159. package/src/util/error.js +0 -33
  160. package/src/util/event_emitter.js +0 -196
  161. package/src/util/number.js +0 -118
  162. package/src/util/query.js +0 -32
  163. package/src/util/string.js +0 -76
  164. package/src/util/uuid.js +0 -35
  165. package/src/util/validate.js +0 -247
  166. package/types/const/http_status.d.ts +0 -131
  167. package/types/const/http_status.d.ts.map +0 -1
  168. package/types/index.d.ts +0 -26
  169. package/types/index.d.ts.map +0 -1
  170. package/types/logging/AbstractLogHandler.d.ts +0 -20
  171. package/types/logging/AbstractLogHandler.d.ts.map +0 -1
  172. package/types/logging/ConsoleLogHandler.d.ts +0 -9
  173. package/types/logging/ConsoleLogHandler.d.ts.map +0 -1
  174. package/types/logging/Logger.d.ts +0 -66
  175. package/types/logging/Logger.d.ts.map +0 -1
  176. package/types/logging/OpenTelemetryLogHandler.d.ts +0 -11
  177. package/types/logging/OpenTelemetryLogHandler.d.ts.map +0 -1
  178. package/types/logging/SentryLogHandler.d.ts +0 -9
  179. package/types/logging/SentryLogHandler.d.ts.map +0 -1
  180. package/types/logging/const.d.ts +0 -14
  181. package/types/logging/const.d.ts.map +0 -1
  182. package/types/logging/util.d.ts +0 -4
  183. package/types/logging/util.d.ts.map +0 -1
  184. package/types/pagelifecycle/const.d.ts +0 -15
  185. package/types/pagelifecycle/const.d.ts.map +0 -1
  186. package/types/pagelifecycle/typedef.d.ts +0 -4
  187. package/types/pagelifecycle/typedef.d.ts.map +0 -1
  188. package/types/pagelifecycle/util.d.ts +0 -8
  189. package/types/pagelifecycle/util.d.ts.map +0 -1
  190. package/types/typecheck/TypeCheckError.d.ts +0 -13
  191. package/types/typecheck/TypeCheckError.d.ts.map +0 -1
  192. package/types/typecheck/TypeChecker.d.ts +0 -40
  193. package/types/typecheck/TypeChecker.d.ts.map +0 -1
  194. package/types/typecheck/util.d.ts +0 -4
  195. package/types/typecheck/util.d.ts.map +0 -1
  196. package/types/util/async.d.ts +0 -4
  197. package/types/util/async.d.ts.map +0 -1
  198. package/types/util/error.d.ts +0 -3
  199. package/types/util/error.d.ts.map +0 -1
  200. package/types/util/event_emitter.d.ts +0 -69
  201. package/types/util/event_emitter.d.ts.map +0 -1
  202. package/types/util/fetch.d.ts +0 -22
  203. package/types/util/fetch.d.ts.map +0 -1
  204. package/types/util/number.d.ts +0 -11
  205. package/types/util/number.d.ts.map +0 -1
  206. package/types/util/object.d.ts +0 -6
  207. package/types/util/object.d.ts.map +0 -1
  208. package/types/util/query.d.ts +0 -3
  209. package/types/util/query.d.ts.map +0 -1
  210. package/types/util/state.d.ts +0 -2
  211. package/types/util/state.d.ts.map +0 -1
  212. package/types/util/string.d.ts +0 -7
  213. package/types/util/string.d.ts.map +0 -1
  214. package/types/util/uuid.d.ts +0 -4
  215. package/types/util/uuid.d.ts.map +0 -1
  216. package/types/util/validate.d.ts +0 -45
  217. package/types/util/validate.d.ts.map +0 -1
@@ -0,0 +1,203 @@
1
+ import { getObjValueByPath, setObjValueByPath, purgeObject, deepMerge } from './object.js';
2
+
3
+ describe('deepMerge', () => {
4
+ test('should override deep properties correctly', () => {
5
+ const defaultObj = { a: { b: 1, c: 2 }, d: 3 };
6
+ const overrideObj = { a: { b: 42 } };
7
+ const expectedResult = { a: { b: 42, c: 2 }, d: 3 };
8
+
9
+ expect(deepMerge({ ...defaultObj }, overrideObj)).toEqual(expectedResult);
10
+ });
11
+
12
+ test('should not modify the original target object', () => {
13
+ const target = { x: { y: 10 } };
14
+ const source = { x: { y: 20 } };
15
+ const copy = { ...target };
16
+
17
+ deepMerge(target, source);
18
+ expect(target).toEqual(copy);
19
+ });
20
+
21
+ test('should handle non-object values correctly', () => {
22
+ expect(deepMerge(null, { a: 1 })).toEqual({ a: 1 });
23
+ expect(deepMerge({ a: 1 }, null)).toEqual({ a: 1 });
24
+ });
25
+
26
+ test('should handle arrays correctly', () => {
27
+ const target = { arr: [1, 2] };
28
+ const source = { arr: [3, 4] };
29
+
30
+ expect(deepMerge(target, source)).toEqual({ arr: [3, 4] });
31
+ });
32
+
33
+ test('should handle nested arrays correctly', () => {
34
+ const target = { obj: { arr: [1, 2] } };
35
+ const source = { obj: { arr: [3, 4] } };
36
+
37
+ expect(deepMerge(target, source)).toEqual({ obj: { arr: [3, 4] } });
38
+ });
39
+
40
+ test('should handle string values', () => {
41
+ const target = { str: 'hello' };
42
+ const source = { str: 'world' };
43
+
44
+ expect(deepMerge(target, source)).toEqual({ str: 'world' });
45
+ });
46
+
47
+ test('should handle number values', () => {
48
+ const target = { num: 42 };
49
+ const source = { num: 100 };
50
+
51
+ expect(deepMerge(target, source)).toEqual({ num: 100 });
52
+ });
53
+
54
+ test('should handle boolean values', () => {
55
+ const target = { bool: true };
56
+ const source = { bool: false };
57
+
58
+ expect(deepMerge(target, source)).toEqual({ bool: false });
59
+ });
60
+
61
+ test('should handle undefined values', () => {
62
+ const target = { undef: undefined };
63
+ const source = { undef: 'value' };
64
+
65
+ expect(deepMerge(target, source)).toEqual({ undef: 'value' });
66
+ });
67
+
68
+ test('should handle null values', () => {
69
+ const target = { nullVal: null };
70
+ const source = { nullVal: 'value' };
71
+
72
+ expect(deepMerge(target, source)).toEqual({ nullVal: 'value' });
73
+ });
74
+
75
+ test('should handle mixed property types', () => {
76
+ const target = {
77
+ str: 'hello',
78
+ num: 42,
79
+ arr: [1, 2],
80
+ obj: { nested: 'value' },
81
+ };
82
+ const source = {
83
+ str: 'world',
84
+ num: 100,
85
+ arr: [3, 4],
86
+ obj: { nested: 'newValue' },
87
+ };
88
+
89
+ expect(deepMerge(target, source)).toEqual({
90
+ str: 'world',
91
+ num: 100,
92
+ arr: [3, 4],
93
+ obj: { nested: 'newValue' },
94
+ });
95
+ });
96
+
97
+ test('should handle constructor and __proto__ protection', () => {
98
+ const target = { a: 1 };
99
+ const source = { b: 2 };
100
+
101
+ expect(deepMerge(target, source)).toEqual({ a: 1, b: 2 });
102
+ });
103
+ });
104
+
105
+ test('Purges object of null and undefined values', () => {
106
+ const a: { k: string | null } = { k: 'v' };
107
+ expect(a.k).toBe('v');
108
+ purgeObject(a);
109
+ expect(a.k).toBe(null);
110
+ });
111
+
112
+ describe('getObjValueByPath', () => {
113
+ test('Gets object value by path', () => {
114
+ const source = { a: { b: { c: 'd' } } };
115
+ expect(getObjValueByPath(source, 'a.b.c')).toBe('d');
116
+ });
117
+
118
+ test('Returns null when object is null or undefined', () => {
119
+ expect(getObjValueByPath(null, 'a.b.c')).toBeNull();
120
+ expect(getObjValueByPath(undefined, 'a.b.c')).toBeNull();
121
+ });
122
+
123
+ test('Returns null when path is empty or null', () => {
124
+ expect(getObjValueByPath({ a: 'b' }, '')).toBeNull();
125
+ expect(getObjValueByPath({ a: 'b' }, null)).toBeNull();
126
+ });
127
+
128
+ test('Returns null when property does not exist', () => {
129
+ const source = { a: { b: 'c' } };
130
+ expect(getObjValueByPath(source, 'a.b.c')).toBeNull();
131
+ });
132
+
133
+ test('Returns null when property is undefined', () => {
134
+ const source = { a: { b: undefined } };
135
+ expect(getObjValueByPath(source, 'a.b')).toBeNull();
136
+ });
137
+
138
+ test('Handles single-level paths correctly', () => {
139
+ const source = { a: 'value' };
140
+ expect(getObjValueByPath(source, 'a')).toBe('value');
141
+ });
142
+
143
+ test('Handles nested paths correctly', () => {
144
+ const source = {
145
+ level1: {
146
+ level2: {
147
+ level3: 'deepValue',
148
+ },
149
+ },
150
+ };
151
+ expect(getObjValueByPath(source, 'level1.level2.level3')).toBe('deepValue');
152
+ });
153
+
154
+ test('Handles arrays in paths', () => {
155
+ const source = {
156
+ items: [{ name: 'item1' }, { name: 'item2' }],
157
+ };
158
+ expect(getObjValueByPath(source, 'items.0.name')).toBe('item1');
159
+ });
160
+ });
161
+
162
+ describe('setObjValueByPath', () => {
163
+ test('Sets object value by path', () => {
164
+ const source = { a: { b: { c: 'd' } } };
165
+ expect(getObjValueByPath(source, 'a.b.c')).toBe('d');
166
+ setObjValueByPath(source, 'a.b.c', 'newValue');
167
+ expect(getObjValueByPath(source, 'a.b.c')).toBe('newValue');
168
+ });
169
+
170
+ test('Handles null or undefined object', () => {
171
+ setObjValueByPath(null, 'a.b.c', 'value');
172
+ setObjValueByPath(undefined, 'a.b.c', 'value');
173
+ });
174
+
175
+ test('Handles null or undefined path', () => {
176
+ const source = { a: 'b' };
177
+ setObjValueByPath(source, null, 'value');
178
+ setObjValueByPath(source, undefined, 'value');
179
+ });
180
+
181
+ test('Sets value at root level', () => {
182
+ const source = { a: 'oldValue' };
183
+ setObjValueByPath(source, 'a', 'newValue');
184
+ expect(source.a).toBe('newValue');
185
+ });
186
+
187
+ test('Creates new nested properties', () => {
188
+ const source: Record<string, unknown> = { a: { b: 'existing' } };
189
+ setObjValueByPath(source, 'a.c.d', 'newNestedValue');
190
+ expect(getObjValueByPath(source, 'a.c.d')).toBe('newNestedValue');
191
+ });
192
+
193
+ test('Handles array paths', () => {
194
+ const source = { items: [{ name: 'item1' }] };
195
+ setObjValueByPath(source, 'items.0.name', 'updatedItem');
196
+ expect(getObjValueByPath(source, 'items.0.name')).toBe('updatedItem');
197
+ });
198
+
199
+ test('Throws error for __proto__ path', () => {
200
+ const source = { a: 'b' };
201
+ expect(() => setObjValueByPath(source, '__proto__.test', 'value')).toThrow(SyntaxError);
202
+ });
203
+ });
@@ -1,10 +1,11 @@
1
1
  const PROHIBITED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
2
2
 
3
+ type AnyRecord = Record<string, any>;
4
+
3
5
  /**
4
6
  * Purges object properties to free up memory.
5
- * @param {object} target - The target object.
6
7
  */
7
- export const purgeObject = (target) => {
8
+ export const purgeObject = (target: AnyRecord | null | undefined): void => {
8
9
  if (!target) {
9
10
  return;
10
11
  }
@@ -15,11 +16,8 @@ export const purgeObject = (target) => {
15
16
 
16
17
  /**
17
18
  * Merge two objects.
18
- * @param {object} target - Target merge object.
19
- * @param {object} source - Source merge object.
20
- * @returns {object} Merged result object.
21
19
  */
22
- export const deepMerge = (target, source) => {
20
+ export const deepMerge = (target: AnyRecord | null, source: AnyRecord | null): AnyRecord | null => {
23
21
  if (typeof target !== 'object' || target === null) {
24
22
  return source;
25
23
  }
@@ -45,24 +43,19 @@ export const deepMerge = (target, source) => {
45
43
 
46
44
  /**
47
45
  * Returns the sum value of an array of objects field.
48
- * @param {object[]} arr - The list of input objects.
49
- * @param {string} prop - The object property key.
50
- * @returns {number} The sum value.
51
46
  */
52
- export const getObjArrayPropSum = (arr, prop) => arr.reduce((accumulator, object) => accumulator + object[prop], 0);
47
+ export const getObjArrayPropSum = (arr: AnyRecord[], prop: string): number =>
48
+ arr.reduce((accumulator, object) => accumulator + (object[prop] as number), 0);
53
49
 
54
50
  /**
55
51
  * Get object value by path.
56
- * @param {object} obj - The source object to get the value from.
57
- * @param {string} path - The path to the property in dot notation (e.g. 'a.b.c').
58
- * @returns {object | null} The value at the specified path or null if not found.
59
52
  */
60
- export const getObjValueByPath = (obj, path) => {
53
+ export const getObjValueByPath = (obj: AnyRecord | null | undefined, path: string | null | undefined): unknown => {
61
54
  if (!obj || !path) {
62
55
  return null;
63
56
  }
64
57
  const keyParts = path.split('.');
65
- const [nextKey] = keyParts;
58
+ const nextKey = keyParts[0]!;
66
59
  if (keyParts.length === 1) {
67
60
  return obj[nextKey] === undefined ? null : obj[nextKey];
68
61
  }
@@ -74,24 +67,24 @@ export const getObjValueByPath = (obj, path) => {
74
67
 
75
68
  /**
76
69
  * Set object value by path.
77
- * @param {object} obj - The source object to set the value in.
78
- * @param {string} path - The path to the property in dot notation (e.g. 'a.b.c').
79
- * @param {object | null | undefined} value - The value to set at the specified path.
80
70
  * @throws {SyntaxError} Error when illegal path value has been provided.
81
71
  */
82
- export const setObjValueByPath = (obj, path, value) => {
72
+ export const setObjValueByPath = (
73
+ obj: AnyRecord | null | undefined,
74
+ path: string | null | undefined,
75
+ value: unknown
76
+ ): void => {
83
77
  if (!obj || !path) {
84
78
  return;
85
79
  }
86
80
  const keyParts = path.split('.');
87
- const [nextKey] = keyParts;
81
+ const nextKey = keyParts[0]!;
88
82
  if (PROHIBITED_KEYS.has(nextKey)) {
89
83
  throw new SyntaxError(`Security violation error. Cannot use "${nextKey}" as parameter.`);
90
84
  }
91
85
  if (keyParts.length === 1) {
92
86
  obj[nextKey] = value;
93
87
  } else {
94
- // Create the nested object if it doesn't exist
95
88
  if (obj[nextKey] === undefined || obj[nextKey] === null) {
96
89
  obj[nextKey] = {};
97
90
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @vitest-environment-options { "url": "https://localhost/app/?language=en&token=123-456-öüó%24D" }
3
+ */
4
+
5
+ import { getURLParam, sanitizeURLParam } from './query.js';
6
+
7
+ describe('getURLParam', () => {
8
+ test('Returns fallback value when parameter is not found', () => {
9
+ const result = getURLParam('nonexistent', 'fallback');
10
+ expect(result).toBe('fallback');
11
+ });
12
+
13
+ test('Handles null/undefined input gracefully', () => {
14
+ const result = getURLParam(null, 'fallback');
15
+ expect(result).toBe('fallback');
16
+ });
17
+
18
+ test('Returns default value when param is null', () => {
19
+ const result = getURLParam('key', 'default');
20
+ expect(result).toBe('default');
21
+ });
22
+
23
+ test('Handles valid url parameter sanitized', () => {
24
+ expect(getURLParam('language')).toBe('en');
25
+ expect(getURLParam('token')).toBe('123-456-D');
26
+ });
27
+
28
+ test('Handles valid url parameter unsanitized', () => {
29
+ expect(getURLParam('token', null, false)).toBe('123-456-öüó$D');
30
+ });
31
+ });
32
+
33
+ describe('sanitizeURLParam', () => {
34
+ test('Sanitizes URL parameter correctly', () => {
35
+ expect(sanitizeURLParam('abc<>-123[]{}-()A_BC')).toBe('abc-123-A_BC');
36
+ });
37
+
38
+ test('Handles null input', () => {
39
+ expect(sanitizeURLParam(null)).toBe(null);
40
+ });
41
+
42
+ test('Handles empty string', () => {
43
+ expect(sanitizeURLParam('')).toBe('');
44
+ });
45
+
46
+ test('Handles valid characters', () => {
47
+ expect(sanitizeURLParam('abc123')).toBe('abc123');
48
+ expect(sanitizeURLParam('test-parameter')).toBe('test-parameter');
49
+ expect(sanitizeURLParam('test_parameter')).toBe('test_parameter');
50
+ });
51
+
52
+ test('Handles special characters', () => {
53
+ expect(sanitizeURLParam('test@#$%')).toBe('test');
54
+ expect(sanitizeURLParam('test!@#$%^&*()')).toBe('test');
55
+ });
56
+
57
+ test('Handles unicode characters', () => {
58
+ expect(sanitizeURLParam('test_äöü')).toBe('test_');
59
+ });
60
+
61
+ test('Handles edge cases with various special characters', () => {
62
+ expect(sanitizeURLParam('test!!!@@@')).toBe('test');
63
+ expect(sanitizeURLParam('test_param-123')).toBe('test_param-123');
64
+ expect(sanitizeURLParam('test param')).toBe('testparam');
65
+ });
66
+
67
+ test('Handles very long parameter names', () => {
68
+ const longParam = 'a'.repeat(1000);
69
+ expect(typeof sanitizeURLParam(longParam)).toBe('string');
70
+ });
71
+ });
@@ -0,0 +1,35 @@
1
+ const urlSearchParams = new URLSearchParams(globalThis.location?.search);
2
+
3
+ /**
4
+ * Sanitizes URL parameters allowing only alpha-numeric characters and dash.
5
+ */
6
+ export function sanitizeURLParam(input: string): string;
7
+ export function sanitizeURLParam(input: null | undefined): null | undefined;
8
+ export function sanitizeURLParam(input: string | null | undefined): string | null | undefined;
9
+ export function sanitizeURLParam(input: string | null | undefined): string | null | undefined {
10
+ if (!input) {
11
+ return input;
12
+ }
13
+ return input.replaceAll(/[^\w-]/giu, '');
14
+ }
15
+
16
+ /**
17
+ * Get a URL parameter value.
18
+ */
19
+ export function getURLParam<T = null>(
20
+ key: string | null | undefined,
21
+ defaultValue?: T,
22
+ isSanitize?: boolean
23
+ ): string | T {
24
+ if (!key) {
25
+ return (defaultValue ?? null) as T;
26
+ }
27
+ const paramValue = urlSearchParams.get(key);
28
+ if (paramValue === null || paramValue === undefined) {
29
+ return (defaultValue ?? null) as T;
30
+ }
31
+ if (isSanitize !== false) {
32
+ return sanitizeURLParam(paramValue) as string;
33
+ }
34
+ return paramValue;
35
+ }
@@ -0,0 +1,47 @@
1
+ import { serverDataToState } from './state.js';
2
+
3
+ describe('state', () => {
4
+ test('serverDataToState() recursive', () => {
5
+ const state = serverDataToState(
6
+ {
7
+ my_array: [{ key_a: 'value1' }],
8
+ my_data: { key_a: 'value1' },
9
+ my_list: [1, 2, 3],
10
+ my_null: null,
11
+ my_number: 1000,
12
+ my_string: 'a',
13
+ my_var: 'test',
14
+ },
15
+ true
16
+ );
17
+ expect(state.myArray[0].keyA).toBe('value1');
18
+ expect(state.myData.keyA).toBe('value1');
19
+ expect(state.myList[0]).toBe(1);
20
+ expect(state.myNull).toBe(null);
21
+ expect(state.myNumber).toBe(1000);
22
+ expect(state.myString).toBe('a');
23
+ expect(state.myVar).toBe('test');
24
+ });
25
+
26
+ test('serverDataToState() non-recursive', () => {
27
+ const state = serverDataToState(
28
+ {
29
+ my_array: [{ key_a: 'value1' }],
30
+ my_data: { key_a: 'value1' },
31
+ my_list: [1, 2, 3],
32
+ my_null: null,
33
+ my_number: 1000,
34
+ my_string: 'a',
35
+ my_var: 'test',
36
+ },
37
+ false
38
+ );
39
+ expect(state.myArray[0].key_a).toBe('value1');
40
+ expect(state.myData.key_a).toBe('value1');
41
+ expect(state.myList[0]).toBe(1);
42
+ expect(state.myNull).toBe(null);
43
+ expect(state.myNumber).toBe(1000);
44
+ expect(state.myString).toBe('a');
45
+ expect(state.myVar).toBe('test');
46
+ });
47
+ });
@@ -2,17 +2,14 @@ import { underscoreToCamelCase } from './string.js';
2
2
 
3
3
  /**
4
4
  * Maps server data to client data.
5
- * @param {object} data - The server input data.
6
- * @param {boolean} isRecursive - Use recursive serialization.
7
- * @returns {object} The output data.
8
5
  */
9
- export const serverDataToState = (data, isRecursive = false) => {
6
+ export const serverDataToState = (data: unknown, isRecursive = false): any => {
10
7
  if (Array.isArray(data)) {
11
8
  return data.map((entry) => serverDataToState(entry, isRecursive));
12
9
  }
13
10
 
14
11
  if (data !== null && typeof data === 'object') {
15
- const result = {};
12
+ const result: Record<string, unknown> = {};
16
13
  for (const [key, value] of Object.entries(data)) {
17
14
  const clientKey = underscoreToCamelCase(key);
18
15
  result[clientKey] = isRecursive ? serverDataToState(value, isRecursive) : value;
@@ -20,5 +17,5 @@ export const serverDataToState = (data, isRecursive = false) => {
20
17
  return result;
21
18
  }
22
19
 
23
- return data; // Return primitives as-is
20
+ return data;
24
21
  };
@@ -0,0 +1,64 @@
1
+ import {
2
+ underscoreToCamelCase,
3
+ capitalize,
4
+ addLeadingZero,
5
+ getTypeFromValue,
6
+ getDisplayValue,
7
+ saveAsFile,
8
+ } from './string.js';
9
+
10
+ describe('string', () => {
11
+ test('addLeadingZero', () => {
12
+ expect(addLeadingZero(1)).toBe('01');
13
+ expect(addLeadingZero('1')).toBe('01');
14
+ expect(addLeadingZero(1, 3)).toBe('001');
15
+ expect(addLeadingZero('21')).toBe('21');
16
+ expect(addLeadingZero(21)).toBe('21');
17
+ expect(addLeadingZero(null)).toBe(null);
18
+ expect(addLeadingZero(undefined)).toBe(null);
19
+ });
20
+
21
+ test('capitalize', () => {
22
+ expect(capitalize('test')).toBe('Test');
23
+ expect(capitalize('TEST')).toBe('Test');
24
+ expect(capitalize('tEST')).toBe('Test');
25
+ expect(capitalize(null)).toBe(null);
26
+ expect(capitalize('')).toBe('');
27
+ expect(capitalize(' ')).toBe(' ');
28
+ expect(capitalize('a')).toBe('A');
29
+ expect(capitalize('A')).toBe('A');
30
+ expect(capitalize('test123')).toBe('Test123');
31
+ });
32
+
33
+ test('underscoreToCamelCase', () => {
34
+ expect(underscoreToCamelCase('test')).toBe('test');
35
+ expect(underscoreToCamelCase('test_variable')).toBe('testVariable');
36
+ expect(underscoreToCamelCase('test_variable_name')).toBe('testVariableName');
37
+ });
38
+
39
+ test('getTypeFromValue', () => {
40
+ expect(getTypeFromValue('test')).toBe('string');
41
+ expect(getTypeFromValue(() => null)).toBe('function');
42
+ expect(getTypeFromValue([])).toBe('array');
43
+ expect(getTypeFromValue({})).toBe('object');
44
+ expect(getTypeFromValue(new Date())).toBe('date');
45
+ expect(getTypeFromValue(null)).toBe('null');
46
+ expect(getTypeFromValue(true)).toBe('boolean');
47
+ expect(getTypeFromValue(undefined)).toBe('undefined');
48
+ });
49
+
50
+ test('getDisplayValue', () => {
51
+ expect(getDisplayValue('test')).toBe('"test"');
52
+ expect(getDisplayValue(() => null)).toBe('() => null');
53
+ expect(getDisplayValue([])).toBe('[]');
54
+ expect(getDisplayValue({})).toBe('{}');
55
+ expect(getDisplayValue(new Date())).not.toBe(null);
56
+ expect(getDisplayValue(null)).toBe('null');
57
+ expect(getDisplayValue(true)).toBe('true');
58
+ expect(getDisplayValue(undefined)).toBe('undefined');
59
+ });
60
+
61
+ test('saveAsFile', () => {
62
+ expect(() => saveAsFile('test.txt', 'test content')).not.toThrowError(Error);
63
+ });
64
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Add leading zeros to a value to ensure it has a minimum width.
3
+ */
4
+ export const addLeadingZero = (value: number | string | null | undefined, size = 2): string | null => {
5
+ if (value === null || value === undefined) {
6
+ return null;
7
+ }
8
+ let str = value.toString();
9
+ while (str.length < size) {
10
+ str = `0${str}`;
11
+ }
12
+ return str;
13
+ };
14
+
15
+ /**
16
+ * Capitalize a string.
17
+ */
18
+ export const capitalize = (value: string | null | undefined): string | null => {
19
+ if (value === null || value === undefined) {
20
+ return null;
21
+ }
22
+ if (!value || value.length === 0) {
23
+ return value;
24
+ }
25
+ const normValue = value.toLowerCase();
26
+ return normValue.charAt(0).toUpperCase() + normValue.slice(1);
27
+ };
28
+
29
+ /**
30
+ * Converts underscore case string to camel case.
31
+ */
32
+ export const underscoreToCamelCase = (value: string): string =>
33
+ value.replaceAll(/(_\w)/gu, (m) => m[1]!.toUpperCase());
34
+
35
+ /**
36
+ * Saves text file.
37
+ */
38
+ export const saveAsFile = (filename: string, text: string): void => {
39
+ const element = document.createElement('a');
40
+ element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`);
41
+ element.setAttribute('download', filename);
42
+ element.style.display = 'none';
43
+ document.body.append(element);
44
+ element.click();
45
+ element.remove();
46
+ };
47
+
48
+ /**
49
+ * Get type from value in human readable format.
50
+ */
51
+ export const getTypeFromValue = (value: unknown): string =>
52
+ Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
53
+
54
+ /**
55
+ * Get value in human readable format.
56
+ */
57
+ export const getDisplayValue = (value: unknown): string => {
58
+ if (typeof value === 'string') {
59
+ return `"${value}"`;
60
+ }
61
+ if (typeof value === 'object') {
62
+ return JSON.stringify(value);
63
+ }
64
+ return String(value);
65
+ };
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { randomUUIDFallback, uuidv4 } from './uuid.js';
3
+
4
+ const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
5
+
6
+ describe('UUID functions', () => {
7
+ let originalRandomUUID: typeof crypto.randomUUID;
8
+
9
+ beforeEach(() => {
10
+ originalRandomUUID = crypto.randomUUID;
11
+ });
12
+
13
+ afterEach(() => {
14
+ crypto.randomUUID = originalRandomUUID;
15
+ });
16
+
17
+ it('randomUUIDFallback generates a valid UUID v4', () => {
18
+ const uuid = randomUUIDFallback();
19
+ expect(uuid).toMatch(uuidV4Regex);
20
+ });
21
+
22
+ it('uuidv4 returns a valid UUID v4', () => {
23
+ const uuid = uuidv4();
24
+ expect(uuid).toMatch(uuidV4Regex);
25
+ });
26
+
27
+ it('uuidv4 uses crypto.randomUUID if available', () => {
28
+ crypto.randomUUID = vi.fn(() => 'mock-uuid' as `${string}-${string}-${string}-${string}-${string}`);
29
+ const uuid = uuidv4();
30
+ expect(uuid).toBe('mock-uuid');
31
+ expect(crypto.randomUUID).toHaveBeenCalled();
32
+ });
33
+
34
+ it('randomUUIDFallback fallback works if crypto.randomUUID not available', () => {
35
+ // @ts-expect-error - allow deleting non-optional property for test
36
+ delete crypto.randomUUID;
37
+ const uuid = randomUUIDFallback();
38
+ expect(uuid).toMatch(uuidV4Regex);
39
+ });
40
+
41
+ it('randomUUIDFallback does not generate duplicates', () => {
42
+ const uuidMap = new Map<string, boolean>();
43
+ const numSamples = 1000;
44
+ for (let i = 0; i < numSamples; i += 1) {
45
+ const uuid = randomUUIDFallback();
46
+ if (uuidMap.has(uuid)) {
47
+ throw new Error('Duplicate UUIDv4 found');
48
+ }
49
+ uuidMap.set(uuid, true);
50
+ }
51
+ expect(uuidMap.size).toBe(numSamples);
52
+ });
53
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Convert a byte (0–255) to a 2-character hex string.
3
+ */
4
+ export const byteToHex = (byte: number): string => (byte >>> 4).toString(16) + (byte & 0b1111).toString(16);
5
+
6
+ /**
7
+ * UUIDv4 fallback generator (RFC 4122 compliant).
8
+ */
9
+ export const randomUUIDFallback = (): string => {
10
+ const bytes: number[] | Uint8Array = crypto.getRandomValues
11
+ ? crypto.getRandomValues(new Uint8Array(16))
12
+ : Array.from({ length: 16 }, () => Math.floor(Math.random() * 256));
13
+
14
+ bytes[6] = (bytes[6]! & 0x0f) | 0x40;
15
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80;
16
+
17
+ let uuid = '';
18
+ for (let index = 0; index < bytes.length; index += 1) {
19
+ if (index === 4 || index === 6 || index === 8 || index === 10) {
20
+ uuid += '-';
21
+ }
22
+ uuid += byteToHex(bytes[index]!);
23
+ }
24
+ return uuid;
25
+ };
26
+
27
+ /**
28
+ * Crypto UUIDv4 wrapper with fallback.
29
+ */
30
+ export const uuidv4 = (): string =>
31
+ typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : randomUUIDFallback();