@webergency-utils/typechecker 0.1.0 → 0.1.1

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
@@ -51,35 +51,7 @@ Update your `tsconfig.json` to include the transformer in the `compilerOptions.p
51
51
 
52
52
  ## Usage
53
53
 
54
- ### 1. Decorator-Based Validation (Server Endpoints)
55
-
56
- The transformer automatically intercepts decorators imported from `@webergency-utils/server` (`@Body`, `@Query`, `@Param`). It hashes the underlying TypeScript type, generates a highly optimized validation function, and hoists it to the top of the file using the `MetadataStore`.
57
-
58
- ```typescript
59
- import { Controller, Post, Body } from '@webergency-utils/server';
60
-
61
- interface UserDTO {
62
- id: string;
63
- name: string;
64
- age?: number;
65
- }
66
-
67
- @Controller('/users')
68
- export class UserController {
69
- // @webergency-utils/typechecker automatically generates a validator for UserDTO,
70
- // registers it, and transforms this to @Body("hash_id", "strict")
71
- @Post('/')
72
- createUser(@Body() user: UserDTO) {
73
- return { success: true, user };
74
- }
75
- }
76
- ```
77
-
78
- ---
79
-
80
- ### 2. Manual Runtime Validation APIs
81
-
82
- You can manually validate unknown data anywhere in your code. The transformer intercepts these calls and replaces them with direct, optimized validation functions.
54
+ You can validate unknown data anywhere in your code. The transformer intercepts these calls and replaces them with direct, optimized validation functions.
83
55
 
84
56
  ```typescript
85
57
  import { is, assert, assertGuard, validate } from '@webergency-utils/typechecker';
