From 842dea8dcc7f4eb2102f7d73cef9361b15638dbe Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 28 Nov 2017 11:31:28 -0800 Subject: [PATCH] feat(repository): Add builders and execute() Add builders with fleunt APIs to build Filter and Where objects Add execute method to Repository interfaces --- packages/repository/src/common-types.ts | 16 + packages/repository/src/connector.ts | 13 + .../repository/src/legacy-juggler-bridge.ts | 16 + .../src/loopback-datasource-juggler.ts | 19 +- packages/repository/src/query.ts | 355 +++++++++++++++- packages/repository/src/repository.ts | 36 +- .../test/unit/query/query-builder.ts | 388 ++++++++++++++++++ .../repository-mixin/repository-mixin.test.ts | 20 +- 8 files changed, 840 insertions(+), 23 deletions(-) create mode 100644 packages/repository/test/unit/query/query-builder.ts diff --git a/packages/repository/src/common-types.ts b/packages/repository/src/common-types.ts index a538860932fe..8515f9cb0580 100644 --- a/packages/repository/src/common-types.ts +++ b/packages/repository/src/common-types.ts @@ -60,3 +60,19 @@ export type Callback = ( err: Error | string | null | undefined, result?: T, ) => void; + +/** + * Type for a command + */ +export type Command = string | AnyObject; + +/** + * Named parameters, such as `{x: 1, y: 'a'}` + */ +export type NamedParameters = AnyObject; + +/** + * Positional parameters, such as [1, 'a'] + */ +// tslint:disable-next-line:no-any +export type PositionalParameters = any[]; diff --git a/packages/repository/src/connector.ts b/packages/repository/src/connector.ts index 3eb7bbb9dcb8..dac0f32fbee4 100644 --- a/packages/repository/src/connector.ts +++ b/packages/repository/src/connector.ts @@ -4,6 +4,13 @@ // License text available at https://opensource.org/licenses/MIT import {Model} from './model'; +import { + AnyObject, + Options, + Command, + NamedParameters, + PositionalParameters, +} from '..'; /** * Common properties/operations for connectors @@ -15,4 +22,10 @@ export interface Connector { connect(): Promise; // Connect to the underlying system disconnect(): Promise; // Disconnect from the underlying system ping(): Promise; // Ping the underlying system + execute?( + command: Command, + // tslint:disable:no-any + parameters: NamedParameters | PositionalParameters, + options?: Options, + ): Promise; } diff --git a/packages/repository/src/legacy-juggler-bridge.ts b/packages/repository/src/legacy-juggler-bridge.ts index 09963a40eb05..8d82ddec0930 100644 --- a/packages/repository/src/legacy-juggler-bridge.ts +++ b/packages/repository/src/legacy-juggler-bridge.ts @@ -14,6 +14,12 @@ import {EntityCrudRepository} from './repository'; export * from './loopback-datasource-juggler'; import {juggler} from './loopback-datasource-juggler'; +import { + AnyObject, + Command, + NamedParameters, + PositionalParameters, +} from '../index'; type DataSourceType = juggler.DataSource; export {DataSourceType}; @@ -195,6 +201,16 @@ export class DefaultCrudRepository return ensurePromise(this.modelClass.exists(id, options)); } + async execute( + command: Command, + // tslint:disable:no-any + parameters: NamedParameters | PositionalParameters, + options?: Options, + ): Promise { + /* istanbul ignore next */ + throw new Error('Not implemented'); + } + protected toEntity(model: DataObject): T { return new this.entityClass(model.toObject()) as T; } diff --git a/packages/repository/src/loopback-datasource-juggler.ts b/packages/repository/src/loopback-datasource-juggler.ts index 7d17aa8cf3a5..f14c0a7ecfc2 100644 --- a/packages/repository/src/loopback-datasource-juggler.ts +++ b/packages/repository/src/loopback-datasource-juggler.ts @@ -101,6 +101,7 @@ export declare namespace juggler { * Base model class */ export class ModelBase { + static dataSource?: DataSource; static modelName: string; static definition: ModelDefinition; static attachTo(ds: DataSource): void; @@ -202,7 +203,7 @@ export declare namespace juggler { eq?: any; neq?: any; gt?: any; - get?: any; + gte?: any; lt?: any; lte?: any; inq?: any[]; @@ -221,18 +222,6 @@ export declare namespace juggler { [property: string]: Condition | any; // Other criteria } - /** - * Order by direction - */ - export type Direction = 'ASC' | 'DESC'; - - /** - * Order by - */ - export interface Order { - [property: string]: Direction; - } - /** * Selection of fields */ @@ -245,7 +234,7 @@ export declare namespace juggler { */ export interface Inclusion { relation: string; - scope: Filter; + scope?: Filter; } /** @@ -254,7 +243,7 @@ export declare namespace juggler { export interface Filter { where?: Where; fields?: Fields; - order?: Order[]; + order?: string[]; limit?: number; skip?: number; offset?: number; diff --git a/packages/repository/src/query.ts b/packages/repository/src/query.ts index a6a1fa5ca460..44898dee2463 100644 --- a/packages/repository/src/query.ts +++ b/packages/repository/src/query.ts @@ -1,3 +1,6 @@ +import {AnyObject} from '..'; +import * as assert from 'assert'; + // Copyright IBM Corp. 2017. All Rights Reserved. // Node module: @loopback/repository // This file is licensed under the MIT License. @@ -29,7 +32,7 @@ export interface Condition { eq?: any; neq?: any; gt?: any; - get?: any; + gte?: any; lt?: any; lte?: any; inq?: any[]; @@ -88,7 +91,7 @@ export interface Fields { */ export interface Inclusion { relation: string; - scope: Filter; + scope?: Filter; } /** @@ -104,9 +107,14 @@ export interface Filter { */ fields?: Fields; /** - * Sorting order for matched entities + * Sorting order for matched entities. Each item should be formatted as + * `fieldName ASC` or `fieldName DESC`. + * For example: `['f1 ASC', 'f2 DESC', 'f3 ASC']`. + * + * We might want to use `Order` in the future. Keep it as `string[]` for now + * for compatibility with LoopBack 3.x. */ - order?: Order[]; + order?: string[]; /** * Maximum number of entities */ @@ -124,3 +132,342 @@ export interface Filter { */ include?: Inclusion[]; } + +/** + * A builder for Where object. It provides fleunt APIs to add clauses such as + * `and`, `or`, and other operators. + * + * @example + * ```ts + * const whereBuilder = new WhereBuilder(); + * const where = whereBuilder + * .eq('a', 1) + * .and({x: 'x'}, {y: {gt: 1}}) + * .and({b: 'b'}, {c: {lt: 1}}) + * .or({d: 'd'}, {e: {neq: 1}}) + * .build(); + * ``` + */ +export class WhereBuilder { + where: Where; + + constructor(w?: Where) { + this.where = w || {}; + } + + private add(w: Where): this { + for (const k of Object.keys(w)) { + if (k in this.where) { + // Found conflicting keys, create an `and` operator to join the existing + // conditions with the new one + this.where = {and: [this.where, w]}; + return this; + } + } + // Merge the where items + this.where = Object.assign(this.where, w); + return this; + } + + /** + * Add an `and` clause. + * @param w One or more where objects + */ + and(...w: (Where | Where[])[]): this { + let clauses: Where[] = []; + w.forEach(where => { + clauses = clauses.concat(Array.isArray(where) ? where : [where]); + }); + return this.add({and: clauses}); + } + + /** + * Add an `or` clause. + * @param w One or more where objects + */ + or(...w: (Where | Where[])[]): this { + let clauses: Where[] = []; + w.forEach(where => { + clauses = clauses.concat(Array.isArray(where) ? where : [where]); + }); + return this.add({or: clauses}); + } + + /** + * Add an `=` condition + * @param key Property name + * @param val Property value + */ + eq(key: string, val: any): this { + return this.add({[key]: val}); + } + + /** + * Add a `!=` condition + * @param key Property name + * @param val Property value + */ + neq(key: string, val: any): this { + return this.add({[key]: {neq: val}}); + } + + /** + * Add a `>` condition + * @param key Property name + * @param val Property value + */ + gt(key: string, val: any): this { + return this.add({[key]: {gt: val}}); + } + + /** + * Add a `>=` condition + * @param key Property name + * @param val Property value + */ + gte(key: string, val: any): this { + return this.add({[key]: {gte: val}}); + } + + /** + * Add a `<` condition + * @param key Property name + * @param val Property value + */ + lt(key: string, val: any): this { + return this.add({[key]: {lt: val}}); + } + + /** + * Add a `<=` condition + * @param key Property name + * @param val Property value + */ + lte(key: string, val: any): this { + return this.add({[key]: {lte: val}}); + } + + /** + * Add a `inq` condition + * @param key Property name + * @param val An array of property values + */ + inq(key: string, val: any[]): this { + return this.add({[key]: {inq: val}}); + } + + /** + * Add a `between` condition + * @param key Property name + * @param val1 Property value lower bound + * @param val2 Property value upper bound + */ + between(key: string, val1: any, val2: any): this { + return this.add({[key]: {between: [val1, val2]}}); + } + + /** + * Add a `exists` condition + * @param key Property name + * @param val Exists or not + */ + exists(key: string, val?: boolean): this { + return this.add({[key]: {exists: !!val || val == null}}); + } + + /** + * Get the where object + */ + build() { + return this.where; + } +} + +/** + * A builder for Filter. It provides fleunt APIs to add clauses such as + * `fields`, `order`, `where`, `limit`, `offset`, and `include`. + * + * @example + * ```ts + * const filterBuilder = new FilterBuilder(); + * const filter = filterBuilder + * .fields('id', a', 'b') + * .limit(10) + * .offset(0) + * .order(['a ASC', 'b DESC']) + * .where({id: 1}) + * .build(); + * ``` + */ +export class FilterBuilder { + filter: Filter; + + constructor(f?: Filter) { + this.filter = f || {}; + } + + /** + * Set `limit` + * @param limit Maximum number of records to be returned + */ + limit(limit: number): this { + assert(limit >= 1, `Limit ${limit} must a positive number`); + this.filter.limit = limit; + return this; + } + + /** + * Set `offset` + * @param offset Offset of the number of records to be returned + */ + offset(offset: number): this { + this.filter.offset = offset; + return this; + } + + /** + * Alias to `offset` + * @param skip + */ + skip(skip: number): this { + return this.offset(skip); + } + + /** + * Describe what fields to be included/excluded + * @param f A field name to be included, an array of field names to be + * included, or an Fields object for the inclusion/exclusion + */ + fields(...f: (Fields | string[] | string)[]): this { + if (!this.filter.fields) { + this.filter.fields = {}; + } + f.forEach(field => { + if (Array.isArray(field)) { + field.forEach(i => (this.filter.fields![i] = true)); + } else if (typeof field === 'string') { + this.filter.fields![field] = true; + } else { + Object.assign(this.filter.fields, field); + } + }); + return this; + } + + private validateOrder(order: string) { + assert(order.match(/^[^\s]+( (ASC|DESC))?$/), 'Invalid order: ' + order); + } + + /** + * Describe the sorting order + * @param f A field name with optional direction, an array of field names, + * or an Order object for the field/direction pairs + */ + order(...o: (string | string[] | Order)[]): this { + if (!this.filter.order) { + this.filter.order = []; + } + o.forEach(order => { + if (typeof order === 'string') { + this.validateOrder(order); + if (!order.endsWith(' ASC') && !order.endsWith(' DESC')) { + order = order + ' ASC'; + } + this.filter.order!.push(order); + return this; + } + if (Array.isArray(order)) { + order.forEach(this.validateOrder); + order = order.map(i => { + if (!i.endsWith(' ASC') && !i.endsWith(' DESC')) { + i = i + ' ASC'; + } + return i; + }); + this.filter.order = this.filter.order!.concat(order); + return this; + } + for (const i in order) { + this.filter.order!.push(`${i} ${order[i]}`); + } + }); + return this; + } + + /** + * Declare `include` + * @param i A relation name, an array of relation names, or an `Inclusion` + * object for the relation/scope definitions + */ + include(...i: (string | string[] | Inclusion)[]): this { + if (!this.filter.include) { + this.filter.include = []; + } + i.forEach(include => { + if (typeof include === 'string') { + this.filter.include!.push({relation: include}); + } else if (Array.isArray(include)) { + include.forEach(inc => this.filter.include!.push({relation: inc})); + } else { + this.filter.include!.push(include); + } + }); + return this; + } + + /** + * Declare a where clause + * @param w Where object + */ + where(w: Where): this { + this.filter.where = w; + return this; + } + + /** + * Return the filter object + */ + build() { + return this.filter; + } +} + +/** + * Get nested properties by path + * @param value Value of an object + * @param path Path to the property + */ +function getDeepProperty(value: AnyObject, path: string): any { + const props = path.split('.'); + for (const p of props) { + value = value[p]; + if (value == null) { + return null; + } + } + return value; +} + +export function filterTemplate(strings: TemplateStringsArray, ...keys: any[]) { + return function filter(ctx: AnyObject) { + const tokens = [strings[0]]; + keys.forEach((key, i) => { + if ( + typeof key === 'object' || + typeof key === 'boolean' || + typeof key === 'number' + ) { + tokens.push(JSON.stringify(key), strings[i + 1]); + return; + } + const value = getDeepProperty(ctx, key); + tokens.push(JSON.stringify(value), strings[i + 1]); + }); + const result = tokens.join(''); + try { + return JSON.parse(result); + } catch (e) { + throw new Error('Invalid JSON: ' + result); + } + }; +} diff --git a/packages/repository/src/repository.ts b/packages/repository/src/repository.ts index 916910b34de7..978190f374fb 100644 --- a/packages/repository/src/repository.ts +++ b/packages/repository/src/repository.ts @@ -4,14 +4,35 @@ // License text available at https://opensource.org/licenses/MIT import {Entity, ValueObject, Model} from './model'; -import {Class, DataObject, Options} from './common-types'; +import { + Class, + DataObject, + Options, + AnyObject, + Command, + NamedParameters, + PositionalParameters, +} from './common-types'; import {DataSource} from './datasource'; import {CrudConnector} from './crud-connector'; import {Filter, Where} from './query'; // tslint:disable:no-unused-variable -export interface Repository {} +export interface Repository { + /** + * Execute a query with the given parameter object or an array of parameters + * @param command The query string or command object + * @param parameters The object with name/value pairs or an array of parameter + * values + * @param options Options + */ + execute( + command: Command, + parameters: NamedParameters | PositionalParameters, + options?: Options, + ): Promise; +} /** * Basic CRUD operations for ValueObject and Entity. No ID is required. @@ -305,4 +326,15 @@ export class CrudRepositoryImpl return this.count(where, options).then(result => result > 0); } } + + execute( + command: Command, + parameters: NamedParameters | PositionalParameters, + options?: Options, + ): Promise { + if (typeof this.connector.execute !== 'function') { + throw new Error('Not implemented'); + } + return this.connector.execute(command, parameters, options); + } } diff --git a/packages/repository/test/unit/query/query-builder.ts b/packages/repository/test/unit/query/query-builder.ts new file mode 100644 index 000000000000..dc51bc5898cf --- /dev/null +++ b/packages/repository/test/unit/query/query-builder.ts @@ -0,0 +1,388 @@ +// Copyright IBM Corp. 2013,2017. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import { + FilterBuilder, + Filter, + WhereBuilder, + Where, + filterTemplate, +} from '../../../'; + +describe('WhereBuilder', () => { + it('builds where object', () => { + const whereBuilder = new WhereBuilder(); + const where = whereBuilder + .eq('a', 1) + .gt('b', 2) + .lt('c', 2) + .eq('x', 'x') + .build(); + expect(where).to.eql({a: 1, b: {gt: 2}, c: {lt: 2}, x: 'x'}); + }); + + it('builds where object with multiple clauses using the same key', () => { + const whereBuilder = new WhereBuilder(); + const where = whereBuilder + .gt('a', 2) + .lt('a', 4) + .build(); + expect(where).to.eql({and: [{a: {gt: 2}}, {a: {lt: 4}}]}); + }); + + it('builds where object with inq', () => { + const whereBuilder = new WhereBuilder(); + const where = whereBuilder + .inq('x', [1, 2, 3]) + .inq('y', ['a', 'b']) + .build(); + expect(where).to.eql({x: {inq: [1, 2, 3]}, y: {inq: ['a', 'b']}}); + }); + + it('builds where object with neq', () => { + const whereBuilder = new WhereBuilder(); + const where = whereBuilder.neq('x', 1).build(); + expect(where).to.eql({x: {neq: 1}}); + }); + + it('builds where object with gte', () => { + const whereBuilder = new WhereBuilder(); + const where = whereBuilder.gte('x', 1).build(); + expect(where).to.eql({x: {gte: 1}}); + }); + + it('builds where object with lte', () => { + const whereBuilder = new WhereBuilder(); + const where = whereBuilder.lte('x', 1).build(); + expect(where).to.eql({x: {lte: 1}}); + }); + + it('builds where object with exists', () => { + const whereBuilder = new WhereBuilder(); + const where = whereBuilder.exists('x', true).build(); + expect(where).to.eql({x: {exists: true}}); + }); + + it('builds where object with exists default to true', () => { + const whereBuilder = new WhereBuilder(); + const where = whereBuilder.exists('x').build(); + expect(where).to.eql({x: {exists: true}}); + }); + + it('builds where object with between', () => { + const whereBuilder = new WhereBuilder(); + const where = whereBuilder + .between('x', 1, 2) + .between('y', 'a', 'b') + .build(); + expect(where).to.eql({x: {between: [1, 2]}, y: {between: ['a', 'b']}}); + }); + + it('builds where object with or', () => { + const whereBuilder = new WhereBuilder(); + const where = whereBuilder + .eq('a', 1) + .gt('b', 2) + .lt('c', 2) + .or({x: 'x'}, {y: {gt: 1}}, [{a: 1}, {b: 2}]) + .build(); + expect(where).to.eql({ + a: 1, + b: {gt: 2}, + c: {lt: 2}, + or: [{x: 'x'}, {y: {gt: 1}}, {a: 1}, {b: 2}], + }); + }); + + it('builds where object with and', () => { + const whereBuilder = new WhereBuilder(); + const where = whereBuilder + .eq('a', 1) + .gt('b', 2) + .lt('c', 2) + .and({x: 'x'}, {y: {gt: 1}}, [{a: 1}, {b: 2}]) + .build(); + expect(where).to.eql({ + a: 1, + b: {gt: 2}, + c: {lt: 2}, + and: [{x: 'x'}, {y: {gt: 1}}, {a: 1}, {b: 2}], + }); + }); + + it('builds where object with existing and', () => { + const whereBuilder = new WhereBuilder(); + const where = whereBuilder + .eq('a', 1) + .and({x: 'x'}, {y: {gt: 1}}) + .and({b: 'b'}, {c: {lt: 1}}) + .build(); + expect(where).to.eql({ + and: [ + { + a: 1, + and: [{x: 'x'}, {y: {gt: 1}}], + }, + { + and: [{b: 'b'}, {c: {lt: 1}}], + }, + ], + }); + }); + + it('builds where object from an existing one', () => { + const whereBuilder = new WhereBuilder({y: 'y'}); + const where = whereBuilder + .eq('a', 1) + .gt('b', 2) + .lt('c', 2) + .eq('x', 'x') + .build(); + expect(where).to.eql({y: 'y', a: 1, b: {gt: 2}, c: {lt: 2}, x: 'x'}); + }); +}); + +describe('FilterBuilder', () => { + it('builds a filter object with field names', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.fields('a', 'b', 'c'); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + fields: { + a: true, + b: true, + c: true, + }, + }); + }); + + it('builds a filter object with field object', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.fields({a: true, b: false}); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + fields: { + a: true, + b: false, + }, + }); + }); + + it('builds a filter object with mixed field names/objects/arrays', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.fields({a: true, b: false}, 'c', ['d', 'e']); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + fields: { + a: true, + b: false, + c: true, + d: true, + e: true, + }, + }); + }); + + it('builds a filter object with multiple fields', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder + .fields({a: true, b: false}) + .fields('c') + .fields(['d', 'e']); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + fields: { + a: true, + b: false, + c: true, + d: true, + e: true, + }, + }); + }); + + it('builds a filter object with limit/offset', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.limit(10).offset(5); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + limit: 10, + offset: 5, + }); + }); + + it('builds a filter object with limit/skip', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.limit(10).skip(5); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + limit: 10, + offset: 5, + }); + }); + + it('validates limit', () => { + expect(() => { + const filterBuilder = new FilterBuilder(); + filterBuilder.limit(-10).offset(5); + }).to.throw(/Limit \-10 must a positive number/); + }); + + it('builds a filter object with order names', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.order('a', 'b', 'c'); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + order: ['a ASC', 'b ASC', 'c ASC'], + }); + }); + + it('builds a filter object with order object', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.order({a: 'ASC', b: 'DESC'}); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + order: ['a ASC', 'b DESC'], + }); + }); + + it('builds a filter object with mixed order names/objects/arrays', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.order({a: 'ASC', b: 'DESC'}, 'c DESC', ['d', 'e DESC']); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + order: ['a ASC', 'b DESC', 'c DESC', 'd ASC', 'e DESC'], + }); + }); + + it('builds a filter object with multiple orders', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.order('a', 'b').order('c DESC'); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + order: ['a ASC', 'b ASC', 'c DESC'], + }); + }); + + it('validates order', () => { + expect(() => { + const filterBuilder = new FilterBuilder(); + filterBuilder.order('a x'); + }).to.throw(/Invalid order/); + }); + + it('builds a filter object with where', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.where({x: 1, and: [{a: {gt: 2}}, {b: 2}]}); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + where: {x: 1, and: [{a: {gt: 2}}, {b: 2}]}, + }); + }); + + it('builds a filter object with included relation names', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.include('orders', 'friends'); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + include: [{relation: 'orders'}, {relation: 'friends'}], + }); + }); + + it('builds a filter object with included an array of relation names', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.include(['orders', 'friends']); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + include: [{relation: 'orders'}, {relation: 'friends'}], + }); + }); + + it('builds a filter object with inclusion objects', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.include( + {relation: 'orders'}, + {relation: 'friends', scope: {where: {name: 'ray'}}}, + ); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + include: [ + {relation: 'orders'}, + {relation: 'friends', scope: {where: {name: 'ray'}}}, + ], + }); + }); + + it('builds a filter object with multiple includes', () => { + const filterBuilder = new FilterBuilder(); + filterBuilder.include(['orders']).include('friends'); + const filter = filterBuilder.build(); + expect(filter).to.eql({ + include: [{relation: 'orders'}, {relation: 'friends'}], + }); + }); +}); + +describe('FilterTemplate', () => { + it('builds filter object', () => { + const filter = filterTemplate`{"limit": ${'limit'}, + "where": {${'key'}: ${'value'}}}`; + const result = filter({limit: 10, key: 'name', value: 'John'}); + expect(result).to.eql({ + limit: 10, + where: { + name: 'John', + }, + }); + }); + + it('builds filter object with nesting properties', () => { + const filter = filterTemplate`{"limit": ${'pagination.limit'}, + "where": {${'key'}: ${'value'}}}`; + const result = filter({ + pagination: {limit: 10}, + key: 'age', + value: 25, + }); + expect(result).to.eql({ + limit: 10, + where: { + age: 25, + }, + }); + }); + + it('builds filter object with null nesting properties', () => { + const filter = filterTemplate`{"where": {${'x.key'}: ${'x.value'}}}`; + const result = filter({x: {key: 'name'}}); + expect(result).to.eql({ + where: { + name: null, + }, + }); + }); + + it('builds filter object with numner literals', () => { + const value = 25; + const filter = filterTemplate`{"where": {${'key'}: ${value}}}`; + const result = filter({ + key: 'age', + }); + expect(result).to.eql({ + where: { + age: 25, + }, + }); + }); + + it('reports error if the template does not generate a valid json object', () => { + expect(() => { + const filter = filterTemplate`{"limit": ${'limit'}, + where": {${'key'}: ${'value'}}}`; + filter({limit: 10, key: 'name', value: 'John'}); + }).throw(/Invalid JSON/); + }); +}); diff --git a/packages/repository/test/unit/repository-mixin/repository-mixin.test.ts b/packages/repository/test/unit/repository-mixin/repository-mixin.test.ts index 74ae34fa6926..fd62540ae761 100644 --- a/packages/repository/test/unit/repository-mixin/repository-mixin.test.ts +++ b/packages/repository/test/unit/repository-mixin/repository-mixin.test.ts @@ -9,6 +9,12 @@ import { juggler, DataSourceConstructor, Class, + Options, + Repository, + AnyObject, + Command, + NamedParameters, + PositionalParameters, } from '../../../'; import {Application, Component} from '@loopback/core'; @@ -69,8 +75,8 @@ describe('RepositoryMixin', () => { class AppWithRepoMixin extends RepositoryMixin(Application) {} - class NoteRepo { - model: any; + class NoteRepo implements Repository { + model: typeof juggler.PersistedModel; constructor() { const ds: juggler.DataSource = new DataSourceConstructor({ @@ -84,6 +90,16 @@ describe('RepositoryMixin', () => { {}, ); } + + async execute( + query: Command, + // tslint:disable:no-any + parameters: NamedParameters | PositionalParameters, + options?: Options, + ): Promise { + /* istanbul ignore next */ + throw Error('Not implemented'); + } } class TestComponent {