Skip to content

Commit

Permalink
feat(core): add $exists mongodb operator with SQL fallback to `is n…
Browse files Browse the repository at this point in the history
…ot null`

Closes #3295
  • Loading branch information
B4nan committed Jul 30, 2022
1 parent 94442f9 commit 112f2be
Show file tree
Hide file tree
Showing 6 changed files with 36 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/core/src/drivers/DatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD
let unknownProp = false;

Object.keys(data[prop.name]).forEach(kk => {
const operator = Object.keys(data[prop.name]).some(f => Utils.isOperator(f));
// explicitly allow `$exists` operator here as it cant be misused this way
const operator = Object.keys(data[prop.name]).some(f => Utils.isOperator(f) && f !== '$exists');

if (operator) {
throw ValidationError.cannotUseOperatorsInsideEmbeddables(meta.name!, prop.name, data);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export enum QueryOperator {
$not = 'not',
$like = 'like',
$re = 'regexp',
$exists = 'not null',
$ilike = 'ilike', // postgres only
$overlap = '&&', // postgres only
$contains = '@>', // postgres only
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export type OperatorMap<T> = {
$overlap?: string[];
$contains?: string[];
$contained?: string[];
$exists?: boolean;
};

export type FilterValue2<T> = T | ExpandScalar<T> | Primary<T>;
Expand Down
5 changes: 5 additions & 0 deletions packages/knex/src/query/QueryBuilderHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,11 @@ export class QueryBuilderHelper {
private getOperatorReplacement(op: string, value: Dictionary): string {
let replacement = QueryOperator[op];

if (op === '$exists') {
replacement = value[op] ? 'is not' : 'is';
value[op] = null;
}

if (value[op] === null && ['$eq', '$ne'].includes(op)) {
replacement = op === '$eq' ? 'is' : 'is not';
}
Expand Down
5 changes: 5 additions & 0 deletions tests/EntityManager.mongo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,11 @@ describe('EntityManagerMongo', () => {
});
});

test('using $exists operator', async () => {
await orm.em.nativeInsert(Author, { name: 'n', email: 'e' });
await orm.em.findOneOrFail(Author, { foo: { $exists: false } });
});

test('connection returns correct URL', async () => {
const conn1 = new MongoConnection(new Configuration({
type: 'mongo',
Expand Down
22 changes: 22 additions & 0 deletions tests/QueryBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,28 @@ describe('QueryBuilder', () => {
expect(qb.getParams()).toEqual(['^c.o.*l-te.*st.c.m$']);
});

test('$exists operator', async () => {
let qb = orm.em.createQueryBuilder(Publisher2);
qb.select('*').where({ name: { $exists: true } });
expect(qb.getQuery()).toEqual('select `e0`.* from `publisher2` as `e0` where `e0`.`name` is not null');
expect(qb.getParams()).toEqual([]);

qb = orm.em.createQueryBuilder(Publisher2);
qb.select('*').where({ name: { $exists: false } });
expect(qb.getQuery()).toEqual('select `e0`.* from `publisher2` as `e0` where `e0`.`name` is null');
expect(qb.getParams()).toEqual([]);

qb = orm.em.createQueryBuilder(Publisher2);
qb.select('*').where({ books: { title: { $exists: true } } });
expect(qb.getQuery()).toEqual('select `e0`.* from `publisher2` as `e0` left join `book2` as `e1` on `e0`.`id` = `e1`.`publisher_id` where `e1`.`title` is not null');
expect(qb.getParams()).toEqual([]);

qb = orm.em.createQueryBuilder(Publisher2);
qb.select('*').where({ books: { title: { $exists: false } } });
expect(qb.getQuery()).toEqual('select `e0`.* from `publisher2` as `e0` left join `book2` as `e1` on `e0`.`id` = `e1`.`publisher_id` where `e1`.`title` is null');
expect(qb.getParams()).toEqual([]);
});

test('select by m:1', async () => {
const qb = orm.em.createQueryBuilder(Author2);
qb.select('*').where({ favouriteBook: '123' });
Expand Down

0 comments on commit 112f2be

Please sign in to comment.