Skip to content

Commit 552eb12

Browse files
committed
Fixed implementation logic
1 parent cfe1bc4 commit 552eb12

File tree

8 files changed

+849
-10564
lines changed

8 files changed

+849
-10564
lines changed

aidlc-docs/audit.md

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

package-lock.json

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

packages/event-handler/debug-test.mjs

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

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

Lines changed: 2 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
MethodNotAllowedError,
5252
NotFoundError,
5353
} from './errors.js';
54+
import { createValidationMiddleware } from './middleware/validation.js';
5455
import { Route } from './Route.js';
5556
import { RouteHandlerRegistry } from './RouteHandlerRegistry.js';
5657
import {
@@ -449,7 +450,7 @@ class Router {
449450

450451
// Create validation middleware if validation config provided
451452
const allMiddleware = validation
452-
? [...middleware, this.#createValidationMiddleware(validation)]
453+
? [...middleware, createValidationMiddleware(validation)]
453454
: middleware;
454455

455456
for (const method of methods) {
@@ -459,132 +460,6 @@ class Router {
459460
}
460461
}
461462

462-
#createValidationMiddleware(
463-
config: RestRouteOptions['validation']
464-
): Middleware {
465-
if (!config) return async ({ next }) => next();
466-
467-
const reqSchemas = config.req;
468-
const resSchemas = config.res;
469-
470-
return async ({ reqCtx, next }) => {
471-
// Validate request
472-
if (reqSchemas) {
473-
if (reqSchemas.body) {
474-
// Use event.body which is the raw string, parse if JSON
475-
let bodyData: unknown = reqCtx.event.body;
476-
const contentType = reqCtx.req.headers.get('content-type');
477-
if (
478-
contentType?.includes('application/json') &&
479-
typeof bodyData === 'string'
480-
) {
481-
try {
482-
bodyData = JSON.parse(bodyData);
483-
} catch {
484-
// If parsing fails, validate the raw string
485-
}
486-
}
487-
await this.#validateComponent(
488-
reqSchemas.body,
489-
bodyData,
490-
'body',
491-
true
492-
);
493-
}
494-
if (reqSchemas.headers) {
495-
const headers = Object.fromEntries(reqCtx.req.headers.entries());
496-
await this.#validateComponent(
497-
reqSchemas.headers,
498-
headers,
499-
'headers',
500-
true
501-
);
502-
}
503-
if (reqSchemas.path) {
504-
await this.#validateComponent(
505-
reqSchemas.path,
506-
reqCtx.params,
507-
'path',
508-
true
509-
);
510-
}
511-
if (reqSchemas.query) {
512-
const query = Object.fromEntries(
513-
new URL(reqCtx.req.url).searchParams.entries()
514-
);
515-
await this.#validateComponent(reqSchemas.query, query, 'query', true);
516-
}
517-
}
518-
519-
// Execute handler
520-
const response = await next();
521-
522-
// Validate response
523-
if (resSchemas && response && typeof response === 'object') {
524-
if (resSchemas.body && 'body' in response) {
525-
await this.#validateComponent(
526-
resSchemas.body,
527-
response.body,
528-
'body',
529-
false
530-
);
531-
}
532-
if (resSchemas.headers && 'headers' in response) {
533-
const headers =
534-
response.headers instanceof Headers
535-
? Object.fromEntries(response.headers.entries())
536-
: response.headers;
537-
await this.#validateComponent(
538-
resSchemas.headers,
539-
headers,
540-
'headers',
541-
false
542-
);
543-
}
544-
}
545-
546-
return response;
547-
};
548-
}
549-
550-
async #validateComponent(
551-
schema: {
552-
'~standard': {
553-
version: 1;
554-
vendor: string;
555-
validate: (
556-
value: unknown
557-
) => Promise<{ value: unknown }> | { value: unknown };
558-
};
559-
},
560-
data: unknown,
561-
component: 'body' | 'headers' | 'path' | 'query',
562-
isRequest: boolean
563-
): Promise<void> {
564-
try {
565-
const result = await schema['~standard'].validate(data);
566-
if (!('value' in result)) {
567-
throw new Error('Validation failed');
568-
}
569-
} catch (error) {
570-
const message = `Validation failed for ${isRequest ? 'request' : 'response'} ${component}`;
571-
if (isRequest) {
572-
const { RequestValidationError } = await import('./errors.js');
573-
throw new RequestValidationError(
574-
message,
575-
component,
576-
error instanceof Error ? error : undefined
577-
);
578-
}
579-
const { ResponseValidationError } = await import('./errors.js');
580-
throw new ResponseValidationError(
581-
message,
582-
component as 'body' | 'headers',
583-
error instanceof Error ? error : undefined
584-
);
585-
}
586-
}
587-
588463
/**
589464
* Handles errors by finding a registered error handler or falling
590465
* 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+
}

packages/event-handler/src/types/http.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
GenericLogger,
44
JSONValue,
55
} from '@aws-lambda-powertools/commons/types';
6+
import type { StandardSchemaV1 } from '@standard-schema/spec';
67
import type {
78
ALBEvent,
89
ALBResult,
@@ -253,36 +254,22 @@ type RouterResponse =
253254
| APIGatewayProxyResult
254255
| APIGatewayProxyStructuredResultV2
255256
| ALBResult;
256-
/**
257-
* Standard Schema interface for validation
258-
* @see https://github.com/standard-schema/standard-schema
259-
*/
260-
interface StandardSchema<Input = unknown, Output = Input> {
261-
'~standard': {
262-
version: 1;
263-
vendor: string;
264-
validate: (
265-
value: unknown
266-
) => Promise<{ value: Output }> | { value: Output };
267-
};
268-
}
269-
270257
/**
271258
* Configuration for request validation
272259
*/
273260
type RequestValidationConfig<T = unknown> = {
274-
body?: StandardSchema<unknown, T>;
275-
headers?: StandardSchema<unknown, Record<string, string>>;
276-
path?: StandardSchema<unknown, Record<string, string>>;
277-
query?: StandardSchema<unknown, Record<string, string>>;
261+
body?: StandardSchemaV1<unknown, T>;
262+
headers?: StandardSchemaV1<unknown, Record<string, string>>;
263+
path?: StandardSchemaV1<unknown, Record<string, string>>;
264+
query?: StandardSchemaV1<unknown, Record<string, string>>;
278265
};
279266

280267
/**
281268
* Configuration for response validation
282269
*/
283270
type ResponseValidationConfig<T = unknown> = {
284-
body?: StandardSchema<unknown, T>;
285-
headers?: StandardSchema<unknown, Record<string, string>>;
271+
body?: StandardSchemaV1<unknown, T>;
272+
headers?: StandardSchemaV1<unknown, Record<string, string>>;
286273
};
287274

288275
/**
@@ -334,7 +321,6 @@ export type {
334321
NextFunction,
335322
V1Headers,
336323
WebResponseToProxyResultOptions,
337-
StandardSchema,
338324
RequestValidationConfig,
339325
ResponseValidationConfig,
340326
ValidationConfig,

0 commit comments

Comments
 (0)