Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(operators): add containedBy and containsAnyOf operators #669

Merged
merged 1 commit into from
Apr 5, 2024
Merged
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
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions lib/postgresql.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
60 changes: 46 additions & 14 deletions test/postgresql.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down