4.0.23 • Published 2 years ago

@iopa/schema-router v4.0.23

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

IOPA @iopa/schema-router

NPM

NPM

About

@iopa/schema-router is a compiled schema validator and router for the IOPA framework

It validates JSON Schema and JSON Type Definitions for both inbound requests and outbound responses

Usage

Routing

Include whereever you would otherwise use @iopa/router

See @iopa/router for all routing documentation

Installation

import { RouterApp } from 'iopa'
import SchemaRouter, { type ISchemaAppOptions } from '@iopa/schema-router'

const app: ISchemaApp = new RouterApp()

//
// . . . .
//

app.use<ISchemaAppOptions>(SchemaRouter, 'Schema Router', {
  jsonShorthand: false
})
app.get(
  '/api/helloworld-us-only',
  async (context) => {
    return 'Hello World'
  },
  {
    schema: {
      headers: {
        type: 'object',
        properties: { 'cf-ipcountry': { type: 'string', pattern: '^US$' } },
        required: ['cf-ipcountry']
      }
    }
  }
)

Validation and Serialization

Iopa Schema Router uses a schema-based approach, and even if it is not mandatory we recommend using JSON Schema to validate your routes and serialize your outputs. Internally, Iopa Schema Router compiles the schema into a highly performant function.

Validation will only be attempted if the content type is application-json, as described in the documentation for the content type parser.

All the examples in this section are using the JSON Schema Draft 7 specification.

⚠ Security Notice

Treat the schema definition as application code. Validation and serialization features dynamically evaluate code with new Function(), which is not safe to use with user-provided schemas. See Ajv and fast-json-stringify for more details.

Moreover, the $async Ajv feature should not be used as part of the first validation strategy. This option is used to access Databases and reading them during the validation process may lead to Denial of Service Attacks to your application.

Core concepts

The validation and the serialization tasks are processed by two different, and customizable, actors:

These two separate entities share only the JSON schemas added to Iopa app instance through .addSchema(schema).

Adding a shared schema

Thanks to the addSchema API added to the Iopa app instance, you can add multiple schemas to the Iopa Schema Router and then reuse them in multiple parts of your application.

The shared schemas can be reused through the JSON Schema $ref keyword. Here is an overview of how references work:

  • myField: { $ref: '#foo'} will search for field with $id: '#foo' inside the current schema
  • myField: { $ref: '#/definitions/foo'} will search for field definitions.foo inside the current schema
  • myField: { $ref: 'http://url.com/sh.json#'} will search for a shared schema added with $id: 'http://url.com/sh.json'
  • myField: { $ref: 'http://url.com/sh.json#/definitions/foo'} will search for a shared schema added with $id: 'http://url.com/sh.json' and will use the field definitions.foo
  • myField: { $ref: 'http://url.com/sh.json#foo'} will search for a shared schema added with $id: 'http://url.com/sh.json' and it will look inside of it for object with $id: '#foo'

Simple usage:

app.addSchema({
  $id: 'http://example.com/',
  type: 'object',
  properties: {
    hello: { type: 'string' }
  }
})

app.post('/', 
  myHandlerFunction,
  {
  schema: {
    body: {
      type: 'array',
      items: { $ref: 'http://example.com#/properties/hello' }
    }
  }
})

$ref as root reference:

app.addSchema({
  $id: 'commonSchema',
  type: 'object',
  properties: {
    hello: { type: 'string' }
  }
})

a[[]].post('/', {
  myHandlerFunction,
  schema: {
    body: { $ref: 'commonSchema#' },
    headers: { $ref: 'commonSchema#' }
  }
})

Retrieving the shared schemas

If the validator and the serializer are customized, the .addSchema method will not be useful since the actors are no longer controlled by Iopa Schema Router. To access the schemas added to the Iopa Schema Router instance, you can simply use .getSchemas() available on the urn:io.iopa.schema:controller capability:

app.capability('urn:io.iopa.schema:controller').addSchema({
  $id: 'schemaId',
  type: 'object',
  properties: {
    hello: { type: 'string' }
  }
})

const mySchemas = app.capability('urn:io.iopa.schema:controller').getSchemas()
const mySchema = app.capability('urn:io.iopa.schema:controller').getSchema('schemaId')

The function getSchemas returns the shared schemas available in the selected scope:

