@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 +142 -49
- package/dist/runtime/validators.js +11 -2
- package/dist/transformer.js +0 -33
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
484
|
-
|
|
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
|
}
|
package/dist/transformer.js
CHANGED
|
@@ -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);
|