@webergency-utils/typechecker 0.2.2 → 0.2.3
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/README.md +9 -3
- package/dist/engine/generators.js +6 -5
- package/dist/engine/hoister.js +23 -17
- package/dist/engine/resolver.d.ts +1 -1
- package/dist/engine/resolver.js +16 -9
- package/dist/runtime/casing.js +18 -1
- package/dist/runtime/validators.d.ts +8 -5
- package/dist/runtime/validators.js +733 -76
- package/dist/transformer.js +77 -24
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -224,6 +224,10 @@ Generates a raw JSON Schema draft-07 object matching type `T` at compile time.
|
|
|
224
224
|
|
|
225
225
|
Validates `input` against a **runtime JSON Schema** value (not a TypeScript generic).
|
|
226
226
|
|
|
227
|
+
Only the documented schema subset is compiled. Recognized validation keywords that are not implemented
|
|
228
|
+
(for example `enum`, `oneOf`, and `not`) throw during schema compilation instead of silently accepting data.
|
|
229
|
+
Schema `pattern` values are rejected when they exceed the safety limit or contain common catastrophic-backtracking constructs.
|
|
230
|
+
|
|
227
231
|
- **Parameters**:
|
|
228
232
|
- `schema`: JSON Schema object.
|
|
229
233
|
- `input`: The value to validate.
|
|
@@ -252,6 +256,8 @@ Like `assertGuard`, but against a JSON Schema value. Root replacement throws.
|
|
|
252
256
|
#### `convertPropertyCasing<T, C extends CasingFormat>(obj: T, casing: C, options?: ConvertCasingOptions): ConvertPropertyCasing<T, C>`
|
|
253
257
|
|
|
254
258
|
Recursively converts all property keys of an object to the specified casing format.
|
|
259
|
+
If two source keys normalize to the same output key, conversion throws instead of silently discarding a value.
|
|
260
|
+
Special keys such as `__proto__` are preserved as own data properties without changing the result prototype.
|
|
255
261
|
|
|
256
262
|
- **Parameters**:
|
|
257
263
|
- `obj`: The source object.
|
|
@@ -339,7 +345,7 @@ Extends `GuardOptions` for `validate` / `validateSchema` (adds `mutate`; no `err
|
|
|
339
345
|
| :--- | :--- | :--- | :--- |
|
|
340
346
|
| `mode` | `ValidationMode` | `'strict'` | Unknown-key policy (`strict` / `relaxed` / `strip`). Not coercion — see `ValidationMode` above. |
|
|
341
347
|
| `from` | `'json' \| 'query' \| ((val, ctx) => any)` | `undefined` | Input conversion mode. `'json'` revives JSON-impossible types. `'query'` also coerces querystring shapes. A custom function is `(val, { key, path, parent, root, index?, kind }) => any` and runs only on type mismatch. `kind` is a `CoercionKind` dispatch tag (not `typeof` / a TS type). `key` is the nearest named path segment (for `[n]` leaves, the closest named key above). |
|
|
342
|
-
| `mutate` | `boolean` | `false` | `true`:
|
|
348
|
+
| `mutate` | `boolean` | `false` | `true`: write in place while validating (half-changed input on failure is allowed; union arms still use a side tree). `false`: always allocate new containers. |
|
|
343
349
|
|
|
344
350
|
#### `interface AssertOptions`
|
|
345
351
|
|
|
@@ -360,7 +366,7 @@ Extends `ValidationOptions` for `assert` / `assertSchema`.
|
|
|
360
366
|
Result object returned by `validate`.
|
|
361
367
|
|
|
362
368
|
- `success`: `boolean`
|
|
363
|
-
- `data?`: `T`
|
|
369
|
+
- `data?`: `T` — present only when `success` is `true`
|
|
364
370
|
- `errors?`: `IValidationError[]`
|
|
365
371
|
|
|
366
372
|
#### `interface IValidationError`
|
|
@@ -443,7 +449,7 @@ Sanitizes and converts input values during validation.
|
|
|
443
449
|
- `transform.LowerCase`: Converts string to lowercase.
|
|
444
450
|
- `transform.UpperCase`: Converts string to uppercase.
|
|
445
451
|
- `transform.Capitalize`: Capitalizes the first letter.
|
|
446
|
-
- `transform.ToNumber`: Same coercion as `from: 'query'` for numbers
|
|
452
|
+
- `transform.ToNumber`: Same coercion as `from: 'query'` for numbers. The entire trimmed string must be a finite decimal/scientific number; partial strings, hexadecimal syntax, `NaN`, and infinities are rejected.
|
|
447
453
|
- `transform.ToBoolean`: Same coercion as `from: 'query'` for booleans (`true`/`false`/`1`/`0`/`yes`/`no`/`on`/`off`); unknown values are left unchanged and fail the boolean check.
|
|
448
454
|
- `transform.ToDate`: Same coercion as `from: 'query'` for dates (parseable strings and finite timestamps).
|
|
449
455
|
- `transform.Custom<Fn>`: Custom mapping function: `(val) => any`.
|
|
@@ -76,7 +76,7 @@ export function createConstrainedPrimitiveCheck(baseType, constraints, requiredU
|
|
|
76
76
|
return `validators.multipleOf(v, path, ctx, ${valStr}${msgArg})`;
|
|
77
77
|
}
|
|
78
78
|
if (c.type === 'pattern') {
|
|
79
|
-
return `validators.pattern(v, path, ctx,
|
|
79
|
+
return `validators.pattern(v, path, ctx, validators.safeRegExp(${JSON.stringify(c.value)}), ${JSON.stringify('Pattern<' + c.value + '>')}${msgArg})`;
|
|
80
80
|
}
|
|
81
81
|
if (c.type === 'format') {
|
|
82
82
|
return `v = validators.format(v, path, ctx, ${JSON.stringify(c.value)}${msgArg})`;
|
|
@@ -171,7 +171,7 @@ export function createArrayCheck(elementValidator, requiredUtils) {
|
|
|
171
171
|
}
|
|
172
172
|
export function createTemplateLiteralCheck(regexStr, expected, requiredUtils) {
|
|
173
173
|
requiredUtils.add('validators');
|
|
174
|
-
const tpl = `(v, path, ctx) => validators.templateLiteral(v, path, ctx,
|
|
174
|
+
const tpl = `(v, path, ctx) => validators.templateLiteral(v, path, ctx, validators.safeRegExp(${JSON.stringify(regexStr)}), ${JSON.stringify(expected)})`;
|
|
175
175
|
return stripPositions(templateToAst(tpl));
|
|
176
176
|
}
|
|
177
177
|
export function createUnionCheck(checks, requiredUtils, expected = 'Type<Union>') {
|
|
@@ -196,6 +196,7 @@ export function createObjectCheck(props, requiredUtils, expected = 'object', ind
|
|
|
196
196
|
p.validator
|
|
197
197
|
]));
|
|
198
198
|
const allowedKeys = props.map(p => ts.factory.createStringLiteral(p.name));
|
|
199
|
+
const allowedKeySet = ts.factory.createNewExpression(ts.factory.createIdentifier('Set'), undefined, [ts.factory.createArrayLiteralExpression(allowedKeys)]);
|
|
199
200
|
if (indexValidator) {
|
|
200
201
|
const tpl = `
|
|
201
202
|
(v, path, ctx) => {
|
|
@@ -208,7 +209,7 @@ export function createObjectCheck(props, requiredUtils, expected = 'object', ind
|
|
|
208
209
|
}
|
|
209
210
|
`;
|
|
210
211
|
return injectNodes(templateToAst(tpl), {
|
|
211
|
-
'__KEYS__':
|
|
212
|
+
'__KEYS__': allowedKeySet,
|
|
212
213
|
'__EXPECTED__': ts.factory.createStringLiteral(expected),
|
|
213
214
|
'__PROPS__': ts.factory.createArrayLiteralExpression(propDefinitions, true),
|
|
214
215
|
'__INDEX__': indexValidator
|
|
@@ -225,7 +226,7 @@ export function createObjectCheck(props, requiredUtils, expected = 'object', ind
|
|
|
225
226
|
}
|
|
226
227
|
`;
|
|
227
228
|
return injectNodes(templateToAst(tpl), {
|
|
228
|
-
'__KEYS__':
|
|
229
|
+
'__KEYS__': allowedKeySet,
|
|
229
230
|
'__EXPECTED__': ts.factory.createStringLiteral(expected),
|
|
230
231
|
'__PROPS__': ts.factory.createArrayLiteralExpression(propDefinitions, true)
|
|
231
232
|
});
|
|
@@ -266,7 +267,7 @@ export function createIntersectionCheck(checks, requiredUtils) {
|
|
|
266
267
|
let data = validators.objectShell(v, ctx);
|
|
267
268
|
for (let i = 0; i < checks.length; i++) {
|
|
268
269
|
const val = checks[i](v, path, ctx);
|
|
269
|
-
if (typeof val === "object" && val !== null && !Array.isArray(val) && typeof data === "object" && data !== null && !Array.isArray(data))
|
|
270
|
+
if (typeof val === "object" && val !== null && !Array.isArray(val) && typeof data === "object" && data !== null && !Array.isArray(data)) validators.assign(data, val);
|
|
270
271
|
else data = val;
|
|
271
272
|
}
|
|
272
273
|
return data;
|
package/dist/engine/hoister.js
CHANGED
|
@@ -1,39 +1,43 @@
|
|
|
1
1
|
import ts from '../ts.js';
|
|
2
2
|
export function hoistRegistrations(sourceFile, cache, requiredUtils, schemasMap) {
|
|
3
|
-
|
|
3
|
+
const hasSchemas = !!(schemasMap && schemasMap.size > 0);
|
|
4
|
+
if (cache.size === 0 && requiredUtils.size === 0 && !hasSchemas) {
|
|
4
5
|
return sourceFile;
|
|
5
6
|
}
|
|
7
|
+
const declaredNames = collectDeclaredNames(sourceFile.statements);
|
|
6
8
|
const utilityStatements = [
|
|
7
9
|
// 1. import "@webergency-utils/typechecker/runtime";
|
|
8
10
|
ts.factory.createImportDeclaration(undefined, undefined, ts.factory.createStringLiteral('@webergency-utils/typechecker/runtime'), undefined)
|
|
9
11
|
];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
const utilityNames = new Set();
|
|
13
|
+
if (!declaredNames.has('validators') && !utilityNames.has('validators')) {
|
|
14
|
+
utilityNames.add('validators');
|
|
12
15
|
utilityStatements.push(ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
13
16
|
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('validators'), undefined, undefined, ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('globalThis'), '__WEBERGENCY_TYPECHECKER_VALIDATORS__'))
|
|
14
17
|
], ts.NodeFlags.Const)));
|
|
15
18
|
}
|
|
16
|
-
if (!
|
|
17
|
-
|
|
19
|
+
if (!declaredNames.has('MetadataStore') && !utilityNames.has('MetadataStore')) {
|
|
20
|
+
utilityNames.add('MetadataStore');
|
|
18
21
|
utilityStatements.push(ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
19
22
|
ts.factory.createVariableDeclaration(ts.factory.createIdentifier('MetadataStore'), undefined, undefined, ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('globalThis'), '__WEBERGENCY_TYPECHECKER_METADATA_STORE__'))
|
|
20
23
|
], ts.NodeFlags.Const)));
|
|
21
24
|
}
|
|
22
25
|
const variablePrepends = [];
|
|
23
26
|
const registrationAppends = [];
|
|
27
|
+
const prependedNames = new Set(utilityNames);
|
|
24
28
|
for (const [hash, expr] of cache.entries()) {
|
|
29
|
+
const valName = `__val_${hash}`;
|
|
25
30
|
// const __val_hash = expr;
|
|
26
|
-
if (!
|
|
27
|
-
|
|
28
|
-
!hasVariableDeclaration(variablePrepends, `__val_${hash}`)) {
|
|
31
|
+
if (!declaredNames.has(valName) && !prependedNames.has(valName)) {
|
|
32
|
+
prependedNames.add(valName);
|
|
29
33
|
variablePrepends.push(ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
|
|
30
|
-
ts.factory.createVariableDeclaration(ts.factory.createIdentifier(
|
|
34
|
+
ts.factory.createVariableDeclaration(ts.factory.createIdentifier(valName), undefined, undefined, expr)
|
|
31
35
|
], ts.NodeFlags.Const)));
|
|
32
36
|
}
|
|
33
37
|
// MetadataStore.registerValidator(hash, __val_hash);
|
|
34
38
|
registrationAppends.push(ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('MetadataStore'), 'registerValidator'), undefined, [
|
|
35
39
|
ts.factory.createStringLiteral(hash),
|
|
36
|
-
ts.factory.createIdentifier(
|
|
40
|
+
ts.factory.createIdentifier(valName)
|
|
37
41
|
])));
|
|
38
42
|
}
|
|
39
43
|
if (schemasMap) {
|
|
@@ -92,15 +96,17 @@ function findInsertionIndex(statements) {
|
|
|
92
96
|
}
|
|
93
97
|
return statements.length;
|
|
94
98
|
}
|
|
95
|
-
function
|
|
99
|
+
function collectDeclaredNames(statements) {
|
|
100
|
+
const names = new Set();
|
|
96
101
|
for (const statement of statements) {
|
|
97
|
-
if (ts.isVariableStatement(statement)) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
if (!ts.isVariableStatement(statement)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
for (const decl of statement.declarationList.declarations) {
|
|
106
|
+
if (ts.isIdentifier(decl.name)) {
|
|
107
|
+
names.add(decl.name.text);
|
|
102
108
|
}
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
|
-
return
|
|
111
|
+
return names;
|
|
106
112
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from '../ts.js';
|
|
2
|
-
export declare function buildValidator(type: ts.Type, checker: ts.TypeChecker, validatorsMap: Map<string, ts.Expression>, requiredUtils: Set<string
|
|
2
|
+
export declare function buildValidator(type: ts.Type, checker: ts.TypeChecker, validatorsMap: Map<string, ts.Expression>, requiredUtils: Set<string>, hash?: string): ts.Expression;
|
|
3
3
|
export declare function generateHash(type: ts.Type, checker: ts.TypeChecker): string;
|
|
4
4
|
export declare function objectToAst(val: any): ts.Expression;
|
|
5
5
|
export declare function getTypeComplexity(type: ts.Type, checker: ts.TypeChecker, visited: Set<number>): number;
|
package/dist/engine/resolver.js
CHANGED
|
@@ -129,13 +129,13 @@ function tryMergeObjectIntersection(types, checker, validatorsMap, requiredUtils
|
|
|
129
129
|
}
|
|
130
130
|
return createObjectCheck([...propMap.values()], requiredUtils, expected, indexValidator);
|
|
131
131
|
}
|
|
132
|
-
export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
133
|
-
const
|
|
134
|
-
if (validatorsMap.has(
|
|
135
|
-
return ts.factory.createIdentifier(`__val_${
|
|
132
|
+
export function buildValidator(type, checker, validatorsMap, requiredUtils, hash) {
|
|
133
|
+
const resolvedHash = hash ?? generateHash(type, checker);
|
|
134
|
+
if (validatorsMap.has(resolvedHash)) {
|
|
135
|
+
return ts.factory.createIdentifier(`__val_${resolvedHash}`);
|
|
136
136
|
}
|
|
137
137
|
// Set placeholder to handle circularity
|
|
138
|
-
validatorsMap.set(
|
|
138
|
+
validatorsMap.set(resolvedHash, ts.factory.createIdentifier(`PENDING_${resolvedHash}`));
|
|
139
139
|
let result;
|
|
140
140
|
const flags = type.getFlags();
|
|
141
141
|
const isUnion = (((flags & ts.TypeFlags.Union) !== 0 || type.isUnion()) && type.types) ? true : false;
|
|
@@ -537,9 +537,9 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
537
537
|
result = createPrimitiveCheck('any', requiredUtils);
|
|
538
538
|
}
|
|
539
539
|
}
|
|
540
|
-
validatorsMap.delete(
|
|
541
|
-
validatorsMap.set(
|
|
542
|
-
return ts.factory.createIdentifier(`__val_${
|
|
540
|
+
validatorsMap.delete(resolvedHash);
|
|
541
|
+
validatorsMap.set(resolvedHash, result);
|
|
542
|
+
return ts.factory.createIdentifier(`__val_${resolvedHash}`);
|
|
543
543
|
}
|
|
544
544
|
function buildStructuralSignature(type, checker, visited = new Set()) {
|
|
545
545
|
const flags = type.getFlags();
|
|
@@ -674,9 +674,16 @@ function buildStructuralSignature(type, checker, visited = new Set()) {
|
|
|
674
674
|
}
|
|
675
675
|
return 'any';
|
|
676
676
|
}
|
|
677
|
+
const hashByType = new WeakMap();
|
|
677
678
|
export function generateHash(type, checker) {
|
|
679
|
+
const cached = hashByType.get(type);
|
|
680
|
+
if (cached) {
|
|
681
|
+
return cached;
|
|
682
|
+
}
|
|
678
683
|
const structuralSig = buildStructuralSignature(type, checker);
|
|
679
|
-
|
|
684
|
+
const hash = createHash('sha256').update(structuralSig).digest('hex').substring(0, 16);
|
|
685
|
+
hashByType.set(type, hash);
|
|
686
|
+
return hash;
|
|
680
687
|
}
|
|
681
688
|
export function objectToAst(val) {
|
|
682
689
|
if (val === null) {
|
package/dist/runtime/casing.js
CHANGED
|
@@ -70,9 +70,26 @@ export function convertPropertyCasing(obj, casing, options) {
|
|
|
70
70
|
return obj.map((item) => convertPropertyCasing(item, casing, options));
|
|
71
71
|
}
|
|
72
72
|
const result = {};
|
|
73
|
+
const sourceKeys = new Map();
|
|
73
74
|
for (const [key, value] of Object.entries(obj)) {
|
|
74
75
|
const newKey = formatCasing(key, casing, options);
|
|
75
|
-
|
|
76
|
+
const previousKey = sourceKeys.get(newKey);
|
|
77
|
+
if (previousKey !== undefined) {
|
|
78
|
+
throw new Error(`Casing conversion collision: ${previousKey} and ${key} both map to ${newKey}`);
|
|
79
|
+
}
|
|
80
|
+
sourceKeys.set(newKey, key);
|
|
81
|
+
const nested = convertPropertyCasing(value, casing, options);
|
|
82
|
+
if (newKey !== '__proto__' && newKey !== 'constructor' && newKey !== 'prototype') {
|
|
83
|
+
result[newKey] = nested;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
Object.defineProperty(result, newKey, {
|
|
87
|
+
value: nested,
|
|
88
|
+
enumerable: true,
|
|
89
|
+
configurable: true,
|
|
90
|
+
writable: true
|
|
91
|
+
});
|
|
92
|
+
}
|
|
76
93
|
}
|
|
77
94
|
return result;
|
|
78
95
|
}
|
|
@@ -50,13 +50,14 @@ export interface AssertGuardOptions extends GuardOptions {
|
|
|
50
50
|
}
|
|
51
51
|
/** Options for `validate` / `validateSchema`. */
|
|
52
52
|
export interface ValidationOptions extends GuardOptions {
|
|
53
|
-
/** `true`:
|
|
53
|
+
/** `true`: write in place while validating. `false` (default): allocate new containers. */
|
|
54
54
|
mutate?: boolean;
|
|
55
55
|
}
|
|
56
56
|
/** Options for `assert` / `assertSchema`. */
|
|
57
57
|
export interface AssertOptions extends ValidationOptions {
|
|
58
58
|
errorFactory?: (errors: IValidationError[]) => Error;
|
|
59
59
|
}
|
|
60
|
+
declare function createSafeRegex(source: string, flags?: string): RegExp;
|
|
60
61
|
/** Query-style number coercion — shared by `from: 'query'` and `transform.ToNumber`. */
|
|
61
62
|
export declare function coerceQueryNumber(v: any): any;
|
|
62
63
|
/** Query-style boolean coercion — shared by `from: 'query'` and `transform.ToBoolean`. */
|
|
@@ -70,6 +71,8 @@ export declare const validators: {
|
|
|
70
71
|
coerceQueryBoolean: typeof coerceQueryBoolean;
|
|
71
72
|
coerceQueryDate: typeof coerceQueryDate;
|
|
72
73
|
coerceJsonDate: typeof coerceJsonDate;
|
|
74
|
+
safeRegExp: typeof createSafeRegex;
|
|
75
|
+
assign: (target: any, source: any) => any;
|
|
73
76
|
string: (v: any, path: string, ctx: ValidationContext) => any;
|
|
74
77
|
number: (v: any, path: string, ctx: ValidationContext) => any;
|
|
75
78
|
bigint: (v: any, path: string, ctx: ValidationContext) => any;
|
|
@@ -83,9 +86,9 @@ export declare const validators: {
|
|
|
83
86
|
array: (v: any, path: string, ctx: ValidationContext, childValidator: Function) => any;
|
|
84
87
|
props: (v: any, data: any, path: string, ctx: ValidationContext, props: [string, boolean, Function][]) => void;
|
|
85
88
|
objectShell: (v: any, ctx: ValidationContext) => any;
|
|
86
|
-
stripExtras: (data: any, ctx: ValidationContext, allowedKeys?: string[]) => any;
|
|
87
|
-
additionalProps: (v: any, data: any, path: string, ctx: ValidationContext, knownKeys: string[]
|
|
88
|
-
object: (v: any, path: string, ctx: ValidationContext, allowedKeys?: string[]
|
|
89
|
+
stripExtras: (data: any, ctx: ValidationContext, allowedKeys?: string[] | Set<string>) => any;
|
|
90
|
+
additionalProps: (v: any, data: any, path: string, ctx: ValidationContext, knownKeys: string[] | Set<string>, childValidator: Function) => void;
|
|
91
|
+
object: (v: any, path: string, ctx: ValidationContext, allowedKeys?: string[] | Set<string>, expected?: string) => any;
|
|
89
92
|
templateLiteral: (v: any, path: string, ctx: ValidationContext, regex: RegExp, expected: string) => any;
|
|
90
93
|
minLength: (v: string, path: string, ctx: ValidationContext, min: number, message?: string) => string;
|
|
91
94
|
maxLength: (v: string, path: string, ctx: ValidationContext, max: number, message?: string) => string;
|
|
@@ -126,7 +129,7 @@ export declare class MetadataStoreClass {
|
|
|
126
129
|
validate(validator: Function, value: any, options?: ValidationMode | ValidationOptions): {
|
|
127
130
|
success: boolean;
|
|
128
131
|
errors: IValidationError[];
|
|
129
|
-
data
|
|
132
|
+
data?: any;
|
|
130
133
|
};
|
|
131
134
|
}
|
|
132
135
|
export declare function groupErrorsByPath(errors: IValidationError[]): Record<string, {
|