ascertain 2.0.88 → 3.0.0
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 +323 -153
- package/build/index.cjs +483 -180
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +231 -186
- package/build/index.js +474 -180
- package/build/index.js.map +1 -1
- package/package.json +22 -19
- package/src/index.ts +991 -0
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# Ascertain
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
0-Deps, simple, blazing fast, for browser and Node.js object schema validator
|
|
3
|
+
Zero-dependency, high-performance schema validator for Node.js and browsers.
|
|
6
4
|
|
|
7
5
|
[![Coverage Status][codecov-image]][codecov-url]
|
|
8
6
|
[![Build Status][github-image]][github-url]
|
|
@@ -10,182 +8,354 @@
|
|
|
10
8
|
[![Downloads][downloads-image]][npm-url]
|
|
11
9
|
[![Snyk][snyk-image]][snyk-url]
|
|
12
10
|
|
|
13
|
-
|
|
14
11
|
[Documentation](https://3axap4ehko.github.io/ascertain/)
|
|
15
12
|
|
|
16
13
|
## Features
|
|
17
14
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
- Type
|
|
21
|
-
-
|
|
15
|
+
- **Zero dependencies** - Minimal footprint, no external dependencies
|
|
16
|
+
- **High performance** - Compiles schemas to optimized JS functions (~6x faster than dynamic validation)
|
|
17
|
+
- **Type-safe** - Full TypeScript support with type inference
|
|
18
|
+
- **Flexible schemas** - AND, OR, optional, tuple, discriminated operators
|
|
19
|
+
- **Type casting** - Built-in parsers for numbers (hex, octal, binary), dates, JSON, base64
|
|
20
|
+
- **Object validation** - Validate keys/values with `$keys`, `$values`, `$strict`
|
|
21
|
+
- **Partial validation** - `createValidator` validates subsets with type narrowing
|
|
22
|
+
- **Detailed errors** - Clear error messages with paths for debugging
|
|
23
|
+
- **Standard Schema v1** - Interoperable with tRPC, TanStack Form, and other ecosystem tools
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install ascertain
|
|
29
|
+
```
|
|
22
30
|
|
|
23
|
-
##
|
|
31
|
+
## Quick Start
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- Array Values: Arrays are used to represent an expected array type, where every item in the array must match the specified type (acting as an "and" operator).
|
|
28
|
-
- Regular Expressions: Regular expressions are used to validate that a value matches a specified string pattern.
|
|
29
|
-
- Object Types: Non-null objects are used as templates for expected properties, where each property of the object must match the corresponding schema definition.
|
|
33
|
+
```typescript
|
|
34
|
+
import { ascertain, or, optional } from 'ascertain';
|
|
30
35
|
|
|
36
|
+
ascertain({
|
|
37
|
+
name: String,
|
|
38
|
+
age: Number,
|
|
39
|
+
role: or('admin', 'user'),
|
|
40
|
+
email: optional(String),
|
|
41
|
+
}, userData);
|
|
42
|
+
```
|
|
31
43
|
|
|
44
|
+
## Performance
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
Ascertain compiles schemas into optimized JavaScript functions. Compiled validators run **~6x faster** than dynamic validation.
|
|
34
47
|
|
|
35
|
-
### Schema compilation
|
|
36
48
|
```typescript
|
|
37
|
-
import { compile
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
49
|
+
import { compile } from 'ascertain';
|
|
50
|
+
|
|
51
|
+
// Compile once
|
|
52
|
+
const validateUser = compile(userSchema);
|
|
53
|
+
|
|
54
|
+
// Validate many (no recompilation)
|
|
55
|
+
validateUser(user1);
|
|
56
|
+
validateUser(user2);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| When to use | Function | Speed |
|
|
60
|
+
|-------------|----------|-------|
|
|
61
|
+
| Repeated validation (API handlers, loops) | `compile()` | Fastest |
|
|
62
|
+
| One-off validation | `ascertain()` | Convenient |
|
|
63
|
+
|
|
64
|
+
### Benchmark
|
|
65
|
+
|
|
66
|
+
| Library | Mode | Valid (ops/s) | Invalid (ops/s) |
|
|
67
|
+
|---------|------|---------------|-----------------|
|
|
68
|
+
| **Ascertain** | first-error | 80M | 41M |
|
|
69
|
+
| **Ascertain** | all-errors | 80M | 25M |
|
|
70
|
+
| AJV | first-error | 52M | 35M |
|
|
71
|
+
| AJV | all-errors | 52M | 20M |
|
|
72
|
+
| Zod | all-errors | 34M | 77K |
|
|
73
|
+
|
|
74
|
+
## Schema Reference
|
|
75
|
+
|
|
76
|
+
| Schema | Validates | Example |
|
|
77
|
+
|--------|-----------|---------|
|
|
78
|
+
| `String`, `Number`, `Boolean` | Type check | `{ age: Number }` |
|
|
79
|
+
| `Date`, `Array`, `Object` | Instance check | `{ created: Date }` |
|
|
80
|
+
| `Function` | Any callable | `{ handler: Function }` |
|
|
81
|
+
| Primitives | Exact value | `{ status: 'active' }` |
|
|
82
|
+
| RegExp | Pattern match | `{ email: /^.+@.+$/ }` |
|
|
83
|
+
| `[Schema]` | Array of type | `{ tags: [String] }` |
|
|
84
|
+
| `{ key: Schema }` | Object shape | `{ user: { name: String } }` |
|
|
85
|
+
| `or(a, b, ...)` | Any match | `or(String, Number)` |
|
|
86
|
+
| `and(a, b, ...)` | All match | `and(Date, { toJSON: Function })` |
|
|
87
|
+
| `optional(s)` | Nullable | `optional(String)` |
|
|
88
|
+
| `tuple(a, b)` | Fixed array | `tuple(Number, Number)` |
|
|
89
|
+
| `discriminated(schemas, key)` | Tagged union | `discriminated([{ type: 'a' }, { type: 'b' }], 'type')` |
|
|
90
|
+
|
|
91
|
+
### Special Symbols
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { $keys, $values, $strict } from 'ascertain';
|
|
95
|
+
|
|
96
|
+
const schema = {
|
|
97
|
+
[$keys]: /^[a-z]+$/, // Validate all keys
|
|
98
|
+
[$values]: Number, // Validate all values
|
|
99
|
+
[$strict]: true, // No extra properties
|
|
100
|
+
};
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Type Casting
|
|
104
|
+
|
|
105
|
+
Parse strings into typed values (environment variables, query params):
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { as } from 'ascertain';
|
|
109
|
+
|
|
110
|
+
as.number('42') // 42
|
|
111
|
+
as.number('3.14') // 3.14
|
|
112
|
+
as.number('0xFF') // 255 (hex)
|
|
113
|
+
as.number('0o77') // 63 (octal)
|
|
114
|
+
as.number('0b1010') // 10 (binary)
|
|
115
|
+
as.number('1e10') // 10000000000
|
|
116
|
+
|
|
117
|
+
as.boolean('true') // true
|
|
118
|
+
as.boolean('1') // true
|
|
119
|
+
|
|
120
|
+
as.time('500ms') // 500
|
|
121
|
+
as.time('30s') // 30000
|
|
122
|
+
as.time('5m') // 300000
|
|
123
|
+
as.time('2h') // 7200000
|
|
124
|
+
as.time('1d') // 86400000
|
|
125
|
+
|
|
126
|
+
as.date('2024-12-31') // Date object
|
|
127
|
+
as.array('a,b,c', ',') // ['a', 'b', 'c']
|
|
128
|
+
as.json('{"x":1}') // { x: 1 }
|
|
129
|
+
as.base64('dGVzdA==') // 'test'
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Invalid values return `TypeError` for deferred validation:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const config = {
|
|
136
|
+
port: as.number(process.env.PORT), // TypeError if invalid
|
|
137
|
+
host: as.string(process.env.HOST),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Errors surface with clear paths
|
|
141
|
+
ascertain({ port: Number, host: String }, config);
|
|
142
|
+
// → TypeError: "Invalid value undefined, expected non-nullable"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Patterns
|
|
146
|
+
|
|
147
|
+
### Batch Validation
|
|
148
|
+
|
|
149
|
+
Compile once, validate many:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const validateUser = compile(userSchema);
|
|
153
|
+
|
|
154
|
+
const results = users.map((user, i) => {
|
|
155
|
+
if (validateUser(user)) {
|
|
156
|
+
return { index: i, valid: true };
|
|
157
|
+
}
|
|
158
|
+
return { index: i, valid: false, error: validateUser.issues[0].message };
|
|
70
159
|
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Discriminated Unions
|
|
163
|
+
|
|
164
|
+
Use `discriminated()` for efficient tagged union validation. Instead of trying each variant like `or()`, it checks the discriminant field first and only validates the matching variant:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { compile, discriminated } from 'ascertain';
|
|
168
|
+
|
|
169
|
+
const messageSchema = discriminated([
|
|
170
|
+
{ type: 'email', address: String },
|
|
171
|
+
{ type: 'sms', phone: String },
|
|
172
|
+
{ type: 'push', token: String },
|
|
173
|
+
], 'type');
|
|
71
174
|
|
|
175
|
+
const validate = compile(messageSchema);
|
|
176
|
+
|
|
177
|
+
validate({ type: 'email', address: 'user@example.com' }); // true
|
|
178
|
+
validate({ type: 'sms', phone: '123456' }); // true
|
|
179
|
+
validate({ type: 'push', token: 123 }); // false
|
|
180
|
+
validate({ type: 'unknown' }); // false
|
|
72
181
|
```
|
|
73
182
|
|
|
74
|
-
|
|
75
|
-
|
|
183
|
+
Discriminant values must be string, number, or boolean literals.
|
|
184
|
+
|
|
185
|
+
### Conditional Rules
|
|
186
|
+
|
|
187
|
+
Use `or()` and `and()` for complex conditions:
|
|
188
|
+
|
|
76
189
|
```typescript
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
function: () => {},
|
|
85
|
-
array: [],
|
|
86
|
-
object: {},
|
|
87
|
-
date: new Date,
|
|
88
|
-
regexp: 'regexp',
|
|
89
|
-
oneOfValue: 1,
|
|
90
|
-
arrayOfNumbers: [1,2,3,4,5],
|
|
91
|
-
objectSchema: {
|
|
92
|
-
number: 1,
|
|
93
|
-
},
|
|
94
|
-
optional: null,
|
|
95
|
-
keyValue: {
|
|
96
|
-
keyOne: 1,
|
|
97
|
-
keyTwo: 2,
|
|
98
|
-
keyThree: 3,
|
|
99
|
-
},
|
|
100
|
-
// fault tolernat type casting
|
|
101
|
-
parsedNumber: as.number('1'),
|
|
102
|
-
parsedString: as.string('string'),
|
|
103
|
-
parsedBoolean: as.boolean('false'),
|
|
104
|
-
parsedArray: as.array('1,2,3,4,5', ','),
|
|
105
|
-
parsedJSON: as.json('{ "number": 1 }'),
|
|
106
|
-
parsedBase64: as.base64('dGVzdA=='),
|
|
107
|
-
parsedTime: as.time('2m'),
|
|
108
|
-
parsedDate: as.date('31-12-2024'),
|
|
190
|
+
const schema = {
|
|
191
|
+
type: or('email', 'sms'),
|
|
192
|
+
// Conditional: email requires address, sms requires phone
|
|
193
|
+
contact: or(
|
|
194
|
+
and({ type: 'email' }, { address: String }),
|
|
195
|
+
and({ type: 'sms' }, { phone: String }),
|
|
196
|
+
),
|
|
109
197
|
};
|
|
198
|
+
```
|
|
110
199
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
},
|
|
126
|
-
optional: optional({
|
|
127
|
-
number: Number,
|
|
128
|
-
}),
|
|
129
|
-
keyValue: {
|
|
130
|
-
[$keys]: /^key[A-Z]/,
|
|
131
|
-
[$values]: Number
|
|
132
|
-
},
|
|
133
|
-
parsedNumber: Number,
|
|
134
|
-
parsedString: String,
|
|
135
|
-
parsedBoolean: Boolean,
|
|
136
|
-
parsedArray: [String],
|
|
137
|
-
parsedJSON: {
|
|
138
|
-
number: 1,
|
|
139
|
-
},
|
|
140
|
-
parsedBase64: String,
|
|
141
|
-
parsedTime: 2 * 60 * 1000, // two minutes
|
|
142
|
-
parsedDate: Date,
|
|
200
|
+
### Schema Composition
|
|
201
|
+
|
|
202
|
+
Build schemas from reusable parts:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
const addressSchema = {
|
|
206
|
+
street: String,
|
|
207
|
+
city: String,
|
|
208
|
+
zip: /^\d{5}$/,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const personSchema = {
|
|
212
|
+
name: String,
|
|
213
|
+
address: addressSchema,
|
|
143
214
|
};
|
|
144
215
|
|
|
145
|
-
|
|
146
|
-
|
|
216
|
+
const companySchema = {
|
|
217
|
+
name: String,
|
|
218
|
+
headquarters: addressSchema,
|
|
219
|
+
employees: [personSchema],
|
|
220
|
+
};
|
|
147
221
|
```
|
|
148
222
|
|
|
149
|
-
###
|
|
223
|
+
### Versioned Schemas
|
|
224
|
+
|
|
225
|
+
Version schemas as modules:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// schemas/user.v1.ts
|
|
229
|
+
export const userSchemaV1 = { name: String, email: String };
|
|
230
|
+
|
|
231
|
+
// schemas/user.v2.ts
|
|
232
|
+
export const userSchemaV2 = {
|
|
233
|
+
...userSchemaV1,
|
|
234
|
+
phone: optional(String),
|
|
235
|
+
createdAt: Date,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// api/handler.ts
|
|
239
|
+
import { userSchemaV2 } from './schemas/user.v2';
|
|
240
|
+
const validate = compile(userSchemaV2);
|
|
150
241
|
```
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
242
|
+
|
|
243
|
+
### Config Validation
|
|
244
|
+
|
|
245
|
+
Validate only what each module needs:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { createValidator, as } from 'ascertain';
|
|
249
|
+
|
|
250
|
+
const config = {
|
|
251
|
+
app: { name: as.string(process.env.APP_NAME), port: as.number(process.env.PORT) },
|
|
252
|
+
db: { host: as.string(process.env.DB_HOST), pool: as.number(process.env.DB_POOL) },
|
|
253
|
+
cache: { ttl: as.time(process.env.CACHE_TTL) },
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const validate = createValidator(config);
|
|
257
|
+
|
|
258
|
+
// Each module validates only what it needs
|
|
259
|
+
const { db } = validate({
|
|
260
|
+
db: { host: String, pool: Number },
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
db.host; // string - validated and typed
|
|
264
|
+
db.pool; // number - validated and typed
|
|
265
|
+
// db.xxx // TypeScript error - property doesn't exist
|
|
266
|
+
|
|
267
|
+
// cache not validated = not accessible
|
|
268
|
+
// cache.ttl // TypeScript error - cache not in returned type
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Compile Options
|
|
272
|
+
|
|
273
|
+
By default `compile()` stops at the first validation error (fastest for invalid data). Pass `{ allErrors: true }` to collect all errors:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { compile } from 'ascertain';
|
|
277
|
+
|
|
278
|
+
const schema = { name: String, age: Number, active: Boolean };
|
|
279
|
+
|
|
280
|
+
// First-error mode (default) - stops at first failure
|
|
281
|
+
const validate = compile(schema);
|
|
282
|
+
if (!validate({ name: 123, age: 'bad', active: 'no' })) {
|
|
283
|
+
console.log(validate.issues.length); // 1
|
|
284
|
+
console.log(validate.issues[0].path); // ['name']
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// All-errors mode - collects every failure
|
|
288
|
+
const validateAll = compile(schema, { allErrors: true });
|
|
289
|
+
if (!validateAll({ name: 123, age: 'bad', active: 'no' })) {
|
|
290
|
+
console.log(validateAll.issues.length); // 3
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Standard Schema
|
|
295
|
+
|
|
296
|
+
Wrap a schema for [Standard Schema v1](https://standardschema.dev/) compliance, enabling interoperability with tRPC, TanStack Form, and other ecosystem libraries:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { standardSchema, or, optional } from 'ascertain';
|
|
300
|
+
|
|
301
|
+
const userValidator = standardSchema({
|
|
302
|
+
name: String,
|
|
303
|
+
age: Number,
|
|
304
|
+
role: or('admin', 'user'),
|
|
305
|
+
email: optional(String),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Use as regular validator (throws on error)
|
|
309
|
+
userValidator({ name: 'Alice', age: 30, role: 'admin' });
|
|
310
|
+
|
|
311
|
+
// Use Standard Schema interface (returns result object)
|
|
312
|
+
const result = userValidator['~standard'].validate(unknownData);
|
|
313
|
+
if (result.issues) {
|
|
314
|
+
console.log(result.issues);
|
|
315
|
+
} else {
|
|
316
|
+
console.log(result.value);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Complete Example
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
import { compile, or, optional, and, tuple, $keys, $values, $strict, as } from 'ascertain';
|
|
324
|
+
|
|
325
|
+
const schema = {
|
|
326
|
+
id: Number,
|
|
327
|
+
name: String,
|
|
328
|
+
email: /^[^@]+@[^@]+$/,
|
|
329
|
+
status: or('active', 'inactive', 'pending'),
|
|
330
|
+
role: optional(or('admin', 'user')),
|
|
331
|
+
|
|
332
|
+
profile: {
|
|
333
|
+
bio: optional(String),
|
|
334
|
+
avatar: optional(String),
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
settings: {
|
|
338
|
+
[$keys]: /^[a-z_]+$/,
|
|
339
|
+
[$values]: or(String, Number, Boolean),
|
|
340
|
+
[$strict]: true,
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
coordinates: optional(tuple(Number, Number)),
|
|
344
|
+
createdAt: and(Date, { toISOString: Function }),
|
|
345
|
+
|
|
346
|
+
retries: as.number(process.env.MAX_RETRIES),
|
|
347
|
+
timeout: as.time(process.env.TIMEOUT),
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const validate = compile(schema);
|
|
351
|
+
if (!validate(data)) {
|
|
352
|
+
console.error(validate.issues);
|
|
353
|
+
}
|
|
184
354
|
```
|
|
185
355
|
|
|
186
356
|
## License
|
|
187
|
-
|
|
188
|
-
|
|
357
|
+
|
|
358
|
+
[MIT](http://opensource.org/licenses/MIT) - Ivan Zakharchanka
|
|
189
359
|
|
|
190
360
|
[npm-url]: https://www.npmjs.com/package/ascertain
|
|
191
361
|
[downloads-image]: https://img.shields.io/npm/dw/ascertain.svg?maxAge=43200
|