fix: better support of relation-based properties in where clauses (#7805)

This commit is contained in:
James Ward 2021-07-03 13:29:56 -05:00 committed by GitHub
parent 9119879d7b
commit 3221c50d87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 566 additions and 95 deletions

View File

@ -160,7 +160,10 @@ userRepository.find({
relations: ["profile", "photos", "videos"],
where: {
firstName: "Timber",
lastName: "Saw"
lastName: "Saw",
profile: {
userName: "tshaw"
}
},
order: {
name: "ASC",

View File

@ -99,47 +99,19 @@ export class FindOptionsUtils {
if (options.select) {
qb.select([]);
options.select.forEach(select => {
if (!metadata.findColumnWithPropertyPath(String(select)))
if (!metadata.hasColumnWithPropertyPath(`${select}`))
throw new TypeORMError(`${select} column was not found in the ${metadata.name} entity.`);
qb.addSelect(qb.alias + "." + select);
const columns = metadata.findColumnsWithPropertyPath(`${select}`);
for (const column of columns) {
qb.addSelect(qb.alias + "." + column.propertyPath);
}
});
}
if (options.where)
qb.where(options.where);
if ((options as FindManyOptions<T>).skip)
qb.skip((options as FindManyOptions<T>).skip!);
if ((options as FindManyOptions<T>).take)
qb.take((options as FindManyOptions<T>).take!);
if (options.order)
Object.keys(options.order).forEach(key => {
const order = ((options as FindOneOptions<T>).order as any)[key as any];
if (!metadata.findColumnWithPropertyPath(key))
throw new TypeORMError(`${key} column was not found in the ${metadata.name} entity.`);
switch (order) {
case 1:
qb.addOrderBy(qb.alias + "." + key, "ASC");
break;
case -1:
qb.addOrderBy(qb.alias + "." + key, "DESC");
break;
case "ASC":
qb.addOrderBy(qb.alias + "." + key, "ASC");
break;
case "DESC":
qb.addOrderBy(qb.alias + "." + key, "DESC");
break;
}
});
if (options.relations) {
const allRelations = options.relations.map(relation => relation);
const allRelations = options.relations;
this.applyRelationsRecursively(qb, allRelations, qb.expressionMap.mainAlias!.name, qb.expressionMap.mainAlias!.metadata, "");
// recursive removes found relations from allRelations array
// if there are relations left in this array it means those relations were not found in the entity structure
@ -207,6 +179,38 @@ export class FindOptionsUtils {
qb.loadAllRelationIds(options.loadRelationIds as any);
}
if (options.where)
qb.where(options.where);
if ((options as FindManyOptions<T>).skip)
qb.skip((options as FindManyOptions<T>).skip!);
if ((options as FindManyOptions<T>).take)
qb.take((options as FindManyOptions<T>).take!);
if (options.order)
Object.keys(options.order).forEach(key => {
const order = ((options as FindOneOptions<T>).order as any)[key as any];
if (!metadata.findColumnWithPropertyPath(key))
throw new Error(`${key} column was not found in the ${metadata.name} entity.`);
switch (order) {
case 1:
qb.addOrderBy(qb.alias + "." + key, "ASC");
break;
case -1:
qb.addOrderBy(qb.alias + "." + key, "DESC");
break;
case "ASC":
qb.addOrderBy(qb.alias + "." + key, "ASC");
break;
case "DESC":
qb.addOrderBy(qb.alias + "." + key, "DESC");
break;
}
});
return qb;
}

View File

@ -654,6 +654,14 @@ export class EntityMetadata {
return this.columns.find(column => column.databaseName === databaseName);
}
/**
* Checks if there is a column or relationship with a given property path.
*/
hasColumnWithPropertyPath(propertyPath: string): boolean {
const hasColumn = this.columns.some(column => column.propertyPath === propertyPath);
return hasColumn || this.hasRelationWithPropertyPath(propertyPath);
}
/**
* Finds column with a given property path.
*/
@ -682,13 +690,20 @@ export class EntityMetadata {
// in the case if column with property path was not found, try to find a relation with such property path
// if we find relation and it has a single join column then its the column user was seeking
const relation = this.relations.find(relation => relation.propertyPath === propertyPath);
const relation = this.findRelationWithPropertyPath(propertyPath);
if (relation && relation.joinColumns)
return relation.joinColumns;
return [];
}
/**
* Checks if there is a relation with the given property path.
*/
hasRelationWithPropertyPath(propertyPath: string): boolean {
return this.relations.some(relation => relation.propertyPath === propertyPath);
}
/**
* Finds relation with the given property path.
*/
@ -740,6 +755,8 @@ export class EntityMetadata {
/**
* Creates a property paths for a given entity.
*
* @deprecated
*/
static createPropertyPath(metadata: EntityMetadata, entity: ObjectLiteral, prefix: string = "") {
const paths: string[] = [];

View File

@ -853,6 +853,154 @@ export abstract class QueryBuilder<Entity> {
: whereStrings[0];
}
private findColumnsForPropertyPath(propertyPath: string): [ Alias, string[], ColumnMetadata[] ] {
// Make a helper to iterate the entity & relations?
// Use that to set the correct alias? Or the other way around?
// Start with the main alias with our property paths
let alias = this.expressionMap.mainAlias;
const root: string[] = [];
const propertyPathParts = propertyPath.split(".");
while (propertyPathParts.length > 1) {
const part = propertyPathParts[0];
if (!alias?.hasMetadata) {
// If there's no metadata, we're wasting our time
// and can't actually look any of this up.
break;
}
if (alias.metadata.hasEmbeddedWithPropertyPath(part)) {
// If this is an embedded then we should combine the two as part of our lookup.
// Instead of just breaking, we keep going with this in case there's an embedded/relation
// inside an embedded.
propertyPathParts.unshift(
`${propertyPathParts.shift()}.${propertyPathParts.shift()}`
);
continue;
}
if (alias.metadata.hasRelationWithPropertyPath(part)) {
// If this is a relation then we should find the aliases
// that match the relation & then continue further down
// the property path
const joinAttr = this.expressionMap.joinAttributes.find(
(joinAttr) => joinAttr.relationPropertyPath === part
);
if (!joinAttr?.alias) {
const fullRelationPath = root.length > 0 ? `${root.join(".")}.${part}` : part;
throw new Error(`Cannot find alias for relation at ${fullRelationPath}`);
}
alias = joinAttr.alias;
root.push(...part.split("."));
propertyPathParts.shift();
continue;
}
break;
}
if (!alias) {
throw new Error(`Cannot find alias for property ${propertyPath}`);
}
// Remaining parts are combined back and used to find the actual property path
const aliasPropertyPath = propertyPathParts.join(".");
const columns = alias.metadata.findColumnsWithPropertyPath(aliasPropertyPath);
if (!columns.length) {
throw new EntityColumnNotFound(propertyPath);
}
return [ alias, root, columns ];
}
/**
* Creates a property paths for a given ObjectLiteral.
*/
protected createPropertyPath(metadata: EntityMetadata, entity: ObjectLiteral, prefix: string = "") {
const paths: string[] = [];
for (const key of Object.keys(entity)) {
const path = prefix ? `${prefix}.${key}` : key;
// There's times where we don't actually want to traverse deeper.
// If the value is a `FindOperator`, or null, or not an object, then we don't, for example.
if (entity[key] === null || typeof entity[key] !== "object" || entity[key] instanceof FindOperator) {
paths.push(path);
continue;
}
if (metadata.hasEmbeddedWithPropertyPath(path)) {
const subPaths = this.createPropertyPath(metadata, entity[key], path);
paths.push(...subPaths);
continue;
}
if (metadata.hasRelationWithPropertyPath(path)) {
const relation = metadata.findRelationWithPropertyPath(path)!;
// There's also cases where we don't want to return back all of the properties.
// These handles the situation where someone passes the model & we don't need to make
// a HUGE `where` to uniquely look up the entity.
// In the case of a *-to-one, there's only ever one possible entity on the other side
// so if the join columns are all defined we can return just the relation itself
// because it will fetch only the join columns and do the lookup.
if (relation.relationType === "one-to-one" || relation.relationType === "many-to-one") {
const joinColumns = relation.joinColumns
.map(j => j.referencedColumn)
.filter((j): j is ColumnMetadata => !!j);
const hasAllJoinColumns = joinColumns.length > 0 && joinColumns.every(
column => column.getEntityValue(entity[key], false)
);
if (hasAllJoinColumns) {
paths.push(path);
continue;
}
}
if (relation.relationType === "one-to-many" || relation.relationType === "many-to-many") {
throw new Error(`Cannot query across ${relation.relationType} for property ${path}`);
}
// For any other case, if the `entity[key]` contains all of the primary keys we can do a
// lookup via these. We don't need to look up via any other values 'cause these are
// the unique primary keys.
// This handles the situation where someone passes the model & we don't need to make
// a HUGE where.
const primaryColumns = relation.inverseEntityMetadata.primaryColumns;
const hasAllPrimaryKeys = primaryColumns.length > 0 && primaryColumns.every(
column => column.getEntityValue(entity[key], false)
);
if (hasAllPrimaryKeys) {
const subPaths = primaryColumns.map(
column => `${path}.${column.propertyPath}`
);
paths.push(...subPaths);
continue;
}
// If nothing else, just return every property that's being passed to us.
const subPaths = this.createPropertyPath(relation.inverseEntityMetadata, entity[key])
.map(p => `${path}.${p}`);
paths.push(...subPaths);
continue;
}
paths.push(path);
}
return paths;
}
/**
* Computes given where argument - transforms to a where string all forms it can take.
*/
@ -884,19 +1032,28 @@ export abstract class QueryBuilder<Entity> {
if (this.expressionMap.mainAlias!.hasMetadata) {
andConditions = wheres.map((where, whereIndex) => {
const propertyPaths = EntityMetadata.createPropertyPath(this.expressionMap.mainAlias!.metadata, where);
const propertyPaths = this.createPropertyPath(this.expressionMap.mainAlias!.metadata, where);
return propertyPaths.map((propertyPath, propertyIndex) => {
const columns = this.expressionMap.mainAlias!.metadata.findColumnsWithPropertyPath(propertyPath);
if (!columns.length) {
throw new EntityColumnNotFound(propertyPath);
}
const [ alias, aliasPropertyPath, columns ] = this.findColumnsForPropertyPath(propertyPath);
return columns.map((column, columnIndex) => {
const aliasPath = this.expressionMap.aliasNamePrefixingEnabled ? `${this.alias}.${propertyPath}` : column.propertyPath;
let parameterValue = column.getEntityValue(where, true);
// Use the correct alias & the property path from the column
const aliasPath = this.expressionMap.aliasNamePrefixingEnabled ? `${alias.name}.${column.propertyPath}` : column.propertyPath;
let containedWhere = where;
for (const part of aliasPropertyPath) {
if (!containedWhere || !(part in containedWhere)) {
containedWhere = {};
break;
}
containedWhere = containedWhere[part];
}
let parameterValue = column.getEntityValue(containedWhere, true);
if (parameterValue === null) {
return `${aliasPath} IS NULL`;

View File

@ -9,7 +9,6 @@ import {SqlServerDriver} from "../driver/sqlserver/SqlServerDriver";
import {PostgresDriver} from "../driver/postgres/PostgresDriver";
import {WhereExpression} from "./WhereExpression";
import {Brackets} from "./Brackets";
import {EntityMetadata} from "../metadata/EntityMetadata";
import {UpdateResult} from "./result/UpdateResult";
import {ReturningStatementNotSupportedError} from "../error/ReturningStatementNotSupportedError";
import {ReturningResultsEntityUpdator} from "./ReturningResultsEntityUpdator";
@ -398,7 +397,7 @@ export class UpdateQueryBuilder<Entity> extends QueryBuilder<Entity> implements
const updateColumnAndValues: string[] = [];
const updatedColumns: ColumnMetadata[] = [];
if (metadata) {
EntityMetadata.createPropertyPath(metadata, valuesSet).forEach(propertyPath => {
this.createPropertyPath(metadata, valuesSet).forEach(propertyPath => {
// todo: make this and other query builder to work with properly with tables without metadata
const columns = metadata.findColumnsWithPropertyPath(propertyPath);

View File

@ -2,6 +2,8 @@ import {Entity} from "../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../../src/decorator/columns/Column";
import {VersionColumn} from "../../../../../src/decorator/columns/VersionColumn";
import { Post } from "./Post";
import { OneToMany } from "../../../../../src";
@Entity()
export class Category {
@ -18,4 +20,7 @@ export class Category {
@VersionColumn()
version: string;
}
@OneToMany(() => Post, (post) => post.category)
posts: Post[]
}

View File

@ -0,0 +1,21 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToOne,
} from "../../../../../src";
import { Post } from "./Post";
@Entity()
export class HeroImage {
@PrimaryGeneratedColumn()
id: number;
@Column()
url: string;
@OneToOne(() => Post, (post) => post.heroImage)
post: Post;
}

View File

@ -1,9 +1,16 @@
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../../src/decorator/columns/Column";
import {VersionColumn} from "../../../../../src/decorator/columns/VersionColumn";
import {Category} from "./Category";
import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne";
import {
Entity,
PrimaryGeneratedColumn,
Column,
VersionColumn,
ManyToOne,
JoinTable,
ManyToMany,
OneToOne, JoinColumn,
} from "../../../../../src";
import { Tag } from "./Tag";
import { Category } from "./Category";
import { HeroImage } from "./HeroImage";
@Entity()
export class Post {
@ -23,7 +30,15 @@ export class Post {
@VersionColumn()
version: string;
@OneToOne(() => HeroImage, (hero) => hero.post)
@JoinColumn()
heroImage: HeroImage;
@ManyToOne(type => Category)
category: Category;
}
@ManyToMany(() => Tag, (tag) => tag.posts)
@JoinTable()
tags: Tag[]
}

View File

@ -0,0 +1,16 @@
import { Post } from "./Post";
import { Entity, ManyToMany, Column, PrimaryGeneratedColumn } from "../../../../../src";
@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Post, (post) => post.tags)
posts: Post[]
}

View File

@ -1,17 +1,18 @@
import "reflect-metadata";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
import {Connection} from "../../../../src/connection/Connection";
import {Post} from "./entity/Post";
import {expect} from "chai";
import {EntityNotFoundError} from "../../../../src/error/EntityNotFoundError";
import { EntityNotFoundError, Connection, IsNull, In, Raw } from "../../../../src";
import {MysqlDriver} from "../../../../src/driver/mysql/MysqlDriver";
import { Category } from "./entity/Category";
import {Post} from "./entity/Post";
import { Tag } from "./entity/Tag";
import { HeroImage } from "./entity/HeroImage";
describe("query builder > select", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["mysql"]
entities: [Category, Post, Tag, HeroImage],
enabledDrivers: ["sqlite"],
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
@ -26,6 +27,7 @@ describe("query builder > select", () => {
"post.description AS post_description, " +
"post.rating AS post_rating, " +
"post.version AS post_version, " +
"post.heroImageId AS post_heroImageId, " +
"post.categoryId AS post_categoryId " +
"FROM post post");
})));
@ -41,6 +43,7 @@ describe("query builder > select", () => {
"post.description AS post_description, " +
"post.rating AS post_rating, " +
"post.version AS post_version, " +
"post.heroImageId AS post_heroImageId, " +
"post.categoryId AS post_categoryId " +
"FROM post post");
})));
@ -56,10 +59,11 @@ describe("query builder > select", () => {
"post.description AS post_description, " +
"post.rating AS post_rating, " +
"post.version AS post_version, " +
"post.heroImageId AS post_heroImageId, " +
"post.categoryId AS post_categoryId, " +
"category.id AS category_id, " +
"category.name AS category_name," +
" category.description AS category_description, " +
"category.name AS category_name, " +
"category.description AS category_description, " +
"category.version AS category_version " +
"FROM post post LEFT JOIN category category");
})));
@ -77,6 +81,7 @@ describe("query builder > select", () => {
"FROM post post LEFT JOIN category category");
})));
it("should append entity mapped columns to select statement, if they passed as array", () => Promise.all(connections.map(async connection => {
const sql = connection.createQueryBuilder(Post, "post")
.select(["post.id", "post.title"])
@ -113,48 +118,277 @@ describe("query builder > select", () => {
expect(sql).to.equal("SELECT post.name FROM post post");
})));
it("should return a single entity for getOne when found", () => Promise.all(connections.map(async connection => {
await connection.getRepository(Post).save({ id: 1, title: "Hello", description: 'World', rating: 0 });
describe("with relations and where clause", () => {
describe("many-to-one", () => {
it("should craft query with exact value", () => Promise.all(connections.map(async connection => {
// For github issues #2707
const entity = await connection.createQueryBuilder(Post, "post")
.where("post.id = :id", { id: 1 })
.getOne();
const [sql, params] = connection.createQueryBuilder(Post, "post")
.select("post.id")
.leftJoin("post.category", "category_join")
.where({
"category": {
"name": "Foo"
}
})
.getQueryAndParameters();
expect(entity).not.to.be.undefined;
expect(entity!.id).to.equal(1);
expect(entity!.title).to.equal("Hello");
})));
expect(sql).to.equal(
'SELECT "post"."id" AS "post_id" FROM "post" "post" ' +
'LEFT JOIN "category" "category_join" ON "category_join"."id"="post"."categoryId" ' +
'WHERE "category_join"."name" = ?'
);
it("should return undefined for getOne when not found", () => Promise.all(connections.map(async connection => {
await connection.getRepository(Post).save({ id: 1, title: "Hello", description: 'World', rating: 0 });
expect(params).to.eql(["Foo"]);
})));
const entity = await connection.createQueryBuilder(Post, "post")
.where("post.id = :id", { id: 2 })
.getOne();
it("should craft query with FindOperator", () => Promise.all(connections.map(async connection => {
const [sql, params] = connection.createQueryBuilder(Post, "post")
.select("post.id")
.leftJoin("post.category", "category_join")
.where({
"category": {
"name": IsNull()
}
})
.getQueryAndParameters();
expect(entity).to.be.undefined;
})));
expect(sql).to.equal(
'SELECT "post"."id" AS "post_id" FROM "post" "post" ' +
'LEFT JOIN "category" "category_join" ON "category_join"."id"="post"."categoryId" ' +
'WHERE "category_join"."name" IS NULL'
);
it("should return a single entity for getOneOrFail when found", () => Promise.all(connections.map(async connection => {
await connection.getRepository(Post).save({ id: 1, title: "Hello", description: 'World', rating: 0 });
expect(params).to.eql([]);
})));
const entity = await connection.createQueryBuilder(Post, "post")
.where("post.id = :id", { id: 1 })
.getOneOrFail();
it("should craft query with Raw", () => Promise.all(connections.map(async connection => {
// For github issue #6264
const [sql, params] = connection.createQueryBuilder(Post, "post")
.select("post.id")
.leftJoin("post.category", "category_join")
.where({
"category": {
"name": Raw(path => `SOME_FUNCTION(${path})`)
}
})
.getQueryAndParameters();
expect(entity.id).to.equal(1);
expect(entity.title).to.equal("Hello");
})));
expect(sql).to.equal(
'SELECT "post"."id" AS "post_id" FROM "post" "post" ' +
'LEFT JOIN "category" "category_join" ON "category_join"."id"="post"."categoryId" ' +
'WHERE SOME_FUNCTION("category_join"."name")'
);
it("should throw an Error for getOneOrFail when not found", () => Promise.all(connections.map(async connection => {
await connection.getRepository(Post).save({ id: 1, title: "Hello", description: 'World', rating: 0 });
expect(params).to.eql([]);
})));
})
await expect(
connection.createQueryBuilder(Post, "post")
.where("post.id = :id", { id: 2 })
.getOneOrFail()
).to.be.rejectedWith(EntityNotFoundError);
})));
describe("one-to-many", () => {
it("should craft query with exact value", () => Promise.all(connections.map(async connection => {
expect(() => {
connection.createQueryBuilder(Category, "category")
.select("category.id")
.leftJoin("category.posts", "posts")
.where({
posts: {
id: 10
}
})
.getQueryAndParameters();
}).to.throw();
})));
it("should craft query with FindOperator", () => Promise.all(connections.map(async connection => {
// For github issue #6647
expect(() => {
connection.createQueryBuilder(Category, "category")
.select("category.id")
.leftJoin("category.posts", "posts")
.where({
posts: {
id: IsNull()
}
})
.getQueryAndParameters();
}).to.throw();
})));
});
describe("many-to-many", () => {
it("should craft query with exact value", () => Promise.all(connections.map(async connection => {
expect(() => {
connection.createQueryBuilder(Post, "post")
.select("post.id")
.leftJoin("post.tags", "tags_join")
.where({
"tags": {
"name": "Foo"
}
})
.getQueryAndParameters();
}).to.throw();
})));
it("should craft query with FindOperator", () => Promise.all(connections.map(async connection => {
expect(() => {
connection.createQueryBuilder(Post, "post")
.select("post.id")
.leftJoin("post.tags", "tags_join")
.where({
"tags": {
"name": IsNull()
}
})
.getQueryAndParameters();
}).to.throw();
})));
});
describe("one-to-one", () => {
it("should craft query with exact value", () => Promise.all(connections.map(async connection => {
const [sql, params] = connection.createQueryBuilder(Post, "post")
.select("post.id")
.leftJoin("post.heroImage", "hero_join")
.where({
heroImage: {
url: "Foo"
}
})
.getQueryAndParameters();
expect(sql).to.equal(
'SELECT "post"."id" AS "post_id" FROM "post" "post" ' +
'LEFT JOIN "hero_image" "hero_join" ON "hero_join"."id"="post"."heroImageId" ' +
'WHERE "hero_join"."url" = ?'
);
expect(params).to.eql(["Foo"]);
})));
it("should craft query with FindOperator", () => Promise.all(connections.map(async connection => {
const [sql, params] = connection.createQueryBuilder(Post, "post")
.select("post.id")
.leftJoin("post.heroImage", "hero_join")
.where({
heroImage: {
url: IsNull()
}
})
.getQueryAndParameters();
expect(sql).to.equal(
'SELECT "post"."id" AS "post_id" FROM "post" "post" ' +
'LEFT JOIN "hero_image" "hero_join" ON "hero_join"."id"="post"."heroImageId" ' +
'WHERE "hero_join"."url" IS NULL'
);
expect(params).to.eql([]);
})));
});
describe("deeply nested relations", () => {
it("should craft query with exact value", () => Promise.all(connections.map(async connection => {
// For github issue #7251
const [sql, params] = connection.createQueryBuilder(HeroImage, "hero")
.leftJoin("hero.post", "posts")
.leftJoin("posts.category", "category")
.where({
post: {
category: {
name: "Foo"
}
}
})
.getQueryAndParameters();
expect(sql).to.equal(
'SELECT "hero"."id" AS "hero_id", "hero"."url" AS "hero_url" ' +
'FROM "hero_image" "hero" ' +
'LEFT JOIN "post" "posts" ON "posts"."heroImageId"="hero"."id" ' +
'LEFT JOIN "category" "category" ON "category"."id"="posts"."categoryId" ' +
'WHERE "category"."name" = ?'
);
expect(params).to.eql(["Foo"]);
})));
it("should craft query with FindOperator", () => Promise.all(connections.map(async connection => {
// For github issue #4906
const [sql, params] = connection.createQueryBuilder(HeroImage, "hero")
.leftJoin("hero.post", "posts")
.leftJoin("posts.category", "category")
.where({
post: {
category: {
name: In(["Foo", "Bar", "Baz"])
}
}
})
.getQueryAndParameters();
expect(sql).to.equal(
'SELECT "hero"."id" AS "hero_id", "hero"."url" AS "hero_url" ' +
'FROM "hero_image" "hero" ' +
'LEFT JOIN "post" "posts" ON "posts"."heroImageId"="hero"."id" ' +
'LEFT JOIN "category" "category" ON "category"."id"="posts"."categoryId" ' +
'WHERE "category"."name" IN (?, ?, ?)'
);
expect(params).to.eql(["Foo", "Bar", "Baz"]);
})));
});
});
describe("query execution and retrieval", () => {
it("should return a single entity for getOne when found", () => Promise.all(connections.map(async connection => {
await connection.getRepository(Post).save({ id: 1, title: "Hello", description: "World", rating: 0 });
const entity = await connection.createQueryBuilder(Post, "post")
.where("post.id = :id", { id: 1 })
.getOne();
expect(entity).not.to.be.undefined;
expect(entity!.id).to.equal(1);
expect(entity!.title).to.equal("Hello");
})));
it("should return undefined for getOne when not found", () => Promise.all(connections.map(async connection => {
await connection.getRepository(Post).save({ id: 1, title: "Hello", description: "World", rating: 0 });
const entity = await connection.createQueryBuilder(Post, "post")
.where("post.id = :id", { id: 2 })
.getOne();
expect(entity).to.be.undefined;
})));
it("should return a single entity for getOneOrFail when found", () => Promise.all(connections.map(async connection => {
await connection.getRepository(Post).save({ id: 1, title: "Hello", description: "World", rating: 0 });
const entity = await connection.createQueryBuilder(Post, "post")
.where("post.id = :id", { id: 1 })
.getOneOrFail();
expect(entity.id).to.equal(1);
expect(entity.title).to.equal("Hello");
})));
it("should throw an Error for getOneOrFail when not found", () => Promise.all(connections.map(async connection => {
await connection.getRepository(Post).save({ id: 1, title: "Hello", description: "World", rating: 0 });
await expect(
connection.createQueryBuilder(Post, "post")
.where("post.id = :id", { id: 2 })
.getOneOrFail()
).to.be.rejectedWith(EntityNotFoundError);
})));
})
it("Support max execution time", () => Promise.all(connections.map(async connection => {
// MAX_EXECUTION_TIME supports only in MySQL