feat: leftJoinAndMapOne and innerJoinAndMapOne map result to entity (#9354)

* feat: leftJoinAndMapOne and innerJoinAndMapOne now map correctly with QueryBuilder

When joining to a query builder instead of an entity or table name, typeorm now
correctly supports mapping the result to an entity. This introduces a new argument
to the join functions to supply the join attributes with a source of meta data
for the mapping

For example:

const loadedPost = await connection.manager
      .createQueryBuilder(Post, "post")
      .innerJoinAndMapOne(
           "post.tag",
           qb => qb.from(Tag, "tag"),
           "tag",
           "tag.id = post.tagId",
           undefined,
           // The next argument is new - it helps typeorm know which entity
           // to use to complete the mapping for a query builder.
           Tag
      )
      .where("post.id = :id", { id: post.id })
      .getOne()

* style: Auto Formatting

* trigger CircleCI

---------

Co-authored-by: Umed Khudoiberdiev <pleerock.me@gmail.com>
Co-authored-by: Dmitry Zotov <dmzt08@gmail.com>
This commit is contained in:
acuthbert 2023-04-06 09:51:43 +01:00 committed by GitHub
parent de1228deac
commit 947ffc3432
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 78 additions and 0 deletions

View File

@ -46,6 +46,11 @@ export class JoinAttribute {
*/
isMappingMany?: boolean
/**
* Useful when the joined expression is a custom query to support mapping.
*/
mapAsEntity?: Function | string
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
@ -199,6 +204,11 @@ export class JoinAttribute {
if (this.connection.hasMetadata(this.entityOrProperty))
return this.connection.getMetadata(this.entityOrProperty)
// Overriden mapping entity provided for leftJoinAndMapOne with custom query builder
if (this.mapAsEntity && this.connection.hasMetadata(this.mapAsEntity)) {
return this.connection.getMetadata(this.mapAsEntity)
}
return undefined
/*if (typeof this.entityOrProperty === "string") { // entityOrProperty is a custom table

View File

@ -717,6 +717,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
alias: string,
condition?: string,
parameters?: ObjectLiteral,
mapAsEntity?: Function | string,
): this
/**
@ -781,6 +782,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
alias: string,
condition?: string,
parameters?: ObjectLiteral,
mapAsEntity?: Function | string,
): this {
this.addSelect(alias)
this.join(
@ -791,6 +793,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
parameters,
mapToProperty,
false,
mapAsEntity,
)
return this
}
@ -905,6 +908,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
alias: string,
condition?: string,
parameters?: ObjectLiteral,
mapAsEntity?: Function | string,
): this
/**
@ -969,6 +973,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
alias: string,
condition?: string,
parameters?: ObjectLiteral,
mapAsEntity?: Function | string,
): this {
this.addSelect(alias)
this.join(
@ -979,6 +984,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
parameters,
mapToProperty,
false,
mapAsEntity,
)
return this
}
@ -2008,6 +2014,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
parameters?: ObjectLiteral,
mapToProperty?: string,
isMappingMany?: boolean,
mapAsEntity?: Function | string,
): void {
this.setParameters(parameters || {})
@ -2016,6 +2023,7 @@ export class SelectQueryBuilder<Entity extends ObjectLiteral>
this.expressionMap,
)
joinAttribute.direction = direction
joinAttribute.mapAsEntity = mapAsEntity
joinAttribute.mapToProperty = mapToProperty
joinAttribute.isMappingMany = isMappingMany
joinAttribute.entityOrProperty = entityOrProperty // relationName

View File

@ -540,6 +540,36 @@ describe("query builder > joins", () => {
}),
))
it("should load and map selected data when query builder used as join argument", () =>
Promise.all(
connections.map(async (connection) => {
const tag = new Tag()
tag.name = "audi"
await connection.manager.save(tag)
const post = new Post()
post.title = "about China"
post.tag = tag
await connection.manager.save(post)
const loadedPost = await connection.manager
.createQueryBuilder(Post, "post")
.leftJoinAndMapOne(
"post.tag",
(qb) => qb.from(Tag, "tag"),
"tag",
"tag.id = post.tagId",
undefined,
Tag,
)
.where("post.id = :id", { id: post.id })
.getOne()
expect(loadedPost!.tag).to.not.be.undefined
expect(loadedPost!.tag.id).to.be.equal(1)
}),
))
it("should load and map selected data when data will given from same entity but with different conditions", () =>
Promise.all(
connections.map(async (connection) => {
@ -961,6 +991,36 @@ describe("query builder > joins", () => {
}),
))
it("should load and map selected data when query builder used as join argument", () =>
Promise.all(
connections.map(async (connection) => {
const tag = new Tag()
tag.name = "audi"
await connection.manager.save(tag)
const post = new Post()
post.title = "about China"
post.tag = tag
await connection.manager.save(post)
const loadedPost = await connection.manager
.createQueryBuilder(Post, "post")
.innerJoinAndMapOne(
"post.tag",
(qb) => qb.from(Tag, "tag"),
"tag",
"tag.id = post.tagId",
undefined,
Tag,
)
.where("post.id = :id", { id: post.id })
.getOne()
expect(loadedPost!.tag).to.not.be.undefined
expect(loadedPost!.tag.id).to.be.equal(1)
}),
))
it("should load and map selected data when data will given from same entity but with different conditions", () =>
Promise.all(
connections.map(async (connection) => {