Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/postgresql.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
58 changes: 58 additions & 0 deletions test/postgresql.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading