diff --git a/data-attribute-resolver.js b/data-attribute-resolver.js index 4555977..0c71c84 100644 --- a/data-attribute-resolver.js +++ b/data-attribute-resolver.js @@ -4,7 +4,7 @@ var _ = require('lodash'); var {DataError} = require('@themost/common'); var Symbol = require('symbol'); var {hasOwnProperty} = require('./has-own-property'); -var aliasProperty = Symbol('alias'); +var DataAttributeResolverAliasProperty = Symbol('alias'); var {UnknownAttributeError} = require('./data-errors'); /** * @class @@ -185,7 +185,7 @@ DataAttributeResolver.prototype.resolveNestedAttributeJoin = function(memberExpr var mapping = self.inferMapping(arrMember[0]); if (_.isNil(mapping)) { if (attrMember.type === 'Json') { - var collection = self[aliasProperty] || self.viewAdapter; + var collection = self[DataAttributeResolverAliasProperty] || self.viewAdapter; var objectPath = arrMember.join('.'); var objectGet = new MethodCallExpression('jsonGet', [ new MemberExpression(collection + '.' + objectPath) @@ -219,7 +219,7 @@ DataAttributeResolver.prototype.resolveNestedAttributeJoin = function(memberExpr */ res =QueryUtils.query(self.viewAdapter).select(['*']); expr = QueryUtils.query().where(QueryField.select(childField.name) - .from(self[aliasProperty] || self.viewAdapter)) + .from(self[DataAttributeResolverAliasProperty] || self.viewAdapter)) .equal(QueryField.select(mapping.parentField).from(childFieldName)); entity = new QueryEntity(parentModel.viewAdapter).as(childFieldName).left(); res.join(entity).with(expr); @@ -230,7 +230,7 @@ DataAttributeResolver.prototype.resolveNestedAttributeJoin = function(memberExpr value: parentModel.name }); if (arrMember.length>2) { - parentModel[aliasProperty] = mapping.childField; + parentModel[DataAttributeResolverAliasProperty] = mapping.childField; expr = new DataAttributeResolver().resolveNestedAttributeJoin.call(parentModel, arrMember.slice(1).join('/')); return { $select: expr.$select, @@ -245,7 +245,7 @@ DataAttributeResolver.prototype.resolveNestedAttributeJoin = function(memberExpr var nestedMapping = parentModel.inferMapping(nestedMember); if (nestedMapping && nestedMapping.associationType === 'junction') { // resolve nested member - parentModel[aliasProperty] = mapping.childField; + parentModel[DataAttributeResolverAliasProperty] = mapping.childField; expr = new DataAttributeResolver().resolveJunctionAttributeJoin.call(parentModel, nestedMember); return { $select: expr.$select, @@ -271,7 +271,7 @@ DataAttributeResolver.prototype.resolveNestedAttributeJoin = function(memberExpr throw new Error(sprintf('Referenced field (%s) cannot be found.', mapping.parentField)); } // get parent entity name for this expression - var parentEntity = self[aliasProperty] || self.viewAdapter; + var parentEntity = self[DataAttributeResolverAliasProperty] || self.viewAdapter; // get child entity name for this expression var childEntity = arrMember[0]; res =QueryUtils.query('Unknown').select(['*']); @@ -286,7 +286,7 @@ DataAttributeResolver.prototype.resolveNestedAttributeJoin = function(memberExpr }); if (arrMember.length>2) { // set joined entity alias - childModel[aliasProperty] = childEntity; + childModel[DataAttributeResolverAliasProperty] = childEntity; // resolve additional joins expr = new DataAttributeResolver().resolveNestedAttributeJoin.call(childModel, arrMember.slice(1).join('/')); // concat and return joins @@ -316,7 +316,7 @@ DataAttributeResolver.prototype.resolveNestedAttributeJoin = function(memberExpr if (mapping.associationType === 'junction' && mapping.parentModel === self.name) { return new DataAttributeResolver().resolveJunctionAttributeJoin.call(self, memberExpr); } else { - throw new Error(sprintf('The association type between %s and %s model is not supported for filtering, grouping or sorting data.', mapping.parentModel , mapping.childModel)); + throw new Error(sprintf('The association of type "%s" between "%s" and "%s", defined by "%s", is not supported for filtering, grouping or sorting data.', mapping.associationType, mapping.parentModel , mapping.childModel, mapping.refersTo)); } } } @@ -426,7 +426,7 @@ DataAttributeResolver.prototype.resolveJunctionAttributeJoin = function(attr) { if (mapping && mapping.associationType === 'junction') { //get field var field = self.field(member[0]), entity, expr, q; - var thisAlias = self[aliasProperty] || self.viewAdapter; + var thisAlias = self[DataAttributeResolverAliasProperty] || self.viewAdapter; //first approach (default association adapter) //the underlying model is the parent model e.g. Group > Group Members if (mapping.parentModel === self.name) { @@ -570,5 +570,6 @@ DataAttributeResolver.prototype.resolveZeroOrOneNestedAttribute = function(attr) } module.exports = { - DataAttributeResolver + DataAttributeResolver, + DataAttributeResolverAliasProperty } diff --git a/data-filter-resolver.d.ts b/data-filter-resolver.d.ts index 1d0f244..21b116d 100644 --- a/data-filter-resolver.d.ts +++ b/data-filter-resolver.d.ts @@ -1,11 +1,10 @@ // MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved export declare class DataFilterResolver { - resolveMember(member: string, callback: (err?: Error, res?: any) => void); - resolveMethod(name: string, args: Array, callback: (err?: Error, res?: any) => void); - me(callback: (err?: Error, res?: any) => void); - user(callback: (err?: Error, res?: any) => void); - now(callback: (err?: Error, res?: Date) => void); - today(callback: (err?: Error, res?: Date) => void); - lang(callback: (err?: Error, res?: string) => void); - + resolveMember(member: string, callback: (err?: Error, res?: any) => void): void; + resolveMethod(name: string, args: Array, callback: (err?: Error, res?: any) => void): void; + me(callback: (err?: Error, res?: any) => void): void; + user(callback: (err?: Error, res?: any) => void): void; + now(callback: (err?: Error, res?: Date) => void): void; + today(callback: (err?: Error, res?: Date) => void): void; + lang(callback: (err?: Error, res?: string) => void): void; } diff --git a/data-filter-resolver.js b/data-filter-resolver.js index 910b96e..930cab2 100644 --- a/data-filter-resolver.js +++ b/data-filter-resolver.js @@ -19,6 +19,41 @@ DataFilterResolver.prototype.resolveMember = function(member, callback) { } if (/\//.test(member)) { var arr = member.split('/'); + // validate that the expression resolves an existing attribute + var index = 0; + var model = this; + var context = this.context; + while (index < arr.length) { + var name = arr[index]; + var attr = model.getAttribute(name); + if (attr == null) { + return callback(new UnknownAttributeError(model.name, name)); + } + // if the attribute is a JSON type, then break the loop because + // the given expression e.g. metadata/identifier/name will be handled internally by the database engine + // and the attribute is not mapped to a model (the additionalType property is null) + if (attr.type === 'Json' && attr.additionalType == null) { + break; + } + // get mapping for the attribute + var mapping = model.inferMapping(name); + // throw exception because the attribute is not mapped and the expression is not fully resolved + if (mapping == null && index < arr.length-1) { + return callback(new UnknownAttributeError(model.name, name)); + } + // if mapping is found, then get the model for the mapping + if (mapping != null) { + // throw exception because the mapping defines an association to a collection of primitive typed values + // and the expression is not fully resolved + // e.g. tags/tag/name where tags is an array of strings + if (mapping.childModel == null && index < arr.length-1) { + return callback(new UnknownAttributeError(model.name, name)); + } + var type = attr.additionalType != null ? attr.additionalType : attr.type; + model = context.model(type); + } + index++; + } return callback(null, arr.slice(arr.length-2).join('.')); } var attribute = this.getAttribute(member); diff --git a/data-model.js b/data-model.js index 5ba3577..0edc4d5 100644 --- a/data-model.js +++ b/data-model.js @@ -48,6 +48,7 @@ var { DataStateValidatorListener } = require('./data-state-validator'); var resolver = require('./data-expand-resolver'); var isArrayLikeObject = require('lodash/isArrayLikeObject'); var {DataObjectFinder} = require('./data-object-finder'); +var { DataAttributeResolver:DataAttributeResolverClass } = require('./data-attribute-resolver'); /** * @this DataModel * @param {DataField} field @@ -755,7 +756,7 @@ function filterInternal(params, callback) { mapping = self.inferMapping(member1[0]), expr; if (mapping && mapping.associationType === 'junction') { - var expr1 = DataAttributeResolver.prototype.resolveJunctionAttributeJoin.call(self, member); + var expr1 = DataAttributeResolverClass.prototype.resolveNestedAttributeJoin.call(self, member); expr = expr1.$expand; //replace member expression member = expr1.$select.$name.replace(/\./g,'/'); diff --git a/data-queryable.js b/data-queryable.js index 7924189..b8e36fa 100644 --- a/data-queryable.js +++ b/data-queryable.js @@ -11,13 +11,14 @@ var {QueryField, MethodCallExpression, MemberExpression, ObjectNameValidator} = var {QueryEntity, Expression} = require('@themost/query'); var {QueryUtils} = require('@themost/query'); var Q = require('q'); -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'); - +var { DataAttributeResolver:DataAttributeResolverClass, DataAttributeResolverAliasProperty } = require('./data-attribute-resolver'); +// noinspection JSUnresolvedReference +var aliasProperty = DataAttributeResolverAliasProperty; /** * @param {DataQueryable} target */ @@ -549,7 +550,12 @@ DataAttributeResolver.prototype.resolveNestedAttributeJoin = function(memberExpr return res.$expand; } else { - throw new Error(sprintf('The association type between %s and %s model is not supported for filtering, grouping or sorting data.', mapping.parentModel , mapping.childModel)); + if (mapping.associationType === 'junction') { + const res = new DataAttributeResolverClass().resolveNestedAttributeJoin.call(self, memberExpr); + return res.$expand; + } else { + throw new Error(sprintf('The association of type "%s" between "%s" and "%s", defined by "%s", is not supported for filtering, grouping or sorting data.', mapping.associationType, mapping.parentModel , mapping.childModel, mapping.refersTo)); + } } } };