@@ -213,37 +185,158 @@ This flattened, grouped output is incredibly powerful—it tells the developer (
213
185
 
214
186
  ---
215
187
 
216
- ## Supported JSON-Schema Validation Tags
188
+ ### 5. Deep/External Type Validation (WithModifiers)
189
+
190
+ If you have deeply nested objects or external types that you cannot edit directly, you can add validation tags to specific properties using dot-notation path references. This prevents you from having to retype the entire structure.
191
+
192
+ ```typescript
193
+ import { WithModifiers, constraint, format } from '@webergency-utils/typechecker';
194
+
195
+ // External type we cannot edit directly
196
+ interface ExternalUser {
197
+ id: string;
198
+ profile?: {
199
+ details: {
200
+ email: string;
201
+ password: string;
202
+ }
203
+ }
204
+ }
217
205
 
218
- Add strict runtime metadata to your TypeScript primitives using standard intersection types:
206
+ // Decorate specific paths using dot notation
207
+ type ValidatedUser = WithModifiers<ExternalUser, {
208
+ 'profile.details.email': format.Email;
209
+ 'profile.details.password': constraint.MinLength<8>;
210
+ }>;
211
+ ```
219
212
 
220
- ### String Tags
221
- - `MinLength<N>`: Minimum string length.
222
- - `MaxLength<N>`: Maximum string length.
223
- - `Pattern<RegExp>`: Regular expression validation.
224
- - `Format<T>`: Structural formats: `'email'`, `'uuid'`, `'date'`, `'date-time'`, `'url'`, `'ipv4'`, `'ipv6'`.
213
+ ### 6. Cross-Field Dependencies (Requires)
225
214
 
226
- ### Number Tags
227
- - `Minimum<N>`: Minimum numeric value (inclusive).
228
- - `Maximum<N>`: Maximum numeric value (inclusive).
229
- - `ExclusiveMinimum<N>`: Greater than `N`.
230
- - `ExclusiveMaximum<N>`: Less than `N`.
231
- - `MultipleOf<N>`: Must be a multiple of `N`.
215
+ You can specify that a property requires the presence of other properties in the same object using absolute paths or relative dot-notation paths (e.g., `.sibling`, `..grandparent.cousin`):
232
216
 
233
- ### Array Tags
234
- - `MinItems<N>`: Minimum array items count.
235
- - `MaxItems<N>`: Maximum array items count.
236
- - `UniqueItems`: Enforces all elements in the array to be deeply unique.
217
+ ```typescript
218
+ import { WithModifiers, constraint } from '@webergency-utils/typechecker';
219
+
220
+ interface DBConfig {
221
+ ssl?: boolean;
222
+ cert?: string;
223
+ auth?: {
224
+ username?: string;
225
+ password?: string;
226
+ }
227
+ }
228
+
229
+ type SecureConfig = WithModifiers<DBConfig, {
230
+ // Absolute check: if "ssl" is defined, "cert" must exist at root
231
+ 'ssl': constraint.Requires<'cert'>;
232
+
233
+ // Relative check: if "auth.password" is defined, its sibling "username" must exist
234
+ 'auth.password': constraint.Requires<'.username'>;
235
+ }>;
236
+ ```
237
+
238
+ ### 7. Custom Error Messages
239
+
240
+ You can specify custom error messages for constraint violations. Most constraint tags accept a custom message as an optional template argument, or you can use the `Message<Msg>` tag:
241
+
242
+ ```typescript
243
+ import { validate, constraint, Message } from '@webergency-utils/typechecker';
244
+
245
+ interface User {
246
+ // Direct message in constraint tag
247
+ age: number & constraint.Minimum<18, "Must be 18 or older">;
248
+
249
+ // Fallback property message via Message<T>
250
+ email: string & constraint.Format<'email'> & Message<"Please supply a valid email address">;
251
+ }
252
+ ```
253
+
254
+ ### 8. Zod Compatibility
255
+
256
+ If you are integrating with libraries that expect Zod-style validation errors (such as React Hook Form or API gateways), you can convert the validation errors using the `toZodIssues` helper or throw a `ZodLikeError` which wraps the issues:
257
+
258
+ ```typescript
259
+ import { validate, toZodIssues, ZodLikeError } from '@webergency-utils/typechecker';
260
+
261
+ const result = validate<User>(data);
262
+ if (!result.success) {
263
+ // 1. Convert errors array to Zod-compliant issues
264
+ const zodIssues = toZodIssues(result.errors);
265
+ console.log(zodIssues); // [{ code: "custom", path: ["age"], message: "Minimum<18>", received: 15 }]
266
+
267
+ // 2. Or throw a Zod-like Error
268
+ throw new ZodLikeError(result.errors);
269
+ }
270
+ ```
271
+
272
+ ---
273
+
274
+ ## Supported Validation & Modifier Namespaces
275
+
276
+ The library provides validation tags and modifiers grouped into four logical namespaces. These can be imported directly or used via their namespaces (e.g., `constraint.MinLength` vs `MinLength`).
277
+
278
+ ### 1. `constraint` Namespace
279
+ Impose structural and value-based constraints on primitives:
280
+
281
+ - `MinLength<N, Msg?>`: Minimum string length.
282
+ - `MaxLength<N, Msg?>`: Maximum string length.
283
+ - `Length<Min, Max>`: Helper combining MinLength and MaxLength.
284
+ - `Pattern<RegExp, Msg?>`: Regular expression validation.
285
+ - `Minimum<N, Msg?>` / `Maximum<N, Msg?>`: Inclusive numeric bounds (supports `number | bigint`).
286
+ - `ExclusiveMinimum<N, Msg?>` / `ExclusiveMaximum<N, Msg?>`: Exclusive numeric bounds.
287
+ - `Range<Min, Max>`: Helper combining Minimum and Maximum.
288
+ - `MultipleOf<N, Msg?>`: Enforces that the value is a multiple of `N`.
289
+ - `MinItems<N, Msg?>` / `MaxItems<N, Msg?>`: Array size constraints.
290
+ - `UniqueItems<Msg?>`: Enforces that all elements in an array or Set are deeply unique.
291
+ - `Custom<Fn, Msg?>`: Executes a custom validation function: `(val, ctx) => boolean`.
292
+ - `Requires<Paths, Msg?>`: Enforces cross-field dependency validation.
293
+ - `Message<Msg>`: Attaches a fallback custom error message to a property.
294
+
295
+ ### 2. `format` Namespace
296
+ Pre-defined string formatting shortcuts:
297
+
298
+ - `format.Email`: RFC 5322 Email.
299
+ - `format.UUID`: UUID (v1-v5) format.
300
+ - `format.URL`: Full URL validation (HTTP/HTTPS/FTP).
301
+ - `format.IPv4` / `format.IPv6`: IP address validation.
302
+ - `format.DateTime`: ISO-8601 Date Time string.
303
+ - `format.Date`: ISO Date (YYYY-MM-DD).
304
+ - `format.Time`: Time string (HH:MM:SS).
305
+ - `format.Duration`: ISO-8601 duration (e.g., PT1H).
306
+ - `format.ObjectId`: 24-character MongoDB ObjectId.
307
+ - `format.Byte`: Base64 encoded string.
308
+ - `format.Password`: Always valid string, placeholder for password parameters.
309
+ - `format.Regex`: Valid regular expression string.
310
+ - `format.Hostname`: Valid domain name/hostname.
311
+ - `format.URI`: Valid URI.
312
+
313
+ ### 3. `transform` Namespace
314
+ Coerce or sanitize values before validation:
315
+
316
+ - `transform.Trim`: Trims whitespace from both ends of a string.
317
+ - `transform.LowerCase`: Converts string to lowercase.
318
+ - `transform.UpperCase`: Converts string to uppercase.
319
+ - `transform.Capitalize`: Capitalizes the first letter of a string.
320
+ - `transform.ToNumber`: Coerces inputs to numbers.
321
+ - `transform.ToBoolean`: Coerces inputs to booleans.
322
+ - `transform.ToDate`: Coerces string timestamp to a Date object.
323
+ - `transform.Custom<Fn>`: Applies a custom mapping function `(val) => any`.
324
+
325
+ ### 4. `tag` Namespace
326
+ Define defaults for optional fields:
327
+
328
+ - `tag.Default<Value>`: Injects the specified `Value` if the property is `undefined` at validation time.
237
329
 
238
330
  #### Example:
239
331
  ```typescript
240
- import { MinLength, Minimum, Format, UniqueItems } from '@webergency-utils/typechecker';
332
+ import { MinLength, Minimum, Format, UniqueItems, tag, transform, constraint } from '@webergency-utils/typechecker';
241
333
 
242
334
  interface Profile {
243
- email: string & Format<'email'>;
335
+ email: string & Format<'email'> & transform.Trim & transform.LowerCase;
244
336
  password: string & MinLength<8>;
245
- age: number & Minimum<18>;
337
+ age: number & Minimum<18> & constraint.Message<"Must be 18+">;
246
338
  luckyNumbers: number[] & UniqueItems;
339
+ role: string & tag.Default<"user">;
247
340
  }
248
341
  ```
249
342
 
@@ -480,8 +480,17 @@ function getValueAtPath(obj, path) {
480
480
  if (current === null || current === undefined || typeof current !== 'object') {
481
481
  return undefined;
482
482
  }
483
- const cleanPart = part.replace(/\[\d+\]/g, '');
484
- current = current[cleanPart];
483
+ const matches = part.split('[');
484
+ const baseKey = matches[0];
485
+ current = current[baseKey];
486
+ for (let i = 1; i < matches.length; i++) {
487
+ if (current === null || current === undefined || typeof current !== 'object') {
488
+ return undefined;
489
+ }
490
+ const idxStr = matches[i].replace(']', '');
491
+ const idx = parseInt(idxStr, 10);
492
+ current = current[idx];
493
+ }
485
494
  }
486
495
  return current;
487
496
  }
