diff --git a/package-lock.json b/package-lock.json index 9c320c6..37fce1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@themost/query", - "version": "2.11.4", + "version": "2.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@themost/query", - "version": "2.11.3", + "version": "2.12.0", "license": "BSD-3-Clause", "dependencies": { "@themost/events": "^1.0.5", diff --git a/package.json b/package.json index eb33208..6c977c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@themost/query", - "version": "2.11.4", + "version": "2.12.0", "description": "MOST Web Framework Codename ZeroGravity - Query Module", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", diff --git a/spec/ClosureParser.spec.js b/spec/ClosureParser.spec.js index e8af0df..a54892f 100644 --- a/spec/ClosureParser.spec.js +++ b/spec/ClosureParser.spec.js @@ -66,7 +66,7 @@ describe('ClosureParser', () => { familyName, givenName }) - .from(People).where( x => { + .from(People).where( (x, identifier) => { return x.id === identifier; }, { identifier @@ -216,7 +216,7 @@ describe('ClosureParser', () => { x.name, x.price }) - .from(Products).where( x => { + .from(Products).where( (x, maximumPrice) => { return x.price < maximumPrice; }, { maximumPrice @@ -226,6 +226,19 @@ describe('ClosureParser', () => { result.forEach( x => { expect(x.price).toBeLessThan(maximumPrice); }); + + a = new QueryExpression().select( x => { + x.name, + x.price + }) + .from(Products).where((x, maximumPrice, category) => { + return x.price < maximumPrice && x.category === category; + }, 1000, 'Desktops'); + result = await db.executeAsync(a); + expect(result.length).toBeTruthy(); + result.forEach( x => { + expect(x.price).toBeLessThan(1000); + }); }); it('should use Date.prototype.getFullYear()', async () => { diff --git a/spec/QueryExpression.where.spec.js b/spec/QueryExpression.where.spec.js index 8b54661..a2f1ed1 100644 --- a/spec/QueryExpression.where.spec.js +++ b/spec/QueryExpression.where.spec.js @@ -499,4 +499,23 @@ describe('QueryExpression.where', () => { expect(results.length).toBe(1); }); + + it('should use param array', async () => { + const People = new QueryEntity('PersonData'); + const emailAddress = 'cameron.ball@example.com'; + let query = new QueryExpression() + .select(({ id, familyName: lastName, givenName: firstName, email, dateCreated }) => { + return { id, lastName, firstName, email, dateCreated } + }) + .from(People) + .where((x, value) => { + return x.email === value; + }, emailAddress) + .take(1); + let results = await db.executeAsync(query); + expect(results.length).toBe(1); + expect(results[0].email).toEqual('cameron.ball@example.com'); + expect(results[0].firstName).toEqual('Cameron'); + }); + }); diff --git a/src/closures/ClosureParser.d.ts b/src/closures/ClosureParser.d.ts index 95af358..3727b65 100644 --- a/src/closures/ClosureParser.d.ts +++ b/src/closures/ClosureParser.d.ts @@ -1,8 +1,8 @@ // MOST Web Framework Codename Zero Gravity Copyright (c) 2017-2022, THEMOST LP All rights reserved import {SyncSeriesEventEmitter} from '@themost/events'; -export type SelectClosure = (x: any) => any; -export type FilterClosure = (x: any) => any; +export type SelectClosure = (x: any, ...params: any[]) => any; +export type FilterClosure = (x: any, ...params: any[]) => any; export declare function count(...args: any): number; export declare function round(n: any, precision?: number): number; @@ -18,8 +18,8 @@ export declare function length(value: any): any; */ export declare class ClosureParser { static binaryToExpressionOperator(binaryOperator: string): string; - parseSelect(func: SelectClosure, params?: any): any; - parseFilter(func: FilterClosure, params?: any): any; + parseSelect(func: SelectClosure, ...params: any[]): any; + parseFilter(func: FilterClosure, ...params: any[]): any; parseCommon(expr: any): any; parseLogical(expr: any): any; parseBinary(expr: any): any; diff --git a/src/closures/ClosureParser.js b/src/closures/ClosureParser.js index b0f9ffe..1ce88a0 100644 --- a/src/closures/ClosureParser.js +++ b/src/closures/ClosureParser.js @@ -14,6 +14,7 @@ import { StringMethodParser } from './StringMethodParser'; import { MathMethodParser } from './MathMethodParser'; import { FallbackMethodParser } from './FallbackMethodParser'; import { SyncSeriesEventEmitter } from '@themost/events'; +import { isObjectDeep } from '../is-object'; let ExpressionTypes = { LogicalExpression: 'LogicalExpression', @@ -94,7 +95,6 @@ function round(n, precision) { } return Math.round(n); } - // noinspection JSCommentMatchesSignature /** * @param {...*} args @@ -243,13 +243,16 @@ class ClosureParser { /** * Parses a javascript expression and returns the equivalent select expression. * @param {Function} func The closure expression to parse - * @param {*} params An object which represents closure parameters + * @param {...*} params An object which represents closure parameters */ + // eslint-disable-next-line no-unused-vars parseSelect(func, params) { if (func == null) { return; } - this.params = params; + const args = Array.from(arguments); + // remove first argument + args.splice(0,1); if (typeof func !== 'function') { throw new Error('Select closure must a function.'); } @@ -259,6 +262,8 @@ class ClosureParser { let funcExpr = expr.body[0].expression.argument; //get named parameters this.namedParams = funcExpr.params; + // parse params + this.parseParams(args); let res = this.parseCommon(funcExpr.body); if (res && res instanceof SequenceExpression) { return res.value.map(function (x) { @@ -303,17 +308,48 @@ class ClosureParser { } throw new Error('Invalid select closure'); } + + parseParams(args) { + // closure params can be: + // 1. an object which has properties with the same name with the arguments of the given closure + // e.g. { p1: 'Peter' } where the closure may be something like (x, p1) => x.givenName === p1 + // or + // 2. a param array where closure arguments should be bound by index + // e.g. where((x, p1) => x.givenName === p1, 'Peter') + // for backward compatibility issues we will try to create an object with closure params + this.params = {}; + this.namedParams.forEach((namedParam, index) => { + // omit the first param because it's the reference of the enumerable object + if (index > 0) { + // preserve backward compatibility + if (args.length === 1 && isObjectDeep(args[0])) { + // get param by name + const [arg0] = args; + if (Object.prototype.hasOwnProperty.call(arg0, namedParam.name)) { + Object.assign(this.params, { + [namedParam.name]: arg0[namedParam.name] + }) + } + } else { + // get param by index + Object.assign(this.params, { + [namedParam.name]: args[index - 1] + }) + } + } + }); + } /** * Parses a javascript expression and returns the equivalent QueryExpression instance. * @param {Function} func The closure expression to parse - * @param {*} params An object which represents closure parameters + * @param {...*} params An object which represents closure parameters */ + // eslint-disable-next-line no-unused-vars parseFilter(func, params) { let self = this; if (func == null) { return; } - this.params = params; //convert the given function to javascript expression let expr = parse('void(' + func.toString() + ')'); //get FunctionExpression @@ -321,8 +357,11 @@ class ClosureParser { if (fnExpr == null) { throw new Error('Invalid closure statement. Closure expression cannot be found.'); } - //get named parameters + // get named parameters self.namedParams = fnExpr.params; + const args = Array.from(arguments); + args.splice(0, 1); + this.parseParams(args); //validate expression e.g. return [EXPRESSION]; if (fnExpr.body.type === ExpressionTypes.MemberExpression) { return this.parseMember(fnExpr.body); @@ -960,10 +999,17 @@ class ClosureParser { } } } + parseIdentifier(expr) { if (this.params && Object.prototype.hasOwnProperty.call(this.params, expr.name)) { return new LiteralExpression(this.params[expr.name]); } + const paramIndex = this.namedParams.findIndex( + (param) => param.type === 'Identifier' && param.name === expr.name + ); + if (paramIndex > 0) { + return new LiteralExpression(this.params[paramIndex - 1]); + } const namedParam0 = this.namedParams && this.namedParams[0]; if (namedParam0.type === 'ObjectPattern') { return this.parseMember(expr); diff --git a/src/is-object.js b/src/is-object.js new file mode 100644 index 0000000..43ce46e --- /dev/null +++ b/src/is-object.js @@ -0,0 +1,39 @@ +import {isPlainObject, isObjectLike, isNative} from 'lodash'; + +const objectToString = Function.prototype.toString.call(Object); + +function isObjectDeep(any) { + // check if it is a plain object + let result = isPlainObject(any); + if (result) { + return result; + } + // check if it's object + if (isObjectLike(any) === false) { + return false; + } + // get prototype + let proto = Object.getPrototypeOf(any); + // if prototype exists, try to validate prototype recursively + while(proto != null) { + // get constructor + const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') + && proto.constructor; + // check if constructor is native object constructor + result = (typeof Ctor == 'function') && (Ctor instanceof Ctor) + && Function.prototype.toString.call(Ctor) === objectToString; + // if constructor is not object constructor and belongs to a native class + if (result === false && isNative(Ctor) === true) { + // return false + return false; + } + // otherwise. get parent prototype and continue + proto = Object.getPrototypeOf(proto); + } + // finally, return result + return result; +} + +export { + isObjectDeep +} \ No newline at end of file diff --git a/src/query.d.ts b/src/query.d.ts index 7b907a8..32f1b03 100644 --- a/src/query.d.ts +++ b/src/query.d.ts @@ -50,7 +50,7 @@ export declare class QueryExpression { hasFields(): boolean; hasPaging(): boolean; distinct(value: any): this; - where(expr: (value: T, ...param: any[]) => any, params?: any): this; + where(expr: (value: T, ...param: any[]) => any, ...params: any[]): this; where(expr: string | any): this; injectWhere(where: any): void; delete(entity: string): this; @@ -61,8 +61,8 @@ export declare class QueryExpression { update(entity: string): this; update(entity: QueryEntity): this; set(obj: any): this; - select(expr: QueryFunc, params?: any): this; - select(expr: QueryJoinFunc, params?: any): this; + select(expr: QueryFunc, ...params: any[]): this; + select(expr: QueryJoinFunc, ...params: any[]): this; select(...expr: (string | any)[]): this; count(alias: string): this; from(entity: string): this; @@ -74,27 +74,27 @@ export declare class QueryExpression { rightJoin(entity: any, props?: any, alias?: any): this; rightJoin(entity: QueryEntity): this; with(obj: any): this; - with(expr: (value: T, otherValue: J, ...param: any[]) => any, params?: any): this; + with(expr: (value: T, otherValue: J, ...param: any[]) => any, ...params: any[]): this; orderBy(expr: string | any): this; - orderBy(expr: QueryFunc, params?: any): this; + orderBy(expr: QueryFunc, ...params: any[]): this; orderByDescending(expr: string | any): this; - orderByDescending(expr: QueryFunc, params?: any): this; + orderByDescending(expr: QueryFunc, ...params: any[]): this; thenBy(expr: string | any): this; thenBy(expr: QueryFunc, params?: any): this; thenByDescending(expr: string | any): this; thenByDescending(expr: (value: T) => any): this; groupBy(...expr: (string | any)[]): this; groupBy(arg1: QueryFunc, params?: any): this; - groupBy(arg1: QueryFunc, arg2: QueryFunc, params?: any): this; - groupBy(arg1: QueryFunc, arg2: QueryFunc, arg3: QueryFunc, params?: any): this; + groupBy(arg1: QueryFunc, arg2: QueryFunc, ...params: any[]): this; + groupBy(arg1: QueryFunc, arg2: QueryFunc, arg3: QueryFunc, ...params: any[]): this; groupBy(arg1: QueryFunc, arg2: QueryFunc, arg3: QueryFunc, - arg4: QueryFunc, params?: any): this; + arg4: QueryFunc, ...params: any[]): this; groupBy(arg1: QueryFunc, arg2: QueryFunc, arg3: QueryFunc, - arg4: QueryFunc, arg5: QueryFunc, params?: any): this; + arg4: QueryFunc, arg5: QueryFunc, ...params: any[]): this; groupBy(arg1: QueryFunc, arg2: QueryFunc, arg3: QueryFunc, - arg4: QueryFunc, arg5: QueryFunc, arg6: QueryFunc, params?: any): this; + arg4: QueryFunc, arg5: QueryFunc, arg6: QueryFunc, ...params: any[]): this; groupBy(arg1: QueryFunc, arg2: QueryFunc, arg3: QueryFunc, - arg4: QueryFunc, arg5: QueryFunc, arg6: QueryFunc , arg7: QueryFunc, params?: any): this; + arg4: QueryFunc, arg5: QueryFunc, arg6: QueryFunc , arg7: QueryFunc, ...params: any[]): this; or(field: any): this; and(field: any): this; equal(value: any): this; diff --git a/src/query.js b/src/query.js index d144415..d18ee9c 100644 --- a/src/query.js +++ b/src/query.js @@ -260,14 +260,15 @@ class QueryExpression { /** * @param {*} expr - * @param {*=} params + * @param {...*} params * @returns {QueryExpression} */ + // eslint-disable-next-line no-unused-vars where(expr, params) { if (typeof expr === 'function') { // parse closure const closureParser = this.getClosureParser(); - this.$where = closureParser.parseFilter(expr, params); + this.$where = closureParser.parseFilter.apply(closureParser, Array.from(arguments)) return this; } if (isNil(expr))