app.addSchema({ $id: 'one', my: 'hello' })
// will return only `one` schema
app.get('/', (request, reply) => { reply.send(app.capability('urn:io.iopa.schema:controller').getSchemas()) })

Validation

The route validation internally relies upon Ajv v8 which is a high-performance JSON Schema validator. Validating the input is very easy: just add the fields that you need inside the route schema, and you are done!

The supported validations are:

  • body: validates the body of the request if it is a POST, PUT, or PATCH method.
  • querystring or query: validates the query string.
  • params: validates the route params.
  • headers: validates the request headers.

All the validations can be a complete JSON Schema object (with a type property of 'object' and a 'properties' object containing parameters) or a simpler variation in which the type and properties attributes are forgone and the parameters are listed at the top level (see the example below).

ℹ If you need to use the latest version of Ajv (v8) you should read how to do it in the schemaController section.

Example:

const bodyJsonSchema = {
  type: 'object',
  required: ['requiredKey'],
  properties: {
    someKey: { type: 'string' },
    someOtherKey: { type: 'number' },
    requiredKey: {
      type: 'array',
      maxItems: 3,
      items: { type: 'integer' }
    },
    nullableKey: { type: ['number', 'null'] }, // or { type: 'number', nullable: true }
    multipleTypesKey: { type: ['boolean', 'number'] },
    multipleRestrictedTypesKey: {
      oneOf: [
        { type: 'string', maxLength: 5 },
        { type: 'number', minimum: 10 }
      ]
    },
    enumKey: {
      type: 'string',
      enum: ['John', 'Foo']
    },
    notTypeKey: {
      not: { type: 'array' }
    }
  }
}

const queryStringJsonSchema = {
  type: 'object',
  properties: {
    name: { type: 'string' },
    excitement: { type: 'integer' }
  }
}

const paramsJsonSchema = {
  type: 'object',
  properties: {
    par1: { type: 'string' },
    par2: { type: 'number' }
  }
}

const headersJsonSchema = {
  type: 'object',
  properties: {
    'x-foo': { type: 'string' }
  },
  required: ['x-foo']
}

const schema = {
  body: bodyJsonSchema,
  querystring: queryStringJsonSchema,
  params: paramsJsonSchema,
  headers: headersJsonSchema
}

app.post('/the/url', { schema }, handler)

Note that Ajv will try to coerce the values to the types specified in your schema type keywords, both to pass the validation and to use the correctly typed data afterwards.

The Ajv default configuration in Iopa Schema Router supports coercing array parameters in querystring. Example:

const opts = {
  schema: {
    querystring: {
      type: 'object',
      properties: {
        ids: {
          type: 'array',
          default: []
        },
      },
    }
  }
}

app.get('/', opts, (request, reply) => {
  reply.send({ params: request.query }) // echo the querystring
})

server.listen({ port: 3000 }, (err) => {
  if (err) throw err
})

Ajv Plugins

You can provide a list of plugins you want to use with the default ajv instance. Note that the plugin must be compatible with the Ajv version shipped within Iopa Schema Router.

Refer to ajv options to check plugins format

app.use<ISchemaAppOptions>(SchemaRouter, 'Schema Router', {
  ajv: {
    plugins: [
      require('ajv-merge-patch')
    ]
  }
})

app.post('/', (context, next) => { return { ok: 1 }},
{
  schema: {
    body: {
      $patch: {
        source: {
          type: 'object',
          properties: {
            q: {
              type: 'string'
            }
          }
        },
        with: [
          {
            op: 'add',
            path: '/properties/q',
            value: { type: 'number' }
          }
        ]
      }
    }
  }
})

app.post('/foo', (context, next) => { return { ok: 1 }},
{
  schema: {
    body: {
      $merge: {
        source: {
          type: 'object',
          properties: {
            q: {
              type: 'string'
            }
          }
        },
        with: {
          required: ['q']
        }
      }
    }
  }
})

Schema Validator Configuration

Iopa Schema Router's baseline ajv configuration is:

{
  coerceTypes: true, // change data type of data to match type keyword
  useDefaults: true, // replace missing properties and items with the values from corresponding default keyword
  removeAdditional: true, // remove additional properties
  // Explicitly set allErrors to `false`.
  // When set to `true`, a DoS attack is possible.
  allErrors: false
}

