diff --git a/docs/docs/query-builder.md b/docs/docs/query-builder.md index b7d3bad75572..7b11f3451bf6 100644 --- a/docs/docs/query-builder.md +++ b/docs/docs/query-builder.md @@ -15,7 +15,8 @@ When you need to execute some SQL query without all the ORM stuff involved, you compose the query yourself, or use the `QueryBuilder` helper to construct the query for you: ```typescript -const qb = orm.em.createQueryBuilder(Author); +// since v5 we can also use `em.qb()` shortcut +const qb = em.createQueryBuilder(Author); qb.update({ name: 'test 123', type: PublisherType.GLOBAL }).where({ id: 123, type: PublisherType.LOCAL }); console.log(qb.getQuery()); @@ -30,55 +31,6 @@ const res1 = await qb.execute(); `QueryBuilder` also supports [smart query conditions](query-conditions.md). -## Using Knex.js - -Under the hood, `QueryBuilder` uses [`Knex.js`](https://knexjs.org) to compose and run queries. -You can access configured `knex` instance via `qb.getKnexQuery()` method: - -```typescript -const qb = orm.em.createQueryBuilder(Author); -qb.update({ name: 'test 123', type: PublisherType.GLOBAL }).where({ id: 123, type: PublisherType.LOCAL }); -const knex = qb.getKnexQuery(); // instance of Knex' QueryBuilder - -// do what ever you need with `knex` - -const res = await orm.em.getConnection().execute(knex); -const entities = res.map(a => orm.em.map(Author, a)); -console.log(entities); // Author[] -``` - -You can also get clear and configured knex instance from the connection via `getKnex()` method. -As this method is not available on the base `Connection` class, you will need to either manually -type cast the connection to `AbstractSqlConnection` (or the actual implementation you are using, -e.g. `MySqlConnection`), or provide correct driver type hint to your `EntityManager` instance, -which will be then automatically inferred in `em.getConnection()` method. - -> Driver and connection implementations are not directly exported from `@mikro-orm/core` module. -> You can import them from the driver packages (e.g. `import { PostgreSqlDriver } from '@mikro-orm/postgresql'`). - -```typescript -const conn = orm.em.getConnection() as AbstractSqlConnection; -// you can make sure the `em` is correctly typed to `EntityManager` -// or one of its implementations: -// const em: EntityManager = orm.em; - -const knex = conn.getKnex(); - -// do what ever you need with `knex` - -const res = await knex; -``` - -## Running Native SQL Query - -You can run native SQL via underlying connection - -```typescript -const connection = orm.em.getConnection(); -const res = await connection.execute('select 1 as count'); -console.log(res); // res is array of objects: `[ { count: 1 } ]` -``` - ## Executing the Query You can use `execute(method = 'all', mapResults = true)`'s parameters to control form of result: @@ -94,9 +46,9 @@ is enabled by default). In following example, `Book` entity has `createdAt` prop with implicit underscored field name `created_at`: ```typescript -const res4 = await orm.em.createQueryBuilder(Book).select('*').execute('get', true); +const res4 = await em.createQueryBuilder(Book).select('*').execute('get', true); console.log(res4); // `createdAt` will be defined, while `created_at` will be missing -const res5 = await orm.em.createQueryBuilder(Book).select('*').execute('get', false); +const res5 = await em.createQueryBuilder(Book).select('*').execute('get', false); console.log(res5); // `created_at` will be defined, while `createdAt` will be missing ``` @@ -104,14 +56,64 @@ To get entity instances from the QueryBuilder result, you can use `getResult()` methods: ```typescript -const book = await orm.em.createQueryBuilder(Book).select('*').where({ id: 1 }).getSingleResult(); +const book = await em.createQueryBuilder(Book).select('*').where({ id: 1 }).getSingleResult(); console.log(book instanceof Book); // true -const books = await orm.em.createQueryBuilder(Book).select('*').getResult(); +const books = await em.createQueryBuilder(Book).select('*').getResult(); console.log(books[0] instanceof Book); // true ``` > You can also use `qb.getResultList()` which is alias to `qb.getResult()`. +## Awaiting the QueryBuilder + +Since v5 we can await the `QueryBuilder` instance, which will automatically execute +the QB and return appropriate response. The QB instance is now typed based on usage +of `select/insert/update/delete/truncate` methods to one of: + +- `SelectQueryBuilder` + - awaiting yields array of entities (as `qb.getResultList()`) +- `CountQueryBuilder` + - awaiting yields number (as `qb.getCount()`) +- `InsertQueryBuilder` (extends `RunQueryBuilder`) + - awaiting yields `QueryResult` +- `UpdateQueryBuilder` (extends `RunQueryBuilder`) + - awaiting yields `QueryResult` +- `DeleteQueryBuilder` (extends `RunQueryBuilder`) + - awaiting yields `QueryResult` +- `TruncateQueryBuilder` (extends `RunQueryBuilder`) + - awaiting yields `QueryResult` + +```ts +const res1 = await em.qb(Publisher).insert({ + name: 'p1', + type: PublisherType.GLOBAL, +}); +// res1 is of type `QueryResult` +console.log(res1.insertId); + +const res2 = await em.qb(Publisher) + .select('*') + .where({ name: 'p1' }) + .limit(5); +// res2 is Publisher[] +console.log(res2.map(p => p.name)); + +const res3 = await em.qb(Publisher).count().where({ name: 'p1' }); +// res3 is number +console.log(res3 > 0); + +const res4 = await em.qb(Publisher) + .update({ type: PublisherType.LOCAL }) + .where({ name: 'p1' }); +// res4 is QueryResult +console.log(res4.affectedRows > 0); + +const res5 = await em.qb(Publisher).delete().where({ name: 'p1' }); +// res4 is QueryResult +console.log(res4.affectedRows > 0); +expect(res5.affectedRows > 0).toBe(true); // test the type +``` + ## Mapping Raw Results to Entities Another way to create entity from raw results (that are not necessarily mapped to entity properties) @@ -124,10 +126,10 @@ mapped to entity properties automatically: ```typescript const results = await knex.select('*').from('users').where(knex.raw('id = ?', [id])); -const users = results.map(user => orm.em.map(User, user)); +const users = results.map(user => em.map(User, user)); // or use EntityRepository.map() -const repo = orm.em.getRepository(User); +const repo = em.getRepository(User); const users = results.map(user => repo.map(user)); ``` @@ -136,7 +138,7 @@ const users = results.map(user => repo.map(user)); `QueryBuilder` supports automatic joining based on entity metadata: ```typescript -const qb = orm.em.createQueryBuilder(BookTag, 't'); +const qb = em.createQueryBuilder(BookTag, 't'); qb.select('*').where({ books: 123 }); console.log(qb.getQuery()); @@ -149,7 +151,7 @@ console.log(qb.getQuery()); This also works for multiple levels of nesting: ```typescript -const qb = orm.em.createQueryBuilder(Author); +const qb = em.createQueryBuilder(Author); qb.select('*') .where({ books: { tags: { name: 'Cool' } } }) .orderBy({ books: { tags: { createdBy: QueryOrder.DESC } } }); @@ -172,7 +174,7 @@ the root entity will be selected. To populate its relationships, you can use [`e Another way is to manually specify join property via `join()`/`leftJoin()` methods: ```typescript -const qb = orm.em.createQueryBuilder(BookTag, 't'); +const qb = em.createQueryBuilder(BookTag, 't'); qb.select(['b.uuid', 'b.*', 't.*'], true) .join('t.books', 'b') .where({ 'b.title': 'test 123' }) @@ -193,7 +195,7 @@ To select multiple entities and map them from `QueryBuilder`, we can use ```ts // `res` will contain array of authors, with books and their tags populated -const res = await orm.em.createQueryBuilder(Author, 'a') +const res = await em.createQueryBuilder(Author, 'a') .select('*') .leftJoinAndSelect('a.books', 'b') .leftJoinAndSelect('b.tags', 't') @@ -211,7 +213,7 @@ manually, use `andWhere()`/`orWhere()`, or provide condition object: It is possible to use any SQL fragment in your `WHERE` query or `ORDER BY` clause: ```ts -const users = orm.em.createQueryBuilder(User) +const users = em.createQueryBuilder(User) .select('*') .where({ 'lower(email)': 'foo@bar.baz' }) .orderBy({ [`(point(loc_latitude, loc_longitude) <@> point(0, 0))`]: 'ASC' }) @@ -230,7 +232,7 @@ order by (point(loc_latitude, loclongitude) <@> point(0, 0)) asc ### Custom SQL in where ```typescript -const qb = orm.em.createQueryBuilder(BookTag, 't'); +const qb = em.createQueryBuilder(BookTag, 't'); qb.select(['b.*', 't.*']) .leftJoin('t.books', 'b') .where('b.title = ? or b.title = ?', ['test 123', 'lol 321']) @@ -249,7 +251,7 @@ console.log(qb.getQuery()); ### andWhere() and orWhere() ```typescript -const qb = orm.em.createQueryBuilder(BookTag, 't'); +const qb = em.createQueryBuilder(BookTag, 't'); qb.select(['b.*', 't.*']) .leftJoin('t.books', 'b') .where('b.title = ? or b.title = ?', ['test 123', 'lol 321']) @@ -268,7 +270,7 @@ console.log(qb.getQuery()); ### Conditions Object ```typescript -const qb = orm.em.createQueryBuilder(Test); +const qb = em.createQueryBuilder(Test); qb.select('*').where({ $and: [{ id: { $nin: [3, 4] } }, { id: { $gt: 2 } }] }); console.log(qb.getQuery()); @@ -281,7 +283,7 @@ To create a count query, we can ue `qb.count()`, which will intialize a select c with `count()` function. By default, it will use the primary key. ```typescript -const qb = orm.em.createQueryBuilder(Test); +const qb = em.createQueryBuilder(Test); qb.count().where({ $and: [{ id: { $nin: [3, 4] } }, { id: { $gt: 2 } }] }); console.log(qb.getQuery()); @@ -295,7 +297,7 @@ const count = res ? +res.count : 0; To simplify this process, we can use `qb.getCount()` method. Following code is equivalent: ```typescript -const qb = orm.em.createQueryBuilder(Test); +const qb = em.createQueryBuilder(Test); qb.select('*').limit(10, 20).where({ $and: [{ id: { $nin: [3, 4] } }, { id: { $gt: 2 } }] }); const count = await qb.getCount(); @@ -309,8 +311,8 @@ cloned under the hood, so calling `getCount()` does not mutate the original QB s You can filter using sub-queries in where conditions: ```typescript -const qb1 = orm.em.createQueryBuilder(Book2, 'b').select('b.author').where({ price: { $gt: 100 } }); -const qb2 = orm.em.createQueryBuilder(Author2, 'a').select('*').where({ id: { $in: qb1.getKnexQuery() } }); +const qb1 = em.createQueryBuilder(Book2, 'b').select('b.author').where({ price: { $gt: 100 } }); +const qb2 = em.createQueryBuilder(Author2, 'a').select('*').where({ id: { $in: qb1.getKnexQuery() } }); console.log(qb2.getQuery()); // select `a`.* from `author2` as `a` where `a`.`id` in (select `b`.`author_id` from `book2` as `b` where `b`.`price` > ?) @@ -321,9 +323,9 @@ For sub-queries in selects, use the `qb.as(alias)` method: > The dynamic property (`booksTotal`) needs to be defined at the entity level (as `persist: false`). ```typescript -const knex = orm.em.getKnex(); -const qb1 = orm.em.createQueryBuilder(Book2, 'b').count('b.uuid', true).where({ author: knex.ref('a.id') }).as('Author2.booksTotal'); -const qb2 = orm.em.createQueryBuilder(Author2, 'a'); +const knex = em.getKnex(); +const qb1 = em.createQueryBuilder(Book2, 'b').count('b.uuid', true).where({ author: knex.ref('a.id') }).as('Author2.booksTotal'); +const qb2 = em.createQueryBuilder(Author2, 'a'); qb2.select(['*', qb1]).orderBy({ booksTotal: 'desc' }); console.log(qb2.getQuery()); @@ -331,9 +333,9 @@ console.log(qb2.getQuery()); ``` ```typescript -const knex = orm.em.getKnex(); -const qb3 = orm.em.createQueryBuilder(Book2, 'b').count('b.uuid', true).where({ author: knex.ref('a.id') }).as('books_total'); -const qb4 = orm.em.createQueryBuilder(Author2, 'a'); +const knex = em.getKnex(); +const qb3 = em.createQueryBuilder(Book2, 'b').count('b.uuid', true).where({ author: knex.ref('a.id') }).as('books_total'); +const qb4 = em.createQueryBuilder(Author2, 'a'); qb4.select(['*', qb3]).orderBy({ booksTotal: 'desc' }); console.log(qb4.getQuery()); @@ -346,9 +348,9 @@ When you want to filter by sub-query on the left-hand side of a predicate, you w > You always need to use prefix in the `qb.withSchema()` (so `a.booksTotal`). ```typescript -const knex = orm.em.getKnex(); -const qb1 = orm.em.createQueryBuilder(Book2, 'b').count('b.uuid', true).where({ author: knex.ref('a.id') }).getKnexQuery(); -const qb2 = orm.em.createQueryBuilder(Author2, 'a'); +const knex = em.getKnex(); +const qb1 = em.createQueryBuilder(Book2, 'b').count('b.uuid', true).where({ author: knex.ref('a.id') }).getKnexQuery(); +const qb2 = em.createQueryBuilder(Author2, 'a'); qb2.select('*').withSubQuery(qb1, 'a.booksTotal').where({ 'a.booksTotal': { $in: [1, 2, 3] } }); console.log(qb2.getQuery()); @@ -356,9 +358,9 @@ console.log(qb2.getQuery()); ``` ```typescript -const knex = orm.em.getKnex(); -const qb3 = orm.em.createQueryBuilder(Book2, 'b').count('b.uuid', true).where({ author: knex.ref('a.id') }).getKnexQuery(); -const qb4 = orm.em.createQueryBuilder(Author2, 'a'); +const knex = em.getKnex(); +const qb3 = em.createQueryBuilder(Book2, 'b').count('b.uuid', true).where({ author: knex.ref('a.id') }).getKnexQuery(); +const qb4 = em.createQueryBuilder(Author2, 'a'); qb4.select('*').withSubQuery(qb3, 'a.booksTotal').where({ 'a.booksTotal': 1 }); console.log(qb4.getQuery()); @@ -370,7 +372,7 @@ console.log(qb4.getQuery()); You can use `qb.raw()` to insert raw SQL snippets like this: ```typescript -const qb = orm.em.createQueryBuilder(Book); +const qb = em.createQueryBuilder(Book); qb.update({ price: qb.raw('price + 1') }).where({ uuid: '123' }); console.log(qb.getQuery()); @@ -382,7 +384,7 @@ console.log(qb.getQuery()); We can set the `LockMode` via `qb.setLockMode()`. ```typescript -const qb = orm.em.createQueryBuilder(Test); +const qb = em.createQueryBuilder(Test); qb.select('*').where({ name: 'Lol 321' }).setLockMode(LockMode.PESSIMISTIC_READ); console.log(qb.getQuery()); // for MySQL @@ -403,7 +405,7 @@ Available lock modes: Optionally we can also pass list of table aliases we want to lock via second parameter: ```typescript -const qb = orm.em.createQueryBuilder(User, 'u'); +const qb = em.createQueryBuilder(User, 'u'); qb.select('*') .leftJoinAndSelect('u.identities', 'i') .where({ name: 'Jon' }) @@ -416,3 +418,55 @@ console.log(qb.getQuery()); // for Postgres // where "u"."name" = 'Jon' // for update of "u" skip locked ``` + +## Using Knex.js + +Under the hood, `QueryBuilder` uses [`Knex.js`](https://knexjs.org) to compose and run queries. +You can access configured `knex` instance via `qb.getKnexQuery()` method: + +```typescript +const qb = em.createQueryBuilder(Author); +qb.update({ name: 'test 123', type: PublisherType.GLOBAL }).where({ id: 123, type: PublisherType.LOCAL }); +const knex = qb.getKnexQuery(); // instance of Knex' QueryBuilder + +// do what ever you need with `knex` + +const res = await em.getConnection().execute(knex); +const entities = res.map(a => em.map(Author, a)); +console.log(entities); // Author[] +``` + +You can also get clear and configured knex instance from the connection via `getKnex()` method. +As this method is not available on the base `Connection` class, you will need to either manually +type cast the connection to `AbstractSqlConnection` (or the actual implementation you are using, +e.g. `MySqlConnection`), or provide correct driver type hint to your `EntityManager` instance, +which will be then automatically inferred in `em.getConnection()` method. + +> Driver and connection implementations are not directly exported from `@mikro-orm/core` module. +> You can import them from the driver packages (e.g. `import { PostgreSqlDriver } from '@mikro-orm/postgresql'`). + +```typescript +const conn = em.getConnection() as AbstractSqlConnection; +// you can make sure the `em` is correctly typed to `EntityManager` +// or one of its implementations: +// const em: EntityManager = em; + +const knex = conn.getKnex(); + +// do what ever you need with `knex` + +const res = await knex; +``` + +## Running Native SQL Query + +You can run native SQL via underlying connection + +```typescript +const connection = em.getConnection(); +const res = await connection.execute('select 1 as count'); +console.log(res); // res is array of objects: `[ { count: 1 } ]` +``` + +Since v4 we can also use `em.execute()` which will also handle logging and mapping +of exceptions. diff --git a/docs/docs/upgrading-v4-to-v5.md b/docs/docs/upgrading-v4-to-v5.md index 25de02e28c91..f82beefd1440 100644 --- a/docs/docs/upgrading-v4-to-v5.md +++ b/docs/docs/upgrading-v4-to-v5.md @@ -153,3 +153,8 @@ You can use destructing if you want to have a single entity return type: ```ts const [loadedAuthor] = await em.populate(author, ...); ``` + +## QueryBuilder is awaitable + +Previously awaiting of QB instance was a no-op. In v5, QB is promise-like interface, +so we can await it. More about this in [Awaiting the QueryBuilder](./query-builder.md#awaiting-the-querybuilder) section. diff --git a/packages/knex/src/SqlEntityManager.ts b/packages/knex/src/SqlEntityManager.ts index 0d03687a4987..4ac696aba216 100644 --- a/packages/knex/src/SqlEntityManager.ts +++ b/packages/knex/src/SqlEntityManager.ts @@ -18,6 +18,13 @@ export class SqlEntityManager e return new QueryBuilder(entityName, this.getMetadata(), this.getDriver(), this.getTransactionContext(), alias, type, this); } + /** + * Shortcut for `createQueryBuilder()` + */ + qb(entityName: EntityName, alias?: string, type?: 'read' | 'write') { + return this.createQueryBuilder(entityName, alias, type); + } + /** * Creates raw SQL query that won't be escaped when used as a parameter. */ diff --git a/packages/knex/src/SqlEntityRepository.ts b/packages/knex/src/SqlEntityRepository.ts index 78e90bc35037..c49f15f6fa5f 100644 --- a/packages/knex/src/SqlEntityRepository.ts +++ b/packages/knex/src/SqlEntityRepository.ts @@ -18,6 +18,13 @@ export class SqlEntityRepository extends EntityRepository { return this.em.createQueryBuilder(this.entityName, alias); } + /** + * Shortcut for `createQueryBuilder()` + */ + qb(alias?: string): QueryBuilder { + return this.createQueryBuilder(alias); + } + /** * Returns configured knex instance. */ diff --git a/packages/knex/src/query/QueryBuilder.ts b/packages/knex/src/query/QueryBuilder.ts index 2772acd93e5a..f15528b8489d 100644 --- a/packages/knex/src/query/QueryBuilder.ts +++ b/packages/knex/src/query/QueryBuilder.ts @@ -81,6 +81,7 @@ export class QueryBuilder = AnyEntity> { private lockMode?: LockMode; private lockTables?: string[]; private subQueries: Dictionary = {}; + private innerPromise?: Promise>; private readonly platform = this.driver.getPlatform(); private readonly knex = this.driver.getConnection(this.connectionType).getKnex(); private readonly helper: QueryBuilderHelper; @@ -111,31 +112,31 @@ export class QueryBuilder = AnyEntity> { this.flags.add(QueryFlag.DISTINCT); } - return this.init(QueryType.SELECT); + return this.init(QueryType.SELECT) as SelectQueryBuilder; } addSelect(fields: Field | Field[]): SelectQueryBuilder { if (this.type && this.type !== QueryType.SELECT) { - return this; + return this as SelectQueryBuilder; } return this.select([...Utils.asArray(this._fields), ...Utils.asArray(fields)]); } insert(data: EntityData | EntityData[]): InsertQueryBuilder { - return this.init(QueryType.INSERT, data); + return this.init(QueryType.INSERT, data) as InsertQueryBuilder; } update(data: EntityData): UpdateQueryBuilder { - return this.init(QueryType.UPDATE, data); + return this.init(QueryType.UPDATE, data) as UpdateQueryBuilder; } delete(cond?: QBFilterQuery): DeleteQueryBuilder { - return this.init(QueryType.DELETE, undefined, cond); + return this.init(QueryType.DELETE, undefined, cond) as DeleteQueryBuilder; } truncate(): TruncateQueryBuilder { - return this.init(QueryType.TRUNCATE); + return this.init(QueryType.TRUNCATE) as TruncateQueryBuilder; } count(field?: string | string[], distinct = false): CountQueryBuilder { @@ -145,7 +146,7 @@ export class QueryBuilder = AnyEntity> { this.flags.add(QueryFlag.DISTINCT); } - return this.init(QueryType.COUNT); + return this.init(QueryType.COUNT) as CountQueryBuilder; } join(field: string, alias: string, cond: QBFilterQuery = {}, type: 'leftJoin' | 'innerJoin' | 'pivotJoin' = 'innerJoin', path?: string): this { @@ -517,6 +518,33 @@ export class QueryBuilder = AnyEntity> { return res ? +res.count : 0; } + /** + * Provides promise-like interface so we can await the QB instance. + */ + then(onfulfilled?: ((value: any) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise> { + return this.getInnerPromise().then(onfulfilled, onrejected) as any; + } + + private getInnerPromise() { + if (!this.innerPromise) { + this.innerPromise = (async () => { + switch (this.type) { + case QueryType.INSERT: + case QueryType.UPDATE: + case QueryType.DELETE: + case QueryType.TRUNCATE: + return this.execute('run'); + case QueryType.SELECT: + return this.getResultList(); + case QueryType.COUNT: + return this.getCount(); + } + })(); + } + + return this.innerPromise!; + } + /** * Returns knex instance with sub-query aliased with given alias. * You can provide `EntityName.propName` as alias, then the field name will be used based on the metadata @@ -838,8 +866,10 @@ export class QueryBuilder = AnyEntity> { } -export interface RunQueryBuilder extends Omit, 'getResult' | 'getSingleResult' | 'getResultList'> { +export interface RunQueryBuilder extends Omit, 'getResult' | 'getSingleResult' | 'getResultList' | 'where'> { + where(cond: QBFilterQuery | string, params?: keyof typeof GroupOperator | any[], operator?: keyof typeof GroupOperator): this; execute>(method?: 'all' | 'get' | 'run', mapResults?: boolean): Promise; + then, TResult2 = never>(onfulfilled?: ((value: QueryResult) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise>; } export interface SelectQueryBuilder extends QueryBuilder { @@ -847,13 +877,15 @@ export interface SelectQueryBuilder extends QueryBuilder { execute(method: 'all', mapResults?: boolean): Promise; execute(method: 'get', mapResults?: boolean): Promise; execute>(method: 'run', mapResults?: boolean): Promise; + then(onfulfilled?: ((value: T[]) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise; } -export interface CountQueryBuilder extends SelectQueryBuilder { +export interface CountQueryBuilder extends QueryBuilder { execute(method?: 'all' | 'get' | 'run', mapResults?: boolean): Promise; execute(method: 'all', mapResults?: boolean): Promise; execute(method: 'get', mapResults?: boolean): Promise; execute>(method: 'run', mapResults?: boolean): Promise; + then(onfulfilled?: ((value: number) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise; } export interface InsertQueryBuilder extends RunQueryBuilder {} diff --git a/tests/EntityRepository.test.ts b/tests/EntityRepository.test.ts index 9e0ca8417ddb..9bb4d0f4ded7 100644 --- a/tests/EntityRepository.test.ts +++ b/tests/EntityRepository.test.ts @@ -12,6 +12,7 @@ const methods = { persistAndFlush: jest.fn(), persistLater: jest.fn(), createQueryBuilder: jest.fn(), + qb: jest.fn(), findOne: jest.fn(), findOneOrFail: jest.fn(), find: jest.fn(), @@ -63,6 +64,8 @@ describe('EntityRepository', () => { expect(methods.findOneOrFail.mock.calls[0]).toEqual([Publisher, 'bar', undefined]); await repo.createQueryBuilder(); expect(methods.createQueryBuilder.mock.calls[0]).toEqual([Publisher, undefined]); + await repo.qb(); + expect(methods.createQueryBuilder.mock.calls[0]).toEqual([Publisher, undefined]); repo.remove(e); expect(methods.remove.mock.calls[0]).toEqual([e]); const entity = {} as AnyEntity; diff --git a/tests/QueryBuilder.test.ts b/tests/QueryBuilder.test.ts index a557d1007a3c..238080728b0c 100644 --- a/tests/QueryBuilder.test.ts +++ b/tests/QueryBuilder.test.ts @@ -68,6 +68,34 @@ describe('QueryBuilder', () => { expect(qb.getParams()).toEqual([2, 1]); }); + test('awaiting the QB instance', async () => { + const qb1 = orm.em.qb(Publisher2); + const res1 = await qb1.insert({ name: 'p1', type: PublisherType.GLOBAL }); + expect(res1.insertId > 0).toBe(true); // test the type + expect(res1.insertId).toBeGreaterThanOrEqual(1); + + const qb2 = orm.em.qb(Publisher2); + const res2 = await qb2.select('*').where({ name: 'p1' }).limit(5); + expect(res2.map(p => p.name)).toEqual(['p1']); // test the type + expect(res2).toHaveLength(1); + expect(res2[0]).toBeInstanceOf(Publisher2); + + const qb3 = orm.em.qb(Publisher2); + const res3 = await qb3.count().where({ name: 'p1' }); + expect(res3 > 0).toBe(true); // test the type + expect(res3).toBe(1); + + const qb4 = orm.em.qb(Publisher2); + const res4 = await qb4.update({ type: PublisherType.LOCAL }).where({ name: 'p1' }); + expect(res4.affectedRows > 0).toBe(true); // test the type + expect(res4.affectedRows).toBe(1); + + const qb5 = orm.em.qb(Publisher2); + const res5 = await qb5.delete().where({ name: 'p1' }); + expect(res5.affectedRows > 0).toBe(true); // test the type + expect(res5.affectedRows).toBe(1); + }); + test('select query with order by variants', async () => { const qb1 = orm.em.createQueryBuilder(Publisher2); qb1.select('*').where({ name: 'test 123' }).orderBy({ name: QueryOrder.DESC, type: 'ASC' }).limit(2, 1); @@ -1755,21 +1783,21 @@ describe('QueryBuilder', () => { }); test('pivot joining of m:n when target entity is null (GH issue 548)', async () => { - const qb11 = await orm.em.createQueryBuilder(User2, 'u').select('u.*').where({ cars: null }); + const qb11 = orm.em.createQueryBuilder(User2, 'u').select('u.*').where({ cars: null }); expect(qb11.getQuery()).toMatch('select `u`.* ' + 'from `user2` as `u` ' + 'left join `user2_cars` as `e1` on `u`.`first_name` = `e1`.`user2_first_name` and `u`.`last_name` = `e1`.`user2_last_name` ' + 'where (`e1`.`car2_name`, `e1`.`car2_year`) is null'); expect(qb11.getParams()).toEqual([]); - const qb2 = await orm.em.createQueryBuilder(Book2, 'b').select('b.*').where({ $or: [{ tags: null }, { tags: { $ne: 1 } }] }); + const qb2 = orm.em.createQueryBuilder(Book2, 'b').select('b.*').where({ $or: [{ tags: null }, { tags: { $ne: 1 } }] }); expect(qb2.getQuery()).toMatch('select `b`.*, `b`.price * 1.19 as `price_taxed` ' + 'from `book2` as `b` ' + 'left join `book2_tags` as `e1` on `b`.`uuid_pk` = `e1`.`book2_uuid_pk` ' + 'where (`e1`.`book_tag2_id` is null or `e1`.`book_tag2_id` != ?)'); expect(qb2.getParams()).toEqual(['1']); - const qb3 = await orm.em.createQueryBuilder(Author2, 'a').select('a.*').where({ friends: null }).orderBy({ friends: { name: QueryOrder.ASC } }); + const qb3 = orm.em.createQueryBuilder(Author2, 'a').select('a.*').where({ friends: null }).orderBy({ friends: { name: QueryOrder.ASC } }); expect(qb3.getQuery()).toMatch('select `a`.* ' + 'from `author2` as `a` ' + 'left join `author_to_friend` as `e1` on `a`.`id` = `e1`.`author2_1_id` ' + @@ -1778,7 +1806,7 @@ describe('QueryBuilder', () => { 'order by `e2`.`name` asc'); expect(qb3.getParams()).toEqual([]); - const qb4 = await orm.em.createQueryBuilder(Author2, 'a').select('a.*').where({ friends: null }).orderBy({ friends: QueryOrder.ASC }); + const qb4 = orm.em.createQueryBuilder(Author2, 'a').select('a.*').where({ friends: null }).orderBy({ friends: QueryOrder.ASC }); expect(qb4.getQuery()).toMatch('select `a`.* ' + 'from `author2` as `a` ' + 'left join `author_to_friend` as `e1` on `a`.`id` = `e1`.`author2_1_id` ' + @@ -1788,35 +1816,35 @@ describe('QueryBuilder', () => { }); test('pivot joining of m:n when no target entity needed directly (GH issue 549)', async () => { - const qb1 = await orm.em.createQueryBuilder(Book2, 'b').select('b.*').where({ tags: { id: 1 } }); + const qb1 = orm.em.createQueryBuilder(Book2, 'b').select('b.*').where({ tags: { id: 1 } }); expect(qb1.getQuery()).toMatch('select `b`.*, `b`.price * 1.19 as `price_taxed` ' + 'from `book2` as `b` ' + 'left join `book2_tags` as `e1` on `b`.`uuid_pk` = `e1`.`book2_uuid_pk` ' + 'where `e1`.`book_tag2_id` = ?'); expect(qb1.getParams()).toEqual(['1']); - const qb11 = await orm.em.createQueryBuilder(User2, 'u').select('u.*').where({ cars: { name: 'n', year: 1 } }); + const qb11 = orm.em.createQueryBuilder(User2, 'u').select('u.*').where({ cars: { name: 'n', year: 1 } }); expect(qb11.getQuery()).toMatch('select `u`.* ' + 'from `user2` as `u` ' + 'left join `user2_cars` as `e1` on `u`.`first_name` = `e1`.`user2_first_name` and `u`.`last_name` = `e1`.`user2_last_name` ' + 'where (`e1`.`car2_name`, `e1`.`car2_year`) = (?, ?)'); expect(qb11.getParams()).toEqual(['n', 1]); - const qb12 = await orm.em.createQueryBuilder(User2, 'u').select('u.*').where({ cars: { $in: [{ name: 'n', year: 1 }, { name: 'n', year: 2 }] } }); + const qb12 = orm.em.createQueryBuilder(User2, 'u').select('u.*').where({ cars: { $in: [{ name: 'n', year: 1 }, { name: 'n', year: 2 }] } }); expect(qb12.getQuery()).toMatch('select `u`.* ' + 'from `user2` as `u` ' + 'left join `user2_cars` as `e1` on `u`.`first_name` = `e1`.`user2_first_name` and `u`.`last_name` = `e1`.`user2_last_name` ' + 'where (`e1`.`car2_name`, `e1`.`car2_year`) in ((?, ?), (?, ?))'); expect(qb12.getParams()).toEqual(['n', 1, 'n', 2]); - const qb2 = await orm.em.createQueryBuilder(Book2, 'b').select('b.*').where({ $or: [{ tags: { id: null } }, { tags: { $ne: 1 } }] }); + const qb2 = orm.em.createQueryBuilder(Book2, 'b').select('b.*').where({ $or: [{ tags: { id: null } }, { tags: { $ne: 1 } }] }); expect(qb2.getQuery()).toMatch('select `b`.*, `b`.price * 1.19 as `price_taxed` ' + 'from `book2` as `b` ' + 'left join `book2_tags` as `e1` on `b`.`uuid_pk` = `e1`.`book2_uuid_pk` ' + 'where (`e1`.`book_tag2_id` is null or `e1`.`book_tag2_id` != ?)'); expect(qb2.getParams()).toEqual(['1']); - const qb4 = await orm.em.createQueryBuilder(Author2, 'a').select('a.*').where({ friends: 1 }).orderBy({ friends: { id: QueryOrder.ASC } }); + const qb4 = orm.em.createQueryBuilder(Author2, 'a').select('a.*').where({ friends: 1 }).orderBy({ friends: { id: QueryOrder.ASC } }); expect(qb4.getQuery()).toMatch('select `a`.* ' + 'from `author2` as `a` ' + 'left join `author_to_friend` as `e1` on `a`.`id` = `e1`.`author2_1_id` ' + diff --git a/tests/issues/GH572.test.ts b/tests/issues/GH572.test.ts index a08bf0cb044a..3921ef27d156 100644 --- a/tests/issues/GH572.test.ts +++ b/tests/issues/GH572.test.ts @@ -51,11 +51,11 @@ describe('GH issue 572', () => { }); expect(mock.mock.calls[0][0]).toMatch('select `a0`.*, `b1`.`id` as `b_id` from `a` as `a0` left join `b` as `b1` on `a0`.`id` = `b1`.`a_id` order by `b1`.`camel_case_field` asc'); expect(res1).toHaveLength(0); - const qb1 = await orm.em.createQueryBuilder(A, 'a').select('a.*').orderBy({ b: { camelCaseField: QueryOrder.ASC } }); + const qb1 = orm.em.createQueryBuilder(A, 'a').select('a.*').orderBy({ b: { camelCaseField: QueryOrder.ASC } }); expect(qb1.getQuery()).toMatch('select `a`.* from `a` as `a` left join `b` as `b1` on `a`.`id` = `b1`.`a_id` order by `b1`.`camel_case_field` asc'); - const qb2 = await orm.em.createQueryBuilder(B, 'b').select('b.*').orderBy({ 'b.camelCaseField': QueryOrder.ASC }); + const qb2 = orm.em.createQueryBuilder(B, 'b').select('b.*').orderBy({ 'b.camelCaseField': QueryOrder.ASC }); expect(qb2.getQuery()).toMatch('select `b`.* from `b` as `b` order by `b`.`camel_case_field` asc'); - const qb3 = await orm.em.createQueryBuilder(A, 'a').select('a.*').leftJoin('a.b', 'b_').orderBy({ 'b_.camelCaseField': QueryOrder.ASC }); + const qb3 = orm.em.createQueryBuilder(A, 'a').select('a.*').leftJoin('a.b', 'b_').orderBy({ 'b_.camelCaseField': QueryOrder.ASC }); expect(qb3.getQuery()).toMatch('select `a`.* from `a` as `a` left join `b` as `b_` on `a`.`id` = `b_`.`a_id` order by `b_`.`camel_case_field` asc'); }); });