diff --git a/lib/postgresql.js b/lib/postgresql.js index c850a51c..0a744fe2 100644 --- a/lib/postgresql.js +++ b/lib/postgresql.js @@ -584,6 +584,43 @@ PostgreSQL.prototype.buildExpression = function(columnName, operator, operatorValue); case 'match': return new ParameterizedSQL(`to_tsvector(${columnName}) @@ to_tsquery(?)`, [operatorValue]); + // Map the abstract 'jsonContains' operator to PostgreSQL's JSONB containment operator (@>), + // parsing or normalizing string/array inputs to build parameterized clauses connected with OR. + case 'jsonContains': + let values = operatorValue; + if (typeof values === 'string') { + try { + values = JSON.parse(values); + } catch (error) { + values = [values]; + } + } + if (!Array.isArray(values)) { values = [values]; } + + const itemType = propertyDefinition.type[0]; + const postgresTypeMap = new Map([ + [Number, 'INTEGER'], + [String, 'TEXT'], + ]); + const arrayItemType = postgresTypeMap.get(itemType); + + if (!arrayItemType) { + throw new Error(`Unsupported array item type: ${itemType ? itemType.name : 'unknown'}`); + } + const isArray = propertyDefinition.postgresql.dataType === 'ARRAY'; + const conditions = []; + const params = []; + + values.forEach(value => { + if (isArray) { + conditions.push(`${columnName} @> ARRAY[?]::${arrayItemType}[]`); + params.push(value); + } else { + conditions.push(`${columnName} @> CAST(? AS JSONB)`); + params.push(JSON.stringify([value])); + } + }); + return new ParameterizedSQL(`(${conditions.join(' OR ')})`, params); default: // invoke the base implementation of `buildExpression` return this.invokeSuper('buildExpression', columnName, operator, diff --git a/test/postgresql.test.js b/test/postgresql.test.js index 01a637cb..b63d7355 100644 --- a/test/postgresql.test.js +++ b/test/postgresql.test.js @@ -983,6 +983,91 @@ describe('postgresql connector', function() { done(); }); }); + + context('jsonContains operator', function() { + let ItemWithJson, ItemWithIntegerArray, ItemWithStringArray; + + before(function(done) { + db = global.getDataSource(); + // Define a test model utilizing a jsonb column type to test JSON containment filters. + ItemWithJson = db.define('ItemWithJson', { + name: {type: String}, + tags: { + type: [String], + postgresql: { + dataType: 'jsonb', + }, + }, + }); + ItemWithIntegerArray = db.define('ItemWithIntegerArray', { + name: {type: String}, + tags: { + type: [Number], + postgresql: { + dataType: 'jsonb', + }, + }, + }); + ItemWithStringArray = db.define('ItemWithStringArray', { + name: {type: String}, + tags: { + type: [String], + postgresql: { + dataType: 'jsonb', + }, + }, + }); + db.automigrate(['ItemWithJson', 'ItemWithIntegerArray', 'ItemWithStringArray'], done); + }); + + beforeEach(function(done) { + ItemWithJson.destroyAll(); + ItemWithIntegerArray.destroyAll(); + ItemWithStringArray.destroyAll(done); + }); + + after(function(done) { + ItemWithJson.destroyAll(); + ItemWithIntegerArray.destroyAll(); + ItemWithStringArray.destroyAll(done); + }); + + it('should support unit-level SQL building verification for jsonContains', function() { + const where = db.connector.buildWhere( + 'ItemWithJson', + {tags: {jsonContains: ['bar', 'foo']}}, + ); + + where.toJSON().should.eql({ + sql: 'WHERE ("tags" @> CAST(? AS JSONB) OR "tags" @> CAST(? AS JSONB))', + params: ['["bar"]', '["foo"]'], + }); + }); + + it('should support unit-level SQL building verification for jsonContains with integer array', function() { + const where = db.connector.buildWhere( + 'ItemWithIntegerArray', + {tags: {jsonContains: [1, 2]}}, + ); + + where.toJSON().should.eql({ + sql: 'WHERE ("tags" @> CAST(? AS JSONB) OR "tags" @> CAST(? AS JSONB))', + params: ['[1]', '[2]'], + }); + }); + + it('should support unit-level SQL building verification for jsonContains with string array', function() { + const where = db.connector.buildWhere( + 'ItemWithStringArray', + {tags: {jsonContains: ['bar', 'foo']}}, + ); + + where.toJSON().should.eql({ + sql: 'WHERE ("tags" @> CAST(? AS JSONB) OR "tags" @> CAST(? AS JSONB))', + params: ['["bar"]', '["foo"]'], + }); + }); + }); }); describe('Serial properties', function() {