Skip to content

Init OpenAPIValidator in order to use Ajv objects #685

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ To create custom serializers and/or deserializers, define:
e.g.

```javascript
serDes: [{
serDes: [
// installs dateTime serializer and deserializer
OpenApiValidator.serdes.dateTime,
// installs date serializer and deserializer
Expand All @@ -784,8 +784,8 @@ serDes: [{
format: 'mongo-objectid',
deserialize: (s) => new ObjectID(s),
serialize: (o) => o.toString(),
}
}],
},
],
```

The mongo serializers will trigger on the following schema:
Expand Down Expand Up @@ -1148,6 +1148,59 @@ function routesV2(app) {

module.exports = app;
```
## Use OpenAPIValidator AJV out of express usage

You can get an AJV module as OpenAPIValidator generates it for express.
Then you can use it for other usages such as websocket request validation...
Instead of initialize OpenApiValidator with middleware, you can get a configured AJV object with all OpenApiValidator mecanisms (serdes...) and loaded schemas.


```javascript
const ajvs = await OpenApiValidator.ajv({
apiSpec: './openapi.yaml',
validateRequests: true, // (default)
validateResponses: true, // false by default
});

const customObj = {
id : '507f191e810c19729de860ea',
}

const isReqValid = ajvs.req.validate(
{
type: 'object',
properties: {
id: {
$ref: '#/components/schemas/ObjectId',
},
},
required: ['token'],
additionalProperties: false,
},
customObj
);

// isReqValid = true
// No error in ajvs.req.errors


const isResValid = ajvs.res.validate(
{
type: 'object',
properties: {
id: {
$ref: '#/components/schemas/ObjectId',
},
},
required: ['token'],
additionalProperties: false,
},
customObj
);

// isResValid = true
// No error in ajvs.res.errors
```

## FAQ

Expand Down
32 changes: 30 additions & 2 deletions src/framework/ajv/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,21 @@ function createAjv(
];
return false;
}
obj[propName] = sch.deserialize(data);
try {
obj[propName] = sch.deserialize(data);
}
catch(e) {
(<ajv.ValidateFunction>validate).errors = [
{
keyword: 'serdes',
schemaPath: data,
dataPath: path,
message: `format is invalid`,
params: { 'x-eov-serdes': propName },
},
];
return false;
}
}
return true;
};
Expand Down Expand Up @@ -99,7 +113,21 @@ function createAjv(
return function validate(data, path, obj, propName) {
if (typeof data === 'string') return true;
if (!!sch.serialize) {
obj[propName] = sch.serialize(data);
try {
obj[propName] = sch.serialize(data);
}
catch(e) {
(<ajv.ValidateFunction>validate).errors = [
{
keyword: 'serdes',
schemaPath: data,
dataPath: path,
message: `format is invalid`,
params: { 'x-eov-serdes': propName },
},
];
return false;
}
}
return true;
};
Expand Down
6 changes: 6 additions & 0 deletions src/framework/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as ajv from 'ajv';
import * as multer from 'multer';
import { Request, Response, NextFunction } from 'express';
import { Ajv } from 'ajv';
export { OpenAPIFrameworkArgs };

export type BodySchema =
Expand Down Expand Up @@ -155,6 +156,11 @@ export namespace OpenAPIV3 {
version: string;
}

export interface Ajvs {
req?: Ajv,
res?: Ajv
}

export interface ContactObject {
name?: string;
url?: string;
Expand Down
14 changes: 14 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
// export default openapiValidator;
export const resolvers = res;
export const middleware = openapiValidator;
export const ajv = ajvInstances;
export const error = {
InternalServerError,
UnsupportedMediaType,
Expand All @@ -43,3 +44,16 @@ function openapiValidator(options: OpenApiValidatorOpts) {
}).load(),
);
}

function ajvInstances(options: OpenApiValidatorOpts) {
const oav = new OpenApiValidator(options);
exports.middleware._oav = oav;

return oav.installAjv(
new OpenApiSpecLoader({
apiDoc: cloneDeep(options.apiSpec),
validateApiSpec: options.validateApiSpec,
$refParser: options.$refParser,
}).load(),
);
}
4 changes: 4 additions & 0 deletions src/middlewares/openapi.request.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ export class RequestValidator {
}
}
}

public getAJV () : Ajv {
return this.ajv;
}
}

class Validator {
Expand Down
5 changes: 5 additions & 0 deletions src/middlewares/openapi.response.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '../framework/types';
import * as mediaTypeParser from 'media-typer';
import * as contentTypeParser from 'content-type';
import { Ajv } from 'ajv';

interface ValidateResult {
validators: { [key: string]: ajv.ValidateFunction };
Expand Down Expand Up @@ -318,4 +319,8 @@ export class ResponseValidator {
mediaTypeParsed.subtype === 'json' || mediaTypeParsed.suffix === 'json'
);
}

public getAJV() : Ajv {
return this.ajvBody;
}
}
25 changes: 25 additions & 0 deletions src/openapi.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { OperationHandlerOptions } from './framework/types';
import { defaultSerDes } from './framework/base.serdes';
import { SchemaPreprocessor } from './middlewares/parsers/schema.preprocessor';
import { AjvOptions } from './framework/ajv/options';
import { Ajv } from 'ajv';
import Ajvs = OpenAPIV3.Ajvs;

export {
OpenApiValidatorOpts,
Expand Down Expand Up @@ -90,6 +92,29 @@ export class OpenApiValidator {
this.ajvOpts = new AjvOptions(options);
}

installAjv(spec: Promise<Spec>): Promise<Ajvs> {
return spec
.then((spec) => {
const apiDoc = spec.apiDoc;
const ajvOpts = this.ajvOpts.preprocessor;
const resOpts = this.options.validateResponses as ValidateRequestOpts;
const sp = new SchemaPreprocessor(
apiDoc,
ajvOpts,
resOpts,
).preProcess();
return {
req : new middlewares.RequestValidator(apiDoc, this.ajvOpts.request).getAJV(),
res : new middlewares.ResponseValidator(
apiDoc,
this.ajvOpts.response,
this.options.validateResponses as ValidateResponseOpts,)
.getAJV(),
};
});
}


installMiddleware(spec: Promise<Spec>): OpenApiRequestHandler[] {
const middlewares: OpenApiRequestHandler[] = [];
const pContext = spec
Expand Down
140 changes: 140 additions & 0 deletions test/ajv.return.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import * as path from 'path';
import { expect } from 'chai';

import { date, dateTime } from '../src/framework/base.serdes';
import * as OpenApiValidator from '../src';
import { OpenAPIV3 } from '../src/framework/types';
import Ajvs = OpenAPIV3.Ajvs;

const apiSpecPath = path.join('test', 'resources', 'serdes.yaml');

class ObjectID {
id: string;

constructor(id: string = "5fdefd13a6640bb5fb5fa925") {
this.id = id;
}

toString() {
return this.id;
}
}

describe('ajv.return', () => {
let ajvs : Ajvs = null;

class ReqRes {
id?: string|ObjectID
}

const customSchema = {
type: 'object',
properties: {
id: {
$ref: '#/components/schemas/ObjectId',
},
},
required: ['id'],
additionalProperties: false,
};

before(async () => {
ajvs = await OpenApiValidator.ajv({
apiSpec: apiSpecPath,
validateRequests: {
coerceTypes: true
},
validateResponses: {
coerceTypes: true
},
serDes: [
date,
dateTime,
{
format: "mongo-objectid",
deserialize: (s) => new ObjectID(s),
serialize: (o) => o.toString(),
},
],
unknownFormats: ['string-list'],
});
});

it('should control request and deserialize string to object', async () => {
const req : ReqRes = {
id : '507f191e810c19729de860ea',
}

const isValid = ajvs.req.validate(
customSchema,
req
);
expect(isValid).to.be.equal(true);
expect(ajvs.req.errors).to.be.equal(null);
expect(req.id instanceof ObjectID).to.be.true;
});

it('should control request and return error if id is not set', async () => {
const req : ReqRes = {
// No id but it is required
// id : '507f191e810c19729de860ea',
}

const isValid = ajvs.req.validate(
customSchema,
req
);
expect(isValid).to.be.equal(false);
expect(ajvs.req.errors.length).to.be.equal(1);
expect(ajvs.req.errors[0].message).to.be.equal('should have required property \'id\'');
});

it('should control request and return error if id is in bad format', async () => {
const req : ReqRes = {
id : 'notAnObjectId',
}

const isValid = ajvs.req.validate(
customSchema,
req
);
expect(isValid).to.be.equal(false);
expect(ajvs.req.errors.length).to.be.equal(1);
expect(ajvs.req.errors[0].message).to.be.equal('should match pattern "^[0-9a-fA-F]{24}$"');
});


it('should control response and serialize object to string', async () => {
const res : ReqRes = {
id : new ObjectID('507f191e810c19729de860ea'),
}

const isValid = ajvs.res.validate(
customSchema,
res
);
expect(res.id).to.be.equal('507f191e810c19729de860ea');
expect(isValid).to.be.equal(true);
expect(ajvs.res.errors).to.be.equal(null);
});

it('should control response and return an error if id is not set', async () => {

const res : ReqRes = {
// No id but it is required
// id : '507f191e810c19729de860ea',
//id : new ObjectID('507f191e810c19729de860ea'),
}

const isValid = ajvs.res.validate(
customSchema,
res
);
expect(isValid).to.be.equal(false);
expect(ajvs.res.errors.length).to.be.equal(1);
expect(ajvs.res.errors[0].message).to.be.equal('should have required property \'id\'');
});
});



Loading