diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index 20c400bc70..c344a80b2f 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -55,7 +55,7 @@ Both are also able to parse either an object or JSON string as an input. ## Built-in schemas -Parser comes with the following built-in schemas: +**Parser** comes with the following built-in schemas: | Model name | Description | | -------------------------------------------- | ------------------------------------------------------------------------------------- | @@ -64,6 +64,7 @@ Parser comes with the following built-in schemas: | **APIGatewayRequestAuthorizerEventSchema** | Lambda Event Source payload for Amazon API Gateway Request Authorizer | | **APIGatewayTokenAuthorizerEventSchema** | Lambda Event Source payload for Amazon API Gateway Token Authorizer | | **APIGatewayProxyEventV2Schema** | Lambda Event Source payload for Amazon API Gateway v2 payload | +| **APIGatewayProxyWebsocketEventSchema** | Lambda Event Source payload for Amazon API Gateway WebSocket events | | **APIGatewayRequestAuthorizerEventV2Schema** | Lambda Event Source payload for Amazon API Gateway v2 Authorizer | | **CloudFormationCustomResourceCreateSchema** | Lambda Event Source payload for AWS CloudFormation `CREATE` operation | | **CloudFormationCustomResourceUpdateSchema** | Lambda Event Source payload for AWS CloudFormation `UPDATE` operation | diff --git a/packages/parser/src/schemas/api-gateway-websocket.ts b/packages/parser/src/schemas/api-gateway-websocket.ts new file mode 100644 index 0000000000..d6e56fed24 --- /dev/null +++ b/packages/parser/src/schemas/api-gateway-websocket.ts @@ -0,0 +1,92 @@ +import { z } from 'zod'; + +/** + * A zod schema for API Gateway Proxy WebSocket events. + * + * @example + * ```json + * { + * "type": "REQUEST", + * "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/default/$connect", + * "headers": { + * "Connection": "upgrade", + * "content-length": "0", + * "HeaderAuth1": "headerValue1", + * "Host": "abcdef123.execute-api.us-east-1.amazonaws.com", + * "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits", + * "Sec-WebSocket-Key": "...", + * "Sec-WebSocket-Version": "13", + * "Upgrade": "websocket", + * "X-Amzn-Trace-Id": "..." + * }, + * "multiValueHeaders": { + * "Connection": [ "upgrade" ], + * "content-length": [ "0" ], + * "HeaderAuth1": [ "headerValue1" ], + * "Host": [ "abcdef123.execute-api.us-east-1.amazonaws.com" ], + * "Sec-WebSocket-Extensions": [ "permessage-deflate; client_max_window_bits" ], + * "Sec-WebSocket-Key": [ "..." ], + * "Sec-WebSocket-Version": [ "13" ], + * "Upgrade": [ "websocket" ], + * "X-Amzn-Trace-Id": [ "..." ] + * }, + * "queryStringParameters": { + * "QueryString1": "queryValue1" + * }, + * "multiValueQueryStringParameters": { + * "QueryString1": [ "queryValue1" ] + * }, + * "stageVariables": {}, + * "requestContext": { + * "routeKey": "$connect", + * "eventType": "CONNECT", + * "extendedRequestId": "...", + * "requestTime": "19/Jan/2023:21:13:26 +0000", + * "messageDirection": "IN", + * "stage": "default", + * "connectedAt": 1674162806344, + * "requestTimeEpoch": 1674162806345, + * "identity": { + * "sourceIp": "..." + * }, + * "requestId": "...", + * "domainName": "abcdef123.execute-api.us-east-1.amazonaws.com", + * "connectionId": "...", + * "apiId": "abcdef123" + * }, + * "isBase64Encoded": false, + * "body": null + * } + * ``` + * + * @see {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-develop-integrations.html} + */ +export const APIGatewayProxyWebsocketEventSchema = z.object({ + type: z.string(), + methodArn: z.string(), + headers: z.record(z.string()), + multiValueHeaders: z.record(z.array(z.string())), + queryStringParameters: z.record(z.string()).nullable().optional(), + multiValueQueryStringParameters: z.record(z.array(z.string())).nullable().optional(), + stageVariables: z.record(z.string()).nullable().optional(), + requestContext: z.object({ + routeKey: z.string(), + eventType: z.enum(["CONNECT", "DISCONNECT", "MESSAGE"]), + extendedRequestId: z.string(), + requestTime: z.string(), + messageDirection: z.enum(["IN", "OUT"]), + stage: z.string(), + connectedAt: z.number(), + requestTimeEpoch: z.number(), + identity: z.object({ + sourceIp: z.string(), + userAgent: z.string().optional(), + }), + requestId: z.string(), + domainName: z.string(), + connectionId: z.string(), + apiId: z.string(), + }), + isBase64Encoded: z.boolean(), + body: z.string().optional().nullable(), +}); \ No newline at end of file diff --git a/packages/parser/src/schemas/index.ts b/packages/parser/src/schemas/index.ts index 9687e65064..ed39e519c5 100644 --- a/packages/parser/src/schemas/index.ts +++ b/packages/parser/src/schemas/index.ts @@ -5,6 +5,7 @@ export { APIGatewayTokenAuthorizerEventSchema, APIGatewayEventRequestContextSchema, } from './api-gateway.js'; +export { APIGatewayProxyWebsocketEventSchema } from './api-gateway-websocket.js' export { AppSyncResolverSchema, AppSyncBatchResolverSchema, diff --git a/packages/parser/src/types/schema.ts b/packages/parser/src/types/schema.ts index fb58f39835..a638535576 100644 --- a/packages/parser/src/types/schema.ts +++ b/packages/parser/src/types/schema.ts @@ -3,6 +3,7 @@ import type { APIGatewayEventRequestContextSchema, APIGatewayProxyEventSchema, APIGatewayProxyEventV2Schema, + APIGatewayProxyWebsocketEventSchema, APIGatewayRequestAuthorizerEventSchema, APIGatewayRequestAuthorizerV2Schema, APIGatewayRequestContextV2Schema, @@ -68,6 +69,8 @@ type APIGatewayEventRequestContext = z.infer< type APIGatewayProxyEventV2 = z.infer; +type APIGatewayProxyWebsocketEvent = z.infer; + type APIGatewayRequestAuthorizerV2 = z.infer< typeof APIGatewayRequestAuthorizerV2Schema >; @@ -166,6 +169,7 @@ export type { APIGatewayEventRequestContext, APIGatewayProxyEvent, APIGatewayProxyEventV2, + APIGatewayProxyWebsocketEvent, APIGatewayRequestAuthorizerEvent, APIGatewayRequestAuthorizerV2, APIGatewayRequestContextV2, diff --git a/packages/parser/tests/events/apigw-websocket/connectEvent.json b/packages/parser/tests/events/apigw-websocket/connectEvent.json new file mode 100644 index 0000000000..409859eab7 --- /dev/null +++ b/packages/parser/tests/events/apigw-websocket/connectEvent.json @@ -0,0 +1,43 @@ +{ + "type": "REQUEST", + "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/default/$connect", + "headers": { + "Connection": "upgrade", + "content-length": "0", + "Host": "abcdef123.execute-api.us-east-1.amazonaws.com", + "Upgrade": "websocket" + }, + "multiValueHeaders": { + "Connection": ["upgrade"], + "content-length": ["0"], + "Host": ["abcdef123.execute-api.us-east-1.amazonaws.com"], + "Upgrade": ["websocket"] + }, + "queryStringParameters": { + "QueryString1": "queryValue1" + }, + "multiValueQueryStringParameters": { + "QueryString1": ["queryValue1"] + }, + "stageVariables": {}, + "requestContext": { + "routeKey": "$connect", + "eventType": "CONNECT", + "extendedRequestId": "XYZ123=", + "requestTime": "10/Oct/2024:22:56:18 +0000", + "messageDirection": "IN", + "stage": "default", + "connectedAt": 1674162806344, + "requestTimeEpoch": 1674162806345, + "identity": { + "sourceIp": "192.0.2.1" + }, + "requestId": "abc123", + "domainName": "abcdef123.execute-api.us-east-1.amazonaws.com", + "connectionId": "def456", + "apiId": "abcdef123" + }, + "isBase64Encoded": false, + "body": null + } + \ No newline at end of file diff --git a/packages/parser/tests/unit/schema/apigw-websocket.test.ts b/packages/parser/tests/unit/schema/apigw-websocket.test.ts new file mode 100644 index 0000000000..225458e626 --- /dev/null +++ b/packages/parser/tests/unit/schema/apigw-websocket.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; +import { APIGatewayProxyWebsocketEventSchema } from '../../../src/schemas/api-gateway-websocket.js'; +import type { APIGatewayProxyWebsocketEvent } from '../../../src/types/schema.js'; +import { getTestEvent } from '../helpers/utils.js'; + +describe('Schema: APIGatewayProxyWebsocketEvent', () => { + const baseEvent = getTestEvent({ + eventsPath: 'apigw-websocket', + filename: 'connectEvent', + }); + + it('parses a valid API Gateway WebSocket event', () => { + // Prepare + const event = structuredClone(baseEvent); + + // Act + const result = APIGatewayProxyWebsocketEventSchema.parse(event); + + // Assess + expect(result).toStrictEqual(event); + }); + + it('throws if the event is missing required fields', () => { + // Prepare + const invalidEvent = { + type: 'REQUEST', + methodArn: 'arn:aws:execute-api:us-east-1:123456789012:abcdef123/default/$connect', + headers: {}, + requestContext: { + routeKey: '$connect', + eventType: 'CONNECT', + }, + }; + + // Act & Assess + expect(() => APIGatewayProxyWebsocketEventSchema.parse(invalidEvent)).toThrow(); + }); +}); \ No newline at end of file