diff --git a/lib/crud/WtCrudManager.js b/lib/crud/WtCrudManager.js index 2c64834..eb48efa 100644 --- a/lib/crud/WtCrudManager.js +++ b/lib/crud/WtCrudManager.js @@ -9,6 +9,7 @@ "use strict"; let WtKnexCrudStrategy = require("./WtCrudStrategies.js").WtKnexCrudStrategy; +let WtPromiseCrudStrategy = require("./WtCrudStrategies.js").WtPromiseCrudStrategy; /** * @class @@ -24,6 +25,7 @@ class WtCrudManager { strategy = new WtKnexCrudStrategy(fileSystemConnection); } this.strategy = strategy; + this.streamStrategy = new WtPromiseCrudStrategy(fileSystemConnection); } /** @@ -134,6 +136,10 @@ class WtCrudManager { } } + + queryStream(queryObj, next){ + return this.streamStrategy.queryStream(queryObj.statement, queryObj.parameters, next); + } } module.exports = WtCrudManager; diff --git a/lib/crud/WtCrudStrategies.js b/lib/crud/WtCrudStrategies.js index ff1ae7f..f236e9d 100644 --- a/lib/crud/WtCrudStrategies.js +++ b/lib/crud/WtCrudStrategies.js @@ -108,6 +108,24 @@ class WtCrudStrategy { } +class WtPromiseCrudStrategy extends WtCrudStrategy { + + + /** + * @constructor + * @param{Object} dbConnection + * @param{Object} fsConnection + */ + constructor(fsConnection) { + super(fsConnection); + } + + queryStream(statement, parameters, next) { + + return global.sails.models.data.stream({statement: statement, parameters: parameters}, next); + } +} + class WtKnexCrudStrategy extends WtCrudStrategy { /** @@ -123,17 +141,17 @@ class WtKnexCrudStrategy extends WtCrudStrategy { * @name createDataType * @description transactional DataType creation */ - createDataType(dataType) { + createDataType(dataType) { - let idDataType; + let idDataType; - return global.sails.models.datatype.create({ - name: dataType.name, - model: dataType.model, - schema: dataType.schema, - created_at: new Date(), - updated_at: new Date() - }) + return global.sails.models.datatype.create({ + name: dataType.name, + model: dataType.model, + schema: dataType.schema, + created_at: new Date(), + updated_at: new Date() + }) // .then(function(res) { // idDataType = res.id; @@ -155,7 +173,7 @@ class WtKnexCrudStrategy extends WtCrudStrategy { throw new TransactionError(error.message); }); - } + } /** * @method @@ -165,14 +183,14 @@ class WtKnexCrudStrategy extends WtCrudStrategy { */ updateDataType(dataType) { - return global.sails.models.datatype.update({id : dataType.id },{ - name: dataType.name, - model: dataType.model, - schema: dataType.schema, - parents: dataType.parents, - created_at: new Date(), - updated_at: new Date() - }) + return global.sails.models.datatype.update({id : dataType.id },{ + name: dataType.name, + model: dataType.model, + schema: dataType.schema, + parents: dataType.parents, + created_at: new Date(), + updated_at: new Date() + }) // find all the existing associations // .then(function(idUpdated) { @@ -221,7 +239,7 @@ class WtKnexCrudStrategy extends WtCrudStrategy { return global.sails.models.datatype.destroy({id : id }) .then(function(deleted) { - return deleted && deleted.length; + return deleted && deleted.length; }) .catch(function(error) { throw new TransactionError(error.message); @@ -298,16 +316,16 @@ class WtKnexCrudStrategy extends WtCrudStrategy { if (results.length) { // if there are files store their URIs on the database console.log("WaterlineStrategy.createData - inserting files.."); return _.each(files, function(file) { - global.sails.models.datafile.create({ - uri: file.uri, - samples: file.samples, - details: file.details, - data: file.data, - created_at: new Date(), - updated_at: new Date() - }); + global.sails.models.datafile.create({ + uri: file.uri, + samples: file.samples, + details: file.details, + data: file.data, + created_at: new Date(), + updated_at: new Date() + }); }); - } + } else { // else return an empty array return []; @@ -320,25 +338,25 @@ class WtKnexCrudStrategy extends WtCrudStrategy { console.log("WaterlineStrategy.createData - creating associations..."); if (tableName == 'data'){ - return BluebirdPromise.map(idFiles, function(idFile) { + return BluebirdPromise.map(idFiles, function(idFile) { - return global.sails.models.data.findOne({id: idData}).populate('files') + return global.sails.models.data.findOne({id: idData}).populate('files') .then(function(err,res){ - res.files.add(idFile); - res.save(function(err){console.log(err);}); + res.files.add(idFile); + res.save(function(err){console.log(err);}); }); - }); - } - else if(tableName == 'sample'){ - return BluebirdPromise.map(idFiles, function(idFile) { + }); + } + else if(tableName == 'sample'){ + return BluebirdPromise.map(idFiles, function(idFile) { - return global.sails.models.sample.findOne({id: idData}).populate('files') + return global.sails.models.sample.findOne({id: idData}).populate('files') .then(function(err,res){ - res.files.add(idFile); - res.save(function(err){console.log(err);}); + res.files.add(idFile); + res.save(function(err){console.log(err);}); }); }); @@ -368,18 +386,18 @@ class WtKnexCrudStrategy extends WtCrudStrategy { let result = null; // transaction-safe data creation return global.sails.models.data.create({ - type: data.type, - tags: JSON.stringify(data.tags), - notes: data.notes, - metadata: data.metadata, - acquisition_date: data.date, - parent_subject: data.parentSubject, - parent_sample: data.parentSample, - parent_data: data.parentData, - created_at: new Date(), - updated_at: new Date() + type: data.type, + tags: JSON.stringify(data.tags), + notes: data.notes, + metadata: data.metadata, + acquisition_date: data.date, + parent_subject: data.parentSubject, + parent_sample: data.parentSample, + parent_data: data.parentData, + created_at: new Date(), + updated_at: new Date() - }) + }) // store files on the FileSystem of choice (e.g. iRODS) in their final collection .then(function(res) { result=res; @@ -409,21 +427,21 @@ class WtKnexCrudStrategy extends WtCrudStrategy { updateData(data, dataTypeName) { - let that = this; - let updatedData = null; - let partitionedFiles = _.partition(data.files, file => { - return !file.id; - }); - let existingFiles = partitionedFiles[1], notExistingFiles = partitionedFiles[0]; + let that = this; + let updatedData = null; + let partitionedFiles = _.partition(data.files, file => { + return !file.id; + }); + let existingFiles = partitionedFiles[1], notExistingFiles = partitionedFiles[0]; - console.log("WaterlineStrategy.updateData - new files to insert..."); - console.log(data); - delete data.files; + console.log("WaterlineStrategy.updateData - new files to insert..."); + console.log(data); + delete data.files; // transaction-safe Data update - return global.sails.models.data.update({id: data.id}, + return global.sails.models.data.update({id: data.id}, // NOTE: should I also update parent_subject, parent_sample and/or parent_data? Should it be proper/safe? {tags: JSON.stringify(data.tags), notes: data.notes, @@ -441,7 +459,7 @@ class WtKnexCrudStrategy extends WtCrudStrategy { return _.mapKeys(file, (value, key) => { return _.camelCase(key); }); }); data.files = existingFiles.concat(notExistingFiles); - }) + }) .then(function() { console.log("WaterlineStrategy.updateData: transaction committed updating Data with ID: " + updatedData.id); return updatedData; @@ -461,7 +479,7 @@ class WtKnexCrudStrategy extends WtCrudStrategy { deleteData(id) { return global.sails.models.data.destroy({id : id }) .then(function(deleted) { - return deleted && deleted.length; + return deleted && deleted.length; }) .catch(function(error) { console.log(error); @@ -483,20 +501,20 @@ class WtKnexCrudStrategy extends WtCrudStrategy { // transaction-safe sample creation - console.log ("WaterlineStrategy.createSample - creating new sample instance..."); + console.log ("WaterlineStrategy.createSample - creating new sample instance..."); // store the new Sample entity - let sampleCode = sample.biobankCode || '080001'; - return global.sails.models.sample.create({ - biobankCode: sampleCode, - type: sample.type, - biobank: sample.biobank, - parent_subject: sample.donor, - parent_sample: sample.parentSample, - metadata: sample.metadata, - created_at: new Date(), - updated_at: new Date() - }) + let sampleCode = sample.biobankCode || '080001'; + return global.sails.models.sample.create({ + biobankCode: sampleCode, + type: sample.type, + biobank: sample.biobank, + parent_subject: sample.donor, + parent_sample: sample.parentSample, + metadata: sample.metadata, + created_at: new Date(), + updated_at: new Date() + }) // store files on the FileSystem of choice (e.g. iRODS) in their final collection @@ -530,12 +548,12 @@ class WtKnexCrudStrategy extends WtCrudStrategy { */ updateSample(sample) { - return global.sails.models.sample.update({ id: sample.id },{ - biobank: sample.biobank, - parent_subject: sample.donor, - metadata: sample.metadata, - updated_at: new Date() - }) + return global.sails.models.sample.update({ id: sample.id },{ + biobank: sample.biobank, + parent_subject: sample.donor, + metadata: sample.metadata, + updated_at: new Date() + }) .then(function(res) { let idSample = res.id; @@ -559,7 +577,7 @@ class WtKnexCrudStrategy extends WtCrudStrategy { deleteSample(id) { return global.sails.models.sample.destroy({id : id }) .then(function(deleted) { - return deleted && deleted.length; + return deleted && deleted.length; }) .catch(function(error) { throw new TransactionError(error.message); @@ -577,23 +595,23 @@ class WtKnexCrudStrategy extends WtCrudStrategy { let idSubject = null; - console.log ("WaterlineStrategy.createSubject - creating new subject instance..."); + console.log ("WaterlineStrategy.createSubject - creating new subject instance..."); // create the new PersonalDetails instance (if personalDetails are present) - return BluebirdPromise.try(function() { - if (!subject.personalInfo) { - return; - } - else { - return global.sails.models.personaldetails.create({ - givenName: subject.personalInfo.givenName, - surname: subject.personalInfo.surname, - birthDate: subject.personalInfo.birthDate, - created_at: new Date(), - updated_at: new Date() - }); - } - }) + return BluebirdPromise.try(function() { + if (!subject.personalInfo) { + return; + } + else { + return global.sails.models.personaldetails.create({ + givenName: subject.personalInfo.givenName, + surname: subject.personalInfo.surname, + birthDate: subject.personalInfo.birthDate, + created_at: new Date(), + updated_at: new Date() + }); + } + }) // create the new Subject entity @@ -655,36 +673,36 @@ class WtKnexCrudStrategy extends WtCrudStrategy { // Update or create personal information - return BluebirdPromise.try(function() { - console.log("WaterlineStrategy.updateSubject - trying to create/edit PersonalInfo: " + subject.personalInfo); + return BluebirdPromise.try(function() { + console.log("WaterlineStrategy.updateSubject - trying to create/edit PersonalInfo: " + subject.personalInfo); // if no personalInfo is provided just skip this step (no creation/update) - if (!_.isObject(subject.personalInfo)) { - return; - } + if (!_.isObject(subject.personalInfo)) { + return; + } // you have to create a new personal_details entity (i.e. row) - else if(!subject.personalInfo.id) { + else if(!subject.personalInfo.id) { - return global.sails.models.personaldetails.create({ - givenName: subject.personalInfo.givenName, - surname: subject.personalInfo.surname, - birthDate: subject.personalInfo.birthDate, - created_at: new Date(), - updated_at: new Date() - }); - } + return global.sails.models.personaldetails.create({ + givenName: subject.personalInfo.givenName, + surname: subject.personalInfo.surname, + birthDate: subject.personalInfo.birthDate, + created_at: new Date(), + updated_at: new Date() + }); + } // otherwise update personal_details else { - return global.sails.models.personaldetails.update({ id : subject.personalInfo.id,},{ - surname: subject.personalInfo.surname, - givenName: subject.personalInfo.givenName, - birthDate: subject.personalInfo.birthDate - }); - } + return global.sails.models.personaldetails.update({ id : subject.personalInfo.id},{ + surname: subject.personalInfo.surname, + givenName: subject.personalInfo.givenName, + birthDate: subject.personalInfo.birthDate + }); + } - }) + }) // update Subject entity .then(function(id) { @@ -742,7 +760,7 @@ class WtKnexCrudStrategy extends WtCrudStrategy { // TODO should personalDetails be deleted as well?? return global.sails.models.subject.destroy({ id : id}) .then(function(deleted) { - return deleted && deleted.length; + return deleted && deleted.length; }) .catch(function(error) { throw new TransactionError(error.message); @@ -819,7 +837,7 @@ class WtKnexCrudStrategy extends WtCrudStrategy { * */ putMetadataValuesIntoEAV(data, eavValueTableMap) { - return new NotSupportedError(); + return new NotSupportedError(); // console.log("WaterlineStrategy.putMetadataValuesIntoEAV - eavValueTableMap: " + eavValueTableMap); // let knex = this.knex; @@ -943,3 +961,4 @@ class WtKnexCrudStrategy extends WtCrudStrategy { } module.exports.WtCrudStrategy = WtCrudStrategy; module.exports.WtKnexCrudStrategy = WtKnexCrudStrategy; +module.exports.WtPromiseCrudStrategy = WtPromiseCrudStrategy; diff --git a/lib/query/WtQueryBuilder.js b/lib/query/WtQueryBuilder.js index bd1fb94..603c264 100644 --- a/lib/query/WtQueryBuilder.js +++ b/lib/query/WtQueryBuilder.js @@ -1,14 +1,14 @@ /** * @module * @name WtQueryBuilder - * @author Nicolò Zanardi + * @author Massimiliano Izzo * @description this builder works as a context for the query/search strategy */ /*jshint node: true */ /*jshint esnext: true */ "use strict"; -//let WtJSONBQueryStrategy = require("../../lib/query/WtQueryStrategies.js").WtJSONBQueryStrategy; +let WtJSONBQueryStrategy = require("../../lib/query/WtQueryStrategies.js").WtJSONBQueryStrategy; /** * @class @@ -22,20 +22,20 @@ class WtQueryBuilder { */ constructor(strategy) { - // if (!strategy) { - // strategy = new WtJSONBQueryStrategy(); - // } - // this.strategy = strategy; - // } - // - // get strategy() { - // return this._strategy; - // } - // - // set strategy(strategy) { - // if (strategy) { - // this._strategy = strategy; - // } + if (!strategy) { + strategy = new WtJSONBQueryStrategy(); + } + this.strategy = strategy; + } + + get strategy() { + return this._strategy; + } + + set strategy(strategy) { + if (strategy) { + this._strategy = strategy; + } } /** @@ -44,9 +44,9 @@ class WtQueryBuilder { * @description compose a query given a JSON query object * @param{Object} queryParams - a valid query object */ - compose(queryArgs) { - return global.sails.model.queryArgs.Model.find({type: queryArgs.idDataType}); - } + compose(queryParams) { + return this.strategy.compose(queryParams); + } } diff --git a/lib/query/WtQueryStrategies.js b/lib/query/WtQueryStrategies.js index 7e008c0..70334f7 100644 --- a/lib/query/WtQueryStrategies.js +++ b/lib/query/WtQueryStrategies.js @@ -1,19 +1,514 @@ /** * @module * @name WtCrudStrategies - * @author Nicolò Zanardi + * @author Massimiliano Izzo * @description this handler works as a context for the transaction strategy * */ +/*jshint node: true */ +/*jshint esnext: true */ +"use strict"; - // class WtQueryStrategy {} +let _ = require("lodash"); +const DataTypeClasses = require("../Utils").DataTypeClasses; +const FieldTypes = require("../Utils.js").FieldTypes; +let determineTableByModel = require("../Utils.js").determineTableByModel; +let allowedComparators = require("../Utils.js").allowedComparators; +let specializedProperties = require("../Utils.js").specializedProperties; +let pdProperties = require("../Utils.js").pdProperties; - /** - * @class - * @name WtJSONQueryStrategy - * @extends WtQueryStrategy -*/ +let queryOutput = { + lastPosition: 0, + cteCount: 0, + parameters: [] +}; - //class WtJSONQueryStrategy extends WtQueryStrategy { } +let fieldsForMainQueryMap = new Map([ + [DataTypeClasses.SUBJECT, "d.code, d.sex, "], + [DataTypeClasses.SAMPLE, "d.biobank, d.biobank_code, "], + [DataTypeClasses.DATA, ""] +]); - //module.export=WtJSONBQueryStrategy; +let fieldsForSubqueriesMap = new Map([ + [DataTypeClasses.SUBJECT, "id, code, sex"], + [DataTypeClasses.SAMPLE, "id, biobank_code, parent_subject, parent_sample"], + [DataTypeClasses.DATA, "id, parent_subject, parent_sample, parent_data"], + [undefined, "id, parent_subject, parent_sample, parent_data"] // default to DATA +]); + +String.prototype.toUnderscore = function(){ + return this.replace(/([A-Z])/g, function($1){return "_"+$1.toLowerCase();}); +}; + +/** + * @class + * @name WtQueryStrategy + */ +class WtQueryStrategy { + + /** + * @abstract + * @method + * @name compose + * @param{Object} criteria - the criteria object + */ + compose(criteria) { + throw new Error("Abstract method. Not implemented."); + } + +} + +/** + * @class + * @name WtJSONQueryStrategy + * @extends WtQueryStrategy + */ +class WtJSONQueryStrategy extends WtQueryStrategy { + + /** + * @method + * @name getSubqueryRow + * @param{Object} element - a leaf in the query criteria object. It must contain the following fields: + * 1) fieldName + * 2) fieldType [TEXT, INTEGER, FLOAT, DATE, BOOLEAN] + * 2) comparator + * 3) fieldValue + * 4) fieldUnit [optional] + * @param{Object} previousOutput + * @param{String} tablePrefix + */ + getSubqueryRow(element, previousOutput, tablePrefix) { + if (_.isEmpty(element)) { + return null; + } + if (allowedComparators.indexOf(element.comparator) < 0) { + console.log(element.comparator); + throw new Error("Operation not allowed. Trying to inject a forbidden comparator!!"); + } + let nameParam = '$'+(++previousOutput.lastPosition), valueParam, subquery; + if (element.isList) { + let values = []; + for (let i=0; i" + nameParam + "->>'value')::" + element.fieldType.toLowerCase() + + " " + element.comparator + " (" + valueParam + ")"; + } + else { + valueParam = '$'+(++previousOutput.lastPosition); + subquery = "(" + tablePrefix + "metadata->" + nameParam + "->>'value')::" + element.fieldType.toLowerCase() + + " " + element.comparator + " " + valueParam; + } + previousOutput.parameters.push(element.fieldName, element.fieldValue); + if (element.fieldUnit) { + let unitParam = '$'+(++previousOutput.lastPosition); + subquery += " AND "; + subquery += "(" + tablePrefix + "metadata->" + nameParam + "->>'unit')::text LIKE " + unitParam; + previousOutput.parameters.push(element.fieldUnit); + } + // flatten nested arrays in parameters + previousOutput.parameters = _.flatten(previousOutput.parameters); + return {subquery: subquery}; + } + + /** + * @method + * @name composeSpecializedQuery + * @description compose the part of the query relative to the specialized Model (Model here is intended in the sails.js sense) + * @return {Object} - the query for the specialized parameters + */ + composeSpecializedQuery(criteria, previousOutput, tablePrefix) { + let lastParameterPosition = previousOutput.lastPosition || 0; + previousOutput.parameters = previousOutput.parameters || []; + let dataTypeClass = criteria.specializedQuery; + tablePrefix = tablePrefix || ''; //'d.'; + let query = {}, clauses = [], comparator; + specializedProperties[dataTypeClass].forEach(function(property) { + if (criteria[property]) { + if (_.isArray(criteria[property])) { // if it is a list of options (like in sex) + comparator = allowedComparators.indexOf(criteria[property+"Comparator"]) >= 0 ? criteria[property+"Comparator"] : 'IN'; + let values = []; + for (let i=0; i= 0 ? criteria[property+"Comparator"] : '='; + clauses.push(tablePrefix + property.toUnderscore() + " " + comparator + " $" + (++lastParameterPosition)); + } + previousOutput.parameters.push(criteria[property]); + } + }); + if (clauses.length) { + query.subquery = clauses.join(" AND "); // TODO add possibility to switch and/or + } + query.lastParameterPosition = lastParameterPosition; + query.parameters = _.flatten(previousOutput.parameters); + return query; + } + + /** + * @method + * @name composeSpecializedPersonalDetailsQuery + * @description compose the part of a query pertaining to the personal_details table (personal data) + * @return {Object} + */ + composeSpecializedPersonalDetailsQuery(pdCriteria, previousOutput) { + if (!previousOutput) { + previousOutput = { + lastPosition: 0, + cteCount: 0, + parameters: [] + }; + } + let query = { alias: 'pd'}; + query.select = "SELECT id, given_name, surname, birth_date FROM personal_details"; + query.where = ""; + let whereClauses = []; // comparator; + + pdProperties.forEach(function(property) { + if (pdCriteria[property]) { + let comparator = allowedComparators.indexOf(pdCriteria[property+"Comparator"]) >= 0 ? pdCriteria[property+"Comparator"] : '='; + whereClauses.push(query.alias + "." + property.toUnderscore() + " " + comparator + " $" + (++previousOutput.lastPosition)); + let value = ['givenName', 'surname'].indexOf(property) > -1 ? pdCriteria[property].toUpperCase() : pdCriteria[property]; + previousOutput.parameters.push(value); + } + }); + if (whereClauses.length) { + // query.where = "WHERE " + whereClauses.join(" AND "); + query.subquery = whereClauses.join(" AND "); + } + query.previousOutput = previousOutput; + return query; + } + + + /** + * @method + * @name composeSingle + * @description composes a query based on a single DataType + */ + composeSingle(criteria, previousOutput, query) { // should I pass the parent params?? + if (!previousOutput) { + previousOutput = { + lastPosition: 0, + cteCount: 0, + parameters: [] + }; + } + if (!query) { + query= {}; + } + query.subqueries = []; + query.table = determineTableByModel(criteria.model); + console.log("PostgresJSONQueryStrategy.prototype.composeSingle - model: " + criteria.model); + console.log("PostgresJSONQueryStrategy.prototype.composeSingle - mapped fields: " + fieldsForSubqueriesMap.get(criteria.model)); + query.select= "SELECT " + fieldsForSubqueriesMap.get(criteria.model) + " FROM " + query.table; + let tableAlias = previousOutput.lastPosition ? "" : " d"; + let tablePrefix = previousOutput.lastPosition ? "" : "d."; + query.where = "WHERE " + tablePrefix + "type = $" + (++previousOutput.lastPosition); + previousOutput.parameters.push(criteria.dataType); + let fieldQueries = [], value; + if (criteria.content) { + for (let i=0; i $" + (++previousOutput.lastPosition); + previousOutput.parameters.push(JSON.stringify(param)); + } + + else if (element.isList) { + // if the comparator has a not condition add it a prefix + operatorPrefix = element.comparator === 'NOT IN' ? 'NOT ' : ''; + + subqueries = []; + for (i=0; i $" + (++previousOutput.lastPosition)); + previousOutput.parameters.push(param); + } + // join all conditions from a list with or + subquery = subqueries.join(" OR "); + } + + // if it is an equality matching use JSONB containment (@>) operator + else if (element.comparator === '=' || element.comparator === '<>') { // NOTE: "!=" operator is not allowed since it is not standard SQL + + // if the comparator has an inequality condition add a NOT a prefix + operatorPrefix = element.comparator === '<>' ? 'NOT ' : ''; + + let value = element.fieldType === FieldTypes.INTEGER ? _.parseInt(element.fieldValue) : + element.fieldType === FieldTypes.FLOAT ? Number(element.fieldValue) : + element.caseInsensitive ? element.fieldValue.toUpperCase() : element.fieldValue; + + // param = {}; + param[element.fieldName] = {value: value}; + subquery = operatorPrefix + tablePrefix + "metadata @> $" + (++previousOutput.lastPosition); + previousOutput.parameters.push(JSON.stringify(param)); + } + + // otherwise use the standard JSON/JSONB accessor (->/->>) operator + else { + subquery = "(" + tablePrefix + "metadata->$" + (++previousOutput.lastPosition) + "->>'value')::" + element.fieldType.toLowerCase() + + " " + element.comparator + " $" + (++previousOutput.lastPosition); + previousOutput.parameters.push(element.fieldName); + previousOutput.parameters.push(element.fieldValue); + } + + // add condition on unit if present + if (element.fieldUnit) { + param = "{\"" + element.fieldName + "\":{\"unit\":\"" + element.fieldUnit + "\"}}"; + subquery += " AND "; + subquery += tablePrefix + "metadata @> $" + (++previousOutput.lastPosition); + previousOutput.parameters.push(param); + } + // flatten nested arrays (if any) + console.log(previousOutput.parameters); + previousOutput.parameters = _.flatten(previousOutput.parameters); + return {subquery: subquery, previousOutput: previousOutput}; + + } + + /** + * @method + * @name getSubqueryRowLoop + */ + getSubqueryRowLoop(element, previousOutput, tablePrefix) { + + let subquery = "", operatorPrefix, jsonbValue = {}; + + if (element.comparator === '=' || element.comparator === '<>') { + operatorPrefix = element.comparator === '<>' ? 'NOT ' : ''; + subquery = operatorPrefix + tablePrefix + "metadata @> $" + (++previousOutput.lastPosition); + // if case-insensitive turn the value to uppercase + let val = element.fieldType === FieldTypes.INTEGER ? _.parseInt(element.fieldValue) : + element.fieldType === FieldTypes.FLOAT ? Number(element.fieldValue) : + element.caseInsensitive ? element.fieldValue.toUpperCase() : element.fieldValue; + jsonbValue[element.fieldName] = {values: [val]}; + } + + // ALL VALUES operator + else if (element.comparator === '?&') { + operatorPrefix = element.comparator !== '?&' ? 'NOT ' : ''; // TODO so far no negative query implemented for this one + subquery = operatorPrefix + tablePrefix + "metadata @> $" + (++previousOutput.lastPosition); + let val = element.fieldType === FieldTypes.INTEGER ? _.map(element.fieldValue, el => _.parseInt(el)) : + element.fieldType === FieldTypes.FLOAT ? _.map(element.fieldValue, el => Number(el)) : + element.caseInsensitive ? _.map(element.fieldValue, el => el.toUpperCase()) : element.fieldValue; + jsonbValue[element.fieldName] = {values: val}; + } + + // ANY OF THE VALUES operator + else if (element.comparator === '?|') { + operatorPrefix = ""; // TODO: so far no operator prefix + subquery = "(" + operatorPrefix + tablePrefix + "metadata->$" + (++previousOutput.lastPosition) + "->'values' " + element.comparator + " $" + (++previousOutput.lastPosition) + ")"; + // if case-insensitive turn all values to uppercase + jsonbValue = element.caseInsensitive ? _.map(element.fieldValue, el => el.toUpperCase()) : element.fieldValue; + previousOutput.parameters.push(element.fieldName); + } + + // string pattern matching queries (both case sensitive and insensitive) + else if (['LIKE', 'ILIKE', 'NOT LIKE', 'NOT ILIKE'].indexOf(element.comparator) > -1) { + subquery = "EXISTS (SELECT 1 FROM jsonb_array_elements_text(d.metadata->$" + (++previousOutput.lastPosition) + + "->'values') WHERE value " + element.comparator + " $" + (++previousOutput.lastPosition) + ")"; + previousOutput.parameters.push(element.fieldName, element.fieldValue); + } + + // add the jsonb value only if it not empty + if (!_.isEmpty(jsonbValue)) { + previousOutput.parameters.push(_.isArray(jsonbValue) ? jsonbValue : JSON.stringify(jsonbValue)); + } + + return {subquery: subquery, previousOutput: previousOutput}; + + } + +} + +module.exports.WtQueryStrategy = WtQueryStrategy; +module.exports.WtJSONQueryStrategy = WtJSONQueryStrategy; +module.exports.WtJSONBQueryStrategy = WtJSONBQueryStrategy;