mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
fix: better support of relation-based properties in where clauses (#7805)
This commit is contained in:
parent
9119879d7b
commit
3221c50d87
@ -160,7 +160,10 @@ userRepository.find({
|
||||
relations: ["profile", "photos", "videos"],
|
||||
where: {
|
||||
firstName: "Timber",
|
||||
lastName: "Saw"
|
||||
lastName: "Saw",
|
||||
profile: {
|
||||
userName: "tshaw"
|
||||
}
|
||||
},
|
||||
order: {
|
||||
name: "ASC",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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[] = [];
|
||||
|
||||
@ -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`;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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[]
|
||||
|
||||
}
|
||||
|
||||
21
test/functional/query-builder/select/entity/HeroImage.ts
Normal file
21
test/functional/query-builder/select/entity/HeroImage.ts
Normal 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;
|
||||
|
||||
}
|
||||
@ -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[]
|
||||
|
||||
}
|
||||
|
||||
16
test/functional/query-builder/select/entity/Tag.ts
Normal file
16
test/functional/query-builder/select/entity/Tag.ts
Normal 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[]
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user