From 74ff2fe2b060edba05d325d3395fdefd47022752 Mon Sep 17 00:00:00 2001 From: shubhisood Date: Fri, 4 Sep 2020 01:09:50 +0530 Subject: [PATCH 1/4] feat: adds 'contains' operator for querying arrays Implement support for an extended operator `contains` that can be used to filter records that have an array property containing the selected items. Signed-off-by: shubhisood --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++ lib/postgresql.js | 3 +++ test/postgresql.test.js | 10 +++++++ 3 files changed, 71 insertions(+) diff --git a/README.md b/README.md index 52456ef2..80db2bc3 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,29 @@ const config = { }; ``` + +## Additional properties + + + + + + + + + + + + + + + + + + +
PropertyTypeDefaultDescription
allowExtendedOperatorsBooleanfalseSet to true to enable using Postgres operators such as contains which is used to perform filter on postgres array type field.
+ + ## Defining models LoopBack allows you to specify some database settings through the model definition and/or the property definition. These definitions would be mapped to the database. Please check out the CLI [`lb4 model`](https://loopback.io/doc/en/lb4/Model-generator.html) for generating LB4 models. The following is a typical LoopBack 4 model that specifies the schema, table and column details through model definition and property definitions: @@ -412,6 +435,12 @@ details on LoopBack's data types. VARCHAR2
Default length is 1024 + + + String[] + + VARCHAR2[] + Number @@ -531,6 +560,35 @@ CustomerRepository.find({ }); ``` +## Querying Postgres Array type fields + +**Note** The fields you are querying should be setup to use the postgresql array data type - see Defining models + +Assuming a model such as this: + +```ts + @property({ + type: ['string'], + postgresql: { + dataType: 'varchar[]', + }, + }) + categories?: string[]; +``` + +You can query the tags fields via setting allowExtendedOperators to true + +```ts +Model.dataSource.settings.allowExtendedOperators = true; + const post = await Post.find({where: { + { + categories: {'contains': ['AA']}, + } + } + }); +``` + + ## Discovery and auto-migration ### Model discovery diff --git a/lib/postgresql.js b/lib/postgresql.js index 0c5f9a05..8673e9f5 100644 --- a/lib/postgresql.js +++ b/lib/postgresql.js @@ -522,6 +522,9 @@ PostgreSQL.prototype.buildExpression = function(columnName, operator, const regexOperator = operatorValue.ignoreCase ? ' ~* ?' : ' ~ ?'; return new ParameterizedSQL(columnName + regexOperator, [operatorValue.source]); + case 'contains': + return new ParameterizedSQL(columnName + ' @> array[' + operatorValue.map((v) => `'${v}'`) + ']::' + + propertyDefinition.postgresql.dataType); 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 08346f51..8e5a5606 100644 --- a/test/postgresql.test.js +++ b/test/postgresql.test.js @@ -263,6 +263,16 @@ describe('postgresql connector', function() { }); }); + it('should support where filter for array type field', async () => { + Post.dataSource.settings.allowExtendedOperators = true; + const post = await Post.find({where: {and: [ + { + categories: {'contains': ['AA']}, + }, + ]}}); + should.exist(post); + post.length.should.equal(1); + }); it('should support boolean types with false value', function(done) { Post.create( {title: 'T2', content: 'C2', approved: false, created: created}, From 96415eac68cfea3b1b51ad079d438ec33c267a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 6 Oct 2020 10:56:06 +0200 Subject: [PATCH 2/4] docs: improve README organization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- README.md | 93 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 80db2bc3..0aa11976 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ const config = {
For LoopBack 3 users -Use the [Data source generator](http://loopback.io/doc/en/lb3/Data-source-generator.html) to add a PostgreSQL data source to your application. +Use the [Data source generator](http://loopback.io/doc/en/lb3/Data-source-generator.html) to add a PostgreSQL data source to your application. The generator will prompt for the database server hostname, port, and other settings required to connect to a PostgreSQL database. It will also run the `npm install` command above for you. @@ -96,7 +96,7 @@ const config = { Check out [node-pg-pool](https://github.com/brianc/node-pg-pool) and [node postgres pooling example](https://github.com/brianc/node-postgres#pooling-example) for more information. -### Properties +### Configuration options @@ -106,7 +106,7 @@ Check out [node-pg-pool](https://github.com/brianc/node-pg-pool) and [node postg - + @@ -176,6 +176,14 @@ Check out [node-pg-pool](https://github.com/brianc/node-pg-pool) and [node postg + + + + +
Description
connector StringBoolean/String Set to false to disable default sorting on id column(s). Set to numericIdOnly to only apply to IDs with a number type id.
allowExtendedOperatorsBooleanSet to true to enable PostgreSQL-specific operators + such as contains. Learn more in + Extended operators below. +
@@ -202,29 +210,6 @@ const config = { }; ``` - -## Additional properties - - - - - - - - - - - - - - - - - - -
PropertyTypeDefaultDescription
allowExtendedOperatorsBooleanfalseSet to true to enable using Postgres operators such as contains which is used to perform filter on postgres array type field.
- - ## Defining models LoopBack allows you to specify some database settings through the model definition and/or the property definition. These definitions would be mapped to the database. Please check out the CLI [`lb4 model`](https://loopback.io/doc/en/lb4/Model-generator.html) for generating LB4 models. The following is a typical LoopBack 4 model that specifies the schema, table and column details through model definition and property definitions: @@ -295,7 +280,7 @@ The model definition consists of the following properties. Description - + name Camel-case of the database table name @@ -560,43 +545,65 @@ CustomerRepository.find({ }); ``` -## Querying Postgres Array type fields +## Extended operators + +PostgreSQL supports the following PostgreSQL-specific operators: + +- [`contains`](#operator-contains) + +Please note extended operators are disabled by default, you must enable +them at datasource level or model level by setting `allowExtendedOperators` to +`true`. + +### Operator `contains` -**Note** The fields you are querying should be setup to use the postgresql array data type - see Defining models +The `contains` operator allow you to query array properties and pick only +rows where the stored value contains all of the items specified 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[]', - }, + dataType: 'varchar[]', + }, }) categories?: string[]; +} ``` -You can query the tags fields via setting allowExtendedOperators to true +You can query the tags fields as follows: ```ts -Model.dataSource.settings.allowExtendedOperators = true; - const post = await Post.find({where: { - { - categories: {'contains': ['AA']}, - } - } - }); +const posts = await postRepository.find({ + where: { + { + categories: {'contains': ['AA']}, + } + } +}); ``` - ## Discovery and auto-migration ### Model discovery The PostgreSQL connector supports _model discovery_ that enables you to create LoopBack models based on an existing database schema. Once you defined your datasource: -- LoopBack 4 users could use the commend [`lb4 discover`](https://loopback.io/doc/en/lb4/Discovering-models.html) to discover models. -- For LB3 users, please check [Discovering models from relational databases](https://loopback.io/doc/en/lb3/Discovering-models-from-relational-databases.html). +- LoopBack 4 users could use the commend [`lb4 discover`](https://loopback.io/doc/en/lb4/Discovering-models.html) to discover models. +- For LB3 users, please check [Discovering models from relational databases](https://loopback.io/doc/en/lb3/Discovering-models-from-relational-databases.html). (See [database discovery API](http://apidocs.strongloop.com/loopback-datasource-juggler/#datasource-prototype-discoverandbuildmodels) for related APIs information) @@ -670,7 +677,7 @@ Here are some limitations and tips: ### Auto-migrate/Auto-update models with foreign keys -Foreign key constraints can be defined in the model definition. +Foreign key constraints can be defined in the model definition. **Note**: The order of table creation is important. A referenced table must exist before creating a foreign key constraint. From 12645558d5a218560bb5b0dc03b52e8fc2e33c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 6 Oct 2020 11:33:06 +0200 Subject: [PATCH 3/4] test: fix array tests to handle List values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Array properties are represented as juggler `List` instance, we need to modify test assertions to convert them via `toArray()` before applying `.should.eql` check. Signed-off-by: Miroslav Bajtoš --- test/postgresql.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/postgresql.test.js b/test/postgresql.test.js index 8e5a5606..c710a567 100644 --- a/test/postgresql.test.js +++ b/test/postgresql.test.js @@ -218,7 +218,7 @@ describe('postgresql connector', function() { post.should.have.property('tags'); post.tags.should.be.Array(); post.tags.length.should.eql(2); - post.tags.should.eql(['AA', 'AB']); + post.tags.toArray().should.eql(['AA', 'AB']); return Post.updateAll({where: {id: postId}}, {tags: ['AA', 'AC']}); }) .then(()=> { @@ -228,7 +228,7 @@ describe('postgresql connector', function() { post.should.have.property('tags'); post.tags.should.be.Array(); post.tags.length.should.eql(2); - post.tags.should.eql(['AA', 'AC']); + post.tags.toArray().should.eql(['AA', 'AC']); done(); }) .catch((error) => { @@ -245,7 +245,7 @@ describe('postgresql connector', function() { post.should.have.property('categories'); post.categories.should.be.Array(); post.categories.length.should.eql(2); - post.categories.should.eql(['AA', 'AB']); + post.categories.toArray().should.eql(['AA', 'AB']); return Post.updateAll({where: {id: postId}}, {categories: ['AA', 'AC']}); }) .then(()=> { @@ -255,7 +255,7 @@ describe('postgresql connector', function() { post.should.have.property('categories'); post.categories.should.be.Array(); post.categories.length.should.eql(2); - post.categories.should.eql(['AA', 'AC']); + post.categories.toArray().should.eql(['AA', 'AC']); done(); }) .catch((error) => { From 8f2da81ec6a1546af9d49e296414d544f30253a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 6 Oct 2020 11:34:22 +0200 Subject: [PATCH 4/4] test: clean test for `contains` operator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- test/postgresql.test.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/test/postgresql.test.js b/test/postgresql.test.js index c710a567..bd442167 100644 --- a/test/postgresql.test.js +++ b/test/postgresql.test.js @@ -73,7 +73,7 @@ describe('postgresql connector', function() { dataType: 'varchar[]', }, }, - }); + }, {allowExtendedOperators: true}); created = new Date(); }); @@ -264,15 +264,23 @@ describe('postgresql connector', function() { }); it('should support where filter for array type field', async () => { - Post.dataSource.settings.allowExtendedOperators = true; - const post = await Post.find({where: {and: [ + await Post.create({ + title: 'LoopBack Participates in Hacktoberfest', + categories: ['LoopBack', 'Announcements'], + }); + await Post.create({ + title: 'Growing LoopBack Community', + categories: ['LoopBack', 'Community'], + }); + + const found = await Post.find({where: {and: [ { - categories: {'contains': ['AA']}, + categories: {'contains': ['LoopBack', 'Community']}, }, ]}}); - should.exist(post); - post.length.should.equal(1); + found.map(p => p.title).should.deepEqual(['Growing LoopBack Community']); }); + it('should support boolean types with false value', function(done) { Post.create( {title: 'T2', content: 'C2', approved: false, created: created},