@@ -2,7 +2,6 @@ import ts from 'typescript';
2
2
  import { buildValidator, generateHash, buildJsonSchema, objectToAst } from './engine/resolver.js';
3
3
  export { buildValidator, generateHash, buildJsonSchema } from './engine/resolver.js';
4
4
  import { hoistRegistrations } from './engine/hoister.js';
5
- const TARGET_DECORATORS = ['Query', 'Body', 'Param'];
6
5
  const RUNTIME_FUNCTIONS = ['is', 'assert', 'assertGuard', 'validate', 'jsonSchema'];
7
6
  export default function transformer(program) {
8
7
  const checker = program.getTypeChecker();
@@ -93,38 +92,6 @@ export default function transformer(program) {
93
92
  }
94
93
  }
95
94
  }
96
- // Handle decorators
97
- if (ts.isParameter(node) && ts.canHaveDecorators(node)) {
98
- const decorators = ts.getDecorators(node);
99
- if (decorators) {
100
- const updatedDecorators = decorators.map(decorator => {
101
- if (ts.isCallExpression(decorator.expression) && ts.isIdentifier(decorator.expression.expression)) {
102
- const decName = decorator.expression.expression.text;
103
- if (TARGET_DECORATORS.includes(decName)) {
104
- const typeNode = node.type;
105
- if (typeNode) {
106
- const type = checker.getTypeFromTypeNode(typeNode);
107
- const hash = generateHash(type, checker);
108
- if (!validatorCache.has(hash)) {
109
- buildValidator(type, checker, validatorCache, requiredUtils);
110
- }
111
- if (!schemasCache.has(hash)) {
112
- const schemaObj = buildJsonSchema(type, checker);
113
- schemasCache.set(hash, objectToAst(schemaObj));
114
- }
115
- const mdStoreAccess = ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('MetadataStore'), 'getValidator');
116
- const getCall = ts.factory.createCallExpression(mdStoreAccess, undefined, [ts.factory.createStringLiteral(hash)]);
117
- const decoratorArgs = [...decorator.expression.arguments];
118
- decoratorArgs[1] = getCall;
119
- return ts.factory.updateDecorator(decorator, ts.factory.createCallExpression(decorator.expression.expression, decorator.expression.typeArguments, decoratorArgs));
120
- }
121
- }
122
- }
123
- return decorator;
124
- });
125
- return ts.factory.updateParameterDeclaration(node, ts.getModifiers(node), node.dotDotDotToken, node.name, node.questionToken, node.type, node.initializer);
126
- }
127
- }
128
95
  return ts.visitEachChild(node, visitor, context);
129
96
  };
130
97
  const transformedFile = ts.visitNode(sourceFile, visitor);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webergency-utils/typechecker",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "TypeScript compiler plugin for runtime validation",
6
6
  "author": "radixxko",