diff --git a/.github/workflows/npm-publish-next.yml b/.github/workflows/npm-publish-next.yml deleted file mode 100644 index 39a7e64..0000000 --- a/.github/workflows/npm-publish-next.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Publish Next - -on: - release: - types: [prereleased] - -jobs: - publish-npm: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 12 - registry-url: https://registry.npmjs.org/ - - run: npm ci - - run: npm publish --tag next - env: - NODE_AUTH_TOKEN: ${{secrets.npm_token}} diff --git a/.github/workflows/npmpublish.yml b/.github/workflows/npmpublish.yml index 1590a18..5bc11ae 100644 --- a/.github/workflows/npmpublish.yml +++ b/.github/workflows/npmpublish.yml @@ -12,9 +12,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v2 with: - node-version: 12 + node-version: 16 registry-url: https://registry.npmjs.org/ - run: npm ci - run: npm publish --tag lts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5f57b2b..f4cb89b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x] + node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v2 diff --git a/data-queryable.d.ts b/data-queryable.d.ts index b4817a5..6d5f553 100644 --- a/data-queryable.d.ts +++ b/data-queryable.d.ts @@ -102,3 +102,8 @@ export declare class DataAttributeResolver { testNestedAttribute(s: string): any; resolveJunctionAttributeJoin(attr: string): any; } + +export declare class DataValueResolver { + constructor(target: DataQueryable); + resolve(value: any): string +} diff --git a/data-queryable.js b/data-queryable.js index cb4aa34..96f81f1 100644 --- a/data-queryable.js +++ b/data-queryable.js @@ -13,6 +13,151 @@ var {QueryUtils} = require('@themost/query'); var Q = require('q'); var aliasProperty = Symbol('alias'); var {hasOwnProperty} = require('./has-own-property'); +var {isObjectDeep} = require('./is-object'); + +/** + * @param {DataQueryable} target + * @constructor + */ +function DataValueResolver(target) { + Object.defineProperty(this, 'target', { get: function() { + return target; + }, configurable:false, enumerable:false}); +} + +DataValueResolver.prototype.resolve = function(value) { + /** + * @type {DataQueryable} + */ + var target = this.target; + if (typeof value === 'string' && /^\$it\//.test(value)) { + var attr = value.replace(/^\$it\//,''); + if (DataAttributeResolver.prototype.testNestedAttribute(attr)) { + return DataAttributeResolver.prototype.resolveNestedAttribute.call(target, attr); + } + else { + attr = DataAttributeResolver.prototype.testAttribute(attr); + if (attr) { + return target.fieldOf(attr.name); + } + } + } + if (isObjectDeep(value)) { + // try to get in-process left operand + // noinspection JSUnresolvedReference + var left = target.query.privates && target.query.privates.property; + if (typeof left === 'string' && /\./.test(left)) { + var members = left.split('.'); + if (Array.isArray(members)) { + // try to find member mapping + /** + * @type {import('./data-model').DataModel} + */ + var model = target.model; + var mapping; + var attribute; + var index = 0; + var context = target.model.context; + // if the first segment contains the view adapter name + if (members[0] === target.model.viewAdapter) { + // move next + index++; + } else if (target.query.$expand != null) { + // try to find if the first segment is contained in the collection of joined entities + var joins = Array.isArray(target.query.$expand) ? target.query.$expand : [ target.query.$expand ]; + if (joins.length) { + var found = joins.find(function(x) { + return x.$entity && x.$entity.$as === members[0]; + }); + if (found) { + var mapping1 = model.inferMapping(found.$entity.$as); + if (mapping1 && mapping1.associationType === 'junction') { + // get next segment of members + var nextMember = members[index + 1]; + if (nextMember === mapping1.associationObjectField) { + // the next segment is the association object field + // e.g. groups/group + model = context.model(mapping1.parentModel); + members[index + 1] = mapping1.parentField; + } else if (nextMember === mapping1.associationValueField) { + // the next segment is the association value field + // e.g. groups/user + model = context.model(mapping1.childModel); + members[index + 1] = mapping1.childField; + } else if (model.name === mapping1.parentModel) { + model = context.model(mapping1.childModel); + } else { + model = context.model(mapping1.parentModel); + } + } else if (found.$entity.model != null) { + model = context.model(found.$entity.model); + } else { + throw new Error(sprintf('Expected a valid mapping for property "%s"', found.$entity.$as)); + } + index++; + } + } + } + + var mapValue = function(x) { + if (Object.hasOwnProperty.call(x, name)) { + return x[name]; + } + throw new Error(sprintf('Invalid value for property "%s"', members[members.length - 1])); + } + + while (index < members.length) { + mapping = model.inferMapping(members[index]); + if (mapping) { + if (mapping.associationType === 'association' && mapping.childModel === model.name) { + model = context.model(mapping.parentModel); + if (model) { + attribute = model.getAttribute(mapping.parentField); + } + } else if (mapping.associationType === 'association' && mapping.parentModel === model.name) { + model = context.model(mapping.childModel); + if (model) { + attribute = model.getAttribute(mapping.childField); + } + } else if (mapping.associationType === 'junction' && mapping.childModel === model.name) { + model = context.model(mapping.parentModel); + if (model) { + attribute = model.getAttribute(mapping.parentField); + } + } else if (mapping.associationType === 'junction' && mapping.parentModel === model.name) { + model = context.model(mapping.childModel); + if (model) { + attribute = model.getAttribute(mapping.childField); + } + } + } else { + // if mapping is not found, and we are in the last segment + // try to find if this last segment is a field of the current model + if (index === members.length - 1) { + attribute = model.getAttribute(members[index]); + break; + } + attribute = null; + model = null; + break; + } + index++; + } + if (attribute) { + var name = attribute.property || attribute.name; + if (Array.isArray(value)) { + return value.map(function(x) { + return mapValue(x); + }); + } else { + return mapValue(value); + } + } + } + } + } + return value; +} /** * @class @@ -194,7 +339,7 @@ DataAttributeResolver.prototype.resolveNestedAttributeJoin = function(memberExpr var childFieldName = childField.property || childField.name; /** * store temp query expression - * @type QueryExpression + * @type {import('@themost/query').QueryExpression} */ res =QueryUtils.query(self.viewAdapter).select(['*']); expr = QueryUtils.query().where(QueryField.select(childField.name) @@ -568,6 +713,12 @@ DataAttributeResolver.prototype.resolveJunctionAttributeJoin = function(attr) { //create new join var parentAlias = field.name + '_' + parentModel.name; entity = new QueryEntity(parentModel.viewAdapter).as(parentAlias); + Object.defineProperty(entity, 'model', { + configurable: true, + enumerable: false, + writable: true, + value: parentModel.name + }); expr = QueryUtils.query().where(QueryField.select(mapping.associationObjectField).from(field.name)) .equal(QueryField.select(mapping.parentField).from(parentAlias)); //append join @@ -587,7 +738,7 @@ DataAttributeResolver.prototype.resolveJunctionAttributeJoin = function(attr) { /** * @classdesc Represents a dynamic query helper for filtering, paging, grouping and sorting data associated with an instance of DataModel class. * @class - * @property {QueryExpression|*} query - Gets or sets the current query expression + * @property {import('@themost/query').QueryExpression} query - Gets or sets the current query expression * @property {DataModel|*} model - Gets or sets the underlying data model * @constructor * @param model {DataModel|*} @@ -595,7 +746,7 @@ DataAttributeResolver.prototype.resolveJunctionAttributeJoin = function(attr) { */ function DataQueryable(model) { /** - * @type {QueryExpression} + * @type {import('@themost/query').QueryExpression} * @private */ var q = null; @@ -706,6 +857,17 @@ DataQueryable.prototype.where = function(attr) { this.query.where(DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attr)); return this; } + // check if attribute defines a many-to-many association + var mapping = this.model.inferMapping(attr); + if (mapping && mapping.associationType === 'junction') { + // append mapping id e.g. groups -> groups/id or members -> members/id etc + let attrId = attr + '/' + mapping.parentField; + if (mapping.parentModel === this.model.name) { + attrId = attr + '/' + mapping.childField; + } + this.query.where(DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attrId)); + return this; + } this.query.where(this.fieldOf(attr)); return this; }; @@ -890,7 +1052,7 @@ function resolveValue(obj) { */ DataQueryable.prototype.equal = function(obj) { - this.query.equal(resolveValue.bind(this)(obj)); + this.query.equal(new DataValueResolver(this).resolve(obj)); return this; }; @@ -928,7 +1090,7 @@ DataQueryable.prototype.is = function(obj) { }); */ DataQueryable.prototype.notEqual = function(obj) { - this.query.notEqual(resolveValue.bind(this)(obj)); + this.query.notEqual(new DataValueResolver(this).resolve(obj)); return this; }; // noinspection JSUnusedGlobalSymbols @@ -958,7 +1120,7 @@ DataQueryable.prototype.notEqual = function(obj) { 89 Nvidia GeForce GTX 650 Ti Boost 1625.49 2015-11-21 17:29:21.000+02:00 */ DataQueryable.prototype.greaterThan = function(obj) { - this.query.greaterThan(resolveValue.bind(this)(obj)); + this.query.greaterThan(new DataValueResolver(this).resolve(obj)); return this; }; @@ -979,7 +1141,7 @@ DataQueryable.prototype.greaterThan = function(obj) { }); */ DataQueryable.prototype.greaterOrEqual = function(obj) { - this.query.greaterOrEqual(resolveValue.bind(this)(obj)); + this.query.greaterOrEqual(new DataValueResolver(this).resolve(obj)); return this; }; @@ -1016,7 +1178,7 @@ DataQueryable.prototype.bit = function(value, result) { * @returns {DataQueryable} */ DataQueryable.prototype.lowerThan = function(obj) { - this.query.lowerThan(resolveValue.bind(this)(obj)); + this.query.lowerThan(new DataValueResolver(this).resolve(obj)); return this; }; @@ -1037,7 +1199,7 @@ DataQueryable.prototype.lowerThan = function(obj) { }); */ DataQueryable.prototype.lowerOrEqual = function(obj) { - this.query.lowerOrEqual(resolveValue.bind(this)(obj)); + this.query.lowerOrEqual(new DataValueResolver(this).resolve(obj)); return this; }; // noinspection JSUnusedGlobalSymbols @@ -1217,7 +1379,8 @@ DataQueryable.prototype.notContains = function(value) { 440 Bose SoundLink Bluetooth Mobile Speaker II HS5288 155.27 */ DataQueryable.prototype.between = function(value1, value2) { - this.query.between(resolveValue.bind(this)(value1), resolveValue.bind(this)(value2)); + const resolver = new DataValueResolver(this); + this.query.between(resolver.resolve(value1), resolver.resolve(value2)); return this; }; @@ -3418,5 +3581,6 @@ DataQueryable.prototype.getAllTypedItems = function() { module.exports = { DataQueryable, - DataAttributeResolver + DataAttributeResolver, + DataValueResolver } diff --git a/index.js b/index.js index 83c9d0f..5fba451 100644 --- a/index.js +++ b/index.js @@ -26,7 +26,7 @@ var { DataModelPrivilege } = require('./types'); var { DataModel } = require('./data-model'); -var { DataQueryable } = require('./data-queryable'); +var { DataQueryable, DataAttributeResolver, DataValueResolver } = require('./data-queryable'); var { DataObject } = require('./data-object'); var { NamedDataContext, DefaultDataContext } = require('./data-context'); var { FunctionContext } = require('./functions'); @@ -110,6 +110,8 @@ module.exports = { FileSchemaLoaderStrategy, DataModel, DataQueryable, + DataAttributeResolver, + DataValueResolver, DataObject, FunctionContext, DataCache, diff --git a/package-lock.json b/package-lock.json index 0aaaffb..b9f02d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@themost/data", - "version": "2.6.51", + "version": "2.6.52", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@themost/data", - "version": "2.6.51", + "version": "2.6.52", "license": "BSD-3-Clause", "dependencies": { "@themost/events": "^1.3.0", @@ -26,7 +26,7 @@ "@themost/common": "^2.5.11", "@themost/json-logger": "^1.1.0", "@themost/peers": "^1.0.2", - "@themost/query": "^2.5.22", + "@themost/query": "^2.5.24", "@themost/sqlite": "^2.6.16", "@themost/xml": "^2.5.2", "@types/core-js": "^2.5.0", @@ -5194,10 +5194,11 @@ "integrity": "sha512-wlMYRsNWaz5EJ7AwCIA3yw2kHy4p36a4VTz8XmyYTYDYaKgmXjTi6IJPB/+di0mt/rf6NfFfsYwrmytoLseiIg==" }, "node_modules/@themost/query": { - "version": "2.5.22", - "resolved": "https://registry.npmjs.org/@themost/query/-/query-2.5.22.tgz", - "integrity": "sha512-v9CwxtWyRUpKqrk5Pvyu1R0PyS8K1qTqFhrgUA6wVxWEmTUl2lZt/FYoK3jsfpwovMCg1vyd1HTVgo9KLUOtdA==", + "version": "2.5.24", + "resolved": "https://registry.npmjs.org/@themost/query/-/query-2.5.24.tgz", + "integrity": "sha512-EyMmzPbCwA0RVRPuuGXcQz24AOtp+zdjRRV8gRlctXSQfMhuyywAgQ35KJJYkhYXWFWJxmOCJcj2YO0uCdrj0w==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@themost/events": "^1.0.5", "async": "^2.6.4", @@ -10320,12 +10321,13 @@ "dev": true }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -12975,8 +12977,7 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "requires": {} + "dev": true }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", @@ -16029,9 +16030,9 @@ "integrity": "sha512-wlMYRsNWaz5EJ7AwCIA3yw2kHy4p36a4VTz8XmyYTYDYaKgmXjTi6IJPB/+di0mt/rf6NfFfsYwrmytoLseiIg==" }, "@themost/query": { - "version": "2.5.22", - "resolved": "https://registry.npmjs.org/@themost/query/-/query-2.5.22.tgz", - "integrity": "sha512-v9CwxtWyRUpKqrk5Pvyu1R0PyS8K1qTqFhrgUA6wVxWEmTUl2lZt/FYoK3jsfpwovMCg1vyd1HTVgo9KLUOtdA==", + "version": "2.5.24", + "resolved": "https://registry.npmjs.org/@themost/query/-/query-2.5.24.tgz", + "integrity": "sha512-EyMmzPbCwA0RVRPuuGXcQz24AOtp+zdjRRV8gRlctXSQfMhuyywAgQ35KJJYkhYXWFWJxmOCJcj2YO0uCdrj0w==", "dev": true, "requires": { "@themost/events": "^1.0.5", @@ -16293,8 +16294,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "agent-base": { "version": "6.0.2", @@ -16967,8 +16967,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", - "dev": true, - "requires": {} + "dev": true }, "deep-extend": { "version": "0.6.0", @@ -18861,8 +18860,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "29.6.3", @@ -19915,12 +19913,12 @@ "dev": true }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, diff --git a/package.json b/package.json index 1cd356f..fc1c552 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@themost/data", - "version": "2.6.51", + "version": "2.6.52", "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.5.22", + "@themost/query": "^2.5.24", "@themost/xml": "^2.5.2" }, "engines": { @@ -43,7 +43,7 @@ "@themost/common": "^2.5.11", "@themost/json-logger": "^1.1.0", "@themost/peers": "^1.0.2", - "@themost/query": "^2.5.22", + "@themost/query": "^2.5.24", "@themost/sqlite": "^2.6.16", "@themost/xml": "^2.5.2", "@types/core-js": "^2.5.0", diff --git a/spec/DataValueResolver.spec.ts b/spec/DataValueResolver.spec.ts new file mode 100644 index 0000000..2566f5f --- /dev/null +++ b/spec/DataValueResolver.spec.ts @@ -0,0 +1,126 @@ +import { resolve } from 'path'; +import { DataContext } from '../index'; +import { TestApplication } from './TestApplication'; + +describe('DataValueResolver', () => { + let app: TestApplication; + let context: DataContext; + beforeAll(async () => { + app = new TestApplication(resolve(__dirname, 'test2')); + context = app.createContext(); + }); + afterAll(async () => { + await app.finalize(); + }) + it('should resolve value from an associated parent object', async () => { + const product = await context.model('Product').where('name').equal('Western Digital My Passport Slim (1TB)').getItem(); + expect(product).toBeTruthy(); + const Orders = context.model('Order').silent(); + let items = await Orders.where('orderedItem').equal(product).getItems(); + expect(items.length).toBeTruthy(); + for (const order of items) { + expect(order.orderedItem.id).toEqual(product.id); + } + }); + + it('should fail resolving value from an associated parent object', async () => { + const product = await context.model('Product').where('name').equal('Western Digital My Passport Slim (1TB)').getItem(); + expect(product).toBeTruthy(); + // remove product id + delete product.id; + const Orders = context.model('Order').silent(); + expect(() => Orders.where('orderedItem').equal(product)).toThrowError('Invalid value for property "orderedItem"'); + }); + + it('should resolve value from a property of an associated parent object', async () => { + const product = await context.model('Product').where('name').equal('Western Digital My Passport Slim (1TB)').getItem(); + expect(product).toBeTruthy(); + const Orders = context.model('Order').silent(); + let items = await Orders.where('orderedItem/name').equal(product).getItems(); + expect(items.length).toBeTruthy(); + for (const order of items) { + expect(order.orderedItem.id).toEqual(product.id); + } + }); + + it('should resolve value from an associated child object', async () => { + const Products = context.model('Product').silent(); + const product = await Products.where('name').equal('Western Digital My Passport Slim (1TB)').getItem(); + expect(product).toBeTruthy(); + const Orders = context.model('Order').silent(); + const lastOrder = await Orders.where('orderedItem').equal(product) + .orderByDescending('dateCreated').expand('customer').getItem(); + expect(lastOrder).toBeTruthy(); + const { customer } = lastOrder; + const items = await Products.where('orders/customer').equal(customer).getItems(); + expect(items.length).toBeTruthy(); + const found = items.find((x) => x.id === product.id); + expect(found).toBeTruthy(); + }); + + it('should resolve value from an associated child nested object', async () => { + const Products = context.model('Product').silent(); + const product = await Products.where('name').equal('Western Digital My Passport Slim (1TB)').getItem(); + expect(product).toBeTruthy(); + const Orders = context.model('Order').silent(); + const lastOrder = await Orders.where('orderedItem').equal(product) + .orderByDescending('dateCreated').expand({ + name: 'customer', + options: { $expand: 'address' } + }).getItem(); + expect(lastOrder).toBeTruthy(); + const { customer } = lastOrder; + const items = await Products.where('orders/customer/address').equal(customer.address).getItems(); + expect(items.length).toBeTruthy(); + const found = items.find((x) => x.id === product.id); + expect(found).toBeTruthy(); + }); + + it('should resolve value from a property of an associated child object', async () => { + const Products = context.model('Product').silent(); + const product = await Products.where('name').equal('Western Digital My Passport Slim (1TB)').getItem(); + expect(product).toBeTruthy(); + const Orders = context.model('Order').silent(); + const lastOrder = await Orders.where('orderedItem').equal(product.id) + .orderByDescending('dateCreated').expand('customer').getItem(); + expect(lastOrder).toBeTruthy(); + const { customer } = lastOrder; + const items = await Products.where('orders/customer/id').equal(customer).getItems(); + expect(items.length).toBeTruthy(); + const found = items.find((x) => x.id === product.id); + expect(found).toBeTruthy(); + }); + + it('should fail resolving value from an associated child object', async () => { + const Products = context.model('Product').silent(); + const product = await Products.where('name').equal('Western Digital My Passport Slim (1TB)').getItem(); + expect(product).toBeTruthy(); + const Orders = context.model('Order').silent(); + const lastOrder = await Orders.where('orderedItem').equal(product) + .orderByDescending('dateCreated').expand('customer').getItem(); + expect(lastOrder).toBeTruthy(); + const { customer } = lastOrder; + delete customer.id; + expect(() => Products.where('orders/customer').equal(customer)).toThrowError('Invalid value for property "customer"'); + }); + + it('should resolve value from an object based on a many-to-many association', async () => { + const Users = context.model('User').silent(); + const Groups = context.model('Group').silent(); + const group = await Groups.where('name').equal('Administrators').getItem(); + expect(group).toBeTruthy(); + let items = await Users.where('groups').equal(group).getItems(); + expect(items.length).toBeTruthy(); + }); + + it('should resolve value from an object based on a parent/child many-to-many association', async () => { + const Users = context.model('User').silent(); + const Groups = context.model('Group').silent(); + const user = await Users.where('name').equal('alexis.rees@example.com').getItem(); + expect(user).toBeTruthy(); + let items = await Groups.where('members').equal(user).getItems(); + expect(items.length).toBeTruthy(); + }); + + +});