Skip to content

Commit

Permalink
feat: constrain Filter to excldue where for findById
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Feb 25, 2020
1 parent f227675 commit be725df
Show file tree
Hide file tree
Showing 13 changed files with 147 additions and 42 deletions.
4 changes: 3 additions & 1 deletion docs/site/Controller-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import {
Count,
CountSchema,
Filter,
FilterExcludingWhere,
repository,
Where
} from '@loopback/repository';
Expand All @@ -108,6 +109,7 @@ import {
param,
get,
getFilterSchemaFor,
getFilterExcludingWhereSchemaFor,
getModelSchemaRef,
getWhereSchemaFor,
patch,
Expand Down Expand Up @@ -212,7 +214,7 @@ export class TodoController {
})
async findById(
@param.path.number('id') id: number,
@param.query.object('filter', getFilterSchemaFor(Todo)) filter?: Filter<Todo>
@param.query.object('filter', getFilterExcludingWhereSchemaFor(Todo)) filter?: FilterExcludingWhere<Todo>
): Promise<Todo> {
return this.todoRepository.findById(id, filter);
}
Expand Down
2 changes: 1 addition & 1 deletion docs/site/Sequence.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ from the path object.
})
async findById(
@param.path.string('id') id: string,
@param.query.object('filter', getFilterSchemaFor(Note)) filter?: Filter<Note>
@param.query.object('filter', getFilterExcludingWhereSchemaFor(Note)) filter?: FilterExcludingWhere<Note>
): Promise<Note> {
return this.noteRepository.findById(id, filter);
}
Expand Down
6 changes: 4 additions & 2 deletions examples/todo-list/src/controllers/todo-list.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {
Count,
CountSchema,
Filter,
FilterExcludingWhere,
repository,
Where,
} from '@loopback/repository';
import {
del,
get,
getFilterExcludingWhereSchemaFor,
getFilterSchemaFor,
getModelSchemaRef,
getWhereSchemaFor,
Expand Down Expand Up @@ -128,8 +130,8 @@ export class TodoListController {
})
async findById(
@param.path.number('id') id: number,
@param.query.object('filter', getFilterSchemaFor(TodoList))
filter?: Filter<TodoList>,
@param.query.object('filter', getFilterExcludingWhereSchemaFor(TodoList))
filter?: FilterExcludingWhere<TodoList>,
): Promise<TodoList> {
return this.todoListRepository.findById(id, filter);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import {
Count,
CountSchema,
Filter,
FilterExcludingWhere,
repository,
Where,
} from '@loopback/repository';
import {
post,
param,
get,
getFilterExcludingWhereSchemaFor,
getFilterSchemaFor,
getModelSchemaRef,
getWhereSchemaFor,
Expand Down Expand Up @@ -121,7 +123,7 @@ export class <%= className %>Controller {
})
async findById(
@param.path.<%= idType %>('id') id: <%= idType %>,
@param.query.object('filter', getFilterSchemaFor(<%= modelName %>)) filter?: Filter<<%= modelName %>>
@param.query.object('filter', getFilterExcludingWhereSchemaFor(<%= modelName %>)) filter?: FilterExcludingWhere<<%= modelName %>>
): Promise<<%= modelName %>> {
return this.<%= repositoryNameCamel %>.findById(id, filter);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
post,
param,
get,
getFilterExcludingWhereSchemaFor,
getFilterSchemaFor,
getModelSchemaRef,
getWhereSchemaFor,
Expand Down Expand Up @@ -131,7 +132,7 @@ export class ProductReviewController {
})
async findById(
@param.path.number('id') id: number,
@param.query.object('filter', getFilterSchemaFor(ProductReview)) filter?: Filter<ProductReview>
@param.query.object('filter', getFilterExcludingWhereSchemaFor(ProductReview)) filter?: FilterExcludingWhere<ProductReview>
): Promise<ProductReview> {
return this.barRepository.findById(id, filter);
}
Expand Down Expand Up @@ -191,6 +192,7 @@ import {
Count,
CountSchema,
Filter,
FilterExcludingWhere,
repository,
Where,
} from '@loopback/repository';
Expand Down Expand Up @@ -229,7 +231,7 @@ export class ProductReviewController {
'application/json': {
schema: getModelSchemaRef(ProductReview, {
title: 'NewProductReview',
}),
},
},
Expand Down Expand Up @@ -310,7 +312,7 @@ export class ProductReviewController {
})
async findById(
@param.path.number('id') id: number,
@param.query.object('filter', getFilterSchemaFor(ProductReview)) filter?: Filter<ProductReview>
@param.query.object('filter', getFilterExcludingWhereSchemaFor(ProductReview)) filter?: FilterExcludingWhere<ProductReview>
): Promise<ProductReview> {
return this.barRepository.findById(id, filter);
}
Expand Down
26 changes: 25 additions & 1 deletion packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import {Entity, model, property} from '@loopback/repository';
import {expect} from '@loopback/testlab';
import {getFilterSchemaFor} from '../..';
import {getFilterExcludingWhereSchemaFor, getFilterSchemaFor} from '../..';

