Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright IBM Corp. and LoopBack contributors 2020. All Rights Reserved.
// Node module: @loopback/example-validation-app
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Client, expect} from '@loopback/testlab';
import {ValidationApplication} from '../..';
import {setupApplication} from './test-helper';

const validCat = {
name: 'Kitty',
weight: 5,
kind: 'Cat',
animalProperties: {
color: 'grey',
whiskerLength: 2,
},
};

const validDog = {
name: 'Rex',
weight: 5,
kind: 'Dog',
animalProperties: {
breed: 'poodle',
barkVolume: 5,
},
};

describe('validate properties based on discriminated schemas', () => {
let client: Client;
let app: ValidationApplication;

before(givenAClient);

after(async () => {
await app.stop();
});

async function givenAClient() {
({app, client} = await setupApplication());
}

it('should pass with valid cat properties', async () => {
await client.post('/pets').send(validCat).expect(200);
});

it('should pass with valid dog properties', async () => {
await client.post('/pets').send(validDog).expect(200);
});

it('should fail with error indicating invalid barkVolume type', async () => {
const invalidDog = {...validDog};
invalidDog.animalProperties.barkVolume = 'loud' as unknown as number;
const response = await client.post('/pets').send(invalidDog).expect(422);

expect(response.body.error.details.length).to.equal(1);
expect(response.body.error.details[0].message).to.equal('must be number');
expect(response.body.error.details).to.deepEqual([
{
code: 'type',
info: {
type: 'number',
},
message: 'must be number',
path: '/animalProperties/barkVolume',
},
]);
});

it('should fail with error indicating invalid whiskerLength type', async () => {
const invalidCat = {...validCat};
invalidCat.animalProperties.whiskerLength = 'long' as unknown as number;
const response = await client.post('/pets').send(invalidCat).expect(422);

expect(response.body.error.details.length).to.equal(1);
expect(response.body.error.details[0].message).to.equal('must be number');
expect(response.body.error.details).to.deepEqual([
{
code: 'type',
info: {
type: 'number',
},
message: 'must be number',
path: '/animalProperties/whiskerLength',
},
]);
});
});
8 changes: 7 additions & 1 deletion examples/validation-app/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import {BootMixin} from '@loopback/boot';
import {ApplicationConfig, createBindingFromClass} from '@loopback/core';
import {RepositoryMixin} from '@loopback/repository';
import {RestApplication} from '@loopback/rest';
import {RestApplication, RestBindings} from '@loopback/rest';
import {
RestExplorerBindings,
RestExplorerComponent,
Expand All @@ -32,6 +32,12 @@ export class ValidationApplication extends BootMixin(
// Set up default home page
this.static('/', path.join(__dirname, '../public'));

this.bind(RestBindings.REQUEST_BODY_PARSER_OPTIONS).to({
validation: {
discriminator: true,
},
});

// Customize @loopback/rest-explorer configuration here
this.configure(RestExplorerBindings.COMPONENT).to({
path: '/explorer',
Expand Down
1 change: 1 addition & 0 deletions examples/validation-app/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
// License text available at https://opensource.org/licenses/MIT

export * from './coffee-shop.controller';
export * from './pet.controller';
30 changes: 30 additions & 0 deletions examples/validation-app/src/controllers/pet.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright IBM Corp. and LoopBack contributors 2020. All Rights Reserved.
// Node module: @loopback/example-validation-app
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {post, requestBody} from '@loopback/rest';
import {Cat, Dog, Pet} from '../models';

export class PetController {
constructor() {}

@post('/pets')
async create(
@requestBody({
content: {
'application/json': {
schema: {
discriminator: {
propertyName: 'kind',
},
oneOf: [{'x-ts-type': Cat}, {'x-ts-type': Dog}],
},
},
},
})
request: Pet,
): Promise<Pet> {
return request;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ export class ValidationErrorMiddlewareProvider implements Provider<Middleware> {
err: HttpErrors.HttpError,
): Response | undefined {
// 2. customize error for particular endpoint
if (context.request.url === '/coffee-shops') {
if (
context.request.url === '/coffee-shops' ||
context.request.url === '/pets'
) {
// if this is a validation error from the PATCH method, customize it
// for other validation errors, the default AJV error object will be sent
if (err.statusCode === 422 && context.request.method === 'PATCH') {
Expand Down
1 change: 1 addition & 0 deletions examples/validation-app/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
// License text available at https://opensource.org/licenses/MIT

export * from './coffee-shop.model';
export * from './pet.model';
86 changes: 86 additions & 0 deletions examples/validation-app/src/models/pet.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {Model, model, property} from '@loopback/repository';

@model()
export class CatProperties extends Model {
@property({
type: String,
required: true,
})
color: string;

@property({
type: Number,
required: true,
})
whiskerLength: number;
}

@model()
export class DogProperties extends Model {
@property({
type: String,
required: true,
})
breed: string;

@property({
type: Number,
required: true,
})
barkVolume: number;
}

@model()
export class Pet extends Model {
@property({
type: String,
required: true,
})
name: string;

@property({
type: Number,
required: false,
})
weight?: number;

kind: string;

animalProperties: CatProperties | DogProperties;
}

@model()
export class Dog extends Pet {
@property({
type: String,
jsonSchema: {
enum: ['Dog'],
},
required: true,
})
kind: string;

@property({
type: DogProperties,
required: true,
})
animalProperties: DogProperties;
}

@model()
export class Cat extends Pet {
@property({
type: String,
jsonSchema: {
enum: ['Cat'],
},
required: true,
})
kind: string;

@property({
type: CatProperties,
required: true,
})
animalProperties: CatProperties;
}
5 changes: 4 additions & 1 deletion packages/rest/src/validation/request-body.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ export async function validateRequestBody(
* @param openapiSchema - The OpenAPI schema to convert.
*/
function convertToJsonSchema(openapiSchema: SchemaObject) {
const jsonSchema = toJsonSchema(openapiSchema);
const jsonSchema = toJsonSchema(openapiSchema, {
keepNotSupported: ['discriminator'],
});

delete jsonSchema['$schema'];
/* istanbul ignore if */
if (debug.enabled) {
Expand Down