Nest.js + Mikro-ORM: Collection of entity not initialized when using createQueryBuilder and leftJoin - nestjs

I'm using Nest.js, and considering migrating from TypeORM to Mikro-ORM. I'm using the nestjs-mikro-orm module. But I'm stuck on something that seems very simple...
I've 3 entities, AuthorEntity, BookEntity and BookMetadata. From my Author module, I try to left join the Book and BookMetadata tables with the createQueryBuilder method. But when running my query, I'm getting an error where Collection<BookEntity> of entity AuthorEntity[3390] not initialized. However columns from the Author table are well retrieved.
My 3 entities:
#Entity()
#Unique({ properties: ['key'] })
export class AuthorEntity {
#PrimaryKey()
id!: number;
#Property({ length: 255 })
key!: string;
#OneToMany('BookEntity', 'author', { orphanRemoval: true })
books? = new Collection<BookEntity>(this);
}
#Entity()
export class BookEntity {
#PrimaryKey()
id!: number;
#ManyToOne(() => AuthorEntity)
author!: AuthorEntity;
#OneToMany('BookMetadataEntity', 'book', { orphanRemoval: true })
bookMetadata? = new Collection<BookMetadataEntity>(this);
}
#Entity()
#Unique({ properties: ['book', 'localeKey'] })
export class BookMetadataEntity {
#PrimaryKey()
id!: number;
#Property({ length: 5 })
localeKey!: string;
#ManyToOne(() => BookEntity)
book!: BookEntity;
}
And the service file where I run my query:
#Injectable()
export class AuthorService {
constructor(
#InjectRepository(AuthorEntity)
private readonly authorRepository: EntityRepository<AuthorEntity>,
) {}
async findOneByKey(props: { key: string; localeKey: string; }): Promise<AuthorEntity> {
const { key, localeKey } = props;
return this.authorRepository
.createQueryBuilder('a')
.select(['a.*', 'b.*', 'c.*'])
.leftJoin('a.books', 'b')
.leftJoin('b.bookMetadata', 'c')
.where('a.key = ?', [key])
.andWhere('c.localeKey = ?', [localeKey])
.getSingleResult();
}
}
Am I missing something? Might be not related, but I also noticed that there is a special autoLoadEntities: true for TypeORM users using Nest.js. Is there something similar for Mikro-ORM? Thanks ;)

Mapping of multiple entities from single query is not yet supported, it is planned for v4. You can subscribe here: https://github.com/mikro-orm/mikro-orm/issues/440
In v3 you need to use 2 queries to load 2 entities, which for your use case is much easier without the QB involved.
return this.authorRepository.findOne({ key }, ['books']);
Or you could use qb.execute() to get the raw results and map them yourself, but you would also have to manually alias all the fields to get around duplicities (Author.name vs Book.name), as doing qb.select(['a.*', 'b.*']) will result in query select a.*, b.* ... and the duplicate columns would not be correctly mapped.
https://mikro-orm.io/docs/query-builder/#mapping-raw-results-to-entities
About the autoLoadEntities thing, never heard of that, will take a look how it works, but in general, the nestjs adapter is not developed by me, so if its something only nest related, it would be better to ask on their GH repo.
Or you could use folder based discovery (entitiesDirs).
here is the new example with 3 entities:
return this.authorRepository.findOne({
key,
books: { bookMetadata: localeKey } },
}, ['books.bookMetadata']);
This will produce 3 queries, one for each db table, but the first one will auto-join books and bookMetadata to be able to filter by them. The condition will be propagated down in the second and third query.
If you omit the populate parameter (['books.bookMetadata']), then only the first query will be fired and you will end up with books not being populated (but the Author will be queried with the joined condition).

Related

Can we have two foreign keys pointing to the same table in one TypeORM entity?

I have two entity models (Match and Partner), one of which contains two references to the other (partner1 and partner2).
#Entity("partner")
export class PartnerEntity {
#PrimaryGeneratedColumn("uuid")
id: string
#Column()
text: string
}
#Entity("match")
export class MatchEntity {
#PrimaryGeneratedColumn("uuid")
id: string
#ManyToOne(() => PartnerEntity, partner => partner.id)
#JoinColumn()
partner1?: PartnerEntity
#ManyToOne(() => PartnerEntity, partner => partner.id)
#JoinColumn()
partner2?: PartnerEntity
}
I am trying to persist a MatchEntity object using the Repository class from TypeORM like this:
this.matchRepository.save({
partner1: { text: "AAA" },
partner2: { text: "BBB" }
})
However, the same value is being saved for partner1 and partner2. I can confirm both that the object is correct and that the row stored in the database has the same value for the two foreign keys. The following object is returned:
{
id: "c58f3ea7-5002-463d-92e7-94d0c2992784",
partner1: { id: "10978976-d120-4e48-a490-eba62e7c06e5", text: "AAA" },
partner2: { id: "10978976-d120-4e48-a490-eba62e7c06e5", text: "AAA" }
}
What is wrong with my code? Is there a way to make this work?
Options that were considered but that I'd rather avoid for this use case:
Implementing a ManyToMany relationship instead (convert the two fields into an array with two positions)
Inserting new rows with explicit SQL queries using Repository.createQueryBuilder()

