diff --git a/lib/sql.js b/lib/sql.js index e4479c19..0bd67e9a 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,9 @@ 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) { + const whereClause = this._buildWhere(model, where, options); + if (whereClause.sql) { whereClause.sql = 'WHERE ' + whereClause.sql; } @@ -1043,6 +1044,7 @@ function(columnName, operator, columnValue, propertyValue) { } let sqlExp = columnName; + let jsonContainsParams = []; let clause; if (columnValue instanceof ParameterizedSQL) { clause = columnValue; @@ -1066,6 +1068,29 @@ function(columnName, operator, columnValue, propertyValue) { sqlExp += ' BETWEEN '; clause = buildClause(columnValue, ' AND ', false); break; + case 'jsonContains': + // Handle JSON array containment for referencesMany relations + sqlExp = ''; + // If columnValue is a JSON string array, parse it + if (typeof columnValue === 'string') { + try { + columnValue = JSON.parse(columnValue); + } catch (e) { + columnValue = [columnValue]; + } + } + if (!Array.isArray(columnValue)) { + columnValue = [columnValue]; + } + const conditions = []; + jsonContainsParams = []; + columnValue.forEach(value => { + conditions.push(`JSON_CONTAINS(${columnName}, CAST(? as JSON))`); + jsonContainsParams.push(value); + }); + sqlExp = conditions.join(' OR '); + clause = null; + break; case 'inq': sqlExp += ' IN '; clause = buildClause(columnValue, ',', true); @@ -1092,6 +1117,9 @@ function(columnName, operator, columnValue, propertyValue) { sqlExp += ' REGEXP '; break; } + if (jsonContainsParams.length) { + return new ParameterizedSQL({sql: sqlExp, params: jsonContainsParams}); + } const stmt = ParameterizedSQL.join([sqlExp, clause], ''); return stmt; }; @@ -1102,7 +1130,7 @@ function(columnName, operator, columnValue, propertyValue) { * @param where * @returns {ParameterizedSQL} */ -SQLConnector.prototype._buildWhere = function(model, where) { +SQLConnector.prototype._buildWhere = function(model, where, options) { let columnValue, sqlExp; if (!where) { return new ParameterizedSQL(''); @@ -1124,7 +1152,7 @@ SQLConnector.prototype._buildWhere = function(model, where) { 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]); + const stmtForClause = self._buildWhere(model, clauses[i], options); if (stmtForClause.sql) { stmtForClause.sql = '(' + stmtForClause.sql + ')'; branchParams = branchParams.concat(stmtForClause.params); @@ -1156,7 +1184,12 @@ SQLConnector.prototype._buildWhere = function(model, where) { const operator = Object.keys(expression)[0]; // Get the expression without the operator expression = expression[operator]; - if (operator === 'inq' || operator === 'nin' || operator === 'between') { + if ( + operator === 'inq' || + operator === 'nin' || + operator === 'between' || + operator === 'jsonContains' + ) { columnValue = []; if (Array.isArray(expression)) { // Column value is a list @@ -1189,23 +1222,28 @@ SQLConnector.prototype._buildWhere = function(model, where) { columnValue = this.toColumnValue(p, expression); } sqlExp = self.buildExpression(columnName, operator, columnValue, p); - stmt.merge(sqlExp); + if (operator === 'jsonContains') { + stmt.merge(sqlExp, columnValue); + } else { + 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); } else { - if (columnValue instanceof ParameterizedSQL) { - stmt.merge(columnName + '=').merge(columnValue); - } else { - stmt.merge({ - sql: columnName + '=?', - params: [columnValue], - }); - } + stmt.merge({ + sql: columnName + '=?', + params: [columnValue], + }); } } + if (stmt.sql.includes('JSON_CONTAINS')) { + stmt.sql = `(${stmt.sql})`; + } whereStmts.push(stmt); } let params = []; @@ -1453,7 +1491,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 +1630,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..a7918d08 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,32 @@ describe('sql connector', function() { }); }); + // jsonContains is operator when we have referencesMany with inq + it('builds where with jsonContains', function() { + const where = connector.buildWhere( + 'customer', + {ids: {jsonContains: ['1', '2']}}, + ); + expect(where.toJSON()).to.eql({ + sql: + 'WHERE (JSON_CONTAINS(`IDS`, CAST(? as JSON))' + + ' OR ' + + 'JSON_CONTAINS(`IDS`, CAST(? as JSON)))', + params: ['1', '2'], + }); + }); + + it('builds where with relation options', function() { + const where = connector.buildWhere( + 'customer', + {ids: {inq: ['1', '2']}}, + ); + 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 +329,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 +363,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 +381,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 +414,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 +425,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'], });