diff --git a/data-configuration.d.ts b/data-configuration.d.ts index f7fdbe0..57c3811 100644 --- a/data-configuration.d.ts +++ b/data-configuration.d.ts @@ -45,6 +45,20 @@ export declare interface AuthSettingsConfiguration { loginPage?: string; } +export declare type DataAdapterConstructor = Function & { prototype: T }; + +export declare interface CreateDataAdapterInstance { + name: string; + invariantName?: string; + createInstance?(options: any): any; +} + +export declare interface DataAdapterType { + name: string; + invariantName?: string; + type?: DataAdapterConstructor; +} + export declare class DataConfiguration { constructor(configPath: string); static getCurrent(): DataConfiguration; @@ -58,7 +72,7 @@ export declare class DataConfigurationStrategy extends ConfigurationStrategy{ readonly dataTypes: Map; readonly adapters: Array; - readonly adapterTypes: Array; + readonly adapterTypes: Map; getAuthSettings(): AuthSettingsConfiguration; getAdapterType(invariantName: string): DataAdapterTypeConfiguration; hasDataType(name: string): boolean; diff --git a/data-model.d.ts b/data-model.d.ts index 62854a0..571c656 100644 --- a/data-model.d.ts +++ b/data-model.d.ts @@ -43,9 +43,13 @@ export declare class DataModel extends SequentialEventEmitter{ filter(params: any, callback?: (err?: Error, res?: any) => void): void; filterAsync(params: any): Promise; find(obj: any):DataQueryable; + select(expr: (value: T, ...param: any) => any, ...params: any[]): DataQueryable; + select(expr: (value1: T, value2: J, ...param: any) => any, ...params: any[]): DataQueryable; select(...attr: any[]): DataQueryable; - orderBy(attr: any): DataQueryable; - orderByDescending(attr: any): DataQueryable; + orderBy(attr: any): this; + orderBy(expr: (value: T, ...params: any[]) => any): this; + orderByDescending(attr: any): this; + orderByDescending(expr: (value: T) => any, ...params: any[]): this; take(n: number): DataQueryable; getList():Promise; skip(n: number): DataQueryable; diff --git a/data-queryable.d.ts b/data-queryable.d.ts index 6d5f553..9a62d9e 100644 --- a/data-queryable.d.ts +++ b/data-queryable.d.ts @@ -1,7 +1,7 @@ // MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved import {DataModel} from "./data-model"; import {DataContextEmitter} from "./types"; -import {QueryExpression} from '@themost/query'; +import {QueryExpression, QueryFunc, QueryJoinFunc} from '@themost/query'; export declare class DataQueryable implements DataContextEmitter { constructor(model: DataModel); @@ -9,6 +9,7 @@ export declare class DataQueryable implements DataContextEmitter { readonly query: QueryExpression; clone(): this; where(attr: string): this; + where(expr: QueryFunc, ...params: unknown[]): this; search(text: string): this; join(model: string): this; and(attr: string): this; @@ -29,12 +30,29 @@ export declare class DataQueryable implements DataContextEmitter { contains(value: any): this; notContains(value: any): this; between(value1: any, value2: any): this; + select(expr: (value: T, ...param: any) => any, ...params: any[]): this; + select(expr: (value1: T, value2: J, ...param: any) => any, ...params: any[]): this; select(...attr: any[]): this; orderBy(attr: any): this; + orderBy(expr: (value: T, ...params: any[]) => any): this; orderByDescending(attr: any): this; + orderByDescending(expr: (value: T) => any, ...params: any[]): this; thenBy(attr: any): this; + thenBy(expr: (value: T) => any, ...params: any[]): this; thenByDescending(attr: any): this; + thenByDescending(expr: (value: T) => any, ...params: any[]): this; groupBy(...attr: any[]): this; + groupBy(...args: [...expr:[(value: T) => any], ...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; + groupBy(arg1: QueryFunc, arg2: QueryFunc, arg3: QueryFunc, + arg4: QueryFunc, arg5: QueryFunc, ...params: any[]): this; + groupBy(arg1: QueryFunc, arg2: QueryFunc, arg3: QueryFunc, + 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; skip(n:number): this; take(n:number): this; getItem(): Promise; diff --git a/data-queryable.js b/data-queryable.js index 3d11aa3..70ed6ef 100644 --- a/data-queryable.js +++ b/data-queryable.js @@ -15,6 +15,93 @@ var aliasProperty = Symbol('alias'); var {hasOwnProperty} = require('./has-own-property'); var {isObjectDeep} = require('./is-object'); var { UnknownAttributeError } = require('./data-errors'); +var { DataExpandResolver } = require('./data-expand-resolver'); +var {instanceOf} = require('./instance-of'); + +/** + * @param {DataQueryable} target + */ +function resolveJoinMember(target) { + return function onResolvingJoinMember(event) { + /** + * @type {Array} + */ + var fullyQualifiedMember = event.fullyQualifiedMember.split('.'); + var expr = DataAttributeResolver.prototype.resolveNestedAttribute.call(target, fullyQualifiedMember.join('/')); + if (instanceOf(expr, QueryField)) { + var member = expr.$name.split('.'); + Object.assign(event, { + object: member[0], + member: member[1] + }) + } + if (expr instanceof Expression) { + Object.assign(event, { + member: expr + }) + } + } +} + +// eslint-disable-next-line no-unused-vars +function resolveZeroOrOneJoinMember(target) { + /** + * This method tries to resolve a join member e.g. product.productDimensions + * when this member defines a zero-or-one association + */ + return function onResolvingZeroOrOneJoinMember(event) { + /** + * @type {Array} + */ + // eslint-disable-next-line no-unused-vars + var fullyQualifiedMember = event.fullyQualifiedMember.split('.'); + } +} + +/** + * @param {DataQueryable} target + */ +function resolveMember(target) { + /** + * @param {member:string} event + */ + return function onResolvingMember(event) { + var collection = target.model.viewAdapter; + var member = event.member.replace(new RegExp('^' + collection + '.'), ''); + /** + * @type {import('./types').DataAssociationMapping} + */ + var mapping = target.model.inferMapping(member); + if (mapping == null) { + return; + } + /** + * @type {import('./types').DataField} + */ + var attribute = target.model.getAttribute(member); + if (attribute.multiplicity === 'ZeroOrOne') { + var resolveMember = null; + if (mapping.associationType === 'junction' && mapping.parentModel === self.name) { + // expand child field + resolveMember = attribute.name.concat('/', mapping.childField); + } else if (mapping.associationType === 'junction' && mapping.childModel === self.name) { + // expand parent field + resolveMember = attribute.name.concat('/', mapping.parentField); + } else if (mapping.associationType === 'association' && mapping.parentModel === target.model.name) { + var associatedModel = target.model.context.model(mapping.childModel); + resolveMember = attribute.name.concat('/', associatedModel.primaryKey); + } + if (resolveMember) { + // resolve attribute + var expr = DataAttributeResolver.prototype.resolveNestedAttribute.call(target, resolveMember); + if (instanceOf(expr, QueryField)) { + event.member = expr.$name; + } + } + } + + } +} /** * @param {DataQueryable} target @@ -861,18 +948,6 @@ DataQueryable.prototype.ensureContext = function() { * Serializes the underlying query and clears current filter expression for further filter processing. This operation may be used in complex filtering. * @param {Boolean=} useOr - Indicates whether an or statement will be used in the resulted statement. * @returns {DataQueryable} - * @example - //retrieve a list of order - context.model('Order') - .where('orderStatus').equal(1).and('paymentMethod').equal(2) - .prepare().where('orderStatus').equal(2).and('paymentMethod').equal(2) - .prepare(true) - //(((OrderData.orderStatus=1) AND (OrderData.paymentMethod=2)) OR ((OrderData.orderStatus=2) AND (OrderData.paymentMethod=2))) - .list().then(function(result) { - done(null, result); - }).catch(function(err) { - done(err); - }); */ DataQueryable.prototype.prepare = function(useOr) { this.query.prepare(useOr); @@ -881,20 +956,27 @@ DataQueryable.prototype.prepare = function(useOr) { /** * Initializes a where expression - * @param attr {string} - A string which represents the field name that is going to be used as the left operand of this expression + * @param attr {string|*} - A string which represents the field name that is going to be used as the left operand of this expression * @returns {DataQueryable} - * @example - context.model('Person') - .where('user/name').equal('user1@exampl.com') - .select('description') - .first().then(function(result) { - done(null, result); - }).catch(function(err) { - done(err); - }); */ DataQueryable.prototype.where = function(attr) { Args.check(this.query.$where == null, new Error('The where expression has already been initialized.')); + // get arguments as array + var args = Array.from(arguments); + if (typeof args[0] === 'function') { + /** + * @type {import("@themost/query").QueryExpression} + */ + var query = this.query; + var onResolvingJoinMember = resolveJoinMember(this); + query.resolvingJoinMember.subscribe(onResolvingJoinMember); + try { + query.where.apply(query, args); + } finally { + query.resolvingJoinMember.unsubscribe(onResolvingJoinMember); + } + return this; + } if (typeof attr === 'string' && /\//.test(attr)) { this.query.where(DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attr)); return this; @@ -1063,6 +1145,7 @@ DataQueryable.prototype.or = function(attr) { * @param {*} obj * @returns {*} */ +// eslint-disable-next-line no-unused-vars function resolveValue(obj) { var self = this; if (typeof obj === 'string' && /^\$it\//.test(obj)) { @@ -1463,59 +1546,35 @@ function select_(arg) { /** * Selects a field or a collection of fields of the current model. - * @param {...string} attr An array of fields, a field or a view name + * @param {...*} attr An array of fields, a field or a view name * @returns {DataQueryable} - * @example - //retrieve the last 5 orders - context.model('Order').select('id','customer','orderDate','orderedItem') - .orderBy('orderDate') - .take(5).list().then(function(result) { - console.table(result.records); - done(null, result); - }).catch(function(err) { - done(err); - }); - * @example - //retrieve the last 5 orders by getting the associated customer name and product name - context.model('Order').select('id','customer/description as customerName','orderDate','orderedItem/name as productName') - .orderBy('orderDate') - .take(5).list().then(function(result) { - console.table(result.records); - done(null, result); - }).catch(function(err) { - done(err); - }); - @example //The result set of this example may be: - id customerName orderDate orderedItemName - --- ------------------- ----------------------------- ---------------------------------------------------- - 46 Nicole Armstrong 2014-12-31 13:35:41.000+02:00 LaCie Blade Runner - 288 Cheyenne Hudson 2015-01-01 13:24:21.000+02:00 Canon Pixma MG5420 Wireless Photo All-in-One Printer - 139 Christian Whitehead 2015-01-01 23:21:24.000+02:00 Olympus OM-D E-M1 - 3 Katelyn Kelly 2015-01-02 04:42:58.000+02:00 Kobo Aura - 59 Cheyenne Hudson 2015-01-02 10:47:53.000+02:00 Google Nexus 7 (2013) - - @example - //retrieve the best customers by getting the associated customer name and a count of orders made by the customer - context.model('Order').select('customer/description as customerName','count(id) as orderCount') - .orderBy('count(id)') - .groupBy('customer/description') - .take(3).list().then(function(result) { - done(null, result); - }).catch(function(err) { - done(err); - }); - @example //The result set of this example may be: - customerName orderCount - ---------------- ---------- - Miranda Bird 19 - Alex Miles 16 - Isaiah Morton 16 */ DataQueryable.prototype.select = function(attr) { var self = this, arr, expr, arg = (arguments.length>1) ? Array.prototype.slice.call(arguments): attr; + // get arguments as array + var args = Array.from(arguments); + if (typeof args[0] === 'function') { + /** + * @type {import("@themost/query").QueryExpression} + */ + var query = this.query; + var onResolvingJoinMember = resolveJoinMember(this); + query.resolvingJoinMember.subscribe(onResolvingJoinMember); + var onResolvingMember = resolveMember(this); + query.resolvingMember.subscribe(onResolvingMember); + try { + query.select.apply(query, args); + } finally { + query.resolvingJoinMember.unsubscribe(onResolvingJoinMember); + query.resolvingMember.unsubscribe(onResolvingMember); + } + return this; + } + + if (typeof arg === 'string') { if (arg==='*') { //delete select @@ -1811,8 +1870,23 @@ DataQueryable.prototype.orderBy = function(attr) { HR6205 Samsung Galaxy Note 10.1 (2014 Edition) 2 */ DataQueryable.prototype.groupBy = function(attr) { - var arr = [], - arg = (arguments.length>1) ? Array.prototype.slice.call(arguments): attr; + var arr = []; + var arg = (arguments.length>1) ? Array.prototype.slice.call(arguments): attr; + var args = Array.from(arguments); + if (typeof args[0] === 'function') { + /** + * @type {import("@themost/query").QueryExpression} + */ + var query = this.query; + var onResolvingJoinMember = resolveJoinMember(this); + query.resolvingJoinMember.subscribe(onResolvingJoinMember); + try { + query.groupBy.apply(query, args); + } finally { + query.resolvingJoinMember.unsubscribe(onResolvingJoinMember); + } + return this; + } if (_.isArray(arg)) { for (var i = 0; i < arg.length; i++) { var x = arg[i]; @@ -2973,9 +3047,52 @@ DataQueryable.prototype.cache = function(value) { */ DataQueryable.prototype.expand = function(attr) { - var self = this, - arg = (arguments.length>1) ? Array.prototype.slice.call(arguments): [attr]; + var self = this; + var arg = (arguments.length>1) ? Array.prototype.slice.call(arguments): [attr]; var expanded; + if (typeof attr === 'function') { + var args = Array.from(arguments); + try { + /** + * @type {import("@themost/query").QueryExpression} + */ + var query = this.clone().query; + // clear select + var onResolvingMember = function(event) { + var member = event.member.split('.'); + self.expand(member[1]); + }; + var onResolvingJoinMember = function(event) { + /** + * @type {string} + */ + var member = event.fullyQualifiedMember; + var index = member.lastIndexOf('.'); + while(index >= 0) { + member = member.substring(0, index) + '($expand=' + member.substring(index + 1, member.length) + ')' + index = member.lastIndexOf('.'); + } + var result = new DataExpandResolver().test(member); + if (result && result.length) { + self.expand(result[0]); + } + }; + query.resolvingMember.subscribe(onResolvingMember); + query.resolvingJoinMember.subscribe(onResolvingJoinMember); + // check if last argument is closure parameters + let params = null; + if (typeof args[args.length - 1] !== 'function') { + params = args.pop(); + } + args.forEach(function(argument) { + query.select.call(query, argument, params); + }); + } finally { + query.resolvingMember.unsubscribe(onResolvingMember); + query.resolvingJoinMember.unsubscribe(onResolvingJoinMember); + } + return this; + } if (_.isNil(arg)) { delete self.$expand; } diff --git a/package-lock.json b/package-lock.json index 7e6b929..fe8aa6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@themost/data", - "version": "2.6.71", + "version": "2.6.72", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@themost/data", - "version": "2.6.71", + "version": "2.6.72", "license": "BSD-3-Clause", "dependencies": { "@themost/events": "^1.3.0", @@ -27,7 +27,7 @@ "@themost/common": "^2.5.11", "@themost/json-logger": "^1.1.0", "@themost/peers": "^1.0.2", - "@themost/query": "^2.6.0", + "@themost/query": "^2.6.73", "@themost/sqlite": "^2.8.4", "@themost/xml": "^2.5.2", "@types/core-js": "^2.5.0", @@ -5218,9 +5218,9 @@ "integrity": "sha512-wlMYRsNWaz5EJ7AwCIA3yw2kHy4p36a4VTz8XmyYTYDYaKgmXjTi6IJPB/+di0mt/rf6NfFfsYwrmytoLseiIg==" }, "node_modules/@themost/query": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@themost/query/-/query-2.6.0.tgz", - "integrity": "sha512-kS+m0nSBKgz/sa5Fq2o4litgyTe5V8HImnmnJc+mFtqBWnteKd3+6xqtWJZv0TlTILz4t7pdq3JM21VBNBMXcA==", + "version": "2.6.73", + "resolved": "https://registry.npmjs.org/@themost/query/-/query-2.6.73.tgz", + "integrity": "sha512-Zl2FqaY6VQfmX7dX9xLcg0RV+TfifRG+onR8b1odTEyMZgKeiqCAUTakMbT7ADaBKYUe0Wat9iI4gZSFPHs7Og==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5228,7 +5228,6 @@ "async": "^2.6.4", "esprima": "^4.0.1", "lodash": "^4.17.15", - "package-lock-only": "^0.0.4", "sprintf-js": "^1.1.2", "symbol": "^0.3.1" }, @@ -10976,17 +10975,6 @@ "node": ">=6" } }, - "node_modules/package-lock-only": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/package-lock-only/-/package-lock-only-0.0.4.tgz", - "integrity": "sha512-fV1YHeTMWH5LKmdVqfWskm2/SG0iF2IrxJn3ziaPVx9CnpecGJzt8xXtLV+CYINENZwPFMtbxO5qupz0asNz1A==", - "dev": true, - "hasInstallScript": true, - "license": "ISC", - "dependencies": { - "chalk": "^2.4.1" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -16251,16 +16239,15 @@ "integrity": "sha512-wlMYRsNWaz5EJ7AwCIA3yw2kHy4p36a4VTz8XmyYTYDYaKgmXjTi6IJPB/+di0mt/rf6NfFfsYwrmytoLseiIg==" }, "@themost/query": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@themost/query/-/query-2.6.0.tgz", - "integrity": "sha512-kS+m0nSBKgz/sa5Fq2o4litgyTe5V8HImnmnJc+mFtqBWnteKd3+6xqtWJZv0TlTILz4t7pdq3JM21VBNBMXcA==", + "version": "2.6.73", + "resolved": "https://registry.npmjs.org/@themost/query/-/query-2.6.73.tgz", + "integrity": "sha512-Zl2FqaY6VQfmX7dX9xLcg0RV+TfifRG+onR8b1odTEyMZgKeiqCAUTakMbT7ADaBKYUe0Wat9iI4gZSFPHs7Og==", "dev": true, "requires": { "@themost/events": "^1.0.5", "async": "^2.6.4", "esprima": "^4.0.1", "lodash": "^4.17.15", - "package-lock-only": "^0.0.4", "sprintf-js": "^1.1.2", "symbol": "^0.3.1" } @@ -20615,15 +20602,6 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, - "package-lock-only": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/package-lock-only/-/package-lock-only-0.0.4.tgz", - "integrity": "sha512-fV1YHeTMWH5LKmdVqfWskm2/SG0iF2IrxJn3ziaPVx9CnpecGJzt8xXtLV+CYINENZwPFMtbxO5qupz0asNz1A==", - "dev": true, - "requires": { - "chalk": "^2.4.1" - } - }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/package.json b/package.json index fb79dcf..fee6504 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@themost/data", - "version": "2.6.71", + "version": "2.6.72", "description": "MOST Web Framework Codename Blueshift - Data module", "main": "index.js", "types": "index.d.ts", @@ -9,7 +9,7 @@ }, "peerDependencies": { "@themost/common": "^2.5.11", - "@themost/query": "^2.6.0", + "@themost/query": "lts", "@themost/xml": "^2.5.2" }, "engines": { @@ -44,7 +44,7 @@ "@themost/common": "^2.5.11", "@themost/json-logger": "^1.1.0", "@themost/peers": "^1.0.2", - "@themost/query": "^2.6.0", + "@themost/query": "^2.6.73", "@themost/sqlite": "^2.8.4", "@themost/xml": "^2.5.2", "@types/core-js": "^2.5.0", diff --git a/spec/ClosureParser.spec.ts b/spec/ClosureParser.spec.ts new file mode 100644 index 0000000..279505b --- /dev/null +++ b/spec/ClosureParser.spec.ts @@ -0,0 +1,239 @@ +import {TestUtils} from './adapter/TestUtils'; +import { TestApplication } from './TestApplication'; +import { DataContext } from '../types'; +import { DataConfigurationStrategy } from '../data-configuration'; +import { round, count } from '@themost/query'; +import { resolve } from 'path'; +const { executeInTransaction } = TestUtils; + +describe('ClosureParser', () => { + + let app: TestApplication; + let context: DataContext; + + beforeAll(async () => { + app = new TestApplication(resolve(__dirname, 'test2')); + context = app.createContext(); + }); + + afterAll(async () => { + await context.finalizeAsync(); + await app.finalize(); + }); + + it('should use select closure', async () => { + await executeInTransaction(context, async () => { + const items = await context.model('Product').select((x: any) => { + return { + id: x.id, + name: x.name, + category: x.category, + newPrice: round(x.price, 2) + } + }).silent().take(10).getItems(); + expect(Array.isArray(items)).toBeTruthy(); + expect(items.length).toBeTruthy(); + }); + }); + + it('should use select closure with params', async () => { + await executeInTransaction(context, async () => { + const items = await context.model('Product').asQueryable().select((x: any, digits: number) => { + return { + id: x.id, + name: x.name, + category: x.category, + newPrice: round(x.price, digits) + } + }, 2).silent().take(10).getItems(); + expect(Array.isArray(items)).toBeTruthy(); + expect(items.length).toBeTruthy(); + }); + }); + + it('should use select closure with nested attributes', async () => { + await executeInTransaction(context, async () => { + const items = await context.model('Order').select((x: any) => { + return { + id: x.id, + customer: x.customer.givenName.concat(' ', x.customer.familyName), + streetAddress: x.customer.address.streetAddress, + orderedItem: x.orderedItem.name, + orderStatus: x.orderStatus.name, + orderDate: x.orderDate + } + }).silent().take(10).getItems(); + expect(Array.isArray(items)).toBeTruthy(); + expect(items.length).toBeTruthy(); + for (const item of items) { + expect(Object.prototype.hasOwnProperty.call(item, 'streetAddress')).toBeTruthy(); + expect(typeof item.streetAddress === 'string').toBeTruthy(); + expect(Object.prototype.hasOwnProperty.call(item, 'customer')).toBeTruthy(); + expect(typeof item.customer === 'string').toBeTruthy(); + } + }); + }); + + it('should use select closure with functions', async () => { + await executeInTransaction(context, async () => { + const items = await context.model('Order').select((x: any) => { + return { + id: x.id, + customer: x.customer.givenName.concat(' ', x.customer.familyName), + streetAddress: x.customer.address.streetAddress, + orderedItem: x.orderedItem.name, + releaseYear: x.orderedItem.releaseDate.getFullYear(), + orderStatus: x.orderStatus.name, + orderYear: x.orderDate.getFullYear() + } + }).silent().take(10).getItems(); + expect(Array.isArray(items)).toBeTruthy(); + expect(items.length).toBeTruthy(); + for (const item of items) { + expect(Object.prototype.hasOwnProperty.call(item, 'orderYear')).toBeTruthy(); + expect(typeof item.orderYear === 'number').toBeTruthy(); + expect(Object.prototype.hasOwnProperty.call(item, 'releaseYear')).toBeTruthy(); + expect(typeof item.releaseYear === 'number').toBeTruthy(); + } + }); + }); + + it('should use select closure with nested attribute', async () => { + await executeInTransaction(context, async () => { + const items = await context.model('Order').select((x: any) => { + return { + id: x.id, + orderDate: x.orderDate, + productName: x.orderedItem.name, + productPrice: x.orderedItem.price + } + }).silent().take(10).getItems(); + expect(Array.isArray(items)).toBeTruthy(); + expect(items.length).toBeTruthy(); + }); + }); + + it('should use expand', async () => { + await executeInTransaction(context, async () => { + const items = await context.model('Order').select((x: any) => { + return { + id: x.id, + orderedItem: x.orderedItem, + orderDate: x.orderDate, + productName: x.orderedItem.name, + productPrice: x.orderedItem.price + } + }).expand((x: any) => x.orderedItem).silent().take(10).getItems(); + expect(Array.isArray(items)).toBeTruthy(); + expect(items.length).toBeTruthy(); + for (const item of items) { + expect(item.orderedItem).toBeTruthy(); + } + }); + }); + + it('should use nested expand', async () => { + await executeInTransaction(context, async () => { + const items = await context.model('Order').select((x: any) => { + return { + id: x.id, + customer: x.customer, + country: x.customer.address.addressCountry, + orderDate: x.orderDate, + productName: x.orderedItem.name, + productPrice: x.orderedItem.price + } + }).expand((x: any) => x.customer.address).silent().take(10).getItems(); + expect(Array.isArray(items)).toBeTruthy(); + expect(items.length).toBeTruthy(); + for (const item of items) { + expect(item.customer).toBeTruthy(); + expect(item.customer.address).toBeTruthy(); + } + }); + }); + + it('should use where closure', async () => { + await executeInTransaction(context, async () => { + const Products = context.model('Product'); + const results = await Products.select((x: any) => { + x.id, + x.name, + x.category, + x.model, + x.price + }).where((x: any) => { + return x.price > 400; + }).take(10).silent().getItems(); + results.forEach((item) => { + expect(item.price).toBeGreaterThan(400); + }); + }); + }); + + it('should use order by closure', async () => { + await executeInTransaction(context, async () => { + const Products = context.model('Product'); + const results = await Products.select((x: any) => { + x.id, + x.name, + x.category, + x.model, + x.price + }).where((x: any) => { + return x.price > 400; + }).orderBy( + (x: any) => x.price + ).take(10).silent().getItems(); + results.forEach( (x, index) => { + if (index > 0) { + expect(x.price).toBeGreaterThanOrEqual(results[index-1].price); + } + }); + }); + }); + + it('should use group by closure', async () => { + await executeInTransaction(context, async () => { + const category = 'Laptops'; + const items = await context.model('Order') + .select((x: any) => { + return { + total: count(x.id), + product: x.orderedItem.name, + model: x.orderedItem.model + } + }) + .groupBy((x: any) => x.orderedItem.name, (x: { orderedItem: { model: any; }; }) => x.orderedItem.model) + .where((x: any, category: string) => { + return x.orderedItem.category === category; + }, { + category + }).silent().take(10).getItems(); + expect(Array.isArray(items)).toBeTruthy(); + expect(items.length).toBeTruthy(); + }); + }); + + it('should use group by closure with nested attributes', async () => { + await executeInTransaction(context, async () => { + const orderStatus = 'OrderDelivered'; + const items = await context.model('Order') + .select((x: any) => { + return { + total: count(x.id), + country: x.customer.address.addressCountry.name + } + }) + .where((x: any, orderStatus: string) => { + return x.orderStatus.alternateName === orderStatus; + }, { + orderStatus + }) + .groupBy((x) => x.customer.address.addressCountry).silent().take(10).getItems(); + expect(Array.isArray(items)).toBeTruthy(); + expect(items.length).toBeTruthy(); + }); + }); + +}); \ No newline at end of file diff --git a/spec/DataQueryable.select.spec.ts b/spec/DataQueryable.select.spec.ts new file mode 100644 index 0000000..b5c05dc --- /dev/null +++ b/spec/DataQueryable.select.spec.ts @@ -0,0 +1,100 @@ +import { resolve } from 'path'; +import { DataContext } from '../index'; +import { TestApplication } from './TestApplication'; +import { TestUtils } from './adapter/TestUtils'; +import { round } from '@themost/query'; +import { executeInUnattendedModeAsync } from '../UnattendedMode'; +const {executeInTransaction} = TestUtils; + +describe('DataQueryable.select()', () => { + let app: TestApplication; + let context: DataContext; + beforeAll(async () => { + app = new TestApplication(resolve(__dirname, 'test2')); + context = app.createContext(); + }); + afterAll(async () => { + await app.finalize(); + }) + it('should use a simple select closure', async () => { + const items = await context.model('Product').asQueryable() + .select<{ id?: number, name: string, price?: number }>(({id, name, price}) => ({ + id, + name, + price + })).take(10).getItems(); + expect(items.length).toBeGreaterThan(0); + const [item] = items; + expect(Object.getOwnPropertyNames(item)).toStrictEqual(['id', 'name', 'price']); + }); + + it('should use round', async () => { + const items = await context.model('Product').asQueryable() + .select<{ id?: number, name: string, price?: number }>(({id, name, price}) => { + return { + id, + name, + price, + actualPrice: round(price, 2) + } + }).take(10).getItems(); + expect(items.length).toBeGreaterThan(0); + for (const item of items) { + expect(item.actualPrice).toBeCloseTo(item.price, 2); + } + }); + + it('should use where closure', async () => { + const items = await context.model('Product').asQueryable() + .select(({id, name, price, category}) => { + return { + id, + name, + price, + category + } + }).where((x: any) => { + return x.category === 'Laptops' && x.price > 500; + }).take(10).getItems(); + expect(items.length).toBeGreaterThan(0); + for (const item of items) { + expect(item.category).toEqual('Laptops'); + expect(item.price).toBeGreaterThan(500); + } + }); + + it('should use where closure with params', async () => { + const targetCategory = 'Laptops'; + const items = await context.model('Product').asQueryable() + .select(({id, name, price, category}) => { + return { + id, + name, + price, + category + } + }).where((x: any, category: string) => { + return x.category === category; + }, targetCategory).take(10).getItems(); + expect(items.length).toBeGreaterThan(0); + for (const item of items) { + expect(item.category).toEqual('Laptops'); + } + }); + + it('should use where with nested query', async () => { + await executeInUnattendedModeAsync(context, async () => { + const targetCategory = 'Laptops'; + const items = await context.model('Order').asQueryable() + .where((x: any, category: string) => { + return x.orderedItem.category === category; + }, targetCategory).take(25).getItems(); + expect(items.length).toBeGreaterThan(0); + for (const item of items) { + expect(item.orderedItem.category).toEqual('Laptops'); + } + }); + }); + + +});