Skip to content

Commit

Permalink
resolving a query value passed as object (#158)
Browse files Browse the repository at this point in the history
* resolving a query value passed as object

* validate query values for many-to-many associations

* validate many-to-many associations

* 2.6.52
  • Loading branch information
kbarbounakis authored Sep 26, 2024
1 parent d590b3c commit 7c7218f
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 62 deletions.
19 changes: 0 additions & 19 deletions .github/workflows/npm-publish-next.yml

This file was deleted.

4 changes: 2 additions & 2 deletions .github/workflows/npmpublish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions data-queryable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
186 changes: 175 additions & 11 deletions data-queryable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -587,15 +738,15 @@ 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|*}
* @augments DataContextEmitter
*/
function DataQueryable(model) {
/**
* @type {QueryExpression}
* @type {import('@themost/query').QueryExpression}
* @private
*/
var q = null;
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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;
};

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
};

Expand All @@ -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;
};

Expand Down Expand Up @@ -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;
};

Expand All @@ -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
Expand Down Expand Up @@ -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;
};

Expand Down Expand Up @@ -3418,5 +3581,6 @@ DataQueryable.prototype.getAllTypedItems = function() {

module.exports = {
DataQueryable,
DataAttributeResolver
DataAttributeResolver,
DataValueResolver
}
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -110,6 +110,8 @@ module.exports = {
FileSchemaLoaderStrategy,
DataModel,
DataQueryable,
DataAttributeResolver,
DataValueResolver,
DataObject,
FunctionContext,
DataCache,
Expand Down
Loading

0 comments on commit 7c7218f

Please sign in to comment.