@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
validate,
|
|
4
|
+
validateSync,
|
|
5
|
+
validateBody,
|
|
6
|
+
validateQuery,
|
|
7
|
+
validateParams,
|
|
8
|
+
validateHeaders,
|
|
9
|
+
createValidator,
|
|
10
|
+
WithBody,
|
|
11
|
+
WithQuery,
|
|
12
|
+
isStandardSchema,
|
|
13
|
+
assertStandardSchema
|
|
14
|
+
} from '../../src/validation';
|
|
15
|
+
import { Context } from '../../src/context';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import type { StandardSchema, StandardResult } from '../../src/types';
|
|
18
|
+
|
|
19
|
+
// Typia support - conditionally import if available
|
|
20
|
+
// Note: Typia requires TypeScript transformation to work properly
|
|
21
|
+
// In a real project using Typia, you would use:
|
|
22
|
+
// import typia from 'typia';
|
|
23
|
+
// const TypiaUserSchema = typia.createValidate<IUser>();
|
|
24
|
+
|
|
25
|
+
// User schema for testing
|
|
26
|
+
const UserSchema = z.object({
|
|
27
|
+
name: z.string().min(1),
|
|
28
|
+
email: z.string().email(),
|
|
29
|
+
age: z.number().int().positive().optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const IdSchema = z.object({
|
|
33
|
+
id: z.coerce.number().int().positive(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const QuerySchema = z.object({
|
|
37
|
+
page: z.coerce.number().int().positive().default(1),
|
|
38
|
+
limit: z.coerce.number().int().positive().default(10),
|
|
39
|
+
search: z.string().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const HeadersSchema = z.object({
|
|
43
|
+
authorization: z.string().startsWith('Bearer '),
|
|
44
|
+
'content-type': z.string().optional(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Helper to create a valid Standard Schema for testing
|
|
48
|
+
function createTestSchema<T>(
|
|
49
|
+
validateFn: (data: unknown) => StandardResult<T> | Promise<StandardResult<T>>
|
|
50
|
+
): StandardSchema<unknown, T> {
|
|
51
|
+
return {
|
|
52
|
+
'~standard': {
|
|
53
|
+
version: 1 as const,
|
|
54
|
+
vendor: 'test',
|
|
55
|
+
validate: validateFn,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('Validation', () => {
|
|
61
|
+
describe('validate', () => {
|
|
62
|
+
test('should validate valid data', async () => {
|
|
63
|
+
const data = { name: 'John', email: 'john@example.com' };
|
|
64
|
+
const result = await validate(UserSchema, data);
|
|
65
|
+
|
|
66
|
+
expect(result.success).toBe(true);
|
|
67
|
+
if (result.success) {
|
|
68
|
+
expect(result.data.name).toBe('John');
|
|
69
|
+
expect(result.data.email).toBe('john@example.com');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('should return errors for invalid data', async () => {
|
|
74
|
+
const data = { name: '', email: 'invalid-email' };
|
|
75
|
+
const result = await validate(UserSchema, data);
|
|
76
|
+
|
|
77
|
+
expect(result.success).toBe(false);
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
expect(result.issues.length).toBeGreaterThan(0);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should transform data', async () => {
|
|
84
|
+
const data = { id: '123' };
|
|
85
|
+
const result = await validate(IdSchema, data);
|
|
86
|
+
|
|
87
|
+
expect(result.success).toBe(true);
|
|
88
|
+
if (result.success) {
|
|
89
|
+
expect(result.data.id).toBe(123);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('should handle thrown errors gracefully', async () => {
|
|
94
|
+
const data = { name: 'John', email: 'john@example.com' };
|
|
95
|
+
const result = await validate(UserSchema, data);
|
|
96
|
+
|
|
97
|
+
expect(result.success).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('should handle async validators', async () => {
|
|
101
|
+
const asyncSchema = createTestSchema(async (data) => {
|
|
102
|
+
// Simulate async validation
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
104
|
+
if (typeof data === 'string') {
|
|
105
|
+
return { value: data.toUpperCase() };
|
|
106
|
+
}
|
|
107
|
+
return { issues: [{ message: 'Must be a string' }] };
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await validate(asyncSchema, 'hello');
|
|
111
|
+
|
|
112
|
+
expect(result.success).toBe(true);
|
|
113
|
+
if (result.success) {
|
|
114
|
+
expect(result.data).toBe('HELLO');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('should catch exceptions from validators', async () => {
|
|
119
|
+
const throwingSchema = createTestSchema(() => {
|
|
120
|
+
throw new Error('Validation exploded');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const result = await validate(throwingSchema, 'test');
|
|
124
|
+
|
|
125
|
+
expect(result.success).toBe(false);
|
|
126
|
+
if (!result.success) {
|
|
127
|
+
expect(result.issues[0].message).toBe('Validation exploded');
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('should handle non-Error exceptions', async () => {
|
|
132
|
+
const throwingSchema = createTestSchema(() => {
|
|
133
|
+
throw 'string error';
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = await validate(throwingSchema, 'test');
|
|
137
|
+
|
|
138
|
+
expect(result.success).toBe(false);
|
|
139
|
+
if (!result.success) {
|
|
140
|
+
expect(result.issues[0].message).toBe('Validation failed');
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('validateSync', () => {
|
|
146
|
+
test('should validate valid data synchronously', () => {
|
|
147
|
+
const data = { name: 'John', email: 'john@example.com' };
|
|
148
|
+
const result = validateSync(UserSchema, data);
|
|
149
|
+
|
|
150
|
+
expect(result.success).toBe(true);
|
|
151
|
+
if (result.success) {
|
|
152
|
+
expect(result.data.name).toBe('John');
|
|
153
|
+
expect(result.data.email).toBe('john@example.com');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('should return errors for invalid data synchronously', () => {
|
|
158
|
+
const data = { name: '', email: 'invalid-email' };
|
|
159
|
+
const result = validateSync(UserSchema, data);
|
|
160
|
+
|
|
161
|
+
expect(result.success).toBe(false);
|
|
162
|
+
if (!result.success) {
|
|
163
|
+
expect(result.issues.length).toBeGreaterThan(0);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should transform data synchronously', () => {
|
|
168
|
+
const data = { id: '123' };
|
|
169
|
+
const result = validateSync(IdSchema, data);
|
|
170
|
+
|
|
171
|
+
expect(result.success).toBe(true);
|
|
172
|
+
if (result.success) {
|
|
173
|
+
expect(result.data.id).toBe(123);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('should throw error for async validators', () => {
|
|
178
|
+
// Create a schema that returns a Promise from validate
|
|
179
|
+
const asyncSchema = createTestSchema((data) =>
|
|
180
|
+
Promise.resolve({ value: data })
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(() => validateSync(asyncSchema, { test: 'data' })).toThrow(
|
|
184
|
+
'Schema uses async validation. Use validate() instead.'
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('validateBody', () => {
|
|
190
|
+
test('should validate request body', async () => {
|
|
191
|
+
const request = new Request('http://localhost:3000/users', {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: { 'Content-Type': 'application/json' },
|
|
194
|
+
body: JSON.stringify({ name: 'John', email: 'john@example.com' }),
|
|
195
|
+
});
|
|
196
|
+
const context = new Context(request, {});
|
|
197
|
+
|
|
198
|
+
const result = await validateBody(context, UserSchema);
|
|
199
|
+
|
|
200
|
+
expect(result.success).toBe(true);
|
|
201
|
+
if (result.success) {
|
|
202
|
+
expect(result.data.name).toBe('John');
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('should handle invalid JSON body', async () => {
|
|
207
|
+
const request = new Request('http://localhost:3000/users', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: 'not json',
|
|
211
|
+
});
|
|
212
|
+
const context = new Context(request, {});
|
|
213
|
+
|
|
214
|
+
const result = await validateBody(context, UserSchema);
|
|
215
|
+
|
|
216
|
+
expect(result.success).toBe(false);
|
|
217
|
+
if (!result.success) {
|
|
218
|
+
expect(result.issues[0].message).toBe('Failed to parse request body');
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('should return validation errors for invalid body', async () => {
|
|
223
|
+
const request = new Request('http://localhost:3000/users', {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'Content-Type': 'application/json' },
|
|
226
|
+
body: JSON.stringify({ name: '', email: 'invalid' }),
|
|
227
|
+
});
|
|
228
|
+
const context = new Context(request, {});
|
|
229
|
+
|
|
230
|
+
const result = await validateBody(context, UserSchema);
|
|
231
|
+
|
|
232
|
+
expect(result.success).toBe(false);
|
|
233
|
+
if (!result.success) {
|
|
234
|
+
expect(result.issues.length).toBeGreaterThan(0);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('validateQuery', () => {
|
|
240
|
+
test('should validate query parameters', () => {
|
|
241
|
+
const request = new Request('http://localhost:3000/users?page=2&limit=20&search=john');
|
|
242
|
+
const context = new Context(request, {});
|
|
243
|
+
|
|
244
|
+
const result = validateQuery(context, QuerySchema);
|
|
245
|
+
|
|
246
|
+
expect(result.success).toBe(true);
|
|
247
|
+
if (result.success) {
|
|
248
|
+
expect(result.data.page).toBe(2);
|
|
249
|
+
expect(result.data.limit).toBe(20);
|
|
250
|
+
expect(result.data.search).toBe('john');
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('should use default values', () => {
|
|
255
|
+
const request = new Request('http://localhost:3000/users');
|
|
256
|
+
const context = new Context(request, {});
|
|
257
|
+
|
|
258
|
+
const result = validateQuery(context, QuerySchema);
|
|
259
|
+
|
|
260
|
+
expect(result.success).toBe(true);
|
|
261
|
+
if (result.success) {
|
|
262
|
+
expect(result.data.page).toBe(1);
|
|
263
|
+
expect(result.data.limit).toBe(10);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('should fail for invalid query parameters', () => {
|
|
268
|
+
const request = new Request('http://localhost:3000/users?page=-1');
|
|
269
|
+
const context = new Context(request, {});
|
|
270
|
+
|
|
271
|
+
const result = validateQuery(context, QuerySchema);
|
|
272
|
+
|
|
273
|
+
expect(result.success).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('validateParams', () => {
|
|
278
|
+
test('should validate path parameters', () => {
|
|
279
|
+
const request = new Request('http://localhost:3000/users/123');
|
|
280
|
+
const context = new Context(request, { id: '123' });
|
|
281
|
+
|
|
282
|
+
const result = validateParams(context, IdSchema);
|
|
283
|
+
|
|
284
|
+
expect(result.success).toBe(true);
|
|
285
|
+
if (result.success) {
|
|
286
|
+
expect(result.data.id).toBe(123);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('should fail for invalid path parameter', () => {
|
|
291
|
+
const request = new Request('http://localhost:3000/users/abc');
|
|
292
|
+
const context = new Context(request, { id: 'abc' });
|
|
293
|
+
|
|
294
|
+
const result = validateParams(context, IdSchema);
|
|
295
|
+
|
|
296
|
+
expect(result.success).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('should fail for missing path parameter', () => {
|
|
300
|
+
const request = new Request('http://localhost:3000/users');
|
|
301
|
+
const context = new Context(request, {});
|
|
302
|
+
|
|
303
|
+
const result = validateParams(context, IdSchema);
|
|
304
|
+
|
|
305
|
+
expect(result.success).toBe(false);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('validateHeaders', () => {
|
|
310
|
+
test('should validate headers', () => {
|
|
311
|
+
const request = new Request('http://localhost:3000/users', {
|
|
312
|
+
headers: {
|
|
313
|
+
authorization: 'Bearer token123',
|
|
314
|
+
'content-type': 'application/json',
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
const context = new Context(request, {});
|
|
318
|
+
|
|
319
|
+
const result = validateHeaders(context, HeadersSchema);
|
|
320
|
+
|
|
321
|
+
expect(result.success).toBe(true);
|
|
322
|
+
if (result.success) {
|
|
323
|
+
expect(result.data.authorization).toBe('Bearer token123');
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('should fail for invalid headers', () => {
|
|
328
|
+
const request = new Request('http://localhost:3000/users', {
|
|
329
|
+
headers: {
|
|
330
|
+
authorization: 'Invalid token',
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
const context = new Context(request, {});
|
|
334
|
+
|
|
335
|
+
const result = validateHeaders(context, HeadersSchema);
|
|
336
|
+
|
|
337
|
+
expect(result.success).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('should fail for missing required headers', () => {
|
|
341
|
+
const request = new Request('http://localhost:3000/users');
|
|
342
|
+
const context = new Context(request, {});
|
|
343
|
+
|
|
344
|
+
const result = validateHeaders(context, HeadersSchema);
|
|
345
|
+
|
|
346
|
+
expect(result.success).toBe(false);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('should collect all headers from request', () => {
|
|
350
|
+
const request = new Request('http://localhost:3000/users', {
|
|
351
|
+
headers: {
|
|
352
|
+
authorization: 'Bearer token123',
|
|
353
|
+
'x-custom-header': 'custom-value',
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
const context = new Context(request, {});
|
|
357
|
+
|
|
358
|
+
const allHeadersSchema = z.object({
|
|
359
|
+
authorization: z.string(),
|
|
360
|
+
'x-custom-header': z.string(),
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const result = validateHeaders(context, allHeadersSchema);
|
|
364
|
+
|
|
365
|
+
expect(result.success).toBe(true);
|
|
366
|
+
if (result.success) {
|
|
367
|
+
expect(result.data.authorization).toBe('Bearer token123');
|
|
368
|
+
expect(result.data['x-custom-header']).toBe('custom-value');
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('createValidator', () => {
|
|
374
|
+
test('should create validation middleware for body', async () => {
|
|
375
|
+
const validator = createValidator({
|
|
376
|
+
body: UserSchema,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const request = new Request('http://localhost:3000/users', {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
headers: { 'Content-Type': 'application/json' },
|
|
382
|
+
body: JSON.stringify({ name: 'John', email: 'john@example.com' }),
|
|
383
|
+
});
|
|
384
|
+
const context = new Context(request, {});
|
|
385
|
+
|
|
386
|
+
const next = () => Promise.resolve(new Response('OK'));
|
|
387
|
+
const response = await validator(context, next);
|
|
388
|
+
|
|
389
|
+
expect(response.status).toBe(200);
|
|
390
|
+
expect(context.get('validatedBody')).toBeDefined();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test('should return 400 for body validation errors', async () => {
|
|
394
|
+
const validator = createValidator({
|
|
395
|
+
body: UserSchema,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const request = new Request('http://localhost:3000/users', {
|
|
399
|
+
method: 'POST',
|
|
400
|
+
headers: { 'Content-Type': 'application/json' },
|
|
401
|
+
body: JSON.stringify({ name: '', email: 'invalid' }),
|
|
402
|
+
});
|
|
403
|
+
const context = new Context(request, {});
|
|
404
|
+
|
|
405
|
+
const next = () => Promise.resolve(new Response('OK'));
|
|
406
|
+
const response = await validator(context, next);
|
|
407
|
+
|
|
408
|
+
expect(response.status).toBe(400);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('should create validation middleware for query', async () => {
|
|
412
|
+
const validator = createValidator({
|
|
413
|
+
query: QuerySchema,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const request = new Request('http://localhost:3000/users?page=2&limit=20');
|
|
417
|
+
const context = new Context(request, {});
|
|
418
|
+
|
|
419
|
+
const next = () => Promise.resolve(new Response('OK'));
|
|
420
|
+
const response = await validator(context, next);
|
|
421
|
+
|
|
422
|
+
expect(response.status).toBe(200);
|
|
423
|
+
expect(context.get('validatedQuery')).toBeDefined();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('should return 400 for query validation errors', async () => {
|
|
427
|
+
const validator = createValidator({
|
|
428
|
+
query: QuerySchema,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const request = new Request('http://localhost:3000/users?page=-1');
|
|
432
|
+
const context = new Context(request, {});
|
|
433
|
+
|
|
434
|
+
const next = () => Promise.resolve(new Response('OK'));
|
|
435
|
+
const response = await validator(context, next);
|
|
436
|
+
|
|
437
|
+
expect(response.status).toBe(400);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test('should create validation middleware for params', async () => {
|
|
441
|
+
const validator = createValidator({
|
|
442
|
+
params: IdSchema,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const request = new Request('http://localhost:3000/users/123');
|
|
446
|
+
const context = new Context(request, { id: '123' });
|
|
447
|
+
|
|
448
|
+
const next = () => Promise.resolve(new Response('OK'));
|
|
449
|
+
const response = await validator(context, next);
|
|
450
|
+
|
|
451
|
+
expect(response.status).toBe(200);
|
|
452
|
+
expect(context.get('validatedParams')).toBeDefined();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test('should return 400 for params validation errors', async () => {
|
|
456
|
+
const validator = createValidator({
|
|
457
|
+
params: IdSchema,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const request = new Request('http://localhost:3000/users/abc');
|
|
461
|
+
const context = new Context(request, { id: 'abc' });
|
|
462
|
+
|
|
463
|
+
const next = () => Promise.resolve(new Response('OK'));
|
|
464
|
+
const response = await validator(context, next);
|
|
465
|
+
|
|
466
|
+
expect(response.status).toBe(400);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test('should create validation middleware for headers', async () => {
|
|
470
|
+
const validator = createValidator({
|
|
471
|
+
headers: HeadersSchema,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const request = new Request('http://localhost:3000/users', {
|
|
475
|
+
headers: {
|
|
476
|
+
authorization: 'Bearer token123',
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
const context = new Context(request, {});
|
|
480
|
+
|
|
481
|
+
const next = () => Promise.resolve(new Response('OK'));
|
|
482
|
+
const response = await validator(context, next);
|
|
483
|
+
|
|
484
|
+
expect(response.status).toBe(200);
|
|
485
|
+
expect(context.get('validatedHeaders')).toBeDefined();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test('should return 400 for headers validation errors', async () => {
|
|
489
|
+
const validator = createValidator({
|
|
490
|
+
headers: HeadersSchema,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const request = new Request('http://localhost:3000/users');
|
|
494
|
+
const context = new Context(request, {});
|
|
495
|
+
|
|
496
|
+
const next = () => Promise.resolve(new Response('OK'));
|
|
497
|
+
const response = await validator(context, next);
|
|
498
|
+
|
|
499
|
+
expect(response.status).toBe(400);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('should validate multiple sources at once', async () => {
|
|
503
|
+
const validator = createValidator({
|
|
504
|
+
body: UserSchema,
|
|
505
|
+
query: QuerySchema,
|
|
506
|
+
params: IdSchema,
|
|
507
|
+
headers: HeadersSchema,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const request = new Request('http://localhost:3000/users/123?page=1&limit=10', {
|
|
511
|
+
method: 'POST',
|
|
512
|
+
headers: {
|
|
513
|
+
'Content-Type': 'application/json',
|
|
514
|
+
authorization: 'Bearer token123',
|
|
515
|
+
},
|
|
516
|
+
body: JSON.stringify({ name: 'John', email: 'john@example.com' }),
|
|
517
|
+
});
|
|
518
|
+
const context = new Context(request, { id: '123' });
|
|
519
|
+
|
|
520
|
+
const next = () => Promise.resolve(new Response('OK'));
|
|
521
|
+
const response = await validator(context, next);
|
|
522
|
+
|
|
523
|
+
expect(response.status).toBe(200);
|
|
524
|
+
expect(context.get('validatedBody')).toBeDefined();
|
|
525
|
+
expect(context.get('validatedQuery')).toBeDefined();
|
|
526
|
+
expect(context.get('validatedParams')).toBeDefined();
|
|
527
|
+
expect(context.get('validatedHeaders')).toBeDefined();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test('should return 400 on first validation failure', async () => {
|
|
531
|
+
const validator = createValidator({
|
|
532
|
+
body: UserSchema,
|
|
533
|
+
query: QuerySchema,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const request = new Request('http://localhost:3000/users?page=-1', {
|
|
537
|
+
method: 'POST',
|
|
538
|
+
headers: { 'Content-Type': 'application/json' },
|
|
539
|
+
body: JSON.stringify({ name: 'John', email: 'john@example.com' }),
|
|
540
|
+
});
|
|
541
|
+
const context = new Context(request, {});
|
|
542
|
+
|
|
543
|
+
const next = () => Promise.resolve(new Response('OK'));
|
|
544
|
+
const response = await validator(context, next);
|
|
545
|
+
|
|
546
|
+
expect(response.status).toBe(400);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test('should return JSON error response with issues', async () => {
|
|
550
|
+
const validator = createValidator({
|
|
551
|
+
body: UserSchema,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const request = new Request('http://localhost:3000/users', {
|
|
555
|
+
method: 'POST',
|
|
556
|
+
headers: { 'Content-Type': 'application/json' },
|
|
557
|
+
body: JSON.stringify({ name: '', email: 'invalid' }),
|
|
558
|
+
});
|
|
559
|
+
const context = new Context(request, {});
|
|
560
|
+
|
|
561
|
+
const next = () => Promise.resolve(new Response('OK'));
|
|
562
|
+
const response = await validator(context, next);
|
|
563
|
+
|
|
564
|
+
expect(response.status).toBe(400);
|
|
565
|
+
const body = await response.json();
|
|
566
|
+
expect(body.error).toBe('Validation failed');
|
|
567
|
+
expect(body.issues).toBeDefined();
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
describe('WithBody decorator', () => {
|
|
572
|
+
test('should be a function', () => {
|
|
573
|
+
expect(typeof WithBody).toBe('function');
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test('should return a decorator function', () => {
|
|
577
|
+
const decorator = WithBody(UserSchema);
|
|
578
|
+
expect(typeof decorator).toBe('function');
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
describe('WithQuery decorator', () => {
|
|
583
|
+
test('should be a function', () => {
|
|
584
|
+
expect(typeof WithQuery).toBe('function');
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test('should return a decorator function', () => {
|
|
588
|
+
const decorator = WithQuery(QuerySchema);
|
|
589
|
+
expect(typeof decorator).toBe('function');
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
describe('isStandardSchema', () => {
|
|
594
|
+
test('should return true for valid Standard Schema', () => {
|
|
595
|
+
expect(isStandardSchema(UserSchema)).toBe(true);
|
|
596
|
+
expect(isStandardSchema(IdSchema)).toBe(true);
|
|
597
|
+
expect(isStandardSchema(QuerySchema)).toBe(true);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test('should return false for null', () => {
|
|
601
|
+
expect(isStandardSchema(null)).toBe(false);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test('should return false for undefined', () => {
|
|
605
|
+
expect(isStandardSchema(undefined)).toBe(false);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test('should return false for primitive values', () => {
|
|
609
|
+
expect(isStandardSchema('string')).toBe(false);
|
|
610
|
+
expect(isStandardSchema(123)).toBe(false);
|
|
611
|
+
expect(isStandardSchema(true)).toBe(false);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('should return false for plain objects', () => {
|
|
615
|
+
expect(isStandardSchema({})).toBe(false);
|
|
616
|
+
expect(isStandardSchema({ validate: () => {} })).toBe(false);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test('should return false for objects with invalid ~standard property', () => {
|
|
620
|
+
expect(isStandardSchema({ '~standard': {} })).toBe(false);
|
|
621
|
+
expect(isStandardSchema({ '~standard': { validate: 'not a function' } })).toBe(false);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test('should return true for custom Standard Schema implementation', () => {
|
|
625
|
+
const customSchema = createTestSchema((data) => ({ value: data }));
|
|
626
|
+
|
|
627
|
+
expect(isStandardSchema(customSchema)).toBe(true);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test('should check for validate function specifically', () => {
|
|
631
|
+
const schemaWithNonFunctionValidate = {
|
|
632
|
+
'~standard': {
|
|
633
|
+
version: 1,
|
|
634
|
+
vendor: 'test',
|
|
635
|
+
validate: 'not a function',
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
expect(isStandardSchema(schemaWithNonFunctionValidate)).toBe(false);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
describe('assertStandardSchema', () => {
|
|
644
|
+
test('should not throw for valid Standard Schema', () => {
|
|
645
|
+
expect(() => assertStandardSchema(UserSchema)).not.toThrow();
|
|
646
|
+
expect(() => assertStandardSchema(IdSchema)).not.toThrow();
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
test('should throw for invalid schema with default name', () => {
|
|
650
|
+
expect(() => assertStandardSchema(null)).toThrow(
|
|
651
|
+
'Schema must implement Standard Schema interface. Supported: Zod 4+, Valibot v1+, ArkType, Typia 7+'
|
|
652
|
+
);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test('should throw for invalid schema with custom name', () => {
|
|
656
|
+
expect(() => assertStandardSchema({}, 'MySchema')).toThrow(
|
|
657
|
+
'MySchema must implement Standard Schema interface. Supported: Zod 4+, Valibot v1+, ArkType, Typia 7+'
|
|
658
|
+
);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test('should throw for plain objects', () => {
|
|
662
|
+
expect(() => assertStandardSchema({ validate: () => {} })).toThrow();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test('should narrow type correctly', () => {
|
|
666
|
+
const maybeSchema: unknown = UserSchema;
|
|
667
|
+
assertStandardSchema(maybeSchema);
|
|
668
|
+
// After assertion, maybeSchema is narrowed to StandardSchema
|
|
669
|
+
expect(maybeSchema['~standard'].validate).toBeDefined();
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test('should throw for undefined', () => {
|
|
673
|
+
expect(() => assertStandardSchema(undefined)).toThrow();
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test('should throw for primitives', () => {
|
|
677
|
+
expect(() => assertStandardSchema('string')).toThrow();
|
|
678
|
+
expect(() => assertStandardSchema(123)).toThrow();
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
describe('Typia Support', () => {
|
|
683
|
+
// Note: Typia requires TypeScript transformation at build time.
|
|
684
|
+
// These tests demonstrate how Typia schemas implement the Standard Schema interface.
|
|
685
|
+
// In a real project with Typia properly configured, you would use:
|
|
686
|
+
//
|
|
687
|
+
// import typia from 'typia';
|
|
688
|
+
//
|
|
689
|
+
// interface IUser {
|
|
690
|
+
// name: string;
|
|
691
|
+
// email: string;
|
|
692
|
+
// age?: number;
|
|
693
|
+
// }
|
|
694
|
+
//
|
|
695
|
+
// const typiaUserSchema = typia.createValidate<IUser>();
|
|
696
|
+
//
|
|
697
|
+
// Then use it with the validation functions:
|
|
698
|
+
// const result = await validate(typiaUserSchema, userData);
|
|
699
|
+
|
|
700
|
+
test('Typia createValidate returns Standard Schema compliant object', () => {
|
|
701
|
+
// This test documents the expected Typia interface
|
|
702
|
+
// Typia's createValidate<T>() returns an object that implements StandardSchemaV1
|
|
703
|
+
//
|
|
704
|
+
// The returned object has:
|
|
705
|
+
// - '~standard.version': 1
|
|
706
|
+
// - '~standard.vendor': 'typia'
|
|
707
|
+
// - '~standard.validate': (value: unknown) => IValidation<T>
|
|
708
|
+
//
|
|
709
|
+
// Example usage:
|
|
710
|
+
// const schema = typia.createValidate<IUser>();
|
|
711
|
+
// const result = schema['~standard'].validate({ name: 'John', email: 'john@example.com' });
|
|
712
|
+
// if ('value' in result) { /* success */ }
|
|
713
|
+
|
|
714
|
+
// Simulated Typia-like schema for documentation purposes
|
|
715
|
+
const simulatedTypiaSchema: StandardSchema<unknown, { name: string; email: string }> = {
|
|
716
|
+
'~standard': {
|
|
717
|
+
version: 1,
|
|
718
|
+
vendor: 'typia',
|
|
719
|
+
validate: (data: unknown) => {
|
|
720
|
+
if (typeof data === 'object' && data !== null) {
|
|
721
|
+
const obj = data as Record<string, unknown>;
|
|
722
|
+
if (typeof obj.name === 'string' && typeof obj.email === 'string') {
|
|
723
|
+
return { value: { name: obj.name, email: obj.email } };
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return { issues: [{ message: 'Invalid user object' }] };
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
expect(isStandardSchema(simulatedTypiaSchema)).toBe(true);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
test('Typia schema works with validate function', async () => {
|
|
735
|
+
// Simulated Typia schema
|
|
736
|
+
const typiaLikeSchema: StandardSchema<unknown, { name: string; email: string }> = {
|
|
737
|
+
'~standard': {
|
|
738
|
+
version: 1,
|
|
739
|
+
vendor: 'typia',
|
|
740
|
+
validate: (data: unknown) => {
|
|
741
|
+
if (typeof data === 'object' && data !== null) {
|
|
742
|
+
const obj = data as Record<string, unknown>;
|
|
743
|
+
if (typeof obj.name === 'string' && typeof obj.email === 'string') {
|
|
744
|
+
return { value: { name: obj.name, email: obj.email } };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return { issues: [{ message: 'Invalid user object' }] };
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
const validData = { name: 'John', email: 'john@example.com' };
|
|
753
|
+
const result = await validate(typiaLikeSchema, validData);
|
|
754
|
+
|
|
755
|
+
expect(result.success).toBe(true);
|
|
756
|
+
if (result.success) {
|
|
757
|
+
expect(result.data.name).toBe('John');
|
|
758
|
+
expect(result.data.email).toBe('john@example.com');
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test('Typia schema validation failures work correctly', async () => {
|
|
763
|
+
// Simulated Typia schema with stricter validation
|
|
764
|
+
const typiaLikeSchema: StandardSchema<unknown, { name: string; email: string }> = {
|
|
765
|
+
'~standard': {
|
|
766
|
+
version: 1,
|
|
767
|
+
vendor: 'typia',
|
|
768
|
+
validate: (data: unknown) => {
|
|
769
|
+
if (typeof data === 'object' && data !== null) {
|
|
770
|
+
const obj = data as Record<string, unknown>;
|
|
771
|
+
if (typeof obj.name !== 'string') {
|
|
772
|
+
return { issues: [{ message: 'name must be a string' }] };
|
|
773
|
+
}
|
|
774
|
+
if (typeof obj.email !== 'string') {
|
|
775
|
+
return { issues: [{ message: 'email must be a string' }] };
|
|
776
|
+
}
|
|
777
|
+
return { value: { name: obj.name, email: obj.email } };
|
|
778
|
+
}
|
|
779
|
+
return { issues: [{ message: 'Invalid user object' }] };
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
const invalidData = { name: 123, email: 'john@example.com' };
|
|
785
|
+
const result = await validate(typiaLikeSchema, invalidData);
|
|
786
|
+
|
|
787
|
+
expect(result.success).toBe(false);
|
|
788
|
+
if (!result.success) {
|
|
789
|
+
expect(result.issues.length).toBeGreaterThan(0);
|
|
790
|
+
expect(result.issues[0].message).toBe('name must be a string');
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
});
|