diff --git a/lib/sql.js b/lib/sql.js index e4479c19..01be0345 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -912,7 +912,7 @@ SQLConnector.prototype._buildWhereObjById = function(model, id, data) { */ SQLConnector.prototype.buildUpdate = function(model, where, data, options) { const fields = this.buildFieldsForUpdate(model, data); - return this._constructUpdateQuery(model, where, fields); + return this._constructUpdateQuery(model, where, fields, options); }; /** @@ -936,9 +936,9 @@ SQLConnector.prototype.buildReplace = function(model, where, data, options) { * @returns {Object} update query Constructed update query. * @private */ -SQLConnector.prototype._constructUpdateQuery = function(model, where, fields) { +SQLConnector.prototype._constructUpdateQuery = function(model, where, fields, options) { const updateClause = new ParameterizedSQL('UPDATE ' + this.tableEscaped(model)); - const whereClause = this.buildWhere(model, where); + const whereClause = this.buildWhere(model, where, options); updateClause.merge([fields, whereClause]); return this.parameterize(updateClause); }; @@ -1007,8 +1007,23 @@ Connector.defineAliases(SQLConnector.prototype, 'replace', ['replaceAll']); * @param {object} where An object for the where conditions * @returns {ParameterizedSQL} The SQL WHERE clause */ -SQLConnector.prototype.buildWhere = function(model, where) { - const whereClause = this._buildWhere(model, where); +SQLConnector.prototype.buildWhere = function(model, where, options) { + let relationType = ''; + let relationKeyFrom = ''; + if (options && options['model'] && options['model']['definition']) { + const {relations} = options['model']['definition']; + if (relations) { + const relationKeys = Object.keys(relations); + for (let relationIndex = 0; relationIndex < relationKeys.length; relationIndex++) { + const relationName = relationKeys[relationIndex]; + const relation = relations[relationName]; + relationType = relation.type; + relationKeyFrom = relation.keyFrom; + if (relationType === 'referencesMany') break; + } + } + } + const whereClause = this._buildWhere(model, where, relationType, relationKeyFrom); if (whereClause.sql) { whereClause.sql = 'WHERE ' + whereClause.sql; } @@ -1024,7 +1039,8 @@ SQLConnector.prototype.buildWhere = function(model, where) { * @returns {ParameterizedSQL} The SQL expression */ SQLConnector.prototype.buildExpression = -function(columnName, operator, columnValue, propertyValue) { +function(relationDetails, columnName, operator, columnValue, propertyValue) { + const {relationType, relationKeyFrom} = relationDetails; function buildClause(columnValue, separator, grouping) { const values = []; for (let i = 0, n = columnValue.length; i < n; i++) { @@ -1067,8 +1083,24 @@ function(columnName, operator, columnValue, propertyValue) { clause = buildClause(columnValue, ' AND ', false); break; case 'inq': - sqlExp += ' IN '; - clause = buildClause(columnValue, ',', true); + if (relationType === 'referencesMany' && `\`${relationKeyFrom}\`` === columnName) { + sqlExp = ''; + if (columnValue.length === 1) { + sqlExp = `JSON_CONTAINS(${columnName}, CAST(${columnValue[0]} as JSON))`; + } else { + columnValue.forEach(value => { + sqlExp += `JSON_CONTAINS(${columnName}, CAST(${value} as JSON)) OR `; + }); + const trimmed = sqlExp.trimEnd(); + if (trimmed.endsWith('OR')) { + sqlExp = trimmed.slice(0, trimmed.lastIndexOf('OR')); + } + } + clause = null; + } else { + sqlExp += ' IN '; + clause = buildClause(columnValue, ',', true); + } break; case 'nin': sqlExp += ' NOT IN '; @@ -1102,125 +1134,149 @@ function(columnName, operator, columnValue, propertyValue) { * @param where * @returns {ParameterizedSQL} */ -SQLConnector.prototype._buildWhere = function(model, where) { - let columnValue, sqlExp; - if (!where) { - return new ParameterizedSQL(''); - } - if (typeof where !== 'object' || Array.isArray(where)) { - debug('Invalid value for where: %j', where); - return new ParameterizedSQL(''); - } - const self = this; - const props = self.getModelDefinition(model).properties; - - const whereStmts = []; - for (const key in where) { - const stmt = new ParameterizedSQL('', []); - // Handle and/or operators - if (key === 'and' || key === 'or') { - const branches = []; - let branchParams = []; - const clauses = where[key]; - if (Array.isArray(clauses)) { - for (let i = 0, n = clauses.length; i < n; i++) { - const stmtForClause = self._buildWhere(model, clauses[i]); - if (stmtForClause.sql) { - stmtForClause.sql = '(' + stmtForClause.sql + ')'; - branchParams = branchParams.concat(stmtForClause.params); - branches.push(stmtForClause.sql); +SQLConnector.prototype._buildWhere = + function(model, where, relationType, relationKeyFrom) { + let columnValue, sqlExp; + if (!where) { + return new ParameterizedSQL(''); + } + if (typeof where !== 'object' || Array.isArray(where)) { + debug('Invalid value for where: %j', where); + return new ParameterizedSQL(''); + } + const self = this; + const props = self.getModelDefinition(model).properties; + + const whereStmts = []; + for (const key in where) { + const stmt = new ParameterizedSQL('', []); + // Handle and/or operators + if (key === 'and' || key === 'or') { + const branches = []; + let branchParams = []; + const clauses = where[key]; + if (Array.isArray(clauses)) { + for (let i = 0, n = clauses.length; i < n; i++) { + const stmtForClause = self + ._buildWhere(model, clauses[i], relationType, relationKeyFrom); + if (stmtForClause.sql) { + stmtForClause.sql = '(' + stmtForClause.sql + ')'; + branchParams = branchParams.concat(stmtForClause.params); + branches.push(stmtForClause.sql); + } } + stmt.merge({ + sql: '(' + branches.join(' ' + key.toUpperCase() + ' ') + ')', + params: branchParams, + }); + whereStmts.push(stmt); + continue; } - stmt.merge({ - sql: '(' + branches.join(' ' + key.toUpperCase() + ' ') + ')', - params: branchParams, - }); - whereStmts.push(stmt); + // The value is not an array, fall back to regular fields + } + const p = props[key]; + if (p == null) { + // Unknown property, ignore it + debug('Unknown property %s is skipped for model %s', key, model); continue; } - // The value is not an array, fall back to regular fields - } - const p = props[key]; - if (p == null) { - // Unknown property, ignore it - debug('Unknown property %s is skipped for model %s', key, model); - continue; - } - // eslint-disable one-var - let expression = where[key]; - const columnName = self.columnEscaped(model, key); - // eslint-enable one-var - if (expression === null || expression === undefined) { - stmt.merge(columnName + ' IS NULL'); - } else if (expression && expression.constructor === Object) { - const operator = Object.keys(expression)[0]; - // Get the expression without the operator - expression = expression[operator]; - if (operator === 'inq' || operator === 'nin' || operator === 'between') { - columnValue = []; - if (Array.isArray(expression)) { - // Column value is a list - for (let j = 0, m = expression.length; j < m; j++) { - columnValue.push(this.toColumnValue(p, expression[j])); + // eslint-disable one-var + let expression = where[key]; + const columnName = self.columnEscaped(model, key); + // eslint-enable one-var + if (expression === null || expression === undefined) { + stmt.merge(columnName + ' IS NULL'); + } else if (expression && expression.constructor === Object) { + const operator = Object.keys(expression)[0]; + // Get the expression without the operator + expression = expression[operator]; + if (operator === 'inq' || operator === 'nin' || operator === 'between') { + columnValue = []; + if (Array.isArray(expression)) { + // Column value is a list + for (let j = 0, m = expression.length; j < m; j++) { + columnValue.push(this.toColumnValue(p, expression[j])); + } + } else { + columnValue.push(this.toColumnValue(p, expression)); } + if (operator === 'between') { + // BETWEEN v1 AND v2 + const v1 = columnValue[0] === undefined ? null : columnValue[0]; + const v2 = columnValue[1] === undefined ? null : columnValue[1]; + columnValue = [v1, v2]; + } else { + // IN (v1,v2,v3) or NOT IN (v1,v2,v3) + if (columnValue.length === 0) { + if (operator === 'inq') { + columnValue = [null]; + } else { + // nin () is true + continue; + } + } + } + } else if (operator === 'regexp' && expression instanceof RegExp) { + // do not coerce RegExp based on property definitions + columnValue = expression; } else { - columnValue.push(this.toColumnValue(p, expression)); + columnValue = this.toColumnValue(p, expression); } - if (operator === 'between') { - // BETWEEN v1 AND v2 - const v1 = columnValue[0] === undefined ? null : columnValue[0]; - const v2 = columnValue[1] === undefined ? null : columnValue[1]; - columnValue = [v1, v2]; + sqlExp = self + .buildExpression( + {relationType, relationKeyFrom}, + columnName, operator, columnValue, + p, + ); + if ( + relationType === 'referencesMany' && + `\`${relationKeyFrom}\`` === columnName + ) { + stmt.merge(sqlExp, columnValue); } else { - // IN (v1,v2,v3) or NOT IN (v1,v2,v3) - if (columnValue.length === 0) { - if (operator === 'inq') { - columnValue = [null]; - } else { - // nin () is true - continue; - } - } + stmt.merge(sqlExp); } - } else if (operator === 'regexp' && expression instanceof RegExp) { - // do not coerce RegExp based on property definitions - columnValue = expression; - } else { - columnValue = this.toColumnValue(p, expression); - } - sqlExp = self.buildExpression(columnName, operator, columnValue, p); - stmt.merge(sqlExp); - } else { - // The expression is the field value, not a condition - columnValue = self.toColumnValue(p, expression); - if (columnValue === null) { - stmt.merge(columnName + ' IS NULL'); } else { - if (columnValue instanceof ParameterizedSQL) { - stmt.merge(columnName + '=').merge(columnValue); + // The expression is the field value, not a condition + columnValue = self.toColumnValue(p, expression); + if (columnValue === null) { + stmt.merge(columnName + ' IS NULL'); } else { - stmt.merge({ - sql: columnName + '=?', - params: [columnValue], - }); + if (columnValue instanceof ParameterizedSQL) { + stmt.merge(columnName + '=').merge(columnValue); + } else { + if ( + relationType === 'referencesMany' && + `\`${relationKeyFrom}\`` === columnName + ) { + stmt.merge({ + sql: `JSON_CONTAINS(${columnName}, CAST(? as JSON))`, + params: [columnValue], + }); + } else { + stmt.merge({ + sql: columnName + '=?', + params: [columnValue], + }); + } + } } } + whereStmts.push(stmt); } - whereStmts.push(stmt); - } - let params = []; - const sqls = []; - for (let k = 0, s = whereStmts.length; k < s; k++) { - if (!whereStmts[k].sql) continue; - sqls.push(whereStmts[k].sql); - params = params.concat(whereStmts[k].params); - } - const whereStmt = new ParameterizedSQL({ - sql: sqls.join(' AND '), - params: params, - }); - return whereStmt; -}; + let params = []; + const sqls = []; + for (let k = 0, s = whereStmts.length; k < s; k++) { + if (!whereStmts[k].sql) continue; + sqls.push(whereStmts[k].sql); + params = params.concat(whereStmts[k].params); + } + const whereStmt = new ParameterizedSQL({ + sql: sqls.join(' AND '), + params: params, + }); + return whereStmt; + }; /** * Build the ORDER BY clause @@ -1453,7 +1509,7 @@ SQLConnector.prototype.buildSelect = function(model, filter, options) { if (filter) { if (filter.where) { - const whereStmt = this.buildWhere(model, filter.where); + const whereStmt = this.buildWhere(model, filter.where, options); selectStmt.merge(whereStmt); } @@ -1592,7 +1648,7 @@ SQLConnector.prototype.count = function(model, where, options, cb) { let stmt = new ParameterizedSQL('SELECT count(*) as "cnt" FROM ' + this.tableEscaped(model)); - stmt = stmt.merge(this.buildWhere(model, where)); + stmt = stmt.merge(this.buildWhere(model, where, options)); stmt = this.parameterize(stmt); this.execute(stmt.sql, stmt.params, options, function(err, res) { diff --git a/test/sql.test.js b/test/sql.test.js index 2fd635ea..e8127e27 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -63,7 +63,14 @@ describe('sql connector', function() { column: 'TOKEN', }, }, - address: String, + address: String, ids: { + type: String, + testdb: { + column: 'IDS', + dataType: 'VARCHAR', + dataLength: 32, + }, + }, }, {testdb: {table: 'CUSTOMER'}}); Order = ds.createModel('order', @@ -136,6 +143,31 @@ describe('sql connector', function() { }); }); + it('builds where with relation options', function() { + const where = connector.buildWhere( + 'customer', + {ids: {inq: ['1', '2']}}, + { + model: { + definition: { + relations: { + userComapnies: { + type: 'referencesMany', + model: 'Company', + keyFrom: 'ids', + keyTo: 'id', + }, + }, + }, + }, + }, + ); + expect(where.toJSON()).to.eql({ + sql: 'WHERE `IDS` IN (?,?)', + params: ['1', '2'], + }); + }); + it('builds where with null', function() { const where = connector.buildWhere('customer', {name: null}); expect(where.toJSON()).to.eql({ @@ -296,13 +328,15 @@ describe('sql connector', function() { primaryAddress: '1031 NW 7th Ave, Fort Lauderdale, Florida, United States', }); expect(fields.toJSON()).to.eql({ - sql: 'SET `middle_name`=?,`LASTNAME`=?,`VIP`=?,`primary_address`=?,`ADDRESS`=?', + sql: 'SET `middle_name`=?,`LASTNAME`=?,`VIP`=?,`primary_address`=?,' + + '`ADDRESS`=?,`IDS`=?', params: [ '', 'Libert', true, '1031 NW 7th Ave, Fort Lauderdale, Florida, United States', '1031 NW 7th Ave, Fort Lauderdale, Florida, United States', + null, ], }); }); @@ -328,7 +362,7 @@ describe('sql connector', function() { it('builds column names for SELECT', function() { const cols = connector.buildColumnNames('customer'); expect(cols).to.eql('`NAME`,`middle_name`,`LASTNAME`,`VIP`,' + - '`primary_address`,`TOKEN`,`ADDRESS`'); + '`primary_address`,`TOKEN`,`ADDRESS`,`IDS`'); }); it('builds column names with true fields filter for SELECT', function() { @@ -346,7 +380,7 @@ describe('sql connector', function() { middleName: false, }, }); - expect(cols).to.eql('`VIP`,`ADDRESS`'); + expect(cols).to.eql('`VIP`,`ADDRESS`,`IDS`'); }); it('builds column names with array fields filter for SELECT', function() { @@ -379,7 +413,7 @@ describe('sql connector', function() { expect(sql.toJSON()).to.eql({ sql: 'SELECT `NAME`,`middle_name`,`LASTNAME`,`VIP`,`primary_address`,' + - '`TOKEN`,`ADDRESS` FROM `CUSTOMER` WHERE ((`NAME`=$1) OR (`ADDRESS`=$2)) ' + + '`TOKEN`,`ADDRESS`,`IDS` FROM `CUSTOMER` WHERE ((`NAME`=$1) OR (`ADDRESS`=$2)) ' + 'AND `VIP`=$3 ORDER BY `NAME` LIMIT 5', params: ['Top Cat', 'Trash can', true], }); @@ -390,7 +424,7 @@ describe('sql connector', function() { {order: 'name', limit: 5, where: {name: 'John'}}); expect(sql.toJSON()).to.eql({ sql: 'SELECT `NAME`,`middle_name`,`LASTNAME`,`VIP`,`primary_address`,`TOKEN`,' + - '`ADDRESS` FROM `CUSTOMER`' + + '`ADDRESS`,`IDS` FROM `CUSTOMER`' + ' WHERE `NAME`=$1 ORDER BY `NAME` LIMIT 5', params: ['John'], });