describe('filterSchema', () => {
@model({
Expand Down Expand Up @@ -46,6 +46,30 @@ describe('filterSchema', () => {
});
});

it('generate filter schema excluding where', () => {
const schema = getFilterExcludingWhereSchemaFor(MyUserModel);
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',
})
Expand Down
17 changes: 17 additions & 0 deletions packages/openapi-v3/src/filter-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ export function getFilterSchemaFor(modelCtor: typeof Model): SchemaObject {
return schema;
}

/**
* Build an OpenAPI schema describing the format of the "filter" object excluding
* `where` 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 getFilterExcludingWhereSchemaFor(
modelCtor: typeof Model,
): SchemaObject {
const jsonSchema = getFilterJsonSchemaFor(modelCtor, {excludeWhere: true});
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
excludeWhere: true,
});
orderFilterSchema = getFilterJsonSchemaFor(Order);
});

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -182,6 +201,20 @@ describe('getFilterJsonSchemaFor', () => {
}
});

describe('getFilterJsonSchemaForExcludingWhere', () => {
let customerFilterSchema: JsonSchema;

beforeEach(() => {
customerFilterSchema = getFilterJsonSchemaFor(Customer, {
excludeWhere: true,
});
});

it('excludes "where"', () => {
expect(customerFilterSchema.properties).to.not.have.property('where');
});
});

describe('getFilterJsonSchemaForOptionsSetTitle', () => {
let customerFilterSchema: JsonSchema;

Expand Down
63 changes: 36 additions & 27 deletions packages/repository-json-schema/src/filter-json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -14,6 +14,11 @@ export interface FilterSchemaOptions {
*
*/
setTitle?: boolean;

/**
* Set this flag to exclude `where` property. By default, `where` is included.
*/
excludeWhere?: boolean;
}

/**
Expand Down Expand Up @@ -55,38 +60,42 @@ export function getFilterJsonSchemaFor(
modelCtor: typeof Model,
options: FilterSchemaOptions = {},
): JsonSchema {
const schema: JsonSchema = {
...(options.setTitle !== false && {
title: `${modelCtor.modelName}.Filter`,
}),
properties: {
where: getWhereJsonSchemaFor(modelCtor, options),
const properties: Record<string, JSONSchema6Definition> = {
fields: getFieldsJsonSchemaFor(modelCtor, options),

fields: getFieldsJsonSchemaFor(modelCtor, options),

offset: {
type: 'integer',
minimum: 0,
},
offset: {
type: 'integer',
minimum: 0,
},

limit: {
type: 'integer',
minimum: 1,
examples: [100],
},
limit: {
type: 'integer',
minimum: 1,
examples: [100],
},

skip: {
type: 'integer',
minimum: 0,
},
skip: {
type: 'integer',
minimum: 0,
},

order: {
type: 'array',
items: {
type: 'string',
},
order: {
type: 'array',
items: {
type: 'string',
},
},
};

if (!options.excludeWhere) {
properties.where = getWhereJsonSchemaFor(modelCtor, options);
}

const schema: JsonSchema = {
...(options.setTitle !== false && {
title: `${modelCtor.modelName}.Filter`,
}),
properties,
additionalProperties: false,
};

Expand Down
8 changes: 8 additions & 0 deletions packages/repository/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ export interface Filter<MT extends object = AnyObject> {
include?: Inclusion[];
}

/**
* Filter without `where` property
*/
export type FilterExcludingWhere<MT extends object = AnyObject> = Omit<
Filter<MT>,
'where'
>;

/**
* TypeGuard for Filter
* @param candidate
Expand Down
4 changes: 2 additions & 2 deletions packages/repository/src/repositories/legacy-juggler-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '../common-types';
import {EntityNotFoundError} from '../errors';
import {Entity, Model, PropertyType} from '../model';
import {Filter, Inclusion, Where} from '../query';
import {Filter, Inclusion, Where, FilterExcludingWhere} from '../query';
import {
BelongsToAccessor,
BelongsToDefinition,
Expand Down Expand Up @@ -396,7 +396,7 @@ export class DefaultCrudRepository<

async findById(
id: ID,
filter?: Filter<T>,
filter?: FilterExcludingWhere<T>,
options?: Options,
): Promise<T & Relations> {
const include = filter?.include;
Expand Down
8 changes: 6 additions & 2 deletions packages/repository/src/repositories/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {CrudConnector} from '../connectors';
import {DataSource} from '../datasource';
import {EntityNotFoundError} from '../errors';
import {Entity, Model, ValueObject} from '../model';
import {Filter, Where} from '../query';
import {Filter, FilterExcludingWhere, Where} from '../query';
import {InclusionResolver} from '../relations/relation.types';
import {IsolationLevel, Transaction} from '../transaction';

Expand Down Expand Up @@ -303,7 +303,11 @@ export class CrudRepositoryImpl<T extends Entity, ID>
);
}

async findById(id: ID, filter?: Filter<T>, options?: Options): Promise<T> {
async findById(
id: ID,
filter?: FilterExcludingWhere<T>,
options?: Options,
): Promise<T> {
if (typeof this.connector.findById === 'function') {
return this.toModel(
this.connector.findById(this.entityClass, id, options),
Expand Down
Loading

0 comments on commit be725df

Please sign in to comment.