diff --git a/README.md b/README.md index 972eac4c..beaa8e70 100644 --- a/README.md +++ b/README.md @@ -550,6 +550,8 @@ CustomerRepository.find({ PostgreSQL supports the following PostgreSQL-specific operators: - [`contains`](#operator-contains) +- [`containedBy`](#operator-containedby) +- [`containsAny`](#operator-containsany) - [`match`](#operator-match) Please note extended operators are disabled by default, you must enable @@ -597,6 +599,88 @@ const posts = await postRepository.find({ }); ``` +### Operator `containedBy` + +Inverse of the `contains` operator, the `containedBy` operator allow you to query array properties and pick only +rows where the all the items in the stored value are contained by the query. + +The operator is implemented using PostgreSQL [array operator +`<@`](https://www.postgresql.org/docs/current/functions-array.html). + +**Note** The fields you are querying must be setup to use the postgresql array data type - see [Defining models](#defining-models) above. + +Assuming a model such as this: + +```ts +@model({ + settings: { + allowExtendedOperators: true, + } +}) +class Post { + @property({ + type: ['string'], + postgresql: { + dataType: 'varchar[]', + }, + }) + categories?: string[]; +} +``` + +You can query the tags fields as follows: + +```ts +const posts = await postRepository.find({ + where: { + { + categories: {'containedBy': ['AA']}, + } + } +}); +``` + +### Operator `containsAnyOf` + +The `containsAnyOf` operator allow you to query array properties and pick only +rows where the any of the items in the stored value matches any of the items in the query. + +The operator is implemented using PostgreSQL [array overlap operator +`&&`](https://www.postgresql.org/docs/current/functions-array.html). + +**Note** The fields you are querying must be setup to use the postgresql array data type - see [Defining models](#defining-models) above. + +Assuming a model such as this: + +```ts +@model({ + settings: { + allowExtendedOperators: true, + } +}) +class Post { + @property({ + type: ['string'], + postgresql: { + dataType: 'varchar[]', + }, + }) + categories?: string[]; +} +``` + +You can query the tags fields as follows: + +```ts +const posts = await postRepository.find({ + where: { + { + categories: {'containsAnyOf': ['AA']}, + } + } +}); +``` + ### Operator `match` The `match` operator allows you to perform a [full text search using the `@@` operator](https://www.postgresql.org/docs/10/textsearch-tables.html#TEXTSEARCH-TABLES-SEARCH) in PostgreSQL. diff --git a/lib/postgresql.js b/lib/postgresql.js index e2f72981..6e186392 100644 --- a/lib/postgresql.js +++ b/lib/postgresql.js @@ -565,6 +565,14 @@ PostgreSQL.prototype.buildExpression = function(columnName, operator, return new ParameterizedSQL(columnName + ' @> array[' + operatorValue.map(() => '?') + ']::' + propertyDefinition.postgresql.dataType, operatorValue); + case 'containedBy': + return new ParameterizedSQL(columnName + ' <@ array[' + operatorValue.map(() => '?') + ']::' + + propertyDefinition.postgresql.dataType, + operatorValue); + case 'containsAnyOf': + return new ParameterizedSQL(columnName + ' && array[' + operatorValue.map(() => '?') + ']::' + + propertyDefinition.postgresql.dataType, + operatorValue); case 'match': return new ParameterizedSQL(`to_tsvector(${columnName}) @@ to_tsquery(?)`, [operatorValue]); default: diff --git a/test/postgresql.test.js b/test/postgresql.test.js index e924ce53..01a637cb 100644 --- a/test/postgresql.test.js +++ b/test/postgresql.test.js @@ -273,22 +273,54 @@ describe('postgresql connector', function() { }); }); - it('should support where filter for array type field', async () => { - await Post.create({ - title: 'LoopBack Participates in Hacktoberfest', - categories: ['LoopBack', 'Announcements'], + context('array operators', () => { + before(deleteTestFixtures); + before(createTestFixtures); + it('should support contains filter for array type field', async () => { + const found = await Post.find({where: {and: [ + { + categories: {'contains': ['LoopBack', 'Community']}, + }, + ]}}); + found.map(p => p.title).should.deepEqual(['Growing LoopBack Community']); }); - await Post.create({ - title: 'Growing LoopBack Community', - categories: ['LoopBack', 'Community'], + it('should support containedBy filter for array type field', async () => { + const found = await Post.find({where: {and: [ + { + categories: {'containedBy': ['LoopBack', 'Community', 'SomethingElse']}, + }, + ]}}); + found.map(p => p.title).should.deepEqual(['Growing LoopBack Community']); }); - - const found = await Post.find({where: {and: [ - { - categories: {'contains': ['LoopBack', 'Community']}, - }, - ]}}); - found.map(p => p.title).should.deepEqual(['Growing LoopBack Community']); + it('should support containsAnyOf filter for array type field', async () => { + const found = await Post.find({where: {and: [ + { + categories: {'containsAnyOf': ['LoopBack']}, + }, + ]}}); + found.map(p => p.title).should.deepEqual([ + 'LoopBack Participates in Hacktoberfest', + 'Growing LoopBack Community', + ]); + }); + function deleteTestFixtures(done) { + Post.destroyAll(function(err) { + should.not.exist(err); + done(); + }); + } + function createTestFixtures(done) { + Post.createAll([ + { + title: 'LoopBack Participates in Hacktoberfest', + categories: ['LoopBack', 'Hacktoberfest'], + }, + { + title: 'Growing LoopBack Community', + categories: ['LoopBack', 'Community'], + }, + ], done); + } }); it('should support full text search for text type fields using simple string query', async () => {