@vpmedia/simplify 1.74.0 → 1.76.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 +75 -0
- package/dist/const/http_status.d.ts +66 -0
- package/dist/const/http_status.d.ts.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1119 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/AbstractLogHandler.d.ts +17 -0
- package/dist/logging/AbstractLogHandler.d.ts.map +1 -0
- package/dist/logging/ConsoleLogHandler.d.ts +13 -0
- package/dist/logging/ConsoleLogHandler.d.ts.map +1 -0
- package/dist/logging/Logger.d.ts +19 -0
- package/dist/logging/Logger.d.ts.map +1 -0
- package/dist/logging/OpenTelemetryLogHandler.d.ts +15 -0
- package/dist/logging/OpenTelemetryLogHandler.d.ts.map +1 -0
- package/dist/logging/SentryLogHandler.d.ts +13 -0
- package/dist/logging/SentryLogHandler.d.ts.map +1 -0
- package/dist/logging/const.d.ts +14 -0
- package/dist/logging/const.d.ts.map +1 -0
- package/dist/logging/util.d.ts +14 -0
- package/dist/logging/util.d.ts.map +1 -0
- package/dist/pagelifecycle/const.d.ts +15 -0
- package/dist/pagelifecycle/const.d.ts.map +1 -0
- package/dist/pagelifecycle/typedef.d.ts +4 -0
- package/dist/pagelifecycle/typedef.d.ts.map +1 -0
- package/dist/pagelifecycle/util.d.ts +31 -0
- package/dist/pagelifecycle/util.d.ts.map +1 -0
- package/dist/typecheck/TypeCheckError.d.ts +11 -0
- package/dist/typecheck/TypeCheckError.d.ts.map +1 -0
- package/dist/typecheck/TypeChecker.d.ts +27 -0
- package/dist/typecheck/TypeChecker.d.ts.map +1 -0
- package/dist/typecheck/util.d.ts +17 -0
- package/dist/typecheck/util.d.ts.map +1 -0
- package/dist/util/async.d.ts +13 -0
- package/dist/util/async.d.ts.map +1 -0
- package/dist/util/error.d.ts +16 -0
- package/dist/util/error.d.ts.map +1 -0
- package/dist/util/event_emitter.d.ts +42 -0
- package/dist/util/event_emitter.d.ts.map +1 -0
- package/dist/util/fetch.d.ts +22 -0
- package/dist/util/fetch.d.ts.map +1 -0
- package/dist/util/number.d.ts +23 -0
- package/dist/util/number.d.ts.map +1 -0
- package/dist/util/object.d.ts +24 -0
- package/dist/util/object.d.ts.map +1 -0
- package/dist/util/query.d.ts +11 -0
- package/dist/util/query.d.ts.map +1 -0
- package/dist/util/state.d.ts +5 -0
- package/dist/util/state.d.ts.map +1 -0
- package/dist/util/string.d.ts +25 -0
- package/dist/util/string.d.ts.map +1 -0
- package/dist/util/uuid.d.ts +13 -0
- package/dist/util/uuid.d.ts.map +1 -0
- package/dist/util/validate.d.ts +106 -0
- package/dist/util/validate.d.ts.map +1 -0
- package/package.json +32 -16
- package/src/const/http_status.test.ts +7 -0
- package/src/const/{http_status.js → http_status.ts} +1 -1
- package/src/{index.js → index.ts} +8 -0
- package/src/logging/AbstractLogHandler.ts +31 -0
- package/src/logging/{ConsoleLogHandler.js → ConsoleLogHandler.ts} +15 -13
- package/src/logging/Logger.test.ts +69 -0
- package/src/logging/Logger.ts +77 -0
- package/src/logging/OpenTelemetryLogHandler.ts +40 -0
- package/src/logging/SentryLogHandler.ts +44 -0
- package/src/logging/{const.js → const.ts} +1 -1
- package/src/logging/util.test.ts +33 -0
- package/src/logging/util.ts +36 -0
- package/src/pagelifecycle/{const.js → const.ts} +2 -2
- package/src/pagelifecycle/typedef.ts +5 -0
- package/src/pagelifecycle/util.test.ts +99 -0
- package/src/pagelifecycle/{util.js → util.ts} +14 -27
- package/src/typecheck/{TypeCheckError.js → TypeCheckError.ts} +7 -3
- package/src/typecheck/TypeChecker.test.ts +70 -0
- package/src/typecheck/{TypeChecker.js → TypeChecker.ts} +10 -27
- package/src/typecheck/util.test.ts +36 -0
- package/src/typecheck/util.ts +50 -0
- package/src/util/async.test.ts +74 -0
- package/src/util/{async.js → async.ts} +3 -12
- package/src/util/error.test.ts +32 -0
- package/src/util/error.ts +37 -0
- package/src/util/event_emitter.test.ts +228 -0
- package/src/util/event_emitter.ts +147 -0
- package/src/util/fetch.test.ts +62 -0
- package/src/util/{fetch.js → fetch.ts} +40 -31
- package/src/util/number.test.ts +124 -0
- package/src/util/number.ts +58 -0
- package/src/util/object.test.ts +203 -0
- package/src/util/{object.js → object.ts} +14 -21
- package/src/util/query.test.ts +71 -0
- package/src/util/query.ts +35 -0
- package/src/util/state.test.ts +47 -0
- package/src/util/{state.js → state.ts} +3 -6
- package/src/util/string.test.ts +64 -0
- package/src/util/string.ts +65 -0
- package/src/util/uuid.test.ts +53 -0
- package/src/util/uuid.ts +31 -0
- package/src/util/validate.test.ts +309 -0
- package/src/util/validate.ts +230 -0
- package/.vscode/extensions.json +0 -6
- package/.vscode/settings.json +0 -27
- package/src/logging/AbstractLogHandler.js +0 -23
- package/src/logging/Logger.js +0 -115
- package/src/logging/OpenTelemetryLogHandler.js +0 -30
- package/src/logging/SentryLogHandler.js +0 -46
- package/src/logging/util.js +0 -41
- package/src/pagelifecycle/typedef.js +0 -9
- package/src/typecheck/util.js +0 -60
- package/src/util/error.js +0 -33
- package/src/util/event_emitter.js +0 -196
- package/src/util/number.js +0 -118
- package/src/util/query.js +0 -32
- package/src/util/string.js +0 -76
- package/src/util/uuid.js +0 -35
- package/src/util/validate.js +0 -247
- package/types/const/http_status.d.ts +0 -131
- package/types/const/http_status.d.ts.map +0 -1
- package/types/index.d.ts +0 -26
- package/types/index.d.ts.map +0 -1
- package/types/logging/AbstractLogHandler.d.ts +0 -20
- package/types/logging/AbstractLogHandler.d.ts.map +0 -1
- package/types/logging/ConsoleLogHandler.d.ts +0 -9
- package/types/logging/ConsoleLogHandler.d.ts.map +0 -1
- package/types/logging/Logger.d.ts +0 -66
- package/types/logging/Logger.d.ts.map +0 -1
- package/types/logging/OpenTelemetryLogHandler.d.ts +0 -11
- package/types/logging/OpenTelemetryLogHandler.d.ts.map +0 -1
- package/types/logging/SentryLogHandler.d.ts +0 -9
- package/types/logging/SentryLogHandler.d.ts.map +0 -1
- package/types/logging/const.d.ts +0 -14
- package/types/logging/const.d.ts.map +0 -1
- package/types/logging/util.d.ts +0 -4
- package/types/logging/util.d.ts.map +0 -1
- package/types/pagelifecycle/const.d.ts +0 -15
- package/types/pagelifecycle/const.d.ts.map +0 -1
- package/types/pagelifecycle/typedef.d.ts +0 -4
- package/types/pagelifecycle/typedef.d.ts.map +0 -1
- package/types/pagelifecycle/util.d.ts +0 -8
- package/types/pagelifecycle/util.d.ts.map +0 -1
- package/types/typecheck/TypeCheckError.d.ts +0 -13
- package/types/typecheck/TypeCheckError.d.ts.map +0 -1
- package/types/typecheck/TypeChecker.d.ts +0 -40
- package/types/typecheck/TypeChecker.d.ts.map +0 -1
- package/types/typecheck/util.d.ts +0 -4
- package/types/typecheck/util.d.ts.map +0 -1
- package/types/util/async.d.ts +0 -4
- package/types/util/async.d.ts.map +0 -1
- package/types/util/error.d.ts +0 -3
- package/types/util/error.d.ts.map +0 -1
- package/types/util/event_emitter.d.ts +0 -69
- package/types/util/event_emitter.d.ts.map +0 -1
- package/types/util/fetch.d.ts +0 -22
- package/types/util/fetch.d.ts.map +0 -1
- package/types/util/number.d.ts +0 -11
- package/types/util/number.d.ts.map +0 -1
- package/types/util/object.d.ts +0 -6
- package/types/util/object.d.ts.map +0 -1
- package/types/util/query.d.ts +0 -3
- package/types/util/query.d.ts.map +0 -1
- package/types/util/state.d.ts +0 -2
- package/types/util/state.d.ts.map +0 -1
- package/types/util/string.d.ts +0 -7
- package/types/util/string.d.ts.map +0 -1
- package/types/util/uuid.d.ts +0 -4
- package/types/util/uuid.d.ts.map +0 -1
- package/types/util/validate.d.ts +0 -45
- package/types/util/validate.d.ts.map +0 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { TypeCheckError } from '../typecheck/TypeCheckError.js';
|
|
2
|
+
import {
|
|
3
|
+
fixFloatPrecision,
|
|
4
|
+
getRandomInt,
|
|
5
|
+
isEqual,
|
|
6
|
+
isGreater,
|
|
7
|
+
isGreaterOrEqual,
|
|
8
|
+
isInRange,
|
|
9
|
+
isLess,
|
|
10
|
+
isLessOrEqual,
|
|
11
|
+
deg2rad,
|
|
12
|
+
rad2deg,
|
|
13
|
+
} from './number.js';
|
|
14
|
+
|
|
15
|
+
test('Converts angle in degrees to radians', () => {
|
|
16
|
+
expect(deg2rad(90)).toBe(1.5707963267948966);
|
|
17
|
+
// @ts-expect-error
|
|
18
|
+
expect(() => deg2rad('')).toThrowError(TypeCheckError);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('Converts angle in radians to degrees', () => {
|
|
22
|
+
expect(rad2deg(1.5707963267948966)).toBe(90);
|
|
23
|
+
// @ts-expect-error
|
|
24
|
+
expect(() => rad2deg('')).toThrowError(TypeCheckError);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('fixFloatPrecision', () => {
|
|
28
|
+
test('Fixes float precision issues', () => {
|
|
29
|
+
expect(fixFloatPrecision(0.20000000000000004)).toBe(0.2);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('Handles zero', () => {
|
|
33
|
+
expect(fixFloatPrecision(0)).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('Handles negative numbers', () => {
|
|
37
|
+
expect(fixFloatPrecision(-0.20000000000000004)).toBe(-0.2);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('Handles very small numbers', () => {
|
|
41
|
+
expect(fixFloatPrecision(0.0000000000001)).toBe(0);
|
|
42
|
+
expect(fixFloatPrecision(-0.0000000000001)).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('Handles integer numbers', () => {
|
|
46
|
+
expect(fixFloatPrecision(5)).toBe(5);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('Handles string number input', () => {
|
|
50
|
+
expect(fixFloatPrecision('5.123456789')).toBe(5.123456789);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('Throws error for invalid input', () => {
|
|
54
|
+
expect(fixFloatPrecision('abc')).toBe(Number.NaN);
|
|
55
|
+
expect(fixFloatPrecision(null)).toBe(Number.NaN);
|
|
56
|
+
expect(fixFloatPrecision(undefined)).toBe(Number.NaN);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('getRandomInt', () => {
|
|
61
|
+
test('Throws error if min or max is not finite number', () => {
|
|
62
|
+
// @ts-expect-error
|
|
63
|
+
expect(() => getRandomInt('', 1)).toThrowError(TypeCheckError);
|
|
64
|
+
// @ts-expect-error
|
|
65
|
+
expect(() => getRandomInt(1, '')).toThrowError(TypeCheckError);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('Returns random integer within range when min equals max', () => {
|
|
69
|
+
expect(getRandomInt(1, 1)).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('Returns random integer within range when min is less than max', () => {
|
|
73
|
+
const result = getRandomInt(1, 10);
|
|
74
|
+
expect(result).toBeGreaterThanOrEqual(1);
|
|
75
|
+
expect(result).toBeLessThanOrEqual(10);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('Works with negative numbers', () => {
|
|
79
|
+
const result = getRandomInt(-5, -1);
|
|
80
|
+
expect(result).toBeGreaterThanOrEqual(-5);
|
|
81
|
+
expect(result).toBeLessThanOrEqual(-1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('Works with zero range', () => {
|
|
85
|
+
const result = getRandomInt(0, 0);
|
|
86
|
+
expect(result).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('number', () => {
|
|
91
|
+
test('isEq', () => {
|
|
92
|
+
expect(isEqual(1, 0)).toBe(false);
|
|
93
|
+
expect(isEqual(1, 1)).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('isGt', () => {
|
|
97
|
+
expect(isGreater(1, 0)).toBe(true);
|
|
98
|
+
expect(isGreater(1, 1)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('isGtOrEq', () => {
|
|
102
|
+
expect(isGreaterOrEqual(1, 0)).toBe(true);
|
|
103
|
+
expect(isGreaterOrEqual(1, 1)).toBe(true);
|
|
104
|
+
expect(isGreaterOrEqual(1, 2)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('isGtOrEq', () => {
|
|
108
|
+
expect(isInRange(1, 0, 2)).toBe(true);
|
|
109
|
+
expect(isInRange(1, 0, 1)).toBe(true);
|
|
110
|
+
expect(isInRange(2, 0, 1)).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('isLe', () => {
|
|
114
|
+
expect(isLess(1, 0)).toBe(false);
|
|
115
|
+
expect(isLess(0, 0)).toBe(false);
|
|
116
|
+
expect(isLess(0, 1)).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('isLeOrEq', () => {
|
|
120
|
+
expect(isLessOrEqual(1, 0)).toBe(false);
|
|
121
|
+
expect(isLessOrEqual(0, 0)).toBe(true);
|
|
122
|
+
expect(isLessOrEqual(0, 1)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { TypeCheckError } from '../typecheck/TypeCheckError.js';
|
|
2
|
+
|
|
3
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
4
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
5
|
+
|
|
6
|
+
const PRECISION = 12;
|
|
7
|
+
const EPSILON = 1e-11;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts degrees to radians.
|
|
11
|
+
*/
|
|
12
|
+
export const deg2rad = (degrees: number): number => {
|
|
13
|
+
if (!Number.isFinite(degrees)) {
|
|
14
|
+
throw new TypeCheckError('Argument degrees must be a finite number', { value: degrees });
|
|
15
|
+
}
|
|
16
|
+
return degrees * DEG_TO_RAD;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Converts radians to degrees.
|
|
21
|
+
*/
|
|
22
|
+
export const rad2deg = (radians: number): number => {
|
|
23
|
+
if (!Number.isFinite(radians)) {
|
|
24
|
+
throw new TypeCheckError('Argument radians must be a finite number', { value: radians });
|
|
25
|
+
}
|
|
26
|
+
return radians * RAD_TO_DEG;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns random integer in range.
|
|
31
|
+
*/
|
|
32
|
+
export const getRandomInt = (min: number, max: number): number => {
|
|
33
|
+
if (!Number.isFinite(min)) {
|
|
34
|
+
throw new TypeCheckError('Argument min must be finite number', { value: min });
|
|
35
|
+
}
|
|
36
|
+
if (!Number.isFinite(max)) {
|
|
37
|
+
throw new TypeCheckError('Argument max must be finite number', { value: max });
|
|
38
|
+
}
|
|
39
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalizes floating point precision (e.g. 0.20000000000000004 → 0.2).
|
|
44
|
+
*/
|
|
45
|
+
export const fixFloatPrecision = (value: number | string | null | undefined): number => {
|
|
46
|
+
const parsedValue = typeof value === 'string' ? Number(value) : value;
|
|
47
|
+
if (parsedValue === null || parsedValue === undefined || !Number.isFinite(parsedValue)) {
|
|
48
|
+
return Number.NaN;
|
|
49
|
+
}
|
|
50
|
+
return Math.abs(parsedValue) < EPSILON ? 0 : Number(parsedValue.toPrecision(PRECISION));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const isGreater = (value: number, min: number): boolean => value > min;
|
|
54
|
+
export const isGreaterOrEqual = (value: number, min: number): boolean => value >= min;
|
|
55
|
+
export const isLess = (value: number, min: number): boolean => value < min;
|
|
56
|
+
export const isLessOrEqual = (value: number, min: number): boolean => value <= min;
|
|
57
|
+
export const isInRange = (value: number, min: number, max: number): boolean => value >= min && value <= max;
|
|
58
|
+
export const isEqual = (value: number, expected: number): boolean => value === expected;
|
|
@@ -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)
|
|
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
|
|
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 = (
|
|
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
|
|
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;
|
|
20
|
+
return data;
|
|
24
21
|
};
|