Skip to content

Commit e33ab4c

Browse files
committed
Fixed implementation logic
1 parent f5232c4 commit e33ab4c

File tree

9 files changed

+869
-537
lines changed

9 files changed

+869
-537
lines changed

aidlc-docs/audit.md

Lines changed: 384 additions & 0 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 20 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/event-handler/debug-test.mjs

Lines changed: 0 additions & 13 deletions
This file was deleted.

packages/event-handler/src/rest/Router.ts

Lines changed: 2 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
MethodNotAllowedError,
3737
NotFoundError,
3838
} from './errors.js';
39+
import { createValidationMiddleware } from './middleware/validation.js';
3940
import { Route } from './Route.js';
4041
import { RouteHandlerRegistry } from './RouteHandlerRegistry.js';
4142
import {
@@ -361,7 +362,7 @@ class Router {
361362

362363
// Create validation middleware if validation config provided
363364
const allMiddleware = validation
364-
? [...middleware, this.#createValidationMiddleware(validation)]
365+
? [...middleware, createValidationMiddleware(validation)]
365366
: middleware;
366367

367368
for (const method of methods) {
@@ -371,132 +372,6 @@ class Router {
371372
}
372373
}
373374

374-
#createValidationMiddleware(
375-
config: RestRouteOptions['validation']
376-
): Middleware {
377-
if (!config) return async ({ next }) => next();
378-
379-
const reqSchemas = config.req;
380-
const resSchemas = config.res;
381-
382-
return async ({ reqCtx, next }) => {
383-
// Validate request
384-
if (reqSchemas) {
385-
if (reqSchemas.body) {
386-
// Use event.body which is the raw string, parse if JSON
387-
let bodyData: unknown = reqCtx.event.body;
388-
const contentType = reqCtx.req.headers.get('content-type');
389-
if (
390-
contentType?.includes('application/json') &&
391-
typeof bodyData === 'string'
392-
) {
393-
try {
394-
bodyData = JSON.parse(bodyData);
395-
} catch {
396-
// If parsing fails, validate the raw string
397-
}
398-
}
399-
await this.#validateComponent(
400-
reqSchemas.body,
401-
bodyData,
402-
'body',
403-
true
404-
);
405-
}
406-
if (reqSchemas.headers) {
407-
const headers = Object.fromEntries(reqCtx.req.headers.entries());
408-
await this.#validateComponent(
409-
reqSchemas.headers,
410-
headers,
411-
'headers',
412-
true
413-
);
414-
}
415-
if (reqSchemas.path) {
416-
await this.#validateComponent(
417-
reqSchemas.path,
418-
reqCtx.params,
419-
'path',
420-
true
421-
);
422-
}
423-
if (reqSchemas.query) {
424-
const query = Object.fromEntries(
425-
new URL(reqCtx.req.url).searchParams.entries()
426-
);
427-
await this.#validateComponent(reqSchemas.query, query, 'query', true);
428-
}
429-
}
430-
431-
// Execute handler
432-
const response = await next();
433-
434-
// Validate response
435-
if (resSchemas && response && typeof response === 'object') {
436-
if (resSchemas.body && 'body' in response) {
437-
await this.#validateComponent(
438-
resSchemas.body,
439-
response.body,
440-
'body',
441-
false
442-
);
443-
}
444-
if (resSchemas.headers && 'headers' in response) {
445-
const headers =
446-
response.headers instanceof Headers
447-
? Object.fromEntries(response.headers.entries())
448-
: response.headers;
449-
await this.#validateComponent(
450-
resSchemas.headers,
451-
headers,
452-
'headers',
453-
false
454-
);
455-
}
456-
}
457-
458-
return response;
459-
};
460-
}
461-
462-
async #validateComponent(
463-
schema: {
464-
'~standard': {
465-
version: 1;
466-
vendor: string;
467-
validate: (
468-
value: unknown
469-
) => Promise<{ value: unknown }> | { value: unknown };
470-
};
471-
},
472-
data: unknown,
473-
component: 'body' | 'headers' | 'path' | 'query',
474-
isRequest: boolean
475-
): Promise<void> {
476-
try {
477-
const result = await schema['~standard'].validate(data);
478-
if (!('value' in result)) {
479-
throw new Error('Validation failed');
480-
}
481-
} catch (error) {
482-
const message = `Validation failed for ${isRequest ? 'request' : 'response'} ${component}`;
483-
if (isRequest) {
484-
const { RequestValidationError } = await import('./errors.js');
485-
throw new RequestValidationError(
486-
message,
487-
component,
488-
error instanceof Error ? error : undefined
489-
);
490-
}
491-
const { ResponseValidationError } = await import('./errors.js');
492-
throw new ResponseValidationError(
493-
message,
494-
component as 'body' | 'headers',
495-
error instanceof Error ? error : undefined
496-
);
497-
}
498-
}
499-
500375
/**
501376
* Handles errors by finding a registered error handler or falling
502377
* back to a default handler.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { compress } from './compress.js';
22
export { cors } from './cors.js';
3+
export { createValidationMiddleware } from './validation.js';
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import type { StandardSchemaV1 } from '@standard-schema/spec';
2+
import type { Middleware, RestRouteOptions } from '../../types/rest.js';
3+
import { RequestValidationError, ResponseValidationError } from '../errors.js';
4+
5+
/**
6+
* Creates a validation middleware from the provided validation configuration.
7+
*
8+
* @param config - Validation configuration for request and response
9+
* @returns Middleware function that validates request/response
10+
*/
11+
export const createValidationMiddleware = (
12+
config: RestRouteOptions['validation']
13+
): Middleware => {
14+
const reqSchemas = config?.req;
15+
const resSchemas = config?.res;
16+
17+
return async ({ reqCtx, next }) => {
18+
// Validate request
19+
if (reqSchemas) {
20+
if (reqSchemas.body) {
21+
let bodyData: unknown = reqCtx.event.body;
22+
const contentType = reqCtx.req.headers.get('content-type');
23+
if (
24+
contentType?.includes('application/json') &&
25+
typeof bodyData === 'string'
26+
) {
27+
try {
28+
bodyData = JSON.parse(bodyData);
29+
} catch {
30+
// If parsing fails, validate the raw string
31+
}
32+
}
33+
await validateComponent(reqSchemas.body, bodyData, 'body', true);
34+
}
35+
if (reqSchemas.headers) {
36+
const headers = Object.fromEntries(reqCtx.req.headers.entries());
37+
await validateComponent(reqSchemas.headers, headers, 'headers', true);
38+
}
39+
if (reqSchemas.path) {
40+
await validateComponent(reqSchemas.path, reqCtx.params, 'path', true);
41+
}
42+
if (reqSchemas.query) {
43+
const query = Object.fromEntries(
44+
new URL(reqCtx.req.url).searchParams.entries()
45+
);
46+
await validateComponent(reqSchemas.query, query, 'query', true);
47+
}
48+
}
49+
50+
// Execute handler
51+
await next();
52+
53+
// Validate response
54+
if (resSchemas) {
55+
const response = reqCtx.res;
56+
57+
if (resSchemas.body) {
58+
const clonedResponse = response.clone();
59+
const contentType = response.headers.get('content-type');
60+
61+
let bodyData: unknown;
62+
if (contentType?.includes('application/json')) {
63+
bodyData = await clonedResponse.json();
64+
} else {
65+
bodyData = await clonedResponse.text();
66+
}
67+
68+
await validateComponent(resSchemas.body, bodyData, 'body', false);
69+
}
70+
71+
if (resSchemas.headers) {
72+
const headers = Object.fromEntries(response.headers.entries());
73+
await validateComponent(resSchemas.headers, headers, 'headers', false);
74+
}
75+
}
76+
};
77+
};
78+
79+
async function validateComponent(
80+
schema: StandardSchemaV1,
81+
data: unknown,
82+
component: 'body' | 'headers' | 'path' | 'query',
83+
isRequest: boolean
84+
): Promise<void> {
85+
const result = await schema['~standard'].validate(data);
86+
87+
if ('issues' in result) {
88+
const message = `Validation failed for ${isRequest ? 'request' : 'response'} ${component}`;
89+
const error = new Error('Validation failed');
90+
91+
if (isRequest) {
92+
throw new RequestValidationError(message, component, error);
93+
}
94+
throw new ResponseValidationError(
95+
message,
96+
component as 'body' | 'headers',
97+
error
98+
);
99+
}
100+
}

0 commit comments

Comments
 (0)