exisjs 0.7.11 → 0.7.13
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 +195 -39
- package/dist/cli/commands/dev.d.ts +4 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +136 -32
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/run.d.ts +7 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +99 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/index.js +29 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/manifest.d.ts.map +1 -1
- package/dist/cli/manifest.js +18 -3
- package/dist/cli/manifest.js.map +1 -1
- package/dist/cli/routes.d.ts +6 -0
- package/dist/cli/routes.d.ts.map +1 -1
- package/dist/cli/routes.js +224 -135
- package/dist/cli/routes.js.map +1 -1
- package/dist/config/config.d.ts +2 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +30 -9
- package/dist/config/types.d.ts.map +1 -1
- package/dist/database/lifecycle.d.ts +6 -1
- package/dist/database/lifecycle.d.ts.map +1 -1
- package/dist/database/lifecycle.js +15 -4
- package/dist/database/lifecycle.js.map +1 -1
- package/dist/decorators/params.d.ts +21 -0
- package/dist/decorators/params.d.ts.map +1 -1
- package/dist/decorators/params.js +21 -1
- package/dist/decorators/params.js.map +1 -1
- package/dist/di/container.d.ts +15 -6
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/container.js +26 -3
- package/dist/di/container.js.map +1 -1
- package/dist/di/index.d.ts +1 -1
- package/dist/di/index.d.ts.map +1 -1
- package/dist/di/index.js.map +1 -1
- package/dist/di/inject.d.ts +2 -1
- package/dist/di/inject.d.ts.map +1 -1
- package/dist/di/inject.js +2 -1
- package/dist/di/inject.js.map +1 -1
- package/dist/docs/BENCHMARKS.md +4 -4
- package/dist/docs/content/README.md +43 -44
- package/dist/docs/content/boundaries.mdx +141 -48
- package/dist/docs/content/cli/commands.mdx +161 -107
- package/dist/docs/content/cli/overview.mdx +61 -46
- package/dist/docs/content/cli/usage.mdx +107 -30
- package/dist/docs/content/config.mdx +145 -147
- package/dist/docs/content/controllers.mdx +193 -464
- package/dist/docs/content/cron.mdx +183 -69
- package/dist/docs/content/decorators.mdx +449 -154
- package/dist/docs/content/errors.mdx +202 -305
- package/dist/docs/content/exisjs-mcp.mdx +59 -49
- package/dist/docs/content/installation.mdx +243 -157
- package/dist/docs/content/introduction.mdx +220 -48
- package/dist/docs/content/middleware.mdx +171 -497
- package/dist/docs/content/modules.mdx +141 -83
- package/dist/docs/content/providers.mdx +216 -180
- package/dist/docs/content/requests.mdx +168 -488
- package/dist/docs/content/responses.mdx +200 -533
- package/dist/docs/content/routing.mdx +239 -558
- package/dist/docs/content/security.mdx +233 -57
- package/dist/docs/content/structure.mdx +151 -240
- package/dist/docs/content/swagger.mdx +192 -91
- package/dist/docs/content/testing.mdx +205 -74
- package/dist/docs/content/validation.mdx +479 -341
- package/dist/docs/reference/README.md +34 -2
- package/dist/docs/reference/config.mdx +277 -73
- package/dist/docs/reference/context.mdx +197 -106
- package/dist/docs/reference/cron.mdx +266 -152
- package/dist/docs/reference/database.mdx +135 -53
- package/dist/docs/reference/decorators.mdx +234 -116
- package/dist/docs/reference/di.mdx +146 -112
- package/dist/docs/reference/error.mdx +155 -178
- package/dist/docs/reference/exis.mdx +141 -70
- package/dist/docs/reference/logger.mdx +110 -126
- package/dist/docs/reference/middleware.mdx +235 -158
- package/dist/docs/reference/plugin.mdx +136 -64
- package/dist/docs/reference/router.mdx +277 -251
- package/dist/docs/reference/sanitize.mdx +91 -87
- package/dist/docs/reference/swagger.mdx +280 -100
- package/dist/docs/reference/testing.mdx +223 -89
- package/dist/docs/reference/validator.mdx +194 -206
- package/dist/lib/inspect-routes.d.ts +2 -0
- package/dist/lib/inspect-routes.d.ts.map +1 -0
- package/dist/lib/inspect-routes.js +148 -0
- package/dist/lib/inspect-routes.js.map +1 -0
- package/dist/lib/start-server.js +13 -8
- package/dist/lib/start-server.js.map +1 -1
- package/dist/module/module.d.ts +6 -3
- package/dist/module/module.d.ts.map +1 -1
- package/dist/module/module.js +17 -3
- package/dist/module/module.js.map +1 -1
- package/dist/router/controller-registrar.d.ts.map +1 -1
- package/dist/router/controller-registrar.js +26 -4
- package/dist/router/controller-registrar.js.map +1 -1
- package/dist/router/scanner/cron-mounter.d.ts.map +1 -1
- package/dist/router/scanner/cron-mounter.js +9 -2
- package/dist/router/scanner/cron-mounter.js.map +1 -1
- package/dist/server/app.d.ts +3 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +29 -8
- package/dist/server/app.js.map +1 -1
- package/dist/server/define.d.ts.map +1 -1
- package/dist/server/define.js +5 -0
- package/dist/server/define.js.map +1 -1
- package/dist/server/request.d.ts +2 -0
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +11 -0
- package/dist/server/request.js.map +1 -1
- package/dist/swagger/generator.d.ts.map +1 -1
- package/dist/swagger/generator.js +130 -10
- package/dist/swagger/generator.js.map +1 -1
- package/dist/telemetry/index.d.ts +12 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +121 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/testing/client.d.ts +1 -0
- package/dist/testing/client.d.ts.map +1 -1
- package/dist/testing/client.js +4 -1
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +20 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/validator/builder.d.ts +26 -1
- package/dist/validator/builder.d.ts.map +1 -1
- package/dist/validator/builder.js +31 -0
- package/dist/validator/builder.js.map +1 -1
- package/dist/validator/engine.d.ts +23 -0
- package/dist/validator/engine.d.ts.map +1 -1
- package/dist/validator/engine.js +228 -31
- package/dist/validator/engine.js.map +1 -1
- package/dist/validator/index.d.ts +8 -4
- package/dist/validator/index.d.ts.map +1 -1
- package/dist/validator/index.js +4 -1
- package/dist/validator/index.js.map +1 -1
- package/dist/validator/tex-types.d.ts +15 -0
- package/dist/validator/tex-types.d.ts.map +1 -1
- package/dist/validator/tex-types.js +102 -0
- package/dist/validator/tex-types.js.map +1 -1
- package/dist/validator/tex.d.ts +1 -1
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +3 -1
- package/dist/validator/tex.js.map +1 -1
- package/dist/validator/types.d.ts +1 -0
- package/dist/validator/types.d.ts.map +1 -1
- package/package.json +47 -14
- package/dist/docs/content/dataloader.mdx +0 -170
- package/dist/docs/reference/dataloader.mdx +0 -91
|
@@ -1,403 +1,598 @@
|
|
|
1
1
|
---
|
|
2
|
+
title: "Validation and Schemas"
|
|
3
|
+
description: "Declare, validate, and sanitize request payloads, query strings, parameters, and environment variables using the native tex schema builder."
|
|
2
4
|
nextUrl: "/docs/errors"
|
|
3
5
|
nextTitle: "Error Handling"
|
|
4
6
|
---
|
|
5
7
|
|
|
6
|
-
# Validation
|
|
8
|
+
# Validation and Schemas
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
ExisJS includes a built-in schema validation and sanitization engine named `tex`. It provides compile-time TypeScript type inference and runtime payload validation without requiring external schema dependencies.
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
---
|
|
11
13
|
|
|
12
|
-
## The
|
|
14
|
+
## The `tex` Validator
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
Import `tex` from `exisjs/validator` to declare schemas for request bodies, query strings, path parameters, and headers.
|
|
15
17
|
|
|
16
|
-
```typescript filename="
|
|
18
|
+
```typescript filename="src/http/users/schema.ts"
|
|
17
19
|
import { tex } from 'exisjs/validator'
|
|
20
|
+
import type { ResolveSchema } from 'exisjs/validator'
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
age: tex.number({ optional: true }),
|
|
23
|
-
role: tex.enum(['admin', '
|
|
24
|
-
email: tex.email()
|
|
22
|
+
export const CreateUserSchema = tex.object({
|
|
23
|
+
name: tex.string({ min: 3, max: 50, trim: true }),
|
|
24
|
+
email: tex.email({ toLowerCase: true }),
|
|
25
|
+
age: tex.number({ optional: true, min: 18 }),
|
|
26
|
+
role: tex.enum(['admin', 'member'], { default: 'member' }),
|
|
25
27
|
})
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
const safeData = userSchema.parse(rawInput)
|
|
29
|
+
export type CreateUserInput = ResolveSchema<typeof CreateUserSchema>
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
ExisJS supports a comprehensive set of strict validation primitives:
|
|
34
|
-
|
|
35
|
-
- `tex.string()` - String validation (supports options like `{ min, max, trim, toLowerCase, toUpperCase, escapeHtml, stripHtml, slugify, mask, default }`).
|
|
36
|
-
- `tex.email()` - Validates standard email formatting (supports `{ trim, toLowerCase, mask, default }`).
|
|
37
|
-
- `tex.uuid()` - Validates UUID strings (supports `{ version: 1 | 4, default }`).
|
|
38
|
-
- `tex.cuid()` - Validates CUID identifiers (supports `{ default }`).
|
|
39
|
-
- `tex.password()` - Validates password complexity (`{ min, max, requireNumbers, requireSymbols, requireUppercase, requireLowercase }`).
|
|
40
|
-
- `tex.creditCard()` - Validates credit card number formats with optional masking (`{ mask: true }`).
|
|
41
|
-
- `tex.number()` - Strict number validation (fails if passed a string unless `coerce: true`).
|
|
42
|
-
- `tex.number({ coerce: true, min: 0, max: 100, default: 10 })` - Automatically coerces incoming string representations (e.g., query params) into native numbers with optional default fallback.
|
|
43
|
-
- `tex.boolean()` - Strict boolean validation.
|
|
44
|
-
- `tex.boolean({ coerce: true, default: false })` - Coerces `"true"`/`"1"` and `"false"`/`"0"` into native booleans.
|
|
45
|
-
- `tex.date()` - Strict Date instance validation.
|
|
46
|
-
- `tex.date({ coerce: true, minDate: '2020-01-01', maxDate: '2030-01-01', default: () => new Date() })` - Automatically parses ISO 8601 strings, UNIX timestamps, or date strings into native JavaScript `Date` objects with optional boundary checks and lazy default factories.
|
|
47
|
-
- `tex.array(schema, { coerce: true, min: 1, max: 10, default: [] })` - Validates an array of items. When `{ coerce: true }` is enabled, automatically converts single items or comma-separated query strings (`"apple,banana"`) into typed arrays. Accepts primitive types, `tex.object(...)`, or shorthand plain objects `{ ... }`.
|
|
48
|
-
- `tex.enum(['admin', 'user'], { default: 'user' })` - Ensures the value matches one of the provided exact strings.
|
|
49
|
-
- `tex.literal(value)` - Validates against an exact primitive literal (`string | number | boolean`).
|
|
50
|
-
- `tex.record(schema)` - Validates a dictionary/record of key-value pairs matching the value schema.
|
|
51
|
-
- `tex.any()` - Allows any payload value without constraint.
|
|
52
|
-
- `tex.file({ maxSize: 5 * 1024 * 1024, mimeTypes: ['image/png', 'image/jpeg'] })` - Validates uploaded file payloads (size and MIME type).
|
|
53
|
-
- `tex.object({ ... })` - Validates structured JSON objects.
|
|
54
|
-
|
|
55
|
-
All primitives accept modifier options or chained helper methods:
|
|
56
|
-
- `.optional()` / `{ optional: true }` - Allows `undefined`.
|
|
57
|
-
- `.nullable()` / `{ nullable: true }` - Allows `null`.
|
|
58
|
-
- `.nullish()` / `{ nullish: true }` - Allows both `null` and `undefined`.
|
|
59
|
-
- `{ default: value | (() => value) }` - Provides a static default or dynamic factory function when the field is omitted or `undefined`. Automatically marks the field as optional in schemas and infers concrete types in TypeScript.
|
|
60
|
-
|
|
61
|
-
> [!TIP]
|
|
62
|
-
> **Unified Defaults without Method Chaining**: ExisJS avoids `.default()` chaining overhead in favor of declarative options objects (e.g. `tex.number({ default: 3000 })` or `tex.date({ default: () => new Date() })`). Dynamic factory functions evaluate per request so timestamps and UUIDs remain fresh!
|
|
63
|
-
|
|
64
|
-
> [!TIP]
|
|
65
|
-
> **Automatic Empty String `""` Normalization**: When forms or query parameters pass empty strings `""` for fields marked as `.optional()`, `.nullable()`, or `.nullish()`, ExisJS automatically converts them to `undefined` or `null` before running type validation. This prevents false positive type errors on numeric, UUID, or enum fields!
|
|
66
|
-
|
|
67
|
-
### Data Sanitization (Trimming & Formatting)
|
|
68
|
-
|
|
69
|
-
ExisJS allows you to mutate and sanitize incoming data in real-time before it reaches your controller. It includes a native Rust-powered sanitization engine for Strings and Arrays.
|
|
32
|
+
You can validate data manually using `.parse()`:
|
|
70
33
|
|
|
71
34
|
```typescript filename="example.ts"
|
|
72
|
-
|
|
73
|
-
|
|
35
|
+
const validated = CreateUserSchema.parse(rawInput)
|
|
36
|
+
```
|
|
74
37
|
|
|
75
|
-
|
|
76
|
-
// Automatically trim whitespace, convert to lowercase, and prevent HTML injections
|
|
77
|
-
email: tex.email().sanitize(sanitize.trim, sanitize.toLowerCase, sanitize.escapeHtml),
|
|
78
|
-
password: tex.string({ min: 8 })
|
|
79
|
-
})
|
|
38
|
+
If validation fails, `.parse()` throws a `ValidatorError` containing field-level error descriptors. When used inside routes, ExisJS catches validation failures automatically and returns an HTTP `400 Bad Request` response.
|
|
80
39
|
|
|
81
|
-
|
|
82
|
-
title: tex.string().sanitize(sanitize.collapseWhitespace, sanitize.truncate(100)),
|
|
83
|
-
// Auto-generate slugs from the title using sanitize.slugify!
|
|
84
|
-
slug: tex.string().sanitize(sanitize.slugify),
|
|
85
|
-
tags: tex.array(tex.string()).sanitize(sanitize.dedupe, sanitize.compact)
|
|
86
|
-
})
|
|
87
|
-
```
|
|
40
|
+
---
|
|
88
41
|
|
|
89
|
-
|
|
42
|
+
## Schema Primitives Reference
|
|
43
|
+
|
|
44
|
+
ExisJS provides type-safe primitives that accept modifier options and configuration objects.
|
|
45
|
+
|
|
46
|
+
| Primitive | Signature | Description |
|
|
47
|
+
| :--- | :--- | :--- |
|
|
48
|
+
| `tex.string()` | `tex.string(options?)` | Validates strings. Supports `{ min, max, trim, toLowerCase, toUpperCase, collapseWhitespace, escapeHtml, stripHtml, slugify, mask, preventSql, preventTraversal, coerce, default }`. |
|
|
49
|
+
| `tex.number()` | `tex.number(options?)` | Validates numbers. Supports `{ min, max, coerce, default }`. |
|
|
50
|
+
| `tex.boolean()` | `tex.boolean(options?)` | Validates booleans. Supports `{ coerce, default }`. Coerces `"true"`, `"false"`, `"1"`, `"0"`. |
|
|
51
|
+
| `tex.email()` | `tex.email(options?)` | Validates RFC-compliant email formatting. Supports `{ trim, toLowerCase, mask, default }`. |
|
|
52
|
+
| `tex.uuid()` | `tex.uuid(options?)` | Validates UUID strings. Supports `{ version: 1 \| 4, default }`. |
|
|
53
|
+
| `tex.cuid()` | `tex.cuid(options?)` | Validates CUID identifiers. Supports `{ default }`. |
|
|
54
|
+
| `tex.password()` | `tex.password(options?)` | Validates password complexity. Supports `{ min, max, requireNumbers, requireSymbols, requireUppercase, requireLowercase }`. |
|
|
55
|
+
| `tex.creditCard()` | `tex.creditCard(options?)` | Validates credit card numbers using the Luhn algorithm. Supports `{ mask: true }`. |
|
|
56
|
+
| `tex.date()` | `tex.date(options?)` | Validates JavaScript `Date` instances or ISO strings with `{ coerce: true, minDate, maxDate, default }`. |
|
|
57
|
+
| `tex.array()` | `tex.array(schema, options?)` | Validates arrays of items. Accepts primitive schemas, `tex.object(...)`, or shorthand `{ ... }`. Supports `{ min, max, dedupe, coerce, default }`. |
|
|
58
|
+
| `tex.enum()` | `tex.enum(values, options?)` | Validates against an allowed list of string literals. Supports `{ default }`. |
|
|
59
|
+
| `tex.literal()` | `tex.literal(value, options?)` | Validates against an exact primitive value (`string \| number \| boolean`). |
|
|
60
|
+
| `tex.union()` | `tex.union(schemas, options?)` | Validates against any matching schema in the provided tuple. |
|
|
61
|
+
| `tex.discriminatedUnion()` | `tex.discriminatedUnion(tag, variants)` | Validates polymorphic object variants discriminated by a literal or enum key. |
|
|
62
|
+
| `tex.record()` | `tex.record(valueSchema, options?)` | Validates dynamic key-value dictionaries (`Record<string, T>`). |
|
|
63
|
+
| `tex.any()` | `tex.any(options?)` | Pass-through validator without schema constraints. |
|
|
64
|
+
| `tex.file()` | `tex.file(options?)` | Validates multipart file uploads. Supports `{ maxSize, mimeTypes }`. |
|
|
65
|
+
| `tex.object()` | `tex.object(shape, options?)` | Compiles an object schema. Supports `{ strict: true }` to reject unlisted keys. |
|
|
66
|
+
| `tex.coerce.*` | `tex.coerce.number() \| boolean() \| date() \| string()` | Explicit coercion namespace for query strings, route parameters, and form inputs. |
|
|
67
|
+
| `tex.pagination()` | `tex.pagination(options?)` | Predefined schema for `page`, `limit`, `sort`, and `order` query parameters. |
|
|
68
|
+
| `tex.env()` | `tex.env(shape, options?)` | Environment variable schema parser with automatic type coercion from string values. |
|
|
90
69
|
|
|
91
|
-
|
|
70
|
+
---
|
|
92
71
|
|
|
93
|
-
|
|
94
|
-
import { sanitize } from 'exisjs/sanitize'
|
|
72
|
+
## Modifiers and Defaults
|
|
95
73
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
74
|
+
All primitive builders accept modifier options directly in their configuration argument, or through chaining helper methods:
|
|
75
|
+
|
|
76
|
+
| Modifier | Options Argument | Method Helper | Behavior |
|
|
77
|
+
| :--- | :--- | :--- | :--- |
|
|
78
|
+
| **Optional** | `{ optional: true }` | `.optional()` | Allows `undefined`. |
|
|
79
|
+
| **Nullable** | `{ nullable: true }` | `.nullable()` | Allows `null`. |
|
|
80
|
+
| **Nullish** | `{ nullish: true }` | `.nullish()` | Allows both `null` and `undefined`. |
|
|
81
|
+
| **Default** | `{ default: value \| () => value }` | N/A | Evaluates static value or dynamic factory function when field is missing. Automatically marks the field optional in schemas. |
|
|
82
|
+
|
|
83
|
+
```typescript filename="example.ts"
|
|
84
|
+
import { tex } from 'exisjs/validator'
|
|
85
|
+
|
|
86
|
+
const Schema = tex.object({
|
|
87
|
+
nickname: tex.string({ optional: true }),
|
|
88
|
+
avatarUrl: tex.string().nullable(),
|
|
89
|
+
bio: tex.string().nullish(),
|
|
90
|
+
retryCount: tex.number({ default: 3 }),
|
|
91
|
+
createdAt: tex.date({ default: () => new Date() }),
|
|
92
|
+
})
|
|
101
93
|
```
|
|
102
94
|
|
|
103
|
-
|
|
95
|
+
> [!NOTE]
|
|
96
|
+
> **Empty String Normalization**: When query parameters or form submissions supply empty strings `""` for fields marked `.optional()`, `.nullable()`, or `.nullish()`, ExisJS converts them to `undefined` or `null` before validation to avoid type conversion errors.
|
|
104
97
|
|
|
105
|
-
|
|
98
|
+
---
|
|
106
99
|
|
|
107
|
-
|
|
100
|
+
## Route Validation
|
|
108
101
|
|
|
109
|
-
|
|
102
|
+
Pass schemas directly into route handler configurations or decorators. ExisJS validates incoming requests before the handler executes.
|
|
110
103
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
104
|
+
### Functional Paradigm
|
|
105
|
+
|
|
106
|
+
```typescript filename="src/http/users/route.ts"
|
|
107
|
+
import { controller, route } from 'exisjs/router'
|
|
108
|
+
import { tex } from 'exisjs/validator'
|
|
109
|
+
|
|
110
|
+
const CreateUserBody = tex.object({
|
|
111
|
+
username: tex.string({ min: 4, max: 20 }),
|
|
115
112
|
email: tex.email(),
|
|
116
|
-
password: tex.
|
|
113
|
+
password: tex.password({ min: 8, requireNumbers: true }),
|
|
117
114
|
})
|
|
118
115
|
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
const UserQuery = tex.object({
|
|
117
|
+
source: tex.string({ optional: true }),
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
export default controller({
|
|
121
|
+
create: route.post('/', {
|
|
122
|
+
body: CreateUserBody,
|
|
123
|
+
query: UserQuery,
|
|
124
|
+
async handle({ body, query, res }) {
|
|
125
|
+
// body: { username: string; email: string; password: string }
|
|
126
|
+
// query: { source?: string }
|
|
127
|
+
res.status(201).json({ created: body.username, source: query.source })
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Class-Based (OOP) Paradigm
|
|
134
|
+
|
|
135
|
+
```typescript filename="src/http/users/route.ts"
|
|
136
|
+
import { Controller, Post, Body, Query, HttpCode } from 'exisjs/decorators'
|
|
137
|
+
import { tex } from 'exisjs/validator'
|
|
138
|
+
import type { ResolveSchema } from 'exisjs/validator'
|
|
139
|
+
|
|
140
|
+
const CreateUserBody = tex.object({
|
|
141
|
+
username: tex.string({ min: 4, max: 20 }),
|
|
142
|
+
email: tex.email(),
|
|
143
|
+
password: tex.password({ min: 8, requireNumbers: true }),
|
|
144
|
+
})
|
|
121
145
|
|
|
122
|
-
|
|
123
|
-
|
|
146
|
+
const UserQuery = tex.object({
|
|
147
|
+
source: tex.string({ optional: true }),
|
|
148
|
+
})
|
|
124
149
|
|
|
125
|
-
|
|
126
|
-
|
|
150
|
+
type CreateUserDto = ResolveSchema<typeof CreateUserBody>
|
|
151
|
+
type UserQueryDto = ResolveSchema<typeof UserQuery>
|
|
127
152
|
|
|
128
|
-
|
|
129
|
-
|
|
153
|
+
@Controller('/users')
|
|
154
|
+
export default class UsersController {
|
|
155
|
+
@Post('/')
|
|
156
|
+
@HttpCode(201)
|
|
157
|
+
async create(
|
|
158
|
+
@Body(CreateUserBody) body: CreateUserDto,
|
|
159
|
+
@Query(UserQuery) query: UserQueryDto
|
|
160
|
+
) {
|
|
161
|
+
return { created: body.username, source: query.source }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
130
164
|
```
|
|
131
165
|
|
|
132
|
-
|
|
166
|
+
---
|
|
133
167
|
|
|
134
|
-
|
|
168
|
+
## Query String and Form Coercion
|
|
135
169
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
170
|
+
Query string parameters and `multipart/form-data` payloads arrive as strings. Use `{ coerce: true }` to convert string representations into numbers, booleans, dates, or arrays.
|
|
171
|
+
|
|
172
|
+
### Query Array and Date Coercion
|
|
173
|
+
|
|
174
|
+
```typescript filename="src/http/events/route.ts"
|
|
175
|
+
import { controller, route } from 'exisjs/router'
|
|
176
|
+
import { tex } from 'exisjs/validator'
|
|
177
|
+
|
|
178
|
+
const EventFilterSchema = tex.object({
|
|
179
|
+
// Parses ISO 8601 strings (?from=2026-09-01T00:00:00Z) into Date instances
|
|
180
|
+
from: tex.date({ coerce: true, optional: true }),
|
|
181
|
+
to: tex.date({ coerce: true, optional: true }),
|
|
182
|
+
|
|
183
|
+
// Parses comma-separated (?tags=tech,ai) or repeated (?tags=tech&tags=ai) parameters
|
|
184
|
+
tags: tex.array(tex.string(), { coerce: true, optional: true }),
|
|
185
|
+
|
|
186
|
+
// Parses comma-separated numeric IDs (?ids=1,2,3 -> [1, 2, 3])
|
|
187
|
+
ids: tex.array(tex.number({ coerce: true }), { coerce: true, optional: true }),
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
export default controller({
|
|
191
|
+
list: route.get('/', {
|
|
192
|
+
query: EventFilterSchema,
|
|
193
|
+
handle({ query, res }) {
|
|
194
|
+
res.json({ filter: query })
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
144
197
|
})
|
|
145
198
|
```
|
|
146
199
|
|
|
147
|
-
###
|
|
200
|
+
### Form and Multipart Data Coercion
|
|
148
201
|
|
|
149
|
-
|
|
202
|
+
When working with `multipart/form-data`:
|
|
203
|
+
- Single item payloads passed to array fields (e.g. `{ item: { sku: "A1" } }`) are coerced into single-element arrays `[{ sku: "A1" }]`.
|
|
204
|
+
- JSON-encoded strings submitted within form fields (e.g. `items: "[{\"sku\":\"A1\"}]"`) are deserialized and validated against nested object/array schemas.
|
|
150
205
|
|
|
151
|
-
|
|
206
|
+
---
|
|
152
207
|
|
|
153
|
-
|
|
208
|
+
## Schema Composition
|
|
154
209
|
|
|
155
|
-
|
|
210
|
+
Reuse and extend existing object schemas using `.partial()`, `.pick()`, and `.omit()`.
|
|
156
211
|
|
|
157
212
|
```typescript filename="src/http/users/schema.ts"
|
|
158
213
|
import { tex } from 'exisjs/validator'
|
|
159
214
|
import type { ResolveSchema } from 'exisjs/validator'
|
|
160
215
|
|
|
161
|
-
export const
|
|
162
|
-
id: tex.
|
|
216
|
+
export const UserSchema = tex.object({
|
|
217
|
+
id: tex.string(),
|
|
218
|
+
name: tex.string({ min: 2 }),
|
|
219
|
+
email: tex.email(),
|
|
220
|
+
password: tex.password({ min: 8 }),
|
|
221
|
+
role: tex.enum(['admin', 'member'], { default: 'member' }),
|
|
163
222
|
})
|
|
164
223
|
|
|
165
|
-
|
|
224
|
+
// All fields become optional
|
|
225
|
+
export const UpdateUserSchema = UserSchema.partial()
|
|
226
|
+
|
|
227
|
+
// Retain only specified fields
|
|
228
|
+
export const LoginSchema = UserSchema.pick(['email', 'password'])
|
|
229
|
+
|
|
230
|
+
// Exclude sensitive fields for responses
|
|
231
|
+
export const PublicUserSchema = UserSchema.omit(['password'])
|
|
232
|
+
|
|
233
|
+
export type User = ResolveSchema<typeof UserSchema>
|
|
234
|
+
export type UpdateUserInput = ResolveSchema<typeof UpdateUserSchema>
|
|
235
|
+
export type LoginInput = ResolveSchema<typeof LoginSchema>
|
|
236
|
+
export type PublicUser = ResolveSchema<typeof PublicUserSchema>
|
|
166
237
|
```
|
|
167
238
|
|
|
168
|
-
|
|
239
|
+
---
|
|
169
240
|
|
|
170
|
-
|
|
241
|
+
## Data Sanitization
|
|
171
242
|
|
|
172
|
-
|
|
173
|
-
|
|
243
|
+
ExisJS allows transformations and sanitizations to run during schema validation or as standalone utility functions via `exisjs/sanitize`.
|
|
244
|
+
|
|
245
|
+
### Chaining Sanitizers with `.sanitize()`
|
|
246
|
+
|
|
247
|
+
```typescript filename="src/http/articles/schema.ts"
|
|
174
248
|
import { tex } from 'exisjs/validator'
|
|
249
|
+
import { sanitize } from 'exisjs/sanitize'
|
|
175
250
|
|
|
176
|
-
export
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// 2. Your handler only runs if validation succeeds
|
|
188
|
-
async handle({ body, query, req, res }) {
|
|
189
|
-
// body and query are strongly-typed here
|
|
190
|
-
await db.users.create(body)
|
|
191
|
-
res.status(201).json({ success: true })
|
|
192
|
-
}
|
|
193
|
-
})
|
|
251
|
+
export const CreateArticleSchema = tex.object({
|
|
252
|
+
title: tex.string({ min: 5 }).sanitize(
|
|
253
|
+
sanitize.collapseWhitespace,
|
|
254
|
+
sanitize.stripHtml
|
|
255
|
+
),
|
|
256
|
+
slug: tex.string().sanitize(sanitize.slugify),
|
|
257
|
+
content: tex.string().sanitize(sanitize.escapeHtml),
|
|
258
|
+
tags: tex.array(tex.string()).sanitize(
|
|
259
|
+
sanitize.dedupe,
|
|
260
|
+
sanitize.compact
|
|
261
|
+
),
|
|
194
262
|
})
|
|
195
263
|
```
|
|
196
264
|
|
|
197
|
-
|
|
265
|
+
### Standalone Sanitizer Usage
|
|
198
266
|
|
|
199
|
-
|
|
200
|
-
|
|
267
|
+
You can import `sanitize` directly in services or background jobs:
|
|
268
|
+
|
|
269
|
+
```typescript filename="src/services/formatter.ts"
|
|
270
|
+
import { sanitize } from 'exisjs/sanitize'
|
|
271
|
+
|
|
272
|
+
export function sanitizeTitle(input: string): string {
|
|
273
|
+
const stripped = sanitize.stripHtml(input)
|
|
274
|
+
const collapsed = sanitize.collapseWhitespace(stripped)
|
|
275
|
+
return sanitize.trim(collapsed)
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Available Sanitizers Reference
|
|
280
|
+
|
|
281
|
+
| Category | Function | Description |
|
|
282
|
+
| :--- | :--- | :--- |
|
|
283
|
+
| **Security** | `sanitize.escapeHtml(val)` | Escapes HTML entities (`&`, `<`, `>`, `"`, `'`). |
|
|
284
|
+
| **Security** | `sanitize.stripHtml(val)` | Removes HTML tags and element markup. |
|
|
285
|
+
| **Security** | `sanitize.preventSql(val)` | Neutralizes common SQL injection sequences. |
|
|
286
|
+
| **Security** | `sanitize.preventTraversal(val)` | Strips directory traversal sequences (`../`, `..\\`). |
|
|
287
|
+
| **Masking** | `sanitize.maskEmail(val)` | Masks email local-parts (e.g. `u***r@example.com`). |
|
|
288
|
+
| **Masking** | `sanitize.maskString(val)` | Masks characters leaving only boundary characters visible. |
|
|
289
|
+
| **Formatting** | `sanitize.trim(val)` | Trims leading and trailing whitespace. |
|
|
290
|
+
| **Formatting** | `sanitize.toLowerCase(val)` | Converts string to lowercase. |
|
|
291
|
+
| **Formatting** | `sanitize.toUpperCase(val)` | Converts string to uppercase. |
|
|
292
|
+
| **Formatting** | `sanitize.collapseWhitespace(val)` | Replaces consecutive whitespace with a single space. |
|
|
293
|
+
| **Formatting** | `sanitize.slugify(val)` | Converts string into URL-friendly slug format. |
|
|
294
|
+
| **Formatting** | `sanitize.truncate(length)(val)` | Truncates string to specified maximum length. |
|
|
295
|
+
| **Arrays & Objects** | `sanitize.dedupe(array)` | Removes duplicate elements using a `Set`. |
|
|
296
|
+
| **Arrays & Objects** | `sanitize.compact(array)` | Removes `null`, `undefined`, and empty string values. |
|
|
297
|
+
| **Arrays & Objects** | `sanitize.omit(keys)(obj)` | Omits specified keys from an object. |
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Custom and Asynchronous Refinements
|
|
302
|
+
|
|
303
|
+
Add synchronous validation checks with `.refine()` or asynchronous database/service checks with `.refineAsync()`.
|
|
304
|
+
|
|
305
|
+
```typescript filename="src/http/auth/schema.ts"
|
|
306
|
+
import { tex } from 'exisjs/validator'
|
|
307
|
+
import { UserService } from '../../services/user.service'
|
|
308
|
+
|
|
309
|
+
export const RegisterSchema = tex.object({
|
|
310
|
+
email: tex.email().refineAsync(async (email) => {
|
|
311
|
+
const existing = await UserService.findByEmail(email)
|
|
312
|
+
return !existing // Returns false if email already exists
|
|
313
|
+
}, 'Email address is already in use'),
|
|
314
|
+
|
|
315
|
+
password: tex.password({ min: 8 }),
|
|
316
|
+
confirmPassword: tex.string(),
|
|
317
|
+
}).refine((data) => data.password === data.confirmPassword, {
|
|
318
|
+
message: 'Passwords must match',
|
|
319
|
+
path: 'confirmPassword',
|
|
320
|
+
})
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Discriminated Unions (`tex.discriminatedUnion`)
|
|
326
|
+
|
|
327
|
+
When validating polymorphic payloads where the object structure depends on a discriminator tag field (e.g. event streams, action payloads, webhook types), use `tex.discriminatedUnion()`.
|
|
328
|
+
|
|
329
|
+
```typescript filename="src/http/events/schema.ts"
|
|
201
330
|
import { tex } from 'exisjs/validator'
|
|
202
331
|
import type { ResolveSchema } from 'exisjs/validator'
|
|
203
332
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
333
|
+
const CreateEvent = tex.object({
|
|
334
|
+
type: tex.literal('create'),
|
|
335
|
+
payload: tex.object({
|
|
336
|
+
username: tex.string(),
|
|
337
|
+
email: tex.email(),
|
|
338
|
+
}),
|
|
210
339
|
})
|
|
211
340
|
|
|
212
|
-
|
|
213
|
-
type
|
|
341
|
+
const UpdateEvent = tex.object({
|
|
342
|
+
type: tex.literal('update'),
|
|
343
|
+
payload: tex.object({
|
|
344
|
+
id: tex.uuid(),
|
|
345
|
+
fields: tex.record(tex.any()),
|
|
346
|
+
}),
|
|
347
|
+
})
|
|
214
348
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
349
|
+
const DeleteEvent = tex.object({
|
|
350
|
+
type: tex.literal('delete'),
|
|
351
|
+
payload: tex.object({
|
|
352
|
+
id: tex.uuid(),
|
|
353
|
+
reason: tex.string({ optional: true }),
|
|
354
|
+
}),
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
export const EventSchema = tex.discriminatedUnion('type', [
|
|
358
|
+
CreateEvent,
|
|
359
|
+
UpdateEvent,
|
|
360
|
+
DeleteEvent,
|
|
361
|
+
])
|
|
362
|
+
|
|
363
|
+
export type WebhookEvent = ResolveSchema<typeof EventSchema>
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
When validating, `tex.discriminatedUnion` matches the discriminator property against variant schemas. If the discriminator is missing or does not match any registered variant, ExisJS returns an explicit diagnostic error:
|
|
367
|
+
|
|
368
|
+
```json filename="Discriminator Error Example"
|
|
369
|
+
{
|
|
370
|
+
"statusCode": 400,
|
|
371
|
+
"error": "Bad Request",
|
|
372
|
+
"message": "Validation Error: type: Invalid discriminator value 'archive' for field 'type'",
|
|
373
|
+
"details": [
|
|
374
|
+
{
|
|
375
|
+
"path": "type",
|
|
376
|
+
"message": "Invalid discriminator value 'archive' for field 'type'",
|
|
377
|
+
"expected": "one of matching variant discriminators",
|
|
378
|
+
"received": "'archive'",
|
|
379
|
+
"code": "INVALID_DISCRIMINATOR_VALUE"
|
|
380
|
+
}
|
|
381
|
+
]
|
|
236
382
|
}
|
|
237
383
|
```
|
|
238
384
|
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Schema Composition (`extend`, `merge`, `pick`, `omit`)
|
|
388
|
+
|
|
389
|
+
Derive and reuse schemas across endpoints using composition methods on `TexEngine`:
|
|
239
390
|
|
|
240
|
-
###
|
|
391
|
+
### 1. Extending Schemas (`.extend()`)
|
|
241
392
|
|
|
242
|
-
|
|
393
|
+
Add or override fields on an existing schema:
|
|
243
394
|
|
|
244
395
|
```typescript filename="src/http/users/schema.ts"
|
|
245
|
-
|
|
396
|
+
const BaseUser = tex.object({
|
|
397
|
+
id: tex.uuid(),
|
|
398
|
+
name: tex.string({ min: 2 }),
|
|
399
|
+
email: tex.email(),
|
|
400
|
+
})
|
|
246
401
|
|
|
247
|
-
export const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
bio: tex.string().nullish(), // allows undefined, null, or ""
|
|
251
|
-
avatarUrl: tex.string({ optional: true }),
|
|
252
|
-
socials: tex.object({
|
|
253
|
-
twitter: tex.string({ optional: true }),
|
|
254
|
-
github: tex.string({ optional: true }),
|
|
255
|
-
}),
|
|
256
|
-
tags: tex.array(tex.string({ min: 2 })),
|
|
257
|
-
}),
|
|
258
|
-
// You can use tex.object(...) OR a plain object schema shorthand!
|
|
259
|
-
items: tex.array({
|
|
260
|
-
sku: tex.string(),
|
|
261
|
-
quantity: tex.number({ min: 1 }),
|
|
262
|
-
}),
|
|
402
|
+
export const AdminUser = BaseUser.extend({
|
|
403
|
+
role: tex.enum(['admin', 'superadmin']),
|
|
404
|
+
permissions: tex.array(tex.string()),
|
|
263
405
|
})
|
|
264
406
|
```
|
|
265
407
|
|
|
266
|
-
|
|
267
|
-
- **Single-Object Coercion**: When HTML forms or mobile clients submit a single item instead of an array (e.g. `{ items: { sku: "ABC", quantity: 1 } }`), the Rust engine automatically coerces it into a single-element array `[ { sku: "ABC", quantity: 1 } ]` rather than throwing `"Must be an array"`.
|
|
268
|
-
- **Stringified JSON Support**: When submitting `multipart/form-data`, stringified JSON arrays (e.g. `items: "[{\"sku\":\"ABC\",\"quantity\":2}]"`) or stringified objects are automatically deserialized and validated.
|
|
269
|
-
- **Type Inference**: TypeScript automatically infers the shape of array elements for both `tex.array(tex.object(...))` and `tex.array({ ... })`, ensuring full autocomplete without falling back to `never`.
|
|
408
|
+
### 2. Merging Schemas (`.merge()`)
|
|
270
409
|
|
|
271
|
-
|
|
410
|
+
Combine two independent `TexEngine` schemas into a unified object definition:
|
|
272
411
|
|
|
273
|
-
|
|
412
|
+
```typescript filename="src/http/posts/schema.ts"
|
|
413
|
+
const TimestampSchema = tex.object({
|
|
414
|
+
createdAt: tex.date({ coerce: true }),
|
|
415
|
+
updatedAt: tex.date({ optional: true, coerce: true }),
|
|
416
|
+
})
|
|
274
417
|
|
|
275
|
-
|
|
418
|
+
const PostContentSchema = tex.object({
|
|
419
|
+
title: tex.string({ min: 5 }),
|
|
420
|
+
body: tex.string(),
|
|
421
|
+
})
|
|
276
422
|
|
|
277
|
-
|
|
423
|
+
export const FullPostSchema = PostContentSchema.merge(TimestampSchema)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### 3. Picking and Omitting Fields (`.pick()`, `.omit()`)
|
|
427
|
+
|
|
428
|
+
Construct derived views for public or partial operations:
|
|
429
|
+
|
|
430
|
+
```typescript filename="src/http/users/schema.ts"
|
|
431
|
+
// Select only specific fields
|
|
432
|
+
export const PublicProfile = BaseUser.pick(['name', 'email'])
|
|
433
|
+
|
|
434
|
+
// Omit internal or sensitive fields
|
|
435
|
+
export const CreateUser = BaseUser.omit(['id'])
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Explicit Type Coercion (`tex.coerce`)
|
|
441
|
+
|
|
442
|
+
For query strings, path parameters, and headers where HTTP transports deliver values as strings, use the `tex.coerce` namespace to coerce inputs into numbers, booleans, dates, or strings:
|
|
443
|
+
|
|
444
|
+
```typescript filename="src/http/products/route.ts"
|
|
278
445
|
import { controller, route } from 'exisjs/router'
|
|
279
446
|
import { tex } from 'exisjs/validator'
|
|
280
447
|
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// or single values (?tags=tech -> ['tech']) into a typed array of strings
|
|
288
|
-
tags: tex.array(tex.string(), { coerce: true, optional: true }),
|
|
289
|
-
|
|
290
|
-
// Numbers within comma-separated arrays (?ids=1,2,3 -> [1, 2, 3]) are also coerced
|
|
291
|
-
ids: tex.array(tex.number({ coerce: true }), { coerce: true, optional: true })
|
|
448
|
+
const ProductFilterQuery = tex.object({
|
|
449
|
+
page: tex.coerce.number({ min: 1, default: 1 }),
|
|
450
|
+
limit: tex.coerce.number({ min: 1, max: 100, default: 20 }),
|
|
451
|
+
inStock: tex.coerce.boolean({ optional: true }),
|
|
452
|
+
createdAfter: tex.coerce.date({ optional: true }),
|
|
453
|
+
category: tex.coerce.string({ optional: true }),
|
|
292
454
|
})
|
|
293
455
|
|
|
294
456
|
export default controller({
|
|
295
|
-
|
|
296
|
-
query:
|
|
297
|
-
handle({ query, res }) {
|
|
298
|
-
// query.
|
|
299
|
-
// query.
|
|
300
|
-
// query.
|
|
301
|
-
res.json({
|
|
302
|
-
}
|
|
303
|
-
})
|
|
457
|
+
list: route.get('/', {
|
|
458
|
+
query: ProductFilterQuery,
|
|
459
|
+
async handle({ query, res }) {
|
|
460
|
+
// query.page is number (3)
|
|
461
|
+
// query.inStock is boolean (true)
|
|
462
|
+
// query.createdAfter is Date instance
|
|
463
|
+
res.json({ query })
|
|
464
|
+
},
|
|
465
|
+
}),
|
|
304
466
|
})
|
|
305
467
|
```
|
|
306
468
|
|
|
307
|
-
|
|
469
|
+
---
|
|
308
470
|
|
|
309
|
-
|
|
310
|
-
import { Controller, Get, Query } from 'exisjs/decorators'
|
|
311
|
-
import { tex } from 'exisjs/validator'
|
|
312
|
-
import type { ResolveSchema } from 'exisjs/validator'
|
|
471
|
+
## Data Transformations (`transform`)
|
|
313
472
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
473
|
+
Apply post-validation data transformations at both the individual field level and the complete schema level:
|
|
474
|
+
|
|
475
|
+
### Field-Level Transformations (`TexType.transform`)
|
|
476
|
+
|
|
477
|
+
```typescript filename="src/http/auth/schema.ts"
|
|
478
|
+
export const LoginSchema = tex.object({
|
|
479
|
+
email: tex.string().transform((val) => val.toLowerCase().trim()),
|
|
480
|
+
apiKey: tex.string().transform((key) => Buffer.from(key, 'base64').toString('utf-8')),
|
|
319
481
|
})
|
|
482
|
+
```
|
|
320
483
|
|
|
321
|
-
|
|
484
|
+
### Schema-Level Transformations (`TexEngine.transform`)
|
|
322
485
|
|
|
323
|
-
|
|
324
|
-
export
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
486
|
+
```typescript filename="src/http/users/schema.ts"
|
|
487
|
+
export const UserDtoSchema = tex.object({
|
|
488
|
+
firstName: tex.string(),
|
|
489
|
+
lastName: tex.string(),
|
|
490
|
+
birthYear: tex.number(),
|
|
491
|
+
}).transform((user) => ({
|
|
492
|
+
fullName: `${user.firstName} ${user.lastName}`,
|
|
493
|
+
age: new Date().getFullYear() - user.birthYear,
|
|
494
|
+
}))
|
|
330
495
|
```
|
|
331
496
|
|
|
332
497
|
---
|
|
333
498
|
|
|
334
|
-
##
|
|
499
|
+
## Field Documentation Metadata (`describe`, `example`)
|
|
335
500
|
|
|
336
|
-
|
|
501
|
+
Attach human-readable descriptions and example values to schema fields. These are automatically extracted by the ExisJS Swagger / OpenAPI 3.1 generator:
|
|
337
502
|
|
|
338
|
-
|
|
503
|
+
```typescript filename="src/http/orders/schema.ts"
|
|
504
|
+
export const CreateOrderSchema = tex.object({
|
|
505
|
+
itemSku: tex.string()
|
|
506
|
+
.describe('Stock keeping unit identifier')
|
|
507
|
+
.example('SKU-9872-BLU'),
|
|
508
|
+
quantity: tex.number({ min: 1 })
|
|
509
|
+
.describe('Number of units ordered')
|
|
510
|
+
.example(2),
|
|
511
|
+
})
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Use `tex.pagination()` to standardize query pagination parameters (`page`, `limit`, `sort`, `order`). Pair it with `getPaginationSkip()` and `paginate()` from `exisjs/validator`.
|
|
339
515
|
|
|
340
516
|
```typescript filename="src/http/items/route.ts"
|
|
341
517
|
import { controller, route } from 'exisjs/router'
|
|
342
518
|
import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
|
|
343
519
|
|
|
344
|
-
const
|
|
345
|
-
defaultLimit: 20,
|
|
346
|
-
maxLimit: 100,
|
|
520
|
+
const PaginationQuery = tex.pagination({
|
|
521
|
+
defaultLimit: 20,
|
|
522
|
+
maxLimit: 100,
|
|
347
523
|
})
|
|
348
524
|
|
|
349
525
|
export default controller({
|
|
350
526
|
list: route.get('/', {
|
|
351
|
-
query:
|
|
352
|
-
async handle({ query }) {
|
|
527
|
+
query: PaginationQuery,
|
|
528
|
+
async handle({ query, res }) {
|
|
353
529
|
const { skip, limit, page } = getPaginationSkip(query)
|
|
354
|
-
const items = await
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
530
|
+
const [items, total] = await Promise.all([
|
|
531
|
+
db.items.findMany({ skip, take: limit, orderBy: { [query.sort || 'createdAt']: query.order || 'desc' } }),
|
|
532
|
+
db.items.count(),
|
|
533
|
+
])
|
|
534
|
+
|
|
535
|
+
res.json(paginate(items, total, { page, limit }))
|
|
536
|
+
},
|
|
537
|
+
}),
|
|
360
538
|
})
|
|
361
539
|
```
|
|
362
540
|
|
|
363
|
-
|
|
541
|
+
### Paginated Response Structure
|
|
364
542
|
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
|
|
543
|
+
```json
|
|
544
|
+
{
|
|
545
|
+
"data": [
|
|
546
|
+
{ "id": "1", "name": "Item 1" }
|
|
547
|
+
],
|
|
548
|
+
"pagination": {
|
|
549
|
+
"page": 1,
|
|
550
|
+
"limit": 20,
|
|
551
|
+
"total": 45,
|
|
552
|
+
"totalPages": 3,
|
|
553
|
+
"hasNextPage": true,
|
|
554
|
+
"hasPrevPage": false,
|
|
555
|
+
"hasNext": true,
|
|
556
|
+
"hasPrev": false
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
```
|
|
368
560
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
})
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## Environment Variable Validation (`tex.env`)
|
|
373
564
|
|
|
374
|
-
|
|
375
|
-
export default class ItemsController {
|
|
376
|
-
@Get('/', { query: ItemQuerySchema })
|
|
377
|
-
async list(@Query() query: any) {
|
|
378
|
-
const { skip, limit, page } = getPaginationSkip(query)
|
|
379
|
-
const items = await fetchItems({ skip, limit, sort: query.sort, order: query.order })
|
|
380
|
-
const total = await countItems()
|
|
565
|
+
Use `tex.env()` in `src/config/env.ts` to validate environment variables during application startup. It automatically enables type coercion for numbers, booleans, dates, and arrays.
|
|
381
566
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
567
|
+
```typescript filename="src/config/env.ts"
|
|
568
|
+
import { tex } from 'exisjs/validator'
|
|
569
|
+
|
|
570
|
+
export const env = tex.env({
|
|
571
|
+
PORT: tex.number({ default: 3000 }),
|
|
572
|
+
NODE_ENV: tex.enum(['development', 'production', 'test'], { default: 'development' }),
|
|
573
|
+
DATABASE_URL: tex.string(),
|
|
574
|
+
JWT_SECRET: tex.string({ min: 32 }),
|
|
575
|
+
ALLOWED_ORIGINS: tex.array(tex.string()),
|
|
576
|
+
ENABLE_METRICS: tex.boolean({ default: false }),
|
|
577
|
+
}).parse(process.env)
|
|
385
578
|
```
|
|
386
579
|
|
|
580
|
+
If any required variable is missing or invalid, `tex.env().parse()` throws during initialization and logs the error descriptors before the server accepts traffic.
|
|
581
|
+
|
|
387
582
|
---
|
|
388
583
|
|
|
389
|
-
## Error
|
|
584
|
+
## Error Response Format
|
|
390
585
|
|
|
391
|
-
|
|
586
|
+
When incoming requests fail validation, ExisJS returns an HTTP `400 Bad Request` with structured error details:
|
|
392
587
|
|
|
393
|
-
```json filename="HTTP 400 Bad Request
|
|
588
|
+
```json filename="HTTP 400 Bad Request"
|
|
394
589
|
{
|
|
395
590
|
"statusCode": 400,
|
|
396
591
|
"error": "Bad Request",
|
|
397
|
-
"message": "Validation Error: username: Field must be at least 4 characters,
|
|
592
|
+
"message": "Validation Error: username: Field must be at least 4 characters, email: Invalid email address",
|
|
398
593
|
"errors": {
|
|
399
594
|
"username": "Field must be at least 4 characters",
|
|
400
|
-
"
|
|
595
|
+
"email": "Invalid email address"
|
|
401
596
|
},
|
|
402
597
|
"details": [
|
|
403
598
|
{
|
|
@@ -408,65 +603,25 @@ If a request fails validation, ExisJS intercepts the rejection and returns a sta
|
|
|
408
603
|
"code": "VALUE_TOO_SMALL"
|
|
409
604
|
},
|
|
410
605
|
{
|
|
411
|
-
"path": "
|
|
412
|
-
"message": "
|
|
413
|
-
"expected": "
|
|
414
|
-
"received": "
|
|
415
|
-
"code": "
|
|
606
|
+
"path": "email",
|
|
607
|
+
"message": "Invalid email address",
|
|
608
|
+
"expected": "valid email format",
|
|
609
|
+
"received": "invalid-email",
|
|
610
|
+
"code": "INVALID_EMAIL"
|
|
416
611
|
}
|
|
417
612
|
]
|
|
418
613
|
}
|
|
419
614
|
```
|
|
420
615
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
When validation fails in development mode, ExisJS prints an immediate tabular diagnostic to your terminal displaying the HTTP method, route path, failing HTTP part (`body`, `query`, `params`, `headers`), exact field path, received value, and expected schema constraint:
|
|
424
|
-
|
|
425
|
-
```text
|
|
426
|
-
[VALIDATION FAILED] POST /api/v1/visits (part: body)
|
|
427
|
-
┌─────────────┬───────────────────────────┬──────────────────┐
|
|
428
|
-
│ Field │ Received │ Expected │
|
|
429
|
-
├─────────────┼───────────────────────────┼──────────────────┤
|
|
430
|
-
│ phone │ "123" │ min: 7, max: 20 │
|
|
431
|
-
│ idProofs │ "[object Object]" │ Array of IDProof │
|
|
432
|
-
└─────────────┴───────────────────────────┴──────────────────┘
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
---
|
|
436
|
-
|
|
437
|
-
## Environment Variable Validation (`tex.env`)
|
|
438
|
-
|
|
439
|
-
Because `process.env` always supplies raw string values (e.g. `"4000"`, `"true"`, `"1"`), ExisJS includes a dedicated `tex.env()` schema parser. It automatically injects coercion for numbers, booleans, dates, and arrays, allowing you to validate your environment variables on boot without writing `{ coerce: true }` on every field:
|
|
440
|
-
|
|
441
|
-
```typescript filename="src/config/env.ts"
|
|
442
|
-
import { tex } from 'exisjs/validator'
|
|
443
|
-
|
|
444
|
-
export const env = tex.env({
|
|
445
|
-
PORT: tex.number({ default: 4000 }), // Automatically coerced from "4000" -> 4000
|
|
446
|
-
DEBUG: tex.boolean({ default: false }), // Parsed from "true" | "1" | "false" | "0"
|
|
447
|
-
DATABASE_URL: tex.string(),
|
|
448
|
-
JWT_SECRET: tex.string({ min: 32 }),
|
|
449
|
-
ALLOWED_ORIGINS: tex.array(tex.string()), // Parsed from "http://localhost:3000,https://app.com"
|
|
450
|
-
NODE_ENV: tex.enum(['development', 'production', 'test'] as const, {
|
|
451
|
-
default: 'development',
|
|
452
|
-
}),
|
|
453
|
-
}).parse(process.env)
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
> [!TIP]
|
|
457
|
-
> **Zero-Boilerplate Boot Safety**: `tex.env()` automatically validates `process.env` on server startup. If a required environment variable is missing, it crashes immediately with a clean diagnostic log before the HTTP listener opens.
|
|
616
|
+
In development mode (`NODE_ENV=development`), ExisJS also outputs a tabular summary of the failed validation constraints to the terminal.
|
|
458
617
|
|
|
459
618
|
---
|
|
460
619
|
|
|
461
|
-
## Third-Party
|
|
620
|
+
## Third-Party Schema Interoperability
|
|
462
621
|
|
|
463
|
-
|
|
622
|
+
ExisJS supports third-party schema libraries (such as **Zod**, **Valibot**, and **ArkType**) via duck typing. Any schema exposing a `.parse()` or `.safeParse()` method can be assigned directly to route configurations or decorators.
|
|
464
623
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
### Using Zod in Routes
|
|
468
|
-
|
|
469
|
-
#### Functional Paradigm
|
|
624
|
+
### Using Zod in Functional Routes
|
|
470
625
|
|
|
471
626
|
```typescript filename="src/http/users/route.ts"
|
|
472
627
|
import { controller, route } from 'exisjs/router'
|
|
@@ -475,20 +630,20 @@ import { z } from 'zod'
|
|
|
475
630
|
const CreateUserSchema = z.object({
|
|
476
631
|
name: z.string().min(3),
|
|
477
632
|
email: z.string().email(),
|
|
478
|
-
role: z.enum(['admin', '
|
|
633
|
+
role: z.enum(['admin', 'member']),
|
|
479
634
|
})
|
|
480
635
|
|
|
481
636
|
export default controller({
|
|
482
637
|
create: route.post('/', {
|
|
483
|
-
body: CreateUserSchema,
|
|
638
|
+
body: CreateUserSchema,
|
|
484
639
|
async handle({ body, res }) {
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
})
|
|
640
|
+
res.status(201).json({ success: true, user: body })
|
|
641
|
+
},
|
|
642
|
+
}),
|
|
488
643
|
})
|
|
489
644
|
```
|
|
490
645
|
|
|
491
|
-
|
|
646
|
+
### Using Zod in OOP Controllers
|
|
492
647
|
|
|
493
648
|
```typescript filename="src/http/users/route.ts"
|
|
494
649
|
import { Controller, Post, Body, HttpCode } from 'exisjs/decorators'
|
|
@@ -497,7 +652,7 @@ import { z } from 'zod'
|
|
|
497
652
|
const CreateUserSchema = z.object({
|
|
498
653
|
name: z.string().min(3),
|
|
499
654
|
email: z.string().email(),
|
|
500
|
-
role: z.enum(['admin', '
|
|
655
|
+
role: z.enum(['admin', 'member']),
|
|
501
656
|
})
|
|
502
657
|
|
|
503
658
|
type CreateUserDto = z.infer<typeof CreateUserSchema>
|
|
@@ -512,21 +667,4 @@ export default class UsersController {
|
|
|
512
667
|
}
|
|
513
668
|
```
|
|
514
669
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
```typescript filename="src/config/env.ts"
|
|
518
|
-
import { z } from 'zod'
|
|
519
|
-
|
|
520
|
-
const EnvSchema = z.object({
|
|
521
|
-
PORT: z.coerce.number().default(4000),
|
|
522
|
-
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
523
|
-
DATABASE_URL: z.string().url(),
|
|
524
|
-
JWT_SECRET: z.string().min(32),
|
|
525
|
-
})
|
|
526
|
-
|
|
527
|
-
export const env = EnvSchema.parse(process.env)
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
> [!NOTE]
|
|
531
|
-
> **Universal Error Normalization**: Regardless of which library you use (**Zod**, **Valibot**, **Yup**, **Joi**, **ArkType**, or any Standard Schema-compliant library), ExisJS inspects the error structure (`issues`, `errors`, `inner`) and automatically normalizes it into standardized `400 Bad Request` JSON responses with complete field mappings and dev-mode terminal overlays.
|
|
532
|
-
|
|
670
|
+
ExisJS normalizes error outputs from third-party schema libraries into the standard `400 Bad Request` JSON error envelope.
|