diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index bfbaa2aac3..87038ee15a 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -1,15 +1,13 @@ --- -title: Validation (JSON Schema) +title: Validation descrition: Utility +status: new --- This utility provides [JSON Schema](https://json-schema.org) validation for events and responses, including JMESPath support to unwrap events before validation. -!!! warning - This feature is currently under development. As such it's considered not stable and we might make significant breaking changes before going before its release. You are welcome to [provide feedback](https://github.com/aws-powertools/powertools-lambda-typescript/discussions/3519) and [contribute to its implementation](https://github.com/aws-powertools/powertools-lambda-typescript/milestone/18). - ## Key features - Validate incoming event and response payloads @@ -20,7 +18,7 @@ This utility provides [JSON Schema](https://json-schema.org) validation for even ## Getting started ```bash -npm install @aws-lambda-powertools/validation ajv +npm install @aws-lambda-powertools/validation ``` You can validate inbound and outbound payloads using the validator class method decorator or Middy.js middleware. @@ -41,75 +39,16 @@ If the validation fails, we will throw a `SchemaValidationError`. All our decorators assume that the method they are decorating is an async method. This means that even when decorating a synchronous method, it will return a promise. If this is not the desired behavior, you can use one of the other patterns to validate your payloads. -=== "getting_started_decorator.ts" +=== "gettingStartedDecorator.ts" - ```typescript - import { validator } from '@aws-lambda-powertools/validation'; - import type { Context } from 'aws-lambda'; - import { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - } from './getting_started_schemas.js'; - - class Lambda { - @validator({ - inboundSchema, - outboundSchema, - }) - async handler(event: InboundSchema, context: Context): Promise { - return { - statusCode: 200, - body: `Hello from ${event.userId}`, - } - } - } - - export const handler = new Lambda().handler + ```typescript hl_lines="1 11-14" + --8<-- "examples/snippets/validation/gettingStartedDecorator.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` It's not mandatory to validate both the inbound and outbound payloads. You can either use one, the other, or both. @@ -125,73 +64,16 @@ If you are using Middy.js, you can use the `validator` middleware to validate th Like the class method decorator, if the validation fails, we will throw a `SchemaValidationError`, and you don't need to use both the inbound and outbound schemas if you don't need to. -=== "getting_started_middy.ts" +=== "gettingStartedMiddy.ts" - ```typescript - import { validator } from '@aws-lambda-powertools/validation/middleware'; - import middy from '@middy/core'; - import { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - } from './getting_started_schemas.js'; - - export const handler = middy() - .use(validator({ - inboundSchema, - outboundSchema, - })) - .handler( - async (event: InboundSchema, context: Context): Promise => { - return { - statusCode: 200, - body: `Hello from ${event.userId}`, - } - }); + ```typescript hl_lines="1 12-15" + --8<-- "examples/snippets/validation/gettingStartedMiddy.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` ### Standalone validation @@ -200,80 +82,18 @@ The `validate` function gives you more control over the validation process, and You can also gracefully handle schema validation errors by catching `SchemaValidationError` errors. -=== "getting_started_standalone.ts" +=== "gettingStartedStandalone.ts" - ```typescript - import { validate, SchemaValidationError } from '@aws-lambda-powertools/validation'; - import { Logger } from '@aws-lambda-powertools/logger'; - import { - inboundSchema, - type InboundSchema, - } from './getting_started_schemas.js'; - - const logger = new Logger(); - - export const handler = async (event: InboundSchema, context: Context) => { - try { - await validate({ - payload: event, - schema: inboundSchema, - }) - - return { // since we are not validating the output, we can return anything - message: 'ok' - } - } catch (error) { - if (error instanceof SchemaValidationError) { - logger.error('Schema validation failed', error) - throw new Error('Invalid event payload') - } - - throw error - } - } + ```typescript hl_lines="2 3 10-13 19" + --8<-- "examples/snippets/validation/gettingStartedStandalone.ts" ``` -=== "getting_started_schemas.ts" + 1. Since we are not validating the output, we can return anything + +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` ### Unwrapping events prior to validation @@ -284,92 +104,22 @@ Envelopes are [JMESPath expressions](https://jmespath.org/tutorial.html) to extr Here is a sample custom EventBridge event, where we only want to validate the `detail` part of the event: -=== "getting_started_envelope.ts" +=== "gettingStartedEnvelope.ts" - ```typescript - import { validator } from '@aws-lambda-powertools/validation'; - import type { Context } from 'aws-lambda'; - import { - inboundSchema, - type InboundSchema, - type OutboundSchema - } from './getting_started_schemas.js'; - - class Lambda { - @validator({ - inboundSchema, - envelope: 'detail', - }) - async handler(event: InboundSchema, context: Context) { - return { - message: `processed ${event.userId}`, - success: true, - } - } - } - - export const handler = new Lambda().handler + ```typescript hl_lines="8" + --8<-- "examples/snippets/validation/gettingStartedEnvelope.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` -=== "getting_started_envelope_event.json" +=== "gettingStartedEnvelopeEvent.json" ```json - { - "version": "0", - "id": "12345678-1234-1234-1234-123456789012", - "detail-type": "myDetailType", - "source": "myEventSource", - "account": "123456789012", - "time": "2017-12-22T18:43:48Z", - "region": "us-west-2", - "resources": [], - "detail": { - "userId": "123" - } - } + --8<-- "examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json" ``` This is quite powerful as it allows you to validate only the part of the event that you are interested in, and thanks to JMESPath, you can extract records from [arrays](https://jmespath.org/tutorial.html#list-and-slice-projections), combine [pipe](https://jmespath.org/tutorial.html#pipe-expressions) and filter expressions, and more. @@ -382,115 +132,22 @@ We provide built-in envelopes to easily extract payloads from popular AWS event Here is an example of how you can use the built-in envelope for SQS events: -=== "getting_started_envelope_builtin.ts" +=== "gettingStartedEnvelopeBuiltin.ts" - ```typescript - import { validator } from '@aws-lambda-powertools/validation'; - import { SQS } from '@aws-lambda-powertools/validation/envelopes/sqs'; - import type { Context } from 'aws-lambda'; - import { - inboundSchema, - type InboundSchema, - } from './getting_started_schemas.js'; - - const logger = new Logger(); - - export const handler = middy() - .use(validator({ - inboundSchema, - envelope: SQS, - })) - .handler( - async (event: Array, context: Context) => { - for (const record of event) { - logger.info(`Processing message ${record.userId}`); - } - } - ) + ```typescript hl_lines="1 13" + --8<-- "examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts" ``` -=== "getting_started_schemas.ts" +=== "schemas.ts" ```typescript - const inboundSchema = { - type: 'object', - properties: { - userId: { - type: 'string' - } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - type: 'object', - properties: { - body: { - type: 'string' - }, - statusCode: { - type: 'number' - } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; + --8<-- "examples/snippets/validation/schemas.ts" ``` -=== "getting_started_envelope_event.json" +=== "gettingStartedSQSEnvelopeEvent.json" ```json - { - "Records": [ - { - "messageId": "c80e8021-a70a-42c7-a470-796e1186f753", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "{\"userId\":\"123\"}", - "attributes": { - "ApproximateReceiveCount": "3", - "SentTimestamp": "1529104986221", - "SenderId": "AIDAIC6K7FJUZ7Q", - "ApproximateFirstReceiveTimestamp": "1529104986230" - }, - "messageAttributes": {}, - "md5OfBody": "098f6bcd4621d373cade4e832627b4f6", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", - "awsRegion": "us-west-2" - }, - { - "messageId": "c80e8021-a70a-42c7-a470-796e1186f753", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "{\"userId\":\"456\"}", - "attributes": { - "ApproximateReceiveCount": "3", - "SentTimestamp": "1529104986221", - "SenderId": "AIDAIC6K7FJUZ7Q", - "ApproximateFirstReceiveTimestamp": "1529104986230" - }, - "messageAttributes": {}, - "md5OfBody": "098f6bcd4621d373cade4e832627b4f6", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", - "awsRegion": "us-west-2" - } - ] - } + --8<-- "examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json" ``` For a complete list of built-in envelopes, check the built-in envelopes section [here](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/jmespath/#built-in-envelopes). @@ -505,70 +162,20 @@ This is useful when you have a specific format that is not covered by the built- JSON Schemas with custom formats like `awsaccountid` will fail validation if the format is not defined. You can define custom formats using the `formats` option to any of the validation methods. -=== "schema_with_custom_format.json" +=== "schemaWithCustomFormat.json" ```json - { - "type": "object", - "properties": { - "accountId": { - "type": "string", - "format": "awsaccountid" - }, - "creditCard": { - "type": "string", - "format": "creditcard" - } - }, - "required": ["accountId"] - } + --8<-- "examples/snippets/validation/samples/schemaWithCustomFormat.json" ``` For each one of these custom formats, you need to tell us how to validate them. To do so, you can either pass a `RegExp` object or a function that receives the value and returns a boolean. For example, to validate using the schema above, you can define a custom format for `awsaccountid` like this: -=== "advanced_custom_format.ts" +=== "advancedCustomFormats.ts" - ```typescript - import { validate, SchemaValidationError } from '@aws-lambda-powertools/validation'; - import { Logger } from '@aws-lambda-powertools/logger'; - - const logger = new Logger(); - - const customFormats = { - awsaccountid: new RegExp('^[0-9]{12}$'), - creditcard: (value: string) => { - // Luhn algorithm (for demonstration purposes only - do not use in production) - const sum = value.split('').reverse().reduce((acc, digit, index) => { - const num = parseInt(digit, 10); - return acc + (index % 2 === 0 ? num : num < 5 ? num * 2 : num * 2 - 9); - }, 0); - - return sum % 10 === 0; - } - }; - - export const handler = async (event: any, context: Context) => { - try { - await validate({ - payload: event, - schema: schemaWithCustomFormat, - formats: customFormats, - }) - - return { // since we are not validating the output, we can return anything - message: 'ok' - } - } catch (error) { - if (error instanceof SchemaValidationError) { - logger.error('Schema validation failed', error) - throw new Error('Invalid event payload') - } - - throw error - } - } + ```typescript hl_lines="29" + --8<-- "examples/snippets/validation/advancedCustomFormats.ts" ``` ### Built-in JMESpath functions @@ -589,78 +196,16 @@ JSON Schema allows schemas to reference other schemas using the `$ref` keyword. You can use the `externalRefs` option to pass a list of schemas that you want to reference in your inbound and outbound schemas. -=== "advanced_custom_format.ts" +=== "advancedExternalRefs.ts" - ```typescript - import { validate } from '@aws-lambda-powertools/validation'; - import { - inboundSchema, - outboundSchema, - defsSchema, - type InboundSchema, - } from './schemas_with_external_ref.ts'; - - class Lambda { - @validator({ - inboundSchema, - outboundSchema, - externalRefs: [defsSchema], - }) - async handler(event: InboundSchema, context: Context) { - return { - message: `processed ${event.userId}`, - success: true, - } - } - } + ```typescript hl_lines="14" + --8<-- "examples/snippets/validation/advancedExternalRefs.ts" ``` -=== "schemas_with_external_ref.ts" - - ```ts - const defsSchema = { - $id: 'http://example.com/schemas/defs.json', - definitions: { - int: { type: 'integer' }, - str: { type: 'string' }, - }, - } as const; - - const inboundSchema = { - $id: 'http://example.com/schemas/inbound.json', - type: 'object', - properties: { - userId: { $ref: 'defs.json#/definitions/str' } - }, - required: ['userId'] - } as const; - - type InboundSchema = { - userId: string; - }; - - const outboundSchema = { - $id: 'http://example.com/schemas/outbound.json', - type: 'object', - properties: { - body: { $ref: 'defs.json#/definitions/str' }, - statusCode: { $ref: 'defs.json#/definitions/int' } - }, - required: ['body', 'statusCode'] - } as const; - - type OutboundSchema = { - body: string; - statusCode: number; - }; - - export { - defsSchema, - inboundSchema, - outboundSchema, - type InboundSchema, - type OutboundSchema - }; +=== "schemasWithExternalRefs.ts" + + ```typescript + --8<-- "examples/snippets/validation/schemasWithExternalRefs.ts" ``` ### Bringing your own `ajv` instance @@ -669,39 +214,14 @@ By default, we use JSON Schema draft-07. If you want to use a different draft, y This is also useful if you want to configure `ajv` with custom options like keywords and more. -=== "advanced_custom_format.ts" +=== "advancedBringAjvInstance.ts" - ```typescript - import { validate } from '@aws-lambda-powertools/validation'; - import Ajv2019 from "ajv/dist/2019" - import { Logger } from '@aws-lambda-powertools/logger'; - - const logger = new Logger(); - - const ajv = new Ajv2019(); - - export const handler = async (event: any, context: Context) => { - try { - await validate({ - payload: event, - schema: schemaWithCustomFormat, - ajv, - }) - - return { // since we are not validating the output, we can return anything - message: 'ok' - } - } catch (error) { - if (error instanceof SchemaValidationError) { - logger.error('Schema validation failed', error) - throw new Error('Invalid event payload') - } - - throw error - } - } + ```typescript hl_lines="9 16" + --8<-- "examples/snippets/validation/advancedBringAjvInstance.ts" ``` + 1. You can pass your own `ajv` instance to any of the validation methods. This is useful if you want to configure `ajv` with custom options like keywords and more. + ## Should I use this or Parser? One of Powertools for AWS Lambda [tenets](../index.md#tenets) is to be progressive. This means that our utilities are designed to be incrementally adopted by customers at any stage of their serverless journey. diff --git a/examples/snippets/validation/.gitignore b/examples/snippets/event-handler/.gitignore similarity index 100% rename from examples/snippets/validation/.gitignore rename to examples/snippets/event-handler/.gitignore diff --git a/examples/snippets/validation/advancedBringAjvInstance.ts b/examples/snippets/validation/advancedBringAjvInstance.ts new file mode 100644 index 0000000000..8d0187e09b --- /dev/null +++ b/examples/snippets/validation/advancedBringAjvInstance.ts @@ -0,0 +1,30 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { validate } from '@aws-lambda-powertools/validation'; +import { SchemaValidationError } from '@aws-lambda-powertools/validation/errors'; +import Ajv2019 from 'ajv/dist/2019'; +import { inboundSchema } from './schemas.js'; + +const logger = new Logger(); + +const ajv = new Ajv2019(); + +export const handler = async (event: unknown) => { + try { + await validate({ + payload: event, + schema: inboundSchema, + ajv, // (1)! + }); + + return { + message: 'ok', + }; + } catch (error) { + if (error instanceof SchemaValidationError) { + logger.error('Schema validation failed', error); + throw new Error('Invalid event payload'); + } + + throw error; + } +}; diff --git a/examples/snippets/validation/advancedCustomFormats.ts b/examples/snippets/validation/advancedCustomFormats.ts new file mode 100644 index 0000000000..ccc3f8610b --- /dev/null +++ b/examples/snippets/validation/advancedCustomFormats.ts @@ -0,0 +1,43 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { validate } from '@aws-lambda-powertools/validation'; +import { SchemaValidationError } from '@aws-lambda-powertools/validation/errors'; +import schemaWithCustomFormat from './samples/schemaWithCustomFormat.json'; + +const logger = new Logger(); + +const customFormats = { + awsaccountid: /^\d{12}$/, + creditcard: (value: string) => { + // Luhn algorithm (for demonstration purposes only - do not use in production) + const sum = value + .split('') + .reverse() + .reduce((acc, digit, index) => { + const num = Number.parseInt(digit, 10); + return acc + (index % 2 === 0 ? num : num < 5 ? num * 2 : num * 2 - 9); + }, 0); + + return sum % 10 === 0; + }, +}; + +export const handler = async (event: unknown) => { + try { + await validate({ + payload: event, + schema: schemaWithCustomFormat, + formats: customFormats, + }); + + return { + message: 'ok', + }; + } catch (error) { + if (error instanceof SchemaValidationError) { + logger.error('Schema validation failed', error); + throw new Error('Invalid event payload'); + } + + throw error; + } +}; diff --git a/examples/snippets/validation/advancedExternalRefs.ts b/examples/snippets/validation/advancedExternalRefs.ts new file mode 100644 index 0000000000..87ab0824ac --- /dev/null +++ b/examples/snippets/validation/advancedExternalRefs.ts @@ -0,0 +1,25 @@ +import { validator } from '@aws-lambda-powertools/validation/decorator'; +import type { Context } from 'aws-lambda'; +import { + type InboundSchema, + defsSchema, + inboundSchema, + outboundSchema, +} from './schemasWithExternalRefs.js'; + +class Lambda { + @validator({ + inboundSchema, + outboundSchema, + externalRefs: [defsSchema], + }) + async handler(event: InboundSchema, _context: Context) { + return { + message: `processed ${event.userId}`, + success: true, + }; + } +} + +const lambda = new Lambda(); +export const handler = lambda.handler.bind(lambda); diff --git a/examples/snippets/validation/gettingStartedDecorator.ts b/examples/snippets/validation/gettingStartedDecorator.ts new file mode 100644 index 0000000000..50cf98fa14 --- /dev/null +++ b/examples/snippets/validation/gettingStartedDecorator.ts @@ -0,0 +1,27 @@ +import { validator } from '@aws-lambda-powertools/validation/decorator'; +import type { Context } from 'aws-lambda'; +import { + type InboundSchema, + type OutboundSchema, + inboundSchema, + outboundSchema, +} from './schemas.js'; + +class Lambda { + @validator({ + inboundSchema, + outboundSchema, + }) + async handler( + event: InboundSchema, + _context: Context + ): Promise { + return { + statusCode: 200, + body: `Hello from ${event.userId}`, + }; + } +} + +const lambda = new Lambda(); +export const handler = lambda.handler.bind(lambda); diff --git a/examples/snippets/validation/gettingStartedEnvelope.ts b/examples/snippets/validation/gettingStartedEnvelope.ts new file mode 100644 index 0000000000..520e006bed --- /dev/null +++ b/examples/snippets/validation/gettingStartedEnvelope.ts @@ -0,0 +1,19 @@ +import { validator } from '@aws-lambda-powertools/validation/decorator'; +import type { Context } from 'aws-lambda'; +import { type InboundSchema, inboundSchema } from './schemas.js'; + +class Lambda { + @validator({ + inboundSchema, + envelope: 'detail', + }) + async handler(event: InboundSchema, context: Context) { + return { + message: `processed ${event.userId}`, + success: true, + }; + } +} + +const lambda = new Lambda(); +export const handler = lambda.handler.bind(lambda); diff --git a/examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts b/examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts new file mode 100644 index 0000000000..0cd1f788fa --- /dev/null +++ b/examples/snippets/validation/gettingStartedEnvelopeBuiltin.ts @@ -0,0 +1,20 @@ +import { SQS } from '@aws-lambda-powertools/jmespath/envelopes'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { validator } from '@aws-lambda-powertools/validation/middleware'; +import middy from '@middy/core'; +import { type InboundSchema, inboundSchema } from './schemas.js'; + +const logger = new Logger(); + +export const handler = middy() + .use( + validator({ + inboundSchema, + envelope: SQS, + }) + ) + .handler(async (event: Array) => { + for (const record of event) { + logger.info(`Processing message ${record.userId}`); + } + }); diff --git a/examples/snippets/validation/gettingStartedMiddy.ts b/examples/snippets/validation/gettingStartedMiddy.ts new file mode 100644 index 0000000000..9b700ab9cf --- /dev/null +++ b/examples/snippets/validation/gettingStartedMiddy.ts @@ -0,0 +1,22 @@ +import { validator } from '@aws-lambda-powertools/validation/middleware'; +import middy from '@middy/core'; +import { + type InboundSchema, + type OutboundSchema, + inboundSchema, + outboundSchema, +} from './schemas.js'; + +export const handler = middy() + .use( + validator({ + inboundSchema, + outboundSchema, + }) + ) + .handler( + async (event: InboundSchema): Promise => ({ + statusCode: 200, + body: `Hello from ${event.userId}`, + }) + ); diff --git a/examples/snippets/validation/gettingStartedStandalone.ts b/examples/snippets/validation/gettingStartedStandalone.ts new file mode 100644 index 0000000000..3b8ae7aebd --- /dev/null +++ b/examples/snippets/validation/gettingStartedStandalone.ts @@ -0,0 +1,26 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { validate } from '@aws-lambda-powertools/validation'; +import { SchemaValidationError } from '@aws-lambda-powertools/validation/errors'; +import { type InboundSchema, inboundSchema } from './schemas.js'; + +const logger = new Logger(); + +export const handler = async (event: InboundSchema) => { + try { + validate({ + payload: event, + schema: inboundSchema, + }); + + return { + message: 'ok', // (1)! + }; + } catch (error) { + if (error instanceof SchemaValidationError) { + logger.error('Schema validation failed', error); + throw new Error('Invalid event payload'); + } + + throw error; + } +}; diff --git a/examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json b/examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json new file mode 100644 index 0000000000..17c1c5672e --- /dev/null +++ b/examples/snippets/validation/samples/gettingStartedEnvelopeEvent.json @@ -0,0 +1,13 @@ +{ + "version": "0", + "id": "12345678-1234-1234-1234-123456789012", + "detail-type": "myDetailType", + "source": "myEventSource", + "account": "123456789012", + "time": "2017-12-22T18:43:48Z", + "region": "us-west-2", + "resources": [], + "detail": { + "userId": "123" + } +} \ No newline at end of file diff --git a/examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json b/examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json new file mode 100644 index 0000000000..a8df4bf107 --- /dev/null +++ b/examples/snippets/validation/samples/gettingStartedSQSEnvelopeEvent.json @@ -0,0 +1,36 @@ +{ + "Records": [ + { + "messageId": "c80e8021-a70a-42c7-a470-796e1186f753", + "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", + "body": "{\"userId\":\"123\"}", + "attributes": { + "ApproximateReceiveCount": "3", + "SentTimestamp": "1529104986221", + "SenderId": "AIDAIC6K7FJUZ7Q", + "ApproximateFirstReceiveTimestamp": "1529104986230" + }, + "messageAttributes": {}, + "md5OfBody": "098f6bcd4621d373cade4e832627b4f6", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", + "awsRegion": "us-west-2" + }, + { + "messageId": "c80e8021-a70a-42c7-a470-796e1186f753", + "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", + "body": "{\"userId\":\"456\"}", + "attributes": { + "ApproximateReceiveCount": "3", + "SentTimestamp": "1529104986221", + "SenderId": "AIDAIC6K7FJUZ7Q", + "ApproximateFirstReceiveTimestamp": "1529104986230" + }, + "messageAttributes": {}, + "md5OfBody": "098f6bcd4621d373cade4e832627b4f6", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:my-queue", + "awsRegion": "us-west-2" + } + ] +} \ No newline at end of file diff --git a/examples/snippets/validation/samples/schemaWithCustomFormat.json b/examples/snippets/validation/samples/schemaWithCustomFormat.json new file mode 100644 index 0000000000..954fe05ddd --- /dev/null +++ b/examples/snippets/validation/samples/schemaWithCustomFormat.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "properties": { + "accountId": { + "type": "string", + "format": "awsaccountid" + }, + "creditCard": { + "type": "string", + "format": "creditcard" + } + }, + "required": [ + "accountId" + ] +} \ No newline at end of file diff --git a/examples/snippets/validation/schemas.ts b/examples/snippets/validation/schemas.ts new file mode 100644 index 0000000000..60f6a199e5 --- /dev/null +++ b/examples/snippets/validation/schemas.ts @@ -0,0 +1,38 @@ +const inboundSchema = { + type: 'object', + properties: { + userId: { + type: 'string', + }, + }, + required: ['userId'], +} as const; + +type InboundSchema = { + userId: string; +}; + +const outboundSchema = { + type: 'object', + properties: { + body: { + type: 'string', + }, + statusCode: { + type: 'number', + }, + }, + required: ['body', 'statusCode'], +} as const; + +type OutboundSchema = { + body: string; + statusCode: number; +}; + +export { + inboundSchema, + outboundSchema, + type InboundSchema, + type OutboundSchema, +}; diff --git a/examples/snippets/validation/schemasWithExternalRefs.ts b/examples/snippets/validation/schemasWithExternalRefs.ts new file mode 100644 index 0000000000..d8a8a293ce --- /dev/null +++ b/examples/snippets/validation/schemasWithExternalRefs.ts @@ -0,0 +1,43 @@ +const defsSchema = { + $id: 'http://example.com/schemas/defs.json', + definitions: { + int: { type: 'integer' }, + str: { type: 'string' }, + }, +} as const; + +const inboundSchema = { + $id: 'http://example.com/schemas/inbound.json', + type: 'object', + properties: { + userId: { $ref: 'defs.json#/definitions/str' }, + }, + required: ['userId'], +} as const; + +type InboundSchema = { + userId: string; +}; + +const outboundSchema = { + $id: 'http://example.com/schemas/outbound.json', + type: 'object', + properties: { + body: { $ref: 'defs.json#/definitions/str' }, + statusCode: { $ref: 'defs.json#/definitions/int' }, + }, + required: ['body', 'statusCode'], +} as const; + +type OutboundSchema = { + body: string; + statusCode: number; +}; + +export { + defsSchema, + inboundSchema, + outboundSchema, + type InboundSchema, + type OutboundSchema, +}; diff --git a/mkdocs.yml b/mkdocs.yml index 994dc80125..5bfc63b52c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,6 +46,7 @@ nav: - utilities/batch.md - utilities/jmespath.md - utilities/parser.md + - utilities/validation.md - API reference: api" - Processes: - Roadmap: roadmap.md