Is it possible to maintain flat input structure with nestjs/graphql Args?

I have a simple query:
#Query(() => ProfileOutput)
async profile(#Args('id') id: string) {
return await this.profileFacade.findProfileById(input.id);
}
The problem is that I want to apply #IsMongoId() from class-validator for the id here. I do not want to create new #InputType here, because I do not want to change API specification. Is there a way to apply validator like #IsMongoId here without the need to change query definition for frontend?
For anyone seeking an answer I found a feature called dedicated argument class. Instead of creating new input type as I thought like this:
#InputType()
export class MongoIdBaseInput {
#IsMongoId()
#Field()
id: string;
}
#Query(() => ProfileOutput)
async profile(#Args('data') input: MongoIdBaseInput) {
return await this.profileFacade.findProfileById(input.id);
}
We can define it almost the same, but annotate input with ArgsType it will maintain flat structure of args for us:
#ArgsType()
export class MongoIdBaseInput {
#IsMongoId()
#Field()
id: string;
}
#Query(() => ProfileOutput)
async profile(#Args() input: MongoIdBaseInput) {
return await this.profileFacade.findProfileById(input.id);
}

Attribute contains only ID, not whole entity, in persisted TypeORM entity

I've got this entity class:
#Entity("organization")
export class OrganizationEntity {
// ...
#PrimaryColumn({name: "party_id"})
#OneToOne(() => PartyEntity, {cascade: true})
#JoinColumn({name: "party_id", referencedColumnName: "id"})
party: PartyEntity
}
Then I create a new OrganizationEntity and persist it:
const savedOrganizationEntity = await this.organizationTypeOrmRepository.save(organizationEntity);
// see Repository.save
However, the returned savedOrganizationEntity contains a string in the field party, not a PartyEntity object.
How can I fix this behaviour, so that OrganizationEntity.party contains a PartyEntity, not a string?
The behaviour is working as designed: https://github.com/typeorm/typeorm/issues/3490

Removing many2many entity in typeorm with cascade

I have an entity called Entry and it relates to another entity called Image as Many2Many.
Here is what the Entry & Image relationships look like:
#Entity('entry')
export class EntryEntity extends BaseEntity implements IDeserializable<EntryEntity> {
#ManyToMany(type => ImageEntity, image => image.entries, { onDelete: 'CASCADE', cascade: true })
#JoinTable()
images: ImageEntity[];
}
and the Image entity class:
#Entity('image')
export class ImageEntity extends BaseEntity implements IDeserializable<ImageEntity> {
#ManyToMany(type => EntryEntity, entry => entry.images)
entries: EntryEntity[];
}
The method I use to delete an entry:
public async delete(entryId: number): Promise<void> {
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.getRepository(EntryEntity)
.createQueryBuilder('entry')
.delete()
.from(EntryEntity)
.where('entry.id = :entryId', { entryId })
.execute();
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
}
Expected behaviour:
If I delete some entry then all its images should also be deleted.
Factual behaviour:
Entry gets removed from its table, it also gets removed from the entry_images_image table
but the images associated with this entry stay (they are still present in the image table).
I'm not very familiar with TypeOrm, why does it happen? I would highly appreciate some help.
what the cascade does is to remove the relations in both sides, not the entities themselves. so in you'r case, you will only receive images without relations, as you have mentioned.
Look at it like this: let's say that you have multiple images for multiple entries, all connected to each other. if you delete one entry, would you really like the image entity to be fully deleted? because in that case you will remove alot of data that hasn't been removed.

Nestjs, How to get entity table name?

How to get entity table name ? (ex: member-pre-sale-detail)
I want to set table comment
// Seeder: Clear & set Comment
export default class ClearAllSeed implements Seeder {
public async run(factory: Factory, connection: Connection): Promise<void> {
const deleteEntities = [
{table: OrderHead, comment: '訂單/主表'},
]
for(const entity of deleteEntities){
await connection
.createQueryBuilder()
.delete()
.from(entity.table)
.execute();
await connection
// >>>> but table name is MemberPreSaleDetail not member-pre-sale-detail
.query(`alter table ${entity.table.name} comment '${entity.comment}'`);
}
}
}
// Sampel Entity
#Entity('member-pre-sale-detail')
export class MemberPreSaleDetail {
#PrimaryGeneratedColumn({unsigned: true})
id?: number;
#Column({comment: '幾批(整批)', type: 'mediumint', default: 0})
batchQty: number;
}
Expected behavior
get the 'member-pre-sale-detail' string
Environment
Nest version: 7.0.7
For Tooling issues:
- Node version: v14.5.0
- Platform: Mac
I am guessing you are using TypeORM. In that case:
You could get the entity metadata by calling connection.getMetadata(MemberPreSaleDetail).
This method returns an EntityMetadata, which has name, tableName and givenTableName properties. For your usecase I guess you could simply use givenTableName.

Resources