koas-core
Advanced tools
Comparing version 0.4.1 to 0.5.0
@@ -1,11 +0,57 @@ | ||
import { Middleware } from 'koa'; | ||
import { PreValidatePropertyFunction, RewriteFunction, ValidatorResult } from 'jsonschema'; | ||
import { Context, Middleware } from 'koa'; | ||
import { OpenAPIV3 } from 'openapi-types'; | ||
import { SchemaValidationError, Validator } from './validation'; | ||
export { SchemaValidationError, Validator }; | ||
export interface AdvancedOptions { | ||
import { JSONRefResolver } from './jsonRefs'; | ||
import { SchemaValidationError } from './validation'; | ||
export { JSONRefResolver, SchemaValidationError }; | ||
export interface KoasOptions { | ||
/** | ||
* A function for creating a custom JSON schema validator. | ||
* Convert a schema validation error to an HTTP response body. | ||
* | ||
* @param error - The error that as thrown. | ||
* @param ctx - The Koa context. | ||
* @returns The HTTP response body. | ||
*/ | ||
createValidator?: (spec?: OpenAPIV3.Document) => Validator; | ||
onSchemaValidationError?: (error: SchemaValidationError, ctx: Context) => unknown; | ||
} | ||
interface ValidateOptions { | ||
/** | ||
* The error message. | ||
* | ||
* @default 'JSON schema validation failed' | ||
*/ | ||
message?: string; | ||
/** | ||
* A function to rewrite a property before it’s passed to the JSON schema validation | ||
*/ | ||
preValidateProperty?: PreValidatePropertyFunction; | ||
/** | ||
* A function to rewrite a property after has passed JSON schema validation | ||
*/ | ||
rewrite?: RewriteFunction; | ||
/** | ||
* The HTTP status code to set. | ||
* | ||
* @default 400 | ||
*/ | ||
status?: number; | ||
/** | ||
* Whether or not to throw an error if the schema validation failed. | ||
* | ||
* If `false`, the `ValidatorResult` instance will be returned instead. | ||
* | ||
* @default true | ||
*/ | ||
throw?: boolean; | ||
} | ||
/** | ||
* A JSON schema validator function. | ||
* | ||
* @param instance - The instance to validate | ||
* @param schema - The JSON schema to use. | ||
* @param options - Additional jsonschema validation options. | ||
* | ||
* @returns The validator result | ||
*/ | ||
export declare type Validator = (instance: unknown, schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject, options?: ValidateOptions) => ValidatorResult; | ||
declare module 'koa' { | ||
@@ -21,15 +67,15 @@ interface DefaultContext { | ||
/** | ||
* | ||
* The full OpenAPI document | ||
*/ | ||
operationObject?: OpenAPIV3.OperationObject; | ||
document: OpenAPIV3.Document; | ||
/** | ||
* | ||
* The OpenAPI operation object that describes the current request context. | ||
*/ | ||
openApiObject: OpenAPIV3.Document; | ||
operationObject?: OpenAPIV3.OperationObject; | ||
/** | ||
* | ||
* The path item object that describes the current request context. | ||
*/ | ||
pathItemObject?: OpenAPIV3.PathItemObject; | ||
/** | ||
* | ||
* A function to apply JSON schema validation in the context of the OpenAPI document. | ||
*/ | ||
@@ -46,11 +92,11 @@ validate: Validator; | ||
*/ | ||
rawSpec: OpenAPIV3.Document; | ||
document: OpenAPIV3.Document; | ||
/** | ||
* | ||
*/ | ||
runAlways: (middleware: Middleware) => Middleware; | ||
resolveRef: JSONRefResolver; | ||
/** | ||
* | ||
*/ | ||
spec: OpenAPIV3.Document; | ||
runAlways: (middleware: Middleware) => Middleware; | ||
/** | ||
@@ -64,3 +110,3 @@ * | ||
* | ||
* @param spec - The OpenAPI document from which to create an API. | ||
* @param document - The OpenAPI document from which to create an API. | ||
* @param middlewares - The Koas middlewares to use for creating an API. | ||
@@ -71,2 +117,2 @@ * @param options - Advanced options | ||
*/ | ||
export declare function koas(spec: OpenAPIV3.Document, middlewares?: Plugin[], { createValidator }?: AdvancedOptions): Promise<Middleware>; | ||
export declare function koas(document: OpenAPIV3.Document, middlewares?: Plugin[], { onSchemaValidationError, }?: KoasOptions): Middleware; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.koas = exports.SchemaValidationError = void 0; | ||
const RefParser = require("@apidevtools/json-schema-ref-parser"); | ||
const compose = require("koa-compose"); | ||
const lodash_1 = require("lodash"); | ||
const jsonRefs_1 = require("./jsonRefs"); | ||
const matcher_1 = require("./matcher"); | ||
@@ -39,3 +38,3 @@ const validation_1 = require("./validation"); | ||
* | ||
* @param spec - The OpenAPI document from which to create an API. | ||
* @param document - The OpenAPI document from which to create an API. | ||
* @param middlewares - The Koas middlewares to use for creating an API. | ||
@@ -46,19 +45,31 @@ * @param options - Advanced options | ||
*/ | ||
async function koas(spec, middlewares = [], { createValidator = validation_1.createDefaultValidator } = {}) { | ||
const dereferencedSpec = (await RefParser.dereference(lodash_1.cloneDeep(spec))); | ||
const matchers = Object.entries(dereferencedSpec.paths).map(([pathTemplate, pathItemObject]) => { | ||
const matcher = matcher_1.createMatcher(pathTemplate, pathItemObject.parameters); | ||
function koas(document, middlewares = [], { onSchemaValidationError = (error) => ({ message: error.message, errors: error.result.errors }), } = {}) { | ||
const resolveRef = jsonRefs_1.createResolver(document); | ||
const matchers = Object.entries(document.paths).map(([pathTemplate, pathItemObject]) => { | ||
const matcher = matcher_1.createMatcher(pathTemplate, resolveRef, pathItemObject.parameters); | ||
return [matcher, pathItemObject]; | ||
}); | ||
const validate = createValidator(spec); | ||
const injected = middlewares.map((middleware) => middleware({ | ||
rawSpec: spec, | ||
const validator = validation_1.createValidator(document); | ||
const validate = (instance, schema, { message = 'JSON schema validation failed', preValidateProperty, rewrite, status, throw: throwError = true, } = {}) => { | ||
const result = validator.validate(instance, schema, { | ||
base: '#', | ||
rewrite, | ||
preValidateProperty, | ||
}); | ||
if (throwError && !result.valid) { | ||
throw new validation_1.SchemaValidationError(message, { result, status }); | ||
} | ||
return result; | ||
}; | ||
const pluginOptions = { | ||
document, | ||
resolveRef, | ||
runAlways: markRunAlways, | ||
spec: dereferencedSpec, | ||
validate, | ||
})); | ||
}; | ||
const injected = middlewares.map((middleware) => middleware(pluginOptions)); | ||
const composed = compose(injected); | ||
// @ts-expect-error This is an internal hack. | ||
const runAlways = compose(injected.filter((middleware) => middleware[RUN_ALWAYS])); | ||
return (ctx, next) => { | ||
return async (ctx, next) => { | ||
let params; | ||
@@ -69,21 +80,31 @@ const match = matchers.find(([matcher]) => { | ||
}); | ||
ctx.openApi = { openApiObject: spec, validate }; | ||
if (!match) { | ||
return runAlways(ctx, next); | ||
ctx.openApi = { document, validate }; | ||
try { | ||
if (!match) { | ||
return await runAlways(ctx, next); | ||
} | ||
const [, pathItemObject] = match; | ||
ctx.openApi.pathItemObject = pathItemObject; | ||
ctx.params = params; | ||
const method = ctx.method.toLowerCase(); | ||
if (!methods.has(method)) { | ||
return await runAlways(ctx, next); | ||
} | ||
const operationObject = pathItemObject[method]; | ||
if (!operationObject) { | ||
return await runAlways(ctx, next); | ||
} | ||
ctx.openApi.operationObject = operationObject; | ||
await composed(ctx, next); | ||
} | ||
const [, pathItemObject] = match; | ||
ctx.openApi.pathItemObject = pathItemObject; | ||
ctx.params = params; | ||
const method = ctx.method.toLowerCase(); | ||
if (!methods.has(method)) { | ||
return runAlways(ctx, next); | ||
catch (error) { | ||
if (error instanceof validation_1.SchemaValidationError) { | ||
ctx.status = error.status; | ||
ctx.body = onSchemaValidationError(error, ctx); | ||
return; | ||
} | ||
throw error; | ||
} | ||
const operationObject = pathItemObject[method]; | ||
if (!operationObject) { | ||
return runAlways(ctx, next); | ||
} | ||
ctx.openApi.operationObject = operationObject; | ||
return composed(ctx, next); | ||
}; | ||
} | ||
exports.koas = koas; |
import { OpenAPIV3 } from 'openapi-types'; | ||
import { JSONRefResolver } from './jsonRefs'; | ||
/** | ||
@@ -14,2 +15,3 @@ * A matcher function for extracting URL parameters. | ||
* @param pathTemplate - The OpenAPI path template for which to create a matcher. | ||
* @param resolveRef - A JSON reference resolver. | ||
* @param pathParameters - The OpenAPI path parameter objects used to determine how to process the | ||
@@ -20,2 +22,2 @@ * extracted parameters. | ||
*/ | ||
export declare function createMatcher(pathTemplate: string, pathParameters?: OpenAPIV3.ParameterObject[]): MatcherFunction; | ||
export declare function createMatcher(pathTemplate: string, resolveRef: JSONRefResolver, pathParameters?: (OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject)[]): MatcherFunction; |
@@ -8,2 +8,3 @@ "use strict"; | ||
* @param pathTemplate - The OpenAPI path template for which to create a matcher. | ||
* @param resolveRef - A JSON reference resolver. | ||
* @param pathParameters - The OpenAPI path parameter objects used to determine how to process the | ||
@@ -14,5 +15,6 @@ * extracted parameters. | ||
*/ | ||
function createMatcher(pathTemplate, pathParameters = []) { | ||
function createMatcher(pathTemplate, resolveRef, pathParameters = []) { | ||
const arrayNames = new Set(pathParameters | ||
.filter(({ schema = {} }) => schema.type === 'array') | ||
.map(resolveRef) | ||
.filter(({ schema = {} }) => resolveRef(schema).type === 'array') | ||
.map(({ name }) => name)); | ||
@@ -19,0 +21,0 @@ const names = []; |
@@ -0,15 +1,30 @@ | ||
import { Validator, ValidatorResult } from 'jsonschema'; | ||
import { OpenAPIV3 } from 'openapi-types'; | ||
interface SchemaValidationErrorOptions { | ||
/** | ||
* The HTTP status code for the validation error. | ||
*/ | ||
status?: number; | ||
/** | ||
* The JSON schema validator result. | ||
*/ | ||
result: ValidatorResult; | ||
} | ||
/** | ||
* A JSSO validator agnostic schema validation error. | ||
* An error that’s thrown if JSON schema validation fails. | ||
*/ | ||
export declare class SchemaValidationError extends Error { | ||
errors: unknown[]; | ||
constructor(message: string, errors: unknown[]); | ||
status: number; | ||
result: ValidatorResult; | ||
/** | ||
* @param message - The error message to throw. | ||
* @param options - The error options. | ||
*/ | ||
constructor(message: string, { result, status }: SchemaValidationErrorOptions); | ||
} | ||
export declare type Validator = (data: unknown, schema: OpenAPIV3.SchemaObject) => Promise<boolean>; | ||
/** | ||
* Create a ZSchema based JSON schema validator. | ||
* | ||
* @returns A JSON schema validator. | ||
* @param document - THe OpenAPI document to create a JSON schema validator for. | ||
* @returns A configured JSON schema validator. | ||
*/ | ||
export declare function createDefaultValidator(): Validator; | ||
export declare function createValidator({ components }: OpenAPIV3.Document): Validator; | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createDefaultValidator = exports.SchemaValidationError = void 0; | ||
const ZSchema = require("z-schema"); | ||
ZSchema.registerFormat('binary', () => true); | ||
exports.createValidator = exports.SchemaValidationError = void 0; | ||
const jsonschema_1 = require("jsonschema"); | ||
const jsonRefs_1 = require("./jsonRefs"); | ||
/** | ||
* A JSSO validator agnostic schema validation error. | ||
* Iterate over object entries. | ||
* | ||
* @param object - The object to iterate over. This may be null. | ||
* @param iterator - A function which will be called with the key and value. | ||
*/ | ||
function iter(object, iterator) { | ||
if (object) { | ||
Object.entries(object).forEach(([key, value]) => iterator(key, value)); | ||
} | ||
} | ||
/** | ||
* An error that’s thrown if JSON schema validation fails. | ||
*/ | ||
class SchemaValidationError extends Error { | ||
constructor(message, errors) { | ||
/** | ||
* @param message - The error message to throw. | ||
* @param options - The error options. | ||
*/ | ||
constructor(message, { result, status = 400 }) { | ||
super(message); | ||
this.name = 'SchemaValidationError'; | ||
this.errors = errors; | ||
Error.captureStackTrace(this, this.constructor); | ||
this.status = status; | ||
this.result = result; | ||
} | ||
@@ -19,23 +34,48 @@ } | ||
/** | ||
* Create a ZSchema based JSON schema validator. | ||
* | ||
* @returns A JSON schema validator. | ||
* @param document - THe OpenAPI document to create a JSON schema validator for. | ||
* @returns A configured JSON schema validator. | ||
*/ | ||
function createDefaultValidator() { | ||
const validator = new ZSchema({ | ||
assumeAdditional: true, | ||
breakOnFirstError: false, | ||
reportPathAsArray: true, | ||
}); | ||
return (data, schema) => new Promise((resolve, reject) => { | ||
validator.validate(data, schema, (errors, valid) => { | ||
if (valid) { | ||
resolve(true); | ||
function createValidator({ components = {} }) { | ||
const validator = new jsonschema_1.Validator(); | ||
// Register OpenAPI formats | ||
validator.customFormats.int32 = () => true; | ||
validator.customFormats.int64 = () => true; | ||
validator.customFormats.float = () => true; | ||
validator.customFormats.double = () => true; | ||
validator.customFormats.byte = () => true; | ||
validator.customFormats.binary = () => true; | ||
validator.customFormats.password = () => true; | ||
/** | ||
* Add schemas from a record of JSON schema property wrappers. | ||
* | ||
* @param wrappers - The rescord that holds the JSON schema wrappers | ||
* @param prefix - The prefix of the wrapper. | ||
*/ | ||
function procesSchemaWrapper(wrappers, prefix) { | ||
iter(wrappers, (key, wrapper) => { | ||
if ('schema' in wrapper) { | ||
validator.addSchema(wrapper.schema, `${prefix}/${jsonRefs_1.escapeJsonPointer(key)}/schema`); | ||
} | ||
else { | ||
reject(new SchemaValidationError('JSON schema validation failed', errors)); | ||
} | ||
}); | ||
} | ||
iter(components.schemas, (key, schema) => { | ||
validator.addSchema(schema, `#/components/schemas/${jsonRefs_1.escapeJsonPointer(key)}`); | ||
}); | ||
procesSchemaWrapper(components.headers, '#/components/headers'); | ||
procesSchemaWrapper(components.parameters, '#/components/parameters'); | ||
iter(components.requestBodies, (key, requestBody) => { | ||
if ('content' in requestBody) { | ||
procesSchemaWrapper(requestBody.content, `#/components/requestBodies/${jsonRefs_1.escapeJsonPointer(key)}/content`); | ||
} | ||
}); | ||
iter(components.responses, (key, response) => { | ||
if ('headers' in response) { | ||
procesSchemaWrapper(response.headers, `#/components/responses/${jsonRefs_1.escapeJsonPointer(key)}/headers`); | ||
} | ||
if ('content' in response) { | ||
procesSchemaWrapper(response.content, `#/components/responses/${jsonRefs_1.escapeJsonPointer(key)}/content`); | ||
} | ||
}); | ||
return validator; | ||
} | ||
exports.createDefaultValidator = createDefaultValidator; | ||
exports.createValidator = createValidator; |
{ | ||
"name": "koas-core", | ||
"version": "0.4.1", | ||
"version": "0.5.0", | ||
"keywords": [ | ||
@@ -29,12 +29,9 @@ "koa", | ||
"dependencies": { | ||
"@apidevtools/json-schema-ref-parser": "^9.0.6", | ||
"@types/koa": "^2.11.4", | ||
"@types/lodash": "^4.14.161", | ||
"@types/koa": "^2.11.7", | ||
"jsonschema": "^1.4.0", | ||
"koa-compose": "^4.1.0", | ||
"lodash": "^4.17.20", | ||
"openapi-types": "^7.0.1", | ||
"z-schema": "^4.2.3" | ||
"openapi-types": "^7.2.3" | ||
}, | ||
"devDependencies": { | ||
"axios-test-instance": "^3.1.1" | ||
"axios-test-instance": "^4.0.0" | ||
}, | ||
@@ -41,0 +38,0 @@ "peerDependencies": { |
# Koas-core | ||
> [Koa][] + [Open API Specification][] = Koas | ||
> [Koa][] + [OpenAPI Specification][] = Koas | ||
@@ -22,3 +22,3 @@ Koas aims to make it easy to write a RESTful API based on Koa and an Open API V3 Specification. | ||
const spec = { | ||
const document = { | ||
openapi: '3.0.2', | ||
@@ -38,16 +38,9 @@ info: { | ||
async function main() { | ||
const app = new Koa(); | ||
app.use( | ||
await koas(spec, [ | ||
// Koas plugins go here. | ||
]), | ||
); | ||
app.listen(3333); | ||
} | ||
main().catch((error) => { | ||
console.error(error); | ||
process.exit(1); | ||
}); | ||
const app = new Koa(); | ||
app.use( | ||
koas(document, [ | ||
// Koas plugins go here. | ||
]), | ||
); | ||
app.listen(3333); | ||
``` | ||
@@ -64,3 +57,3 @@ | ||
function myPlugin(options) { | ||
return (koasPluginOptions) => async (ctx, next) => { | ||
return (document, resolveRef, runAlways, validate) => async (ctx, next) => { | ||
await next(); | ||
@@ -86,3 +79,3 @@ }; | ||
export function myPlugin(options: MyPluginOptions): Plugin { | ||
return (koasPluginOptions) => async (ctx, next) => { | ||
return (document, resolveRef, runAlways, validate) => async (ctx, next) => { | ||
await next(); | ||
@@ -92,1 +85,4 @@ }; | ||
``` | ||
[koa]: https://koajs.com | ||
[openapi specification]: https://spec.openapis.org/oas/v3.0.3 |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
20301
5
10
501
84
1
+ Addedjsonschema@^1.4.0
+ Addedjsonschema@1.4.1(transitive)
- Removed@types/lodash@^4.14.161
- Removedlodash@^4.17.20
- Removedz-schema@^4.2.3
- Removed@apidevtools/json-schema-ref-parser@9.1.2(transitive)
- Removed@jsdevtools/ono@7.1.3(transitive)
- Removed@types/json-schema@7.0.15(transitive)
- Removed@types/lodash@4.17.0(transitive)
- Removedargparse@2.0.1(transitive)
- Removedcall-me-maybe@1.0.2(transitive)
- Removedcommander@2.20.3(transitive)
- Removedjs-yaml@4.1.0(transitive)
- Removedlodash@4.17.21(transitive)
- Removedlodash.get@4.4.2(transitive)
- Removedlodash.isequal@4.5.0(transitive)
- Removedvalidator@13.11.0(transitive)
- Removedz-schema@4.2.4(transitive)
Updated@types/koa@^2.11.7
Updatedopenapi-types@^7.2.3