-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: builders for Filter and Where schemas
Implement new APIs for building JSON and OpenAPI schemas describing the "filter" and "where" objects used to query or modify model instances.
- Loading branch information
Showing
13 changed files
with
401 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -224,6 +224,7 @@ export namespace param { | |
name, | ||
in: 'query', | ||
style: 'deepObject', | ||
explode: true, | ||
schema, | ||
}); | ||
}, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// Copyright IBM Corp. 2018. All Rights Reserved. | ||
// Node module: @loopback/openapi-v3 | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
import {SchemaObject} from '@loopback/openapi-v3-types'; | ||
import { | ||
getFilterJsonSchemaFor, | ||
getWhereJsonSchemaFor, | ||
Model, | ||
} from '@loopback/repository-json-schema'; | ||
import {jsonToSchemaObject} from './json-to-schema'; | ||
|
||
/** | ||
* Build an OpenAPI schema describing the format of the "filter" object | ||
* used to query model instances. | ||
* | ||
* Note we don't take the model properties into account yet and return | ||
* a generic json schema allowing any "where" condition. | ||
* | ||
* @param modelCtor The model constructor to build the filter schema for. | ||
*/ | ||
export function getFilterSchemaFor(modelCtor: typeof Model): SchemaObject { | ||
const jsonSchema = getFilterJsonSchemaFor(modelCtor); | ||
const schema = jsonToSchemaObject(jsonSchema); | ||
return schema; | ||
} | ||
|
||
/** | ||
* Build a OpenAPI schema describing the format of the "where" object | ||
* used to filter model instances to query, update or delete. | ||
* | ||
* Note we don't take the model properties into account yet and return | ||
* a generic json schema allowing any "where" condition. | ||
* | ||
* @param modelCtor The model constructor to build the filter schema for. | ||
*/ | ||
export function getWhereSchemaFor(modelCtor: typeof Model): SchemaObject { | ||
const jsonSchema = getWhereJsonSchemaFor(modelCtor); | ||
const schema = jsonToSchemaObject(jsonSchema); | ||
return schema; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// Copyright IBM Corp. 2018. All Rights Reserved. | ||
// Node module: @loopback/repository-json-schema | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
import {Model, model, getModelRelations} from '@loopback/repository'; | ||
import {JSONSchema6 as JsonSchema} from 'json-schema'; | ||
|
||
@model({settings: {strict: false}}) | ||
class EmptyModel extends Model {} | ||
|
||
const scopeFilter = getFilterJsonSchemaFor(EmptyModel); | ||
|
||
/** | ||
* Build a JSON schema describing the format of the "filter" object | ||
* used to query model instances. | ||
* | ||
* Note we don't take the model properties into account yet and return | ||
* a generic json schema allowing any "where" condition. | ||
* | ||
* @param modelCtor The model constructor to build the filter schema for. | ||
*/ | ||
export function getFilterJsonSchemaFor(modelCtor: typeof Model): JsonSchema { | ||
const schema: JsonSchema = { | ||
properties: { | ||
where: getWhereJsonSchemaFor(modelCtor), | ||
|
||
fields: { | ||
type: 'object', | ||
// TODO(bajtos) enumerate "model" properties | ||
// See https://github.com/strongloop/loopback-next/issues/1748 | ||
additionalProperties: true, | ||
}, | ||
|
||
offset: { | ||
type: 'integer', | ||
minimum: 0, | ||
}, | ||
|
||
limit: { | ||
type: 'integer', | ||
minimum: 0, | ||
}, | ||
|
||
skip: { | ||
type: 'integer', | ||
minimum: 0, | ||
}, | ||
|
||
order: { | ||
type: 'array', | ||
items: { | ||
type: 'string', | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
const modelRelations = getModelRelations(modelCtor); | ||
const hasRelations = Object.keys(modelRelations).length > 0; | ||
|
||
if (hasRelations) { | ||
schema.properties!.include = { | ||
type: 'array', | ||
items: { | ||
type: 'object', | ||
properties: { | ||
// TODO(bajtos) restrict values to relations defined by "model" | ||
relation: {type: 'string'}, | ||
// TODO(bajtos) describe the filter for the relation target model | ||
scope: scopeFilter, | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
return schema; | ||
} | ||
|
||
/** | ||
* Build a JSON schema describing the format of the "where" object | ||
* used to filter model instances to query, update or delete. | ||
* | ||
* Note we don't take the model properties into account yet and return | ||
* a generic json schema allowing any "where" condition. | ||
* | ||
* @param modelCtor The model constructor to build the filter schema for. | ||
*/ | ||
export function getWhereJsonSchemaFor(modelCtor: typeof Model): JsonSchema { | ||
const schema: JsonSchema = { | ||
type: 'object', | ||
// TODO(bajtos) enumerate "model" properties and operators like "and" | ||
// See https://github.com/strongloop/loopback-next/issues/1748 | ||
additionalProperties: true, | ||
}; | ||
return schema; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
186 changes: 186 additions & 0 deletions
186
packages/repository-json-schema/test/unit/filter-json-schema.unit.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
// Copyright IBM Corp. 2018. All Rights Reserved. | ||
// Node module: @loopback/repository-json-schema | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
import {Entity, Filter, hasMany, model, property} from '@loopback/repository'; | ||
import {expect} from '@loopback/testlab'; | ||
import * as Ajv from 'ajv'; | ||
import {JsonSchema} from '../../src'; | ||
import { | ||
getFilterJsonSchemaFor, | ||
getWhereJsonSchemaFor, | ||
} from '../../src/filter-json-schema'; | ||
|
||
describe('getFilterJsonSchemaFor', () => { | ||
let ajv: Ajv.Ajv; | ||
let customerFilterSchema: JsonSchema; | ||
let orderFilterSchema: JsonSchema; | ||
|
||
beforeEach(() => { | ||
ajv = new Ajv(); | ||
customerFilterSchema = getFilterJsonSchemaFor(Customer); | ||
orderFilterSchema = getFilterJsonSchemaFor(Order); | ||
}); | ||
|
||
it('produces a valid schema', () => { | ||
const isValid = ajv.validateSchema(customerFilterSchema); | ||
|
||
const SUCCESS_MSG = 'Filter schema is a valid JSON Schema'; | ||
const result = isValid ? SUCCESS_MSG : ajv.errorsText(ajv.errors!); | ||
expect(result).to.equal(SUCCESS_MSG); | ||
}); | ||
|
||
it('allows an empty filter', () => { | ||
expectSchemaToAllowFilter(customerFilterSchema, {}); | ||
}); | ||
|
||
it('allows all top-level filter properties', () => { | ||
const filter: Required<Filter> = { | ||
where: {id: 1}, | ||
fields: {id: true, name: true}, | ||
include: [{relation: 'orders'}], | ||
offset: 0, | ||
limit: 10, | ||
order: ['id DESC'], | ||
skip: 0, | ||
}; | ||
|
||
expectSchemaToAllowFilter(customerFilterSchema, filter); | ||
}); | ||
|
||
it('describes "where" as an object', () => { | ||
const filter = {where: 'invalid-where'}; | ||
ajv.validate(customerFilterSchema, filter); | ||
expect(ajv.errors || []).to.containDeep([ | ||
{ | ||
keyword: 'type', | ||
dataPath: '.where', | ||
message: 'should be object', | ||
}, | ||
]); | ||
}); | ||
|
||
it('describes "fields" as an object', () => { | ||
const filter = {fields: 'invalid-fields'}; | ||
ajv.validate(customerFilterSchema, filter); | ||
expect(ajv.errors || []).to.containDeep([ | ||
{ | ||
keyword: 'type', | ||
dataPath: '.fields', | ||
message: 'should be object', | ||
}, | ||
]); | ||
}); | ||
|
||
it('describes "include" as an array for models with relations', () => { | ||
const filter = {include: 'invalid-include'}; | ||
ajv.validate(customerFilterSchema, filter); | ||
expect(ajv.errors || []).to.containDeep([ | ||
{ | ||
keyword: 'type', | ||
dataPath: '.include', | ||
message: 'should be array', | ||
}, | ||
]); | ||
}); | ||
|
||
it('leaves out "include" for models with no relations', () => { | ||
const filterProperties = Object.keys(orderFilterSchema.properties || {}); | ||
expect(filterProperties).to.not.containEql('include'); | ||
}); | ||
|
||
it('describes "offset" as an integer', () => { | ||
const filter = {offset: 'invalid-offset'}; | ||
ajv.validate(customerFilterSchema, filter); | ||
expect(ajv.errors || []).to.containDeep([ | ||
{ | ||
keyword: 'type', | ||
dataPath: '.offset', | ||
message: 'should be integer', | ||
}, | ||
]); | ||
}); | ||
|
||
it('describes "limit" as an integer', () => { | ||
const filter = {limit: 'invalid-limit'}; | ||
ajv.validate(customerFilterSchema, filter); | ||
expect(ajv.errors || []).to.containDeep([ | ||
{ | ||
keyword: 'type', | ||
dataPath: '.limit', | ||
message: 'should be integer', | ||
}, | ||
]); | ||
}); | ||
|
||
it('describes "skip" as an integer', () => { | ||
const filter = {skip: 'invalid-skip'}; | ||
ajv.validate(customerFilterSchema, filter); | ||
expect(ajv.errors || []).to.containDeep([ | ||
{ | ||
keyword: 'type', | ||
dataPath: '.skip', | ||
message: 'should be integer', | ||
}, | ||
]); | ||
}); | ||
|
||
it('describes "order" as an array', () => { | ||
const filter = {order: 'invalid-order'}; | ||
ajv.validate(customerFilterSchema, filter); | ||
expect(ajv.errors || []).to.containDeep([ | ||
{ | ||
keyword: 'type', | ||
dataPath: '.order', | ||
message: 'should be array', | ||
}, | ||
]); | ||
}); | ||
|
||
function expectSchemaToAllowFilter<T>(schema: JsonSchema, value: T) { | ||
const isValid = ajv.validate(schema, value); | ||
const SUCCESS_MSG = 'Filter instance is valid according to Filter schema'; | ||
const result = isValid ? SUCCESS_MSG : ajv.errorsText(ajv.errors!); | ||
expect(result).to.equal(SUCCESS_MSG); | ||
} | ||
}); | ||
|
||
describe('getWhereJsonSchemaFor', () => { | ||
let ajv: Ajv.Ajv; | ||
let customerWhereSchema: JsonSchema; | ||
|
||
beforeEach(() => { | ||
ajv = new Ajv(); | ||
customerWhereSchema = getWhereJsonSchemaFor(Customer); | ||
}); | ||
|
||
it('produces a valid schema', () => { | ||
const isValid = ajv.validateSchema(customerWhereSchema); | ||
|
||
const SUCCESS_MSG = 'Where schema is a valid JSON Schema'; | ||
const result = isValid ? SUCCESS_MSG : ajv.errorsText(ajv.errors!); | ||
expect(result).to.equal(SUCCESS_MSG); | ||
}); | ||
}); | ||
|
||
@model() | ||
class Order extends Entity { | ||
@property({id: true}) | ||
id: number; | ||
|
||
@property() | ||
customerId: number; | ||
} | ||
|
||
@model() | ||
class Customer extends Entity { | ||
@property({id: true}) | ||
id: number; | ||
|
||
@property() | ||
name: string; | ||
|
||
@hasMany(Order) | ||
orders?: Order[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.