This baseline configuration can be modified by providing customOptions to your Iopa Schema Router use statement.

Serialization

Usually, you will send your data to the clients as JSON, and Iopa Schema Router has a powerful tool to help you, fast-json-stringify, which is used if you have provided an output schema in the route options. We encourage you to use an output schema, as it can drastically increase throughput and help prevent accidental disclosure of sensitive information.

Example:

const schema = {
  response: {
    200: {
      type: 'object',
      properties: {
        value: { type: 'string' },
        otherValue: { type: 'boolean' }
      }
    }
  }
}

app.post('/the/url', handler, { schema })

As you can see, the response schema is based on the status code. If you want to use the same schema for multiple status codes, you can use '2xx' or default, for example:

const schema = {
  response: {
    default: {
      type: 'object',
      properties: {
        error: {
          type: 'boolean',
          default: true
        }
      }
    },
    '2xx': {
      type: 'object',
      properties: {
        value: { type: 'string' },
        otherValue: { type: 'boolean' }
      }
    },
    201: {
      // the contract syntax
      value: { type: 'string' }
    }
  }
}

app.post('/the/url', handler, { schema })

Error Handling

When schema validation fails for a request, Iopa Schema Router will automatically return a status 400 response including the result from the validator in the payload. As an example, if you have the following schema for your route

const schema = {
  body: {
    type: 'object',
    properties: {
      name: { type: 'string' }
    },
    required: ['name']
  }
}

and fail to satisfy it, the route will immediately return a response with the following payload

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "body should have required property 'name'"
}

JSON Schema support

JSON Schema provides utilities to optimize your schemas that, in conjunction with Iopa Schema Router's shared schema, let you reuse all your schemas easily.

Use CaseValidatorSerializer
$ref to $id️️✔️✔️
$ref to /definitions✔️✔️
$ref to shared schema $id✔️✔️
$ref to shared schema /definitions✔️✔️

Examples

Usage of $ref to $id in same JSON Schema
const refToId = {
  type: 'object',
  definitions: {
    foo: {
      $id: '#address',
      type: 'object',
      properties: {
        city: { type: 'string' }
      }
    }
  },
  properties: {
    home: { $ref: '#address' },
    work: { $ref: '#address' }
  }
}
Usage of $ref to /definitions in same JSON Schema
const refToDefinitions = {
  type: 'object',
  definitions: {
    foo: {
      $id: '#address',
      type: 'object',
      properties: {
        city: { type: 'string' }
      }
    }
  },
  properties: {
    home: { $ref: '#/definitions/foo' },
    work: { $ref: '#/definitions/foo' }
  }
}
Usage $ref to a shared schema $id as external schema
app.addSchema({
  $id: 'http://foo/common.json',
  type: 'object',
  definitions: {
    foo: {
      $id: '#address',
      type: 'object',
      properties: {
        city: { type: 'string' }
      }
    }
  }
})

const refToSharedSchemaId = {
  type: 'object',
  properties: {
    home: { $ref: 'http://foo/common.json#address' },
    work: { $ref: 'http://foo/common.json#address' }
  }
}
Usage $ref to a shared schema /definitions as external schema
app.addSchema({
  $id: 'http://foo/shared.json',
  type: 'object',
  definitions: {
    foo: {
      type: 'object',
      properties: {
        city: { type: 'string' }
      }
    }
  }
})

const refToSharedSchemaDefinitions = {
  type: 'object',
  properties: {
    home: { $ref: 'http://foo/shared.json#/definitions/foo' },
    work: { $ref: 'http://foo/shared.json#/definitions/foo' }
  }
}

Resources

License

MIT

4.0.23

2 years ago

4.0.22

2 years ago

4.0.21

2 years ago

4.0.20

2 years ago

4.0.19

2 years ago

4.0.18

2 years ago

4.0.17

2 years ago

4.0.16

2 years ago

4.0.15

2 years ago

4.0.14

2 years ago

4.0.13

2 years ago

4.0.12

2 years ago

4.0.11

2 years ago

4.0.10

2 years ago

4.0.9

2 years ago

4.0.8

2 years ago

4.0.7

2 years ago

4.0.6

2 years ago

4.0.5

2 years ago

4.0.4

2 years ago

4.0.3

2 years ago

4.0.2

2 years ago