From 9db4056135448c93270120f8bda3df09b6b17490 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 25 Feb 2020 13:52:06 -0800 Subject: [PATCH] feat: improve filter schema to allow exclusion --- .../src/__tests__/unit/filter-schema.unit.ts | 24 ++++++ packages/openapi-v3/src/filter-schema.ts | 9 ++- .../__tests__/unit/filter-json-schema.unit.ts | 37 +++++++++ .../src/filter-json-schema.ts | 76 ++++++++++++------- 4 files changed, 117 insertions(+), 29 deletions(-) diff --git a/packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts b/packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts index 3c031328143c..ab04e8c5f938 100644 --- a/packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts +++ b/packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts @@ -46,6 +46,30 @@ describe('filterSchema', () => { }); }); + it('generate filter schema excluding where', () => { + const schema = getFilterSchemaFor(MyUserModel, {exclude: ['where']}); + expect(MyUserModel.definition.name).to.eql('my-user-model'); + expect(schema).to.eql({ + title: 'my-user-model.Filter', + properties: { + fields: { + type: 'object', + title: 'my-user-model.Fields', + properties: { + id: {type: 'boolean'}, + age: {type: 'boolean'}, + }, + additionalProperties: false, + }, + offset: {type: 'integer', minimum: 0}, + limit: {type: 'integer', minimum: 1, example: 100}, + skip: {type: 'integer', minimum: 0}, + order: {type: 'array', items: {type: 'string'}}, + }, + additionalProperties: false, + }); + }); + @model({ name: 'CustomUserModel', }) diff --git a/packages/openapi-v3/src/filter-schema.ts b/packages/openapi-v3/src/filter-schema.ts index e31b0040f9a5..e8d06cd90e76 100644 --- a/packages/openapi-v3/src/filter-schema.ts +++ b/packages/openapi-v3/src/filter-schema.ts @@ -4,6 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import { + FilterSchemaOptions, getFilterJsonSchemaFor, getWhereJsonSchemaFor, Model, @@ -19,9 +20,13 @@ import {SchemaObject} from './types'; * a generic json schema allowing any "where" condition. * * @param modelCtor - The model constructor to build the filter schema for. + * @param options - Options to build the filter schema. */ -export function getFilterSchemaFor(modelCtor: typeof Model): SchemaObject { - const jsonSchema = getFilterJsonSchemaFor(modelCtor); +export function getFilterSchemaFor( + modelCtor: typeof Model, + options?: FilterSchemaOptions, +): SchemaObject { + const jsonSchema = getFilterJsonSchemaFor(modelCtor, options); const schema = jsonToSchemaObject(jsonSchema); return schema; } diff --git a/packages/repository-json-schema/src/__tests__/unit/filter-json-schema.unit.ts b/packages/repository-json-schema/src/__tests__/unit/filter-json-schema.unit.ts index 3d5aadeb907d..21dc2339c556 100644 --- a/packages/repository-json-schema/src/__tests__/unit/filter-json-schema.unit.ts +++ b/packages/repository-json-schema/src/__tests__/unit/filter-json-schema.unit.ts @@ -16,11 +16,15 @@ import { describe('getFilterJsonSchemaFor', () => { let ajv: Ajv.Ajv; let customerFilterSchema: JsonSchema; + let customerFilterExcludingWhereSchema: JsonSchema; let orderFilterSchema: JsonSchema; beforeEach(() => { ajv = new Ajv(); customerFilterSchema = getFilterJsonSchemaFor(Customer); + customerFilterExcludingWhereSchema = getFilterJsonSchemaFor(Customer, { + exclude: ['where'], + }); orderFilterSchema = getFilterJsonSchemaFor(Order); }); @@ -50,6 +54,21 @@ describe('getFilterJsonSchemaFor', () => { expectSchemaToAllowFilter(customerFilterSchema, filter); }); + it('disallows "where"', () => { + const filter = {where: {name: 'John'}}; + // eslint-disable-next-line @typescript-eslint/no-floating-promises + ajv.validate(customerFilterExcludingWhereSchema, filter); + expect(ajv.errors ?? []).to.containDeep([ + { + keyword: 'additionalProperties', + dataPath: '', + schemaPath: '#/additionalProperties', + params: {additionalProperty: 'where'}, + message: 'should NOT have additional properties', + }, + ]); + }); + it('describes "where" as an object', () => { const filter = {where: 'invalid-where'}; // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -182,6 +201,24 @@ describe('getFilterJsonSchemaFor', () => { } }); +describe('getFilterJsonSchemaFor - excluding where', () => { + let customerFilterSchema: JsonSchema; + + it('excludes "where" using string[]', () => { + customerFilterSchema = getFilterJsonSchemaFor(Customer, { + exclude: ['where'], + }); + expect(customerFilterSchema.properties).to.not.have.property('where'); + }); + + it('excludes "where" using string', () => { + customerFilterSchema = getFilterJsonSchemaFor(Customer, { + exclude: 'where', + }); + expect(customerFilterSchema.properties).to.not.have.property('where'); + }); +}); + describe('getFilterJsonSchemaForOptionsSetTitle', () => { let customerFilterSchema: JsonSchema; diff --git a/packages/repository-json-schema/src/filter-json-schema.ts b/packages/repository-json-schema/src/filter-json-schema.ts index 4ece6964ac8c..46cf9bedd9b5 100644 --- a/packages/repository-json-schema/src/filter-json-schema.ts +++ b/packages/repository-json-schema/src/filter-json-schema.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {getModelRelations, Model, model} from '@loopback/repository'; -import {JSONSchema6 as JsonSchema} from 'json-schema'; +import {JSONSchema6 as JsonSchema, JSONSchema6Definition} from 'json-schema'; export interface FilterSchemaOptions { /** @@ -14,6 +14,11 @@ export interface FilterSchemaOptions { * */ setTitle?: boolean; + + /** + * To exclude one or more property from `filter` + */ + exclude?: string[] | string; } /** @@ -50,43 +55,60 @@ export function getScopeFilterJsonSchemaFor( * a generic json schema allowing any "where" condition. * * @param modelCtor - The model constructor to build the filter schema for. + * @param options - Options to build the filter schema. */ export function getFilterJsonSchemaFor( modelCtor: typeof Model, options: FilterSchemaOptions = {}, ): JsonSchema { - const schema: JsonSchema = { - ...(options.setTitle !== false && { - title: `${modelCtor.modelName}.Filter`, - }), - properties: { - where: getWhereJsonSchemaFor(modelCtor, options), + let excluded: string[]; + if (typeof options.exclude === 'string') { + excluded = [options.exclude]; + } else { + excluded = options.exclude ?? []; + } + const properties: Record = { + offset: { + type: 'integer', + minimum: 0, + }, - fields: getFieldsJsonSchemaFor(modelCtor, options), + limit: { + type: 'integer', + minimum: 1, + examples: [100], + }, - offset: { - type: 'integer', - minimum: 0, - }, + skip: { + type: 'integer', + minimum: 0, + }, - limit: { - type: 'integer', - minimum: 1, - examples: [100], + order: { + type: 'array', + items: { + type: 'string', }, + }, + }; - skip: { - type: 'integer', - minimum: 0, - }, + if (!excluded.includes('where')) { + properties.where = getWhereJsonSchemaFor(modelCtor, options); + } + if (!excluded.includes('fields')) { + properties.fields = getFieldsJsonSchemaFor(modelCtor, options); + } - order: { - type: 'array', - items: { - type: 'string', - }, - }, - }, + // Remove excluded properties + for (const p of excluded) { + delete properties[p]; + } + + const schema: JsonSchema = { + ...(options.setTitle !== false && { + title: `${modelCtor.modelName}.Filter`, + }), + properties, additionalProperties: false, };