-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidate-async.ts
More file actions
187 lines (163 loc) · 6.58 KB
/
Copy pathvalidate-async.ts
File metadata and controls
187 lines (163 loc) · 6.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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
184
185
186
187
import { s, validate, validateAsync, ValidationError, DslBuilder } from '../../dist/pure.js'
// ============================================================
// 1. What validateAsync does
// - Returns Promise<T> — resolves to the validated (coerced) data
// - Throws ValidationError on any validation failure
// - Runs AJV sync checks first, then async custom validators (BC-6 fix)
// ============================================================
const signupSchema = s({
username: 'string:3-32!',
email: 'email!',
age: 'integer:18-120!',
})
// Basic async validation — auto-coercion applies, returns validated data
const data = await validateAsync(signupSchema, {
username: 'alice_01',
email: 'alice@example.com',
age: '26', // string coerced to integer
})
console.log('validateAsync.basic.username =', (data as any).username)
console.log('validateAsync.basic.age.type =', typeof (data as any).age) // 'number'
// Failure: throws ValidationError
try {
await validateAsync(signupSchema, { username: 'x', email: 'bad', age: 15 })
} catch (err) {
if (err instanceof ValidationError) {
console.log('validateAsync.throw.errors.length =', err.errors.length)
console.log('validateAsync.throw.errors[0].path =', err.errors[0]?.path)
}
}
// ============================================================
// 2. Async custom validators via .custom()
// Return true → pass
// Return false → fail with default message
// Return string → fail with that string as message
// Throw → fail with the thrown error message
// ============================================================
// Simulate async DB / API checks
async function isUsernameAvailable(username: string): Promise<boolean> {
const takenNames = new Set(['admin', 'root', 'alice'])
return !takenNames.has(username.toLowerCase())
}
async function isEmailRegistered(email: string): Promise<boolean> {
const takenEmails = new Set(['taken@example.com', 'duplicate@example.com'])
return !takenEmails.has(email)
}
const registrationSchema = s({
username: new DslBuilder('string:3-32!')
.label('Username')
.custom(async (value: unknown) => {
const ok = await isUsernameAvailable(value as string)
return ok || 'Username is already taken — please choose another'
}),
email: new DslBuilder('email!')
.label('Email')
.custom(async (value: unknown) => {
const ok = await isEmailRegistered(value as string)
return ok || 'This email address is already registered'
}),
password: new DslBuilder('string:8-64!')
.label('Password')
.custom((value: unknown) => {
const s = value as string
const hasUpper = /[A-Z]/.test(s)
const hasLower = /[a-z]/.test(s)
const hasDigit = /\d/.test(s)
if (!hasUpper || !hasLower || !hasDigit) {
return 'Password must contain uppercase, lowercase and at least one digit'
}
return true
}),
})
// Valid input: async checks pass
const regData = await validateAsync(registrationSchema, {
username: 'new_user',
email: 'new@example.com',
password: 'StrongP4ss',
})
console.log('validateAsync.custom.valid.username =', (regData as any).username)
// Taken username: async check fails
try {
await validateAsync(registrationSchema, {
username: 'admin',
email: 'new@example.com',
password: 'StrongP4ss',
})
} catch (err) {
if (err instanceof ValidationError) {
console.log('validateAsync.custom.taken.message =', err.errors[0]?.message)
}
}
// ============================================================
// 3. Multiple chained custom validators — each runs in sequence
// ============================================================
const apiKeySchema = s({
apiKey: new DslBuilder('string:32-64!')
.custom((value: unknown) => {
// Sync check: hex string
const isHex = /^[a-f0-9]+$/.test(value as string)
return isHex || 'API key must be a lowercase hex string'
})
.custom(async (value: unknown) => {
// Async check: simulate revocation lookup
const revokedKeys = new Set(['aabbccddaabbccddaabbccddaabbccdd'])
const isRevoked = revokedKeys.has(value as string)
return !isRevoked || 'API key has been revoked'
}),
})
// Valid hex key, not revoked
const keyData = await validateAsync(apiKeySchema, { apiKey: '1234567890abcdef1234567890abcdef' })
console.log('validateAsync.chained.valid.key =', typeof (keyData as any).apiKey)
// Revoked key
try {
await validateAsync(apiKeySchema, { apiKey: 'aabbccddaabbccddaabbccddaabbccdd' })
} catch (err) {
if (err instanceof ValidationError) {
console.log('validateAsync.chained.revoked.message =', err.errors[0]?.message)
}
}
// ============================================================
// 4. Async validation with coercion, locale and allErrors
// ============================================================
const productSchema = s({
name: 'string:2-100!',
price: 'number:0.01-!',
stock: 'integer:0-!',
sku: new DslBuilder('alphanum!').custom(async (value: unknown) => {
// Simulate checking SKU uniqueness in a product catalog
const existingSkus = new Set(['SKU001', 'SKU002'])
return !existingSkus.has(value as string) || `SKU '${value}' already exists in catalog`
}),
})
// Valid product with string→number coercion
const product = await validateAsync(productSchema, {
name: 'Widget Pro',
price: '29.99', // coerced → 29.99
stock: '100', // coerced → 100
sku: 'SKUXYZ99',
})
console.log('validateAsync.product.price.coerced =', (product as any).price)
// Duplicate SKU
try {
await validateAsync(productSchema, { name: 'Widget', price: 10, stock: 5, sku: 'SKU001' })
} catch (err) {
if (err instanceof ValidationError) {
console.log('validateAsync.product.sku.message =', err.errors[0]?.message)
}
}
// ============================================================
// 5. Using validate() synchronously when no async custom validators exist
// ============================================================
const simpleSchema = s({ email: 'email!', name: 'string:2-50!' })
// validate() returns ValidationResult — never throws
const syncResult = validate(simpleSchema, { email: 'x@', name: 'A' })
console.log('validateAsync.sync.compare.valid =', syncResult.valid)
console.log('validateAsync.sync.compare.errors.length =', syncResult.errors?.length)
// validateAsync on the same schema — same AJV checks, throws instead of returning {valid:false}
try {
await validateAsync(simpleSchema, { email: 'x@', name: 'A' })
} catch (err) {
if (err instanceof ValidationError) {
console.log('validateAsync.vs.sync.threw =', true)
}
}