Skip to content

Commit

Permalink
feat(core): allow ignoring undefined values in em.find queries (#…
Browse files Browse the repository at this point in the history
…4875)

The ORM will treat explicitly defined `undefined` values in your
`em.find()` queries as `null`s. If you want to ignore them instead, use
`ignoreUndefinedInQuery` option:

```ts
MikroORM.init({
  ignoreUndefinedInQuery: true,
});

// resolves to `em.find(User, {})`
await em.find(User, { email: undefined, { profiles: { foo: undefined } } });
```

Closes #4873
  • Loading branch information
B4nan committed Oct 24, 2023
1 parent 1e3bb0e commit e163bfb
Show file tree
Hide file tree
Showing 8 changed files with 48 additions and 12 deletions.
13 changes: 13 additions & 0 deletions docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,19 @@ MikroORM.init({
});
```

## Ignoring `undefined` values in Find Queries

The ORM will treat explicitly defined `undefined` values in your `em.find()` queries as `null`s. If you want to ignore them instead, use `ignoreUndefinedInQuery` option:

```ts
MikroORM.init({
ignoreUndefinedInQuery: true,
});

// resolves to `em.find(User, {})`
await em.find(User, { email: undefined, { profiles: { foo: undefined } } });
```

## Serialization of new entities

After flushing a new entity, all relations are marked as populated, just like if the entity was loaded from the db. This aligns the serialized output of `e.toJSON()` of a loaded entity and just-inserted one.
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/utils/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export class Configuration<D extends IDatabaseDriver = IDatabaseDriver> {
loadStrategy: LoadStrategy.SELECT_IN,
populateWhere: PopulateHint.ALL,
connect: true,
ignoreUndefinedInQuery: false,
autoJoinOneToOneOwner: true,
propagateToOneOwner: true,
populateAfterFlush: true,
Expand Down Expand Up @@ -514,6 +515,7 @@ export interface MikroORMOptions<D extends IDatabaseDriver = IDatabaseDriver> ex
disableTransactions?: boolean;
connect: boolean;
verbose: boolean;
ignoreUndefinedInQuery?: boolean;
autoJoinOneToOneOwner: boolean;
propagateToOneOwner: boolean;
populateAfterFlush: boolean;
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/utils/QueryHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export class QueryHelper {
QueryHelper.inlinePrimaryKeyObjects(where as Dictionary, meta, metadata);
}

if (options.platform.getConfig().get('ignoreUndefinedInQuery') && where && typeof where === 'object') {
Utils.dropUndefinedProperties(where);
}

where = QueryHelper.processParams(where) ?? {};

/* istanbul ignore next */
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/utils/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,22 +226,24 @@ export class Utils {
/**
* Removes `undefined` properties (recursively) so they are not saved as nulls
*/
static dropUndefinedProperties<T = Dictionary | unknown[]>(o: any, value?: undefined | null): void {
static dropUndefinedProperties<T = Dictionary | unknown[]>(o: any, value?: undefined | null, visited = new Set()): void {
if (Array.isArray(o)) {
return o.forEach((item: unknown) => Utils.dropUndefinedProperties(item, value));
return o.forEach((item: unknown) => Utils.dropUndefinedProperties(item, value, visited));
}

if (!Utils.isObject(o)) {
if (!Utils.isObject(o) || visited.has(o)) {
return;
}

visited.add(o);

Object.keys(o).forEach(key => {
if (o[key] === value) {
delete o[key];
return;
}

Utils.dropUndefinedProperties(o[key], value);
Utils.dropUndefinedProperties(o[key], value, visited);
});
}

Expand Down
8 changes: 7 additions & 1 deletion packages/knex/src/query/QueryBuilderHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,8 +507,13 @@ export class QueryBuilderHelper {
private processObjectSubCondition(cond: any, key: string, qb: Knex.QueryBuilder, method: 'where' | 'having', m: 'where' | 'orWhere' | 'having', type: QueryType): void {
// grouped condition for one field
let value = cond[key];
const size = Utils.getObjectKeysSize(value);

if (Utils.getObjectKeysSize(value) > 1) {
if (Utils.isPlainObject(value) && size === 0) {
return;
}

if (size > 1) {
const subCondition = Object.entries(value).map(([subKey, subValue]) => ({ [key]: { [subKey]: subValue } }));
return subCondition.forEach(sub => this.appendQueryCondition(type, sub, qb, '$and', method));
}
Expand All @@ -520,6 +525,7 @@ export class QueryBuilderHelper {
// operators
const op = Object.keys(QueryOperator).find(op => op in value);

/* istanbul ignore next */
if (!op) {
throw new Error(`Invalid query condition: ${inspect(cond)}`);
}
Expand Down
13 changes: 13 additions & 0 deletions tests/EntityManager.sqlite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,19 @@ describe('EntityManagerSqlite', () => {
await expect(orm.em.findOne(Author3, { name: 'God Persisted!' })).resolves.not.toBeNull();
});

test('find query ignores undefined properties (ignoreUndefinedInQuery)', async () => {
const mock = mockLogger(orm, ['query']);
await orm.em.find(Author3, { email: undefined, name: 'foo' });
await orm.em.find(Author3, { email: undefined, name: 'foo', books: { title: undefined } });
await orm.em.find(Author3, { email: undefined, name: 'foo', books: { title: undefined, createdAt: 123 } });
await orm.em.find(Author3, { email: undefined, name: 'foo', books: { title: { $ne: undefined, $gte: undefined }, createdAt: 123 } });

expect(mock.mock.calls[0][0]).toBe('[query] select `a0`.* from `author3` as `a0` where `a0`.`name` = ?');
expect(mock.mock.calls[1][0]).toBe('[query] select `a0`.* from `author3` as `a0` left join `book3` as `b1` on `a0`.`id` = `b1`.`author_id` where `a0`.`name` = ?');
expect(mock.mock.calls[2][0]).toBe('[query] select `a0`.* from `author3` as `a0` left join `book3` as `b1` on `a0`.`id` = `b1`.`author_id` where `a0`.`name` = ? and `b1`.`created_at` = ?');
expect(mock.mock.calls[3][0]).toBe('[query] select `a0`.* from `author3` as `a0` left join `book3` as `b1` on `a0`.`id` = `b1`.`author_id` where `a0`.`name` = ? and `b1`.`created_at` = ?');
});

test('should load entities', async () => {
expect(orm).toBeInstanceOf(MikroORM);
expect(orm.em).toBeInstanceOf(EntityManager);
Expand Down
6 changes: 0 additions & 6 deletions tests/QueryBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1345,12 +1345,6 @@ describe('QueryBuilder', () => {
expect(() => qb0.select('*').where({ author: { undefinedName: 'Jon Snow' } }).getQuery()).toThrowError(err);
});

test('select with invalid query condition throws error', async () => {
const qb0 = orm.em.createQueryBuilder(Book2);
const err = `Invalid query condition: { 'e0.author': {} }`;
expect(() => qb0.select('*').where({ author: {} }).getQuery()).toThrowError(err);
});

test('pessimistic locking requires active transaction', async () => {
const qb = orm.em.createQueryBuilder(Author2);
qb.select('*').where({ name: '...' });
Expand Down
4 changes: 3 additions & 1 deletion tests/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'reflect-metadata';
import { MongoMemoryReplSet } from 'mongodb-memory-server';
import type { Options } from '@mikro-orm/core';
import { JavaScriptMetadataProvider, LoadStrategy, MikroORM, ReflectMetadataProvider, Utils } from '@mikro-orm/core';
import { JavaScriptMetadataProvider, LoadStrategy, MikroORM, ReflectMetadataProvider, Utils, SimpleLogger } from '@mikro-orm/core';
import type { AbstractSqlDriver } from '@mikro-orm/knex';
import { SqlEntityRepository } from '@mikro-orm/knex';
import { SqliteDriver } from '@mikro-orm/sqlite';
Expand Down Expand Up @@ -155,9 +155,11 @@ export async function initORMSqlite() {
debug: ['query'],
forceUtcTimezone: true,
logger: i => i,
loggerFactory: options => new SimpleLogger(options),
metadataProvider: JavaScriptMetadataProvider,
cache: { enabled: true, pretty: true },
persistOnCreate: false,
ignoreUndefinedInQuery: true,
});

const connection = orm.em.getConnection();
Expand Down

0 comments on commit e163bfb

Please sign in to comment.