From 67e884b4787f6b51997394e42041fb52e52bc106 Mon Sep 17 00:00:00 2001 From: simon Date: Tue, 1 Feb 2022 07:44:18 +0000 Subject: [PATCH 1/5] Extract validators to namespaced folder with scope to add path, and header validators --- examples/parameters/api-gateway.ts | 20 +++++ examples/parameters/custom.ts | 22 ++++++ examples/parameters/default.ts | 18 +++++ examples/parameters/openapi/spec.ts | 74 +++++++++++++++++++ src/compeller/index.ts | 35 +++------ src/compeller/validators/index.ts | 1 + src/compeller/validators/request-body.test.ts | 64 ++++++++++++++++ src/compeller/validators/request-body.ts | 14 ++++ 8 files changed, 224 insertions(+), 24 deletions(-) create mode 100644 examples/parameters/api-gateway.ts create mode 100644 examples/parameters/custom.ts create mode 100644 examples/parameters/default.ts create mode 100644 examples/parameters/openapi/spec.ts create mode 100644 src/compeller/validators/index.ts create mode 100644 src/compeller/validators/request-body.test.ts create mode 100644 src/compeller/validators/request-body.ts diff --git a/examples/parameters/api-gateway.ts b/examples/parameters/api-gateway.ts new file mode 100644 index 0000000..93b4364 --- /dev/null +++ b/examples/parameters/api-gateway.ts @@ -0,0 +1,20 @@ +import { APIGatewayV1Responder, compeller } from '../../src'; +import { OpenAPISpecification } from './openapi/spec'; + +const apiGatewayV1Compeller = compeller(OpenAPISpecification, { + responder: APIGatewayV1Responder, +}); + +console.info( + apiGatewayV1Compeller('v1/version', 'get').response( + '200', + { + version: '1.0.0', + }, + { + 'x-request-id': '', + 'x-rate-limit': 120, + 'Content-Type': 'application/json', + } + ) +); diff --git a/examples/parameters/custom.ts b/examples/parameters/custom.ts new file mode 100644 index 0000000..3dd475b --- /dev/null +++ b/examples/parameters/custom.ts @@ -0,0 +1,22 @@ +import { compeller } from '../../src'; +import { OpenAPISpecification } from './openapi/spec'; + +const customerCompeller = compeller(OpenAPISpecification, { + responder: (statusCode, body) => { + return typeof statusCode === 'string' + ? { + statusCode: parseInt(statusCode), + body: JSON.stringify(body), + } + : { + statusCode, + body: JSON.stringify(body), + }; + }, +}); + +const body: Record = {}; +const headers: Record = {}; +const queryObject: Record = {}; + +console.info(customerCompeller('v1/version', 'post').request.validator({})); diff --git a/examples/parameters/default.ts b/examples/parameters/default.ts new file mode 100644 index 0000000..76b62ab --- /dev/null +++ b/examples/parameters/default.ts @@ -0,0 +1,18 @@ +import { compeller } from '../../src'; +import { OpenAPISpecification } from './openapi/spec'; + +const defaultCompeller = compeller(OpenAPISpecification); + +const res = defaultCompeller('v1/version', 'get').response( + '200', + { + version: '1.0.0', + }, + { + 'x-rate-limit': 123, + 'x-request-id': 'uuid', + 'Content-Type': 'application/json', + } +); + +console.info('Formatted default response', res); diff --git a/examples/parameters/openapi/spec.ts b/examples/parameters/openapi/spec.ts new file mode 100644 index 0000000..233adf8 --- /dev/null +++ b/examples/parameters/openapi/spec.ts @@ -0,0 +1,74 @@ +import { OpenAPIObject } from 'openapi3-ts'; + +export const OpenAPISpecification = { + info: { + title: 'New API generated with compeller', + version: '1.0.0', + }, + openapi: '3.1.0', + paths: { + 'v1/version': { + post: { + parameters: [ + { + name: 'tags', + in: 'query', + description: 'tags to filter by', + required: false, + style: 'form', + schema: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + { + name: 'limit', + in: 'query', + description: 'maximum number of results to return', + required: false, + schema: { + type: 'integer', + format: 'int32', + }, + }, + ], + responses: { + '201': { + description: 'Get the current API version', + headers: { + 'x-rate-limit': { + description: + 'The number of allowed requests in the current period', + schema: { + type: 'number', + } as const, + }, + 'x-request-id': { + description: 'The unique request id header', + schema: { + type: 'string', + } as const, + }, + }, + content: { + 'application/json': { + schema: { + type: 'object', + required: ['version'], + additionalProperties: false, + properties: { + version: { + type: 'string', + }, + }, + } as const, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/src/compeller/index.ts b/src/compeller/index.ts index a7d6f97..3833ce4 100644 --- a/src/compeller/index.ts +++ b/src/compeller/index.ts @@ -4,6 +4,7 @@ import { OpenAPIObject } from 'openapi3-ts'; import { defaultResponder } from './responders'; import { writeSpecification } from './file-utils/write-specification'; +import { requestBodyValidator } from './validators'; export interface ICompellerOptions { /** @@ -89,9 +90,13 @@ export const compeller = < method: RequestMethod ) => { const path = route as string; + const { + requestBody: { + content: { [contentType]: { schema = undefined } = {} } = {}, + } = {}, + } = spec.paths[path][method]; /** - * * Build a response object for the API with the required status and body * format * @@ -128,8 +133,6 @@ export const compeller = < }; /** - * TODO - Validators need to be abstracted like responders - * * The request validator attaches request body validation to the request * handler for a path. * @@ -137,29 +140,13 @@ export const compeller = < */ const validateRequestBody = < SC extends Request['requestBody']['content'][ContentType]['schema'] - >() => { - const { - requestBody: { - content: { [contentType]: { schema = undefined } = {} } = {}, - } = {}, - } = spec.paths[path][method]; - - // TODO: We need to handle the request which do not have a requestBody - // - // Some users might abstract the functional components into a generic - // wrapper, therefore gets might hit the validator path - // - // We don't want to lose type safety - const unsafeSchema = (schema || {}) as JSONSchemaType>; - - const ajv = new Ajv({ - allErrors: true, - }); - - return ajv.compile>(unsafeSchema); + >( + schema: Record + ) => { + return requestBodyValidator(schema); }; - const validator = validateRequestBody(); + const validator = validateRequestBody(schema); return { response, diff --git a/src/compeller/validators/index.ts b/src/compeller/validators/index.ts new file mode 100644 index 0000000..49996f3 --- /dev/null +++ b/src/compeller/validators/index.ts @@ -0,0 +1 @@ +export * from './request-body'; diff --git a/src/compeller/validators/request-body.test.ts b/src/compeller/validators/request-body.test.ts new file mode 100644 index 0000000..6a63e68 --- /dev/null +++ b/src/compeller/validators/request-body.test.ts @@ -0,0 +1,64 @@ +import { requestBodyValidator } from './request-body'; + +describe('validateRequestBody', () => { + it('errors are null for empty body', () => { + const validator = requestBodyValidator({}); + + validator({}); + + expect(validator.errors).toEqual(null); + }); + + it('infers the return type from a JSON schema', () => { + const testSchema = { + type: 'object', + required: ['name', 'meta'], + additionalProperties: false, + properties: { + name: { + type: 'string', + }, + age: { + type: 'number', + maximum: 200, + minimum: 100, + }, + meta: { + type: 'object', + required: ['createdAt'], + additionalProperties: false, + properties: { + createdAt: { + type: 'string', + }, + }, + }, + }, + } as const; + + const validator = requestBodyValidator(testSchema); + + let data = {}; + + if (validator(data)) { + data.name; + } + + expect(validator.errors).toEqual([ + { + instancePath: '', + keyword: 'required', + message: "must have required property 'name'", + params: { missingProperty: 'name' }, + schemaPath: '#/required', + }, + { + instancePath: '', + keyword: 'required', + message: "must have required property 'meta'", + params: { missingProperty: 'meta' }, + schemaPath: '#/required', + }, + ]); + }); +}); diff --git a/src/compeller/validators/request-body.ts b/src/compeller/validators/request-body.ts new file mode 100644 index 0000000..d939185 --- /dev/null +++ b/src/compeller/validators/request-body.ts @@ -0,0 +1,14 @@ +import Ajv, { JSONSchemaType } from 'ajv'; +import { FromSchema } from 'json-schema-to-ts'; + +export const requestBodyValidator = ( + schema: Record +) => { + const unsafeSchema = (schema || {}) as JSONSchemaType>; + + const ajv = new Ajv({ + allErrors: true, + }); + + return ajv.compile>(unsafeSchema); +}; From 344aed10638b332c3c10d6be78a6761574a6b5f1 Mon Sep 17 00:00:00 2001 From: simon Date: Tue, 1 Feb 2022 07:57:41 +0000 Subject: [PATCH 2/5] Add typecheck to hooks to ensure example continue to be up to date --- .husky/pre-commit | 1 + examples/parameters/api-gateway.ts | 4 ++-- examples/parameters/custom.ts | 2 +- examples/parameters/default.ts | 4 ++-- examples/parameters/openapi/spec.ts | 11 ++++++++++- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 36e1003..49e40dc 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,6 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" +yarn typecheck yarn test yarn embed diff --git a/examples/parameters/api-gateway.ts b/examples/parameters/api-gateway.ts index 93b4364..aed9e61 100644 --- a/examples/parameters/api-gateway.ts +++ b/examples/parameters/api-gateway.ts @@ -6,8 +6,8 @@ const apiGatewayV1Compeller = compeller(OpenAPISpecification, { }); console.info( - apiGatewayV1Compeller('v1/version', 'get').response( - '200', + apiGatewayV1Compeller('v1/users/{id}', 'post').response( + '201', { version: '1.0.0', }, diff --git a/examples/parameters/custom.ts b/examples/parameters/custom.ts index 3dd475b..f8153c0 100644 --- a/examples/parameters/custom.ts +++ b/examples/parameters/custom.ts @@ -19,4 +19,4 @@ const body: Record = {}; const headers: Record = {}; const queryObject: Record = {}; -console.info(customerCompeller('v1/version', 'post').request.validator({})); +console.info(customerCompeller('v1/users/{id}', 'post').request.validator({})); diff --git a/examples/parameters/default.ts b/examples/parameters/default.ts index 76b62ab..ad5c145 100644 --- a/examples/parameters/default.ts +++ b/examples/parameters/default.ts @@ -3,8 +3,8 @@ import { OpenAPISpecification } from './openapi/spec'; const defaultCompeller = compeller(OpenAPISpecification); -const res = defaultCompeller('v1/version', 'get').response( - '200', +const res = defaultCompeller('v1/users/{id}', 'post').response( + '201', { version: '1.0.0', }, diff --git a/examples/parameters/openapi/spec.ts b/examples/parameters/openapi/spec.ts index 233adf8..ef9626d 100644 --- a/examples/parameters/openapi/spec.ts +++ b/examples/parameters/openapi/spec.ts @@ -7,9 +7,18 @@ export const OpenAPISpecification = { }, openapi: '3.1.0', paths: { - 'v1/version': { + 'v1/users/{id}': { post: { parameters: [ + { + name: 'id', + in: 'path', + description: 'user id to lookup', + required: true, + schema: { + type: 'number', + }, + }, { name: 'tags', in: 'query', From 498193b4778c671033c65aee7b038a8cbb63d9ec Mon Sep 17 00:00:00 2001 From: simon Date: Sat, 5 Feb 2022 15:33:08 +0000 Subject: [PATCH 3/5] Validator for parameters array --- .gitignore | 5 +- .vscode/extensions.json | 2 +- __tests__/integration/aws/index.test.ts | 4 +- examples/parameters/custom.ts | 4 +- examples/parameters/default.ts | 11 ++++- examples/parameters/openapi/spec.ts | 11 +++-- src/compeller/index.test.ts | 30 ++++++++++-- src/compeller/index.ts | 54 +++++++++++++++++++-- src/compeller/validators/parameters.test.ts | 45 +++++++++++++++++ src/compeller/validators/parameters.ts | 49 +++++++++++++++++++ src/compeller/validators/request-body.ts | 15 +++++- 11 files changed, 207 insertions(+), 23 deletions(-) create mode 100644 src/compeller/validators/parameters.test.ts create mode 100644 src/compeller/validators/parameters.ts diff --git a/.gitignore b/.gitignore index db5f764..af2e497 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,10 @@ node_modules dist - -*.tgz - tmp +coverage +*.tgz *.tsbuildinfo *.test.js *.test.d.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3ebe77e..fc0146c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["gruntfuggly.todo-tree"] + "recommendations": ["gruntfuggly.todo-tree", "orta.vscode-jest"] } diff --git a/__tests__/integration/aws/index.test.ts b/__tests__/integration/aws/index.test.ts index 6e1e0f1..4b4b46d 100644 --- a/__tests__/integration/aws/index.test.ts +++ b/__tests__/integration/aws/index.test.ts @@ -9,14 +9,14 @@ const { response, request } = API('/pets', 'post'); export const handler = (data: Record) => { let body = data; - if (request.validator(body)) { + if (request.validateBody(body)) { console.info('Type-safe object destructured from post request', { name: body.name, }); return response('201', {}); } else { - const { errors } = request.validator; + const { errors } = request.validateBody; if (errors) { return response('422', { diff --git a/examples/parameters/custom.ts b/examples/parameters/custom.ts index f8153c0..f6dc0a0 100644 --- a/examples/parameters/custom.ts +++ b/examples/parameters/custom.ts @@ -19,4 +19,6 @@ const body: Record = {}; const headers: Record = {}; const queryObject: Record = {}; -console.info(customerCompeller('v1/users/{id}', 'post').request.validator({})); +console.info( + customerCompeller('v1/users/{id}', 'post').request.validateBody({}) +); diff --git a/examples/parameters/default.ts b/examples/parameters/default.ts index ad5c145..b8962c4 100644 --- a/examples/parameters/default.ts +++ b/examples/parameters/default.ts @@ -3,7 +3,16 @@ import { OpenAPISpecification } from './openapi/spec'; const defaultCompeller = compeller(OpenAPISpecification); -const res = defaultCompeller('v1/users/{id}', 'post').response( +const { response, request } = defaultCompeller('v1/users/{id}', 'post'); + +// JSON Schema body validation +request.validateBody({}); +// Validate path and query object +// request.validateParameters[0].; +// Validate headers +// request.validateHeaders({ 'x-api-key': '123aef-231' }); + +const res = response( '201', { version: '1.0.0', diff --git a/examples/parameters/openapi/spec.ts b/examples/parameters/openapi/spec.ts index ef9626d..deaa667 100644 --- a/examples/parameters/openapi/spec.ts +++ b/examples/parameters/openapi/spec.ts @@ -1,4 +1,5 @@ -import { OpenAPIObject } from 'openapi3-ts'; +import { JSONSchema } from 'json-schema-to-ts'; +import { ParameterObject } from 'openapi3-ts'; export const OpenAPISpecification = { info: { @@ -17,8 +18,8 @@ export const OpenAPISpecification = { required: true, schema: { type: 'number', - }, - }, + } as JSONSchema, + } as ParameterObject, { name: 'tags', in: 'query', @@ -31,7 +32,7 @@ export const OpenAPISpecification = { type: 'string', }, }, - }, + } as const, { name: 'limit', in: 'query', @@ -41,7 +42,7 @@ export const OpenAPISpecification = { type: 'integer', format: 'int32', }, - }, + } as const, ], responses: { '201': { diff --git a/src/compeller/index.test.ts b/src/compeller/index.test.ts index 982f409..3556d37 100644 --- a/src/compeller/index.test.ts +++ b/src/compeller/index.test.ts @@ -11,6 +11,18 @@ const spec = { paths: { '/test': { get: { + parameters: [ + { + name: 'limit', + in: 'query', + description: 'How many items to return at one time (max 100)', + required: false, + schema: { + type: 'integer', + // format: 'int32', + }, + }, + ], responses: { '200': { description: 'Test response', @@ -38,9 +50,9 @@ const spec = { describe('API Compiler tests', () => { describe('get requests', () => { it('requires a valid API document', () => { - const stuff = compeller(spec); + const compelled = compeller(spec); - const { response } = stuff('/test', 'get'); + const { response } = compelled('/test', 'get'); const resp = response('200', { name: 'Type-safe reply' }); @@ -51,12 +63,12 @@ describe('API Compiler tests', () => { }); it('keeps a local specification json when true', () => { - const stuff = compeller(spec, { + const compelled = compeller(spec, { jsonSpecFile: join(__dirname, 'tmp', 'openapi.json'), responder: defaultResponder, }); - const { response } = stuff('/test', 'get'); + const { response } = compelled('/test', 'get'); const resp = response('200', { name: 'Type-safe reply' }); @@ -66,4 +78,14 @@ describe('API Compiler tests', () => { }); }); }); + + describe('parameter validation', () => { + xit('has schema validation for each parameter', () => { + const compelled = compeller(spec); + + const { request } = compelled('/test', 'get'); + + expect(request.validateParameters).toEqual({}); + }); + }); }); diff --git a/src/compeller/index.ts b/src/compeller/index.ts index 3833ce4..0735080 100644 --- a/src/compeller/index.ts +++ b/src/compeller/index.ts @@ -1,6 +1,11 @@ -import Ajv, { JSONSchemaType } from 'ajv'; +import Ajv, { JSONSchemaType, ValidateFunction } from 'ajv'; import { FromSchema } from 'json-schema-to-ts'; -import { OpenAPIObject } from 'openapi3-ts'; +import { + OpenAPIObject, + ParameterObject, + PathItemObject, + ReferenceObject, +} from 'openapi3-ts'; import { defaultResponder } from './responders'; import { writeSpecification } from './file-utils/write-specification'; @@ -95,6 +100,7 @@ export const compeller = < content: { [contentType]: { schema = undefined } = {} } = {}, } = {}, } = spec.paths[path][method]; + const { parameters } = spec.paths[path][method] as PathItemObject; /** * Build a response object for the API with the required status and body @@ -146,12 +152,52 @@ export const compeller = < return requestBodyValidator(schema); }; - const validator = validateRequestBody(schema); + /** + * The parameters validator validates the parameters section of the template + * and returns the parameters object, or a schema with errors + */ + const validateRequestParameters = < + Parameters extends Request['parameters'], + AllowedParameters extends Array< + Parameters[number] extends ParameterObject ? Parameters[number] : never + > + >( + parameters: Parameters + ): AllowedParameters => { + const removeRefTypes = ( + param: T + ): T extends ParameterObject ? T : never => { + if (param.$ref) { + throw Error('Only parameter types are supported'); + } + return param; + }; + + return parameters; + + // Parameters is an array + // Each member can be a Ref or Param Obj + // We want to return a type as + // { [key: P[number]['name']]: ValidateFunction = { + // [key in T]: ValidateFunction< + // JSONSchemaType> + // >; + // }; + + // return (p: Parameters[number]) => {}; + }; + + const validateBody = validateRequestBody(schema); + const validateParameters = validateRequestParameters(parameters); return { response, request: { - validator, + validateBody, + validateParameters, }, }; }; diff --git a/src/compeller/validators/parameters.test.ts b/src/compeller/validators/parameters.test.ts new file mode 100644 index 0000000..b507037 --- /dev/null +++ b/src/compeller/validators/parameters.test.ts @@ -0,0 +1,45 @@ +import { validateParameters } from './parameters'; + +describe('parameters based validation', () => { + describe('coordination of parameter collection', () => { + it('arranges all required parameters', () => { + const collection = validateParameters([ + { + in: 'query', + name: 'limit', + schema: { + type: 'number', + maximum: 10, + }, + required: true, + }, + { + in: 'query', + name: 'offset', + schema: { + type: 'number', + minimum: 0, + }, + required: true, + }, + ]); + + expect(collection.query).toEqual({ + limit: { + required: true, + schema: { + maximum: 10, + type: 'number', + }, + }, + offset: { + required: true, + schema: { + minimum: 0, + type: 'number', + }, + }, + }); + }); + }); +}); diff --git a/src/compeller/validators/parameters.ts b/src/compeller/validators/parameters.ts new file mode 100644 index 0000000..7b163d8 --- /dev/null +++ b/src/compeller/validators/parameters.ts @@ -0,0 +1,49 @@ +/** + * Parameters validation needs to validate all entries in an array + { + name: 'limit', + in: 'query', + required: false, + schema: { + type: 'integer', + format: 'int32', + }, + } + */ + +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; + +interface INameSchemaMap { + [key: string]: { + required: boolean; + schema: any; + }; +} + +interface IParameterSchemaMap { + query: INameSchemaMap; + header: INameSchemaMap; + path: INameSchemaMap; + cookie: INameSchemaMap; +} + +export interface ICompellerParameterObject { + in: 'query' | 'header' | 'path' | 'cookie'; + name: string; + schema: JSONSchema; + required: boolean; + [key: string]: any; +} + +export const validateParameters = (parameters: ICompellerParameterObject[]) => { + return parameters.reduce((inSchemaMap, parameter) => { + inSchemaMap[parameter.in] = inSchemaMap[parameter.in] ?? {}; + + inSchemaMap[parameter.in][parameter.name] = { + required: parameter.required ?? false, + schema: parameter.schema, + }; + + return inSchemaMap; + }, {} as IParameterSchemaMap); +}; diff --git a/src/compeller/validators/request-body.ts b/src/compeller/validators/request-body.ts index d939185..4c24c94 100644 --- a/src/compeller/validators/request-body.ts +++ b/src/compeller/validators/request-body.ts @@ -1,14 +1,25 @@ -import Ajv, { JSONSchemaType } from 'ajv'; +import Ajv, { JSONSchemaType, ValidateFunction } from 'ajv'; import { FromSchema } from 'json-schema-to-ts'; +/** + * The request body wraps AJV schema validation with a safe failure to using an + * empty object (get, options, head requests etc.) + * + * @param schema A JSON Schema + * @returns {ValidateFunction} A validator function that holds schema errors + */ export const requestBodyValidator = ( schema: Record ) => { const unsafeSchema = (schema || {}) as JSONSchemaType>; + // TODO: We have an AJV instance here, if we make the body, path and headers + // validator share a single AJV instance we will have a faster code, with a + // smaller memory allocation. However, we will need to have all the same error + // handling, and configuration const ajv = new Ajv({ allErrors: true, }); - return ajv.compile>(unsafeSchema); + return ajv.compile(unsafeSchema); }; From e23c7c5d3c7da7877046c3c57ef55d18a7412142 Mon Sep 17 00:00:00 2001 From: simon Date: Sat, 5 Feb 2022 16:22:39 +0000 Subject: [PATCH 4/5] Update to get typing passed through --- examples/parameters/default.ts | 2 +- examples/parameters/openapi/spec.ts | 8 +-- src/compeller/index.ts | 59 +++++++-------------- src/compeller/validators/parameters.test.ts | 45 ---------------- src/compeller/validators/parameters.ts | 49 ----------------- 5 files changed, 25 insertions(+), 138 deletions(-) delete mode 100644 src/compeller/validators/parameters.test.ts delete mode 100644 src/compeller/validators/parameters.ts diff --git a/examples/parameters/default.ts b/examples/parameters/default.ts index b8962c4..2fd64ca 100644 --- a/examples/parameters/default.ts +++ b/examples/parameters/default.ts @@ -8,7 +8,7 @@ const { response, request } = defaultCompeller('v1/users/{id}', 'post'); // JSON Schema body validation request.validateBody({}); // Validate path and query object -// request.validateParameters[0].; +console.info(request.validateParameters); // Validate headers // request.validateHeaders({ 'x-api-key': '123aef-231' }); diff --git a/examples/parameters/openapi/spec.ts b/examples/parameters/openapi/spec.ts index deaa667..60ffd16 100644 --- a/examples/parameters/openapi/spec.ts +++ b/examples/parameters/openapi/spec.ts @@ -18,8 +18,8 @@ export const OpenAPISpecification = { required: true, schema: { type: 'number', - } as JSONSchema, - } as ParameterObject, + } as const, + } as const, { name: 'tags', in: 'query', @@ -31,7 +31,7 @@ export const OpenAPISpecification = { items: { type: 'string', }, - }, + } as const, } as const, { name: 'limit', @@ -43,7 +43,7 @@ export const OpenAPISpecification = { format: 'int32', }, } as const, - ], + ] as const, responses: { '201': { description: 'Get the current API version', diff --git a/src/compeller/index.ts b/src/compeller/index.ts index 0735080..07f6d5a 100644 --- a/src/compeller/index.ts +++ b/src/compeller/index.ts @@ -1,14 +1,7 @@ -import Ajv, { JSONSchemaType, ValidateFunction } from 'ajv'; import { FromSchema } from 'json-schema-to-ts'; -import { - OpenAPIObject, - ParameterObject, - PathItemObject, - ReferenceObject, -} from 'openapi3-ts'; - -import { defaultResponder } from './responders'; +import { OpenAPIObject } from 'openapi3-ts'; import { writeSpecification } from './file-utils/write-specification'; +import { defaultResponder } from './responders'; import { requestBodyValidator } from './validators'; export interface ICompellerOptions { @@ -89,7 +82,8 @@ export const compeller = < RequestPath extends keyof T['paths'], RequestMethod extends keyof T['paths'][RequestPath], Responses extends T['paths'][RequestPath][RequestMethod]['responses'], - Request extends T['paths'][RequestPath][RequestMethod] + Request extends T['paths'][RequestPath][RequestMethod], + Parameters extends T['paths'][RequestPath][RequestMethod]['parameters'] >( route: RequestPath, method: RequestMethod @@ -100,7 +94,7 @@ export const compeller = < content: { [contentType]: { schema = undefined } = {} } = {}, } = {}, } = spec.paths[path][method]; - const { parameters } = spec.paths[path][method] as PathItemObject; + const parameters = spec.paths[path][method].parameters as Parameters; /** * Build a response object for the API with the required status and body @@ -155,39 +149,26 @@ export const compeller = < /** * The parameters validator validates the parameters section of the template * and returns the parameters object, or a schema with errors + [ + { + name: 'limit', + in: 'query', + required: false, + schema: { + type: 'integer', + format: 'int32', + }, + } + ] */ const validateRequestParameters = < - Parameters extends Request['parameters'], - AllowedParameters extends Array< - Parameters[number] extends ParameterObject ? Parameters[number] : never - > + Parameters extends Request['parameters'] >( parameters: Parameters - ): AllowedParameters => { - const removeRefTypes = ( - param: T - ): T extends ParameterObject ? T : never => { - if (param.$ref) { - throw Error('Only parameter types are supported'); - } - return param; + ) => { + return parameters as { + [key in Parameters[number]['name']]: Parameters[number]['schema']; }; - - return parameters; - - // Parameters is an array - // Each member can be a Ref or Param Obj - // We want to return a type as - // { [key: P[number]['name']]: ValidateFunction = { - // [key in T]: ValidateFunction< - // JSONSchemaType> - // >; - // }; - - // return (p: Parameters[number]) => {}; }; const validateBody = validateRequestBody(schema); diff --git a/src/compeller/validators/parameters.test.ts b/src/compeller/validators/parameters.test.ts deleted file mode 100644 index b507037..0000000 --- a/src/compeller/validators/parameters.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { validateParameters } from './parameters'; - -describe('parameters based validation', () => { - describe('coordination of parameter collection', () => { - it('arranges all required parameters', () => { - const collection = validateParameters([ - { - in: 'query', - name: 'limit', - schema: { - type: 'number', - maximum: 10, - }, - required: true, - }, - { - in: 'query', - name: 'offset', - schema: { - type: 'number', - minimum: 0, - }, - required: true, - }, - ]); - - expect(collection.query).toEqual({ - limit: { - required: true, - schema: { - maximum: 10, - type: 'number', - }, - }, - offset: { - required: true, - schema: { - minimum: 0, - type: 'number', - }, - }, - }); - }); - }); -}); diff --git a/src/compeller/validators/parameters.ts b/src/compeller/validators/parameters.ts deleted file mode 100644 index 7b163d8..0000000 --- a/src/compeller/validators/parameters.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Parameters validation needs to validate all entries in an array - { - name: 'limit', - in: 'query', - required: false, - schema: { - type: 'integer', - format: 'int32', - }, - } - */ - -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; - -interface INameSchemaMap { - [key: string]: { - required: boolean; - schema: any; - }; -} - -interface IParameterSchemaMap { - query: INameSchemaMap; - header: INameSchemaMap; - path: INameSchemaMap; - cookie: INameSchemaMap; -} - -export interface ICompellerParameterObject { - in: 'query' | 'header' | 'path' | 'cookie'; - name: string; - schema: JSONSchema; - required: boolean; - [key: string]: any; -} - -export const validateParameters = (parameters: ICompellerParameterObject[]) => { - return parameters.reduce((inSchemaMap, parameter) => { - inSchemaMap[parameter.in] = inSchemaMap[parameter.in] ?? {}; - - inSchemaMap[parameter.in][parameter.name] = { - required: parameter.required ?? false, - schema: parameter.schema, - }; - - return inSchemaMap; - }, {} as IParameterSchemaMap); -}; From 748a128c4fcda65b10c46f3c38e7ffe4f008c8d6 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 5 Feb 2022 19:36:14 +0000 Subject: [PATCH 5/5] Update params of the validate test, and improve readme --- .gitignore | 1 + README.md | 10 ++++++++-- src/compeller/index.test.ts | 11 +++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index af2e497..c6490f9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ coverage *.tsbuildinfo *.test.js *.test.d.ts +*.log \ No newline at end of file diff --git a/README.md b/README.md index 0c64ee6..4531424 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,14 @@ npx compeller@alpha new - [x] Support for request body validation to type guard (ajv) - [x] Support for header response types - [ ] Support for response type mapping -- [ ] Support for path validation -- [ ] Support header validation + - [ ] Return the response statusCode + - [ ] Return the response headers + - [ ] Return the response body +- [ ] Support Parameter validation of request parameters within the OpenAPI specification. + - [ ] Support path validation + - [ ] Support header validation + - [ ] Support query validation + - [ ] Support cookie validation ### Usage diff --git a/src/compeller/index.test.ts b/src/compeller/index.test.ts index 3556d37..984bcdf 100644 --- a/src/compeller/index.test.ts +++ b/src/compeller/index.test.ts @@ -19,8 +19,9 @@ const spec = { required: false, schema: { type: 'integer', - // format: 'int32', - }, + maximum: 10, + minimum: 0 + } as const, }, ], responses: { @@ -80,12 +81,14 @@ describe('API Compiler tests', () => { }); describe('parameter validation', () => { - xit('has schema validation for each parameter', () => { + it('has schema validation for each parameter', () => { const compelled = compeller(spec); const { request } = compelled('/test', 'get'); - expect(request.validateParameters).toEqual({}); + expect(request.validateParameters({ + limit: 200 + })).toEqual(false); }); }); });