@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 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`: always write onto the input. `false`: always allocate new containers. |
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 (non-empty numeric strings `parseFloat`).
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, new RegExp(${JSON.stringify(c.value)}), ${JSON.stringify('Pattern<' + c.value + '>')}${msgArg})`;
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, new RegExp(${JSON.stringify(regexStr)}), ${JSON.stringify(expected)})`;
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__': ts.factory.createArrayLiteralExpression(allowedKeys),
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__': ts.factory.createArrayLiteralExpression(allowedKeys),
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)) Object.assign(data, val);
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;
@@ -1,39 +1,43 @@
1
1
  import ts from '../ts.js';
2
2
  export function hoistRegistrations(sourceFile, cache, requiredUtils, schemasMap) {
3
- if (cache.size === 0 && requiredUtils.size === 0) {
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
- if (!hasVariableDeclaration(sourceFile.statements, 'validators') &&
11
- !hasVariableDeclaration(utilityStatements, 'validators')) {
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 (!hasVariableDeclaration(sourceFile.statements, 'MetadataStore') &&
17
- !hasVariableDeclaration(utilityStatements, 'MetadataStore')) {
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 (!hasVariableDeclaration(sourceFile.statements, `__val_${hash}`) &&
27
- !hasVariableDeclaration(utilityStatements, `__val_${hash}`) &&
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(`__val_${hash}`), undefined, undefined, expr)
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(`__val_${hash}`)
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 hasVariableDeclaration(statements, name) {
99
+ function collectDeclaredNames(statements) {
100
+ const names = new Set();
96
101
  for (const statement of statements) {
97
- if (ts.isVariableStatement(statement)) {
98
- for (const decl of statement.declarationList.declarations) {
99
- if (ts.isIdentifier(decl.name) && decl.name.text === name) {
100
- return true;
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 false;
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>): ts.Expression;
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;
@@ -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 hash = generateHash(type, checker);
134
- if (validatorsMap.has(hash)) {
135
- return ts.factory.createIdentifier(`__val_${hash}`);
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(hash, ts.factory.createIdentifier(`PENDING_${hash}`));
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(hash);
541
- validatorsMap.set(hash, result);
542
- return ts.factory.createIdentifier(`__val_${hash}`);
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
- return createHash('sha256').update(structuralSig).digest('hex').substring(0, 16);
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) {
@@ -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
- result[newKey] = convertPropertyCasing(value, casing, options);
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`: always write onto the input. `false` (default): always allocate new containers. */
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[], childValidator: Function) => void;
88
- object: (v: any, path: string, ctx: ValidationContext, allowedKeys?: string[], expected?: string) => any;
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: any;
132
+ data?: any;
130
133
  };
131
134
  }
132
135
  export declare function groupErrorsByPath(errors: IValidationError[]): Record<string, {