From 952fd2fe7ba619a0906133694e4888f57ad8cecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ad=C3=A1mek?= Date: Mon, 11 May 2020 22:38:13 +0200 Subject: [PATCH] feat(core): add `having` to `FindOptions` --- packages/core/src/EntityManager.ts | 26 ++++++++++---------- packages/core/src/drivers/DatabaseDriver.ts | 4 +-- packages/core/src/drivers/IDatabaseDriver.ts | 12 +++++---- packages/knex/src/AbstractSqlDriver.ts | 6 +++-- packages/knex/src/query/QueryBuilder.ts | 2 +- packages/mongodb/src/MongoDriver.ts | 4 +-- tests/DatabaseDriver.test.ts | 4 +-- tests/EntityManager.mysql.test.ts | 2 ++ 8 files changed, 33 insertions(+), 27 deletions(-) diff --git a/packages/core/src/EntityManager.ts b/packages/core/src/EntityManager.ts index bd9c68efec35..0061e8cd5f6b 100644 --- a/packages/core/src/EntityManager.ts +++ b/packages/core/src/EntityManager.ts @@ -67,7 +67,7 @@ export class EntityManager { /** * Finds all entities matching your `where` query. You can pass additional options via the `options` parameter. */ - async find>(entityName: EntityName, where: FilterQuery, options?: FindOptions): Promise; + async find>(entityName: EntityName, where: FilterQuery, options?: FindOptions): Promise; /** * Finds all entities matching your `where` query. @@ -77,11 +77,11 @@ export class EntityManager { /** * Finds all entities matching your `where` query. */ - async find>(entityName: EntityName, where: FilterQuery, populate?: string[] | boolean | FindOptions, orderBy?: QueryOrderMap, limit?: number, offset?: number): Promise { + async find>(entityName: EntityName, where: FilterQuery, populate?: string[] | boolean | FindOptions, orderBy?: QueryOrderMap, limit?: number, offset?: number): Promise { entityName = Utils.className(entityName); where = SmartQueryHelper.processWhere(where, entityName, this.metadata.get(entityName)); this.validator.validateParams(where); - const options = Utils.isObject(populate) ? populate : { populate, orderBy, limit, offset }; + const options = Utils.isObject>(populate) ? populate : { populate, orderBy, limit, offset }; options.orderBy = options.orderBy || {}; const results = await this.driver.find(entityName, where, { ...options, populate: this.preparePopulate(options.populate) }, this.transactionContext); @@ -106,7 +106,7 @@ export class EntityManager { * Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple * where first element is the array of entities and the second is the count. */ - async findAndCount>(entityName: EntityName, where: FilterQuery, options?: FindOptions): Promise<[T[], number]>; + async findAndCount>(entityName: EntityName, where: FilterQuery, options?: FindOptions): Promise<[T[], number]>; /** * Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple @@ -118,7 +118,7 @@ export class EntityManager { * Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple * where first element is the array of entities and the second is the count. */ - async findAndCount>(entityName: EntityName, where: FilterQuery, populate?: string[] | boolean | FindOptions, orderBy?: QueryOrderMap, limit?: number, offset?: number): Promise<[T[], number]> { + async findAndCount>(entityName: EntityName, where: FilterQuery, populate?: string[] | boolean | FindOptions, orderBy?: QueryOrderMap, limit?: number, offset?: number): Promise<[T[], number]> { const [entities, count] = await Promise.all([ this.find(entityName, where, populate as string[], orderBy, limit, offset), this.count(entityName, where), @@ -130,7 +130,7 @@ export class EntityManager { /** * Finds first entity matching your `where` query. */ - async findOne>(entityName: EntityName, where: FilterQuery, options?: FindOneOptions): Promise; + async findOne>(entityName: EntityName, where: FilterQuery, options?: FindOneOptions): Promise; /** * Finds first entity matching your `where` query. @@ -140,9 +140,9 @@ export class EntityManager { /** * Finds first entity matching your `where` query. */ - async findOne>(entityName: EntityName, where: FilterQuery, populate?: string[] | boolean | FindOneOptions, orderBy?: QueryOrderMap): Promise { + async findOne>(entityName: EntityName, where: FilterQuery, populate?: string[] | boolean | FindOneOptions, orderBy?: QueryOrderMap): Promise { entityName = Utils.className(entityName); - const options = Utils.isObject(populate) ? populate : { populate, orderBy }; + const options = Utils.isObject>(populate) ? populate : { populate, orderBy }; const meta = this.metadata.get(entityName); this.validator.validateEmptyWhere(where); where = SmartQueryHelper.processWhere(where as FilterQuery, entityName, meta); @@ -172,7 +172,7 @@ export class EntityManager { * You can override the factory for creating this method via `options.failHandler` locally * or via `Configuration.findOneOrFailHandler` globally. */ - async findOneOrFail>(entityName: EntityName, where: FilterQuery, options?: FindOneOrFailOptions): Promise; + async findOneOrFail>(entityName: EntityName, where: FilterQuery, options?: FindOneOrFailOptions): Promise; /** * Finds first entity matching your `where` query. If nothing found, it will throw an error. @@ -186,11 +186,11 @@ export class EntityManager { * You can override the factory for creating this method via `options.failHandler` locally * or via `Configuration.findOneOrFailHandler` globally. */ - async findOneOrFail>(entityName: EntityName, where: FilterQuery, populate?: string[] | boolean | FindOneOrFailOptions, orderBy?: QueryOrderMap): Promise { + async findOneOrFail>(entityName: EntityName, where: FilterQuery, populate?: string[] | boolean | FindOneOrFailOptions, orderBy?: QueryOrderMap): Promise { const entity = await this.findOne(entityName, where, populate as string[], orderBy); if (!entity) { - const options = Utils.isObject(populate) ? populate : {}; + const options = Utils.isObject>(populate) ? populate : {}; options.failHandler = options.failHandler || this.config.get('findOneOrFailHandler'); entityName = Utils.className(entityName); throw options.failHandler!(entityName, where); @@ -574,7 +574,7 @@ export class EntityManager { } } - private async lockAndPopulate>(entityName: string, entity: T, where: FilterQuery, options: FindOneOptions): Promise { + private async lockAndPopulate>(entityName: string, entity: T, where: FilterQuery, options: FindOneOptions): Promise { if (options.lockMode === LockMode.OPTIMISTIC) { await this.lock(entity, options.lockMode, options.lockVersion); } @@ -590,6 +590,6 @@ export class EntityManager { } -export interface FindOneOrFailOptions extends FindOneOptions { +export interface FindOneOrFailOptions extends FindOneOptions { failHandler?: (entityName: string, where: Dictionary | IPrimaryKey | any) => Error; } diff --git a/packages/core/src/drivers/DatabaseDriver.ts b/packages/core/src/drivers/DatabaseDriver.ts index e3dff788f230..5abd3da6b48d 100644 --- a/packages/core/src/drivers/DatabaseDriver.ts +++ b/packages/core/src/drivers/DatabaseDriver.ts @@ -21,9 +21,9 @@ export abstract class DatabaseDriver implements IDatabaseD protected constructor(protected readonly config: Configuration, protected readonly dependencies: string[]) { } - abstract async find>(entityName: string, where: FilterQuery, options?: FindOptions, ctx?: Transaction): Promise; + abstract async find>(entityName: string, where: FilterQuery, options?: FindOptions, ctx?: Transaction): Promise; - abstract async findOne>(entityName: string, where: FilterQuery, options?: FindOneOptions, ctx?: Transaction): Promise; + abstract async findOne>(entityName: string, where: FilterQuery, options?: FindOneOptions, ctx?: Transaction): Promise; abstract async nativeInsert>(entityName: string, data: EntityData, ctx?: Transaction): Promise; diff --git a/packages/core/src/drivers/IDatabaseDriver.ts b/packages/core/src/drivers/IDatabaseDriver.ts index c65017a3b493..4fc48a02fa03 100644 --- a/packages/core/src/drivers/IDatabaseDriver.ts +++ b/packages/core/src/drivers/IDatabaseDriver.ts @@ -1,4 +1,4 @@ -import { EntityData, EntityMetadata, EntityProperty, AnyEntity, FilterQuery, Primary, Dictionary } from '../typings'; +import { EntityData, EntityMetadata, EntityProperty, AnyEntity, FilterQuery, Primary, Dictionary, QBFilterQuery } from '../typings'; import { Connection, QueryResult, Transaction } from '../connections'; import { QueryOrderMap, QueryFlag } from '../enums'; import { Platform } from '../platforms'; @@ -27,12 +27,12 @@ export interface IDatabaseDriver { /** * Finds selection of entities */ - find>(entityName: string, where: FilterQuery, options?: FindOptions, ctx?: Transaction): Promise; + find>(entityName: string, where: FilterQuery, options?: FindOptions, ctx?: Transaction): Promise; /** * Finds single entity (table row, document) */ - findOne>(entityName: string, where: FilterQuery, options?: FindOneOptions, ctx?: Transaction): Promise; + findOne>(entityName: string, where: FilterQuery, options?: FindOneOptions, ctx?: Transaction): Promise; nativeInsert>(entityName: string, data: EntityData, ctx?: Transaction): Promise; @@ -74,7 +74,7 @@ export interface IDatabaseDriver { } -export interface FindOptions { +export interface FindOptions { populate?: string[] | boolean; orderBy?: QueryOrderMap; limit?: number; @@ -84,12 +84,14 @@ export interface FindOptions { schema?: string; flags?: QueryFlag[]; groupBy?: string | string[]; + having?: QBFilterQuery; } -export interface FindOneOptions { +export interface FindOneOptions { populate?: string[] | boolean; orderBy?: QueryOrderMap; groupBy?: string | string[]; + having?: QBFilterQuery; lockMode?: LockMode; lockVersion?: number | Date; refresh?: boolean; diff --git a/packages/knex/src/AbstractSqlDriver.ts b/packages/knex/src/AbstractSqlDriver.ts index 4d1fe63fc0d4..2278e182aa2c 100644 --- a/packages/knex/src/AbstractSqlDriver.ts +++ b/packages/knex/src/AbstractSqlDriver.ts @@ -29,7 +29,7 @@ export abstract class AbstractSqlDriver; } - async find>(entityName: string, where: FilterQuery, options?: FindOptions, ctx?: Transaction): Promise { + async find>(entityName: string, where: FilterQuery, options?: FindOptions, ctx?: Transaction): Promise { const meta = this.metadata.get(entityName); options = { populate: [], orderBy: {}, ...(options || {}) }; options.populate = this.autoJoinOneToOneOwner(meta, options.populate as string[]); @@ -44,6 +44,7 @@ export abstract class AbstractSqlDriver>(entityName: string, where: FilterQuery, options?: FindOneOptions, ctx?: Transaction): Promise { + async findOne>(entityName: string, where: FilterQuery, options?: FindOneOptions, ctx?: Transaction): Promise { options = { populate: [], orderBy: {}, ...(options || {}) }; const meta = this.metadata.get(entityName); options.populate = this.autoJoinOneToOneOwner(meta, options.populate as string[]); @@ -75,6 +76,7 @@ export abstract class AbstractSqlDriver = AnyEntity> { return this; } - having(cond: QBFilterQuery | string, params?: any[]): this { + having(cond: QBFilterQuery | string = {}, params?: any[]): this { if (Utils.isString(cond)) { cond = { [`(${cond})`]: Utils.asArray(params) }; } diff --git a/packages/mongodb/src/MongoDriver.ts b/packages/mongodb/src/MongoDriver.ts index 91cff2e0c29e..44d8dc6a54dd 100644 --- a/packages/mongodb/src/MongoDriver.ts +++ b/packages/mongodb/src/MongoDriver.ts @@ -22,14 +22,14 @@ export class MongoDriver extends DatabaseDriver { return new MongoEntityManager(this.config, this, this.metadata, useContext) as unknown as EntityManager; } - async find>(entityName: string, where: FilterQuery, options: FindOptions, ctx?: Transaction): Promise { + async find>(entityName: string, where: FilterQuery, options: FindOptions, ctx?: Transaction): Promise { where = this.renameFields(entityName, where); const res = await this.rethrow(this.getConnection('read').find(entityName, where, options.orderBy, options.limit, options.offset, options.fields, ctx)); return res.map((r: T) => this.mapResult(r, this.metadata.get(entityName))!); } - async findOne>(entityName: string, where: FilterQuery, options: FindOneOptions = { populate: [], orderBy: {} }, ctx?: Transaction): Promise { + async findOne>(entityName: string, where: FilterQuery, options: FindOneOptions = { populate: [], orderBy: {} }, ctx?: Transaction): Promise { if (Utils.isPrimaryKey(where)) { where = { _id: new ObjectId(where as string) } as FilterQuery; } diff --git a/tests/DatabaseDriver.test.ts b/tests/DatabaseDriver.test.ts index 670c04820097..5bf73749da64 100644 --- a/tests/DatabaseDriver.test.ts +++ b/tests/DatabaseDriver.test.ts @@ -11,11 +11,11 @@ class Driver extends DatabaseDriver { return Promise.resolve(0); } - async find(entityName: string, where: FilterQuery, options: FindOptions | undefined, ctx: Transaction | undefined): Promise { + async find(entityName: string, where: FilterQuery, options: FindOptions | undefined, ctx: Transaction | undefined): Promise { return Promise.resolve([]); } - async findOne(entityName: string, where: FilterQuery, options: FindOneOptions | undefined, ctx: Transaction | undefined): Promise { + async findOne(entityName: string, where: FilterQuery, options: FindOneOptions | undefined, ctx: Transaction | undefined): Promise { return null; } diff --git a/tests/EntityManager.mysql.test.ts b/tests/EntityManager.mysql.test.ts index 42b7e12ecd52..20e267d83d6b 100644 --- a/tests/EntityManager.mysql.test.ts +++ b/tests/EntityManager.mysql.test.ts @@ -1986,6 +1986,7 @@ describe('EntityManagerMySql', () => { orderBy: { name: QueryOrder.ASC, books: { title: QueryOrder.ASC } }, limit: 5, groupBy: ['id', 'name', 'e1.title'], + having: { $or: [{ age: { $gt: 0 } }, { age: { $lte: 0 } }, { age: null }] }, // no-op just for testing purposes }); expect(res1).toHaveLength(2); @@ -1996,6 +1997,7 @@ describe('EntityManagerMySql', () => { 'left join `address2` as `e2` on `e0`.`id` = `e2`.`author_id` ' + 'where `e1`.`title` like ? ' + 'group by `e0`.`id`, `e0`.`name`, `e1`.`title` ' + + 'having (`e0`.`age` > ? or `e0`.`age` <= ? or `e0`.`age` is null) ' + 'order by `e0`.`name` asc, `e1`.`title` asc limit ?'); // with paginate flag (and a bit of dark sql magic) we get what we want