diff --git a/lib/postgresql.js b/lib/postgresql.js index c850a51c..1895f025 100644 --- a/lib/postgresql.js +++ b/lib/postgresql.js @@ -584,6 +584,28 @@ 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 (e) { + values = [values]; + } + } + if (!Array.isArray(values)) { + values = [values]; + } + const conditions = []; + const jsonContainsParams = []; + values.forEach(value => { + conditions.push(`${columnName} @> CAST(? AS JSONB)`); + jsonContainsParams.push(JSON.stringify([value])); + }); + const clause = '(' + conditions.join(' OR ') + ')'; + return new ParameterizedSQL(clause, jsonContainsParams); 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..aaa4f027 100644 --- a/test/postgresql.test.js +++ b/test/postgresql.test.js @@ -983,6 +983,64 @@ describe('postgresql connector', function() { done(); }); }); + + context('jsonContains operator', function() { + let Item; + + before(function(done) { + db = global.getDataSource(); + // Define a test model utilizing a jsonb column type to test JSON containment filters. + Item = db.define('ItemWithJson', { + name: {type: String}, + tags: { + type: 'object', + postgresql: { + dataType: 'jsonb', + }, + }, + }); + db.automigrate(['ItemWithJson'], done); + }); + + beforeEach(function(done) { + Item.destroyAll(done); + }); + + after(function(done) { + Item.destroyAll(done); + }); + + it('should find records matching jsonContains filter', async () => { + // Seed test records to verify multi-value containment matching. + await Item.createAll([ + {name: 'Item A', tags: ['foo', 'bar']}, + {name: 'Item B', tags: ['baz']}, + ]); + + // Execute find query using the mapped jsonContains operator. + const found = await Item.find({ + where: { + tags: {jsonContains: ['bar', 'foo']}, + }, + }); + + found.should.have.property('length', 1); + found[0].name.should.equal('Item A'); + }); + + it('should support unit-level SQL building verification for jsonContains', function() { + // Verify that buildWhere constructs the exact PostgreSQL-native JSONB containment syntax and parameters. + 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"]'], + }); + }); + }); }); describe('Serial properties', function() {