Typeorm querybuilder join latest row - node.js

I'm using NestJS with TypeORM
I got two entities with relation:
export class Device {
#PrimaryGeneratedColumn()
id: number;
#Column("macaddr")
mac: string;
#OneToMany(type => DeviceStatus, deviceStatus => deviceStatus.mac)
#JoinColumn()
status: DeviceStatus[]
#Column()
created: Date;
}
export class DeviceStatus {
#PrimaryGeneratedColumn()
id: number;
#ManyToOne(type => Device, device => device.mac)
#JoinColumn({ name: "mac", referencedColumnName: "mac" })
mac: Device;
#Column()
deviceState: number;
#Column()
created: Date;
}
I want to get Device but only it's latest DeviceStatus on its status property.
Now I am doing it like this:
const deviceQuery: Device = await this.deviceRepository
.createQueryBuilder("device")
.where('device.id = :id AND "device"."userId" = :user', {id: id, user: user.id})
.getOne();
const statusQuery: DeviceStatus = await this.deviceStatusRepository.createQueryBuilder("status")
.where('status.mac = :mac', { mac: deviceQuery.mac })
.orderBy('created', "DESC")
.limit(1)
.getOne();
deviceQuery.status = [statusQuery];
How can I do this using just one typeorm queryBuilder query?
I already tried this, but it doesn't map status property to Device, .execute() gets all DeviceStatus properties but it wont map the entities.
const deviceQuery: Device = await this.deviceRepository
.createQueryBuilder("device")
.leftJoinAndSelect(subquery => {
return subquery
.from(DeviceStatus, "status")
.innerJoin(subquery => {
return subquery
.select(["status2.mac"])
.addSelect("MAX(created)", "lastdate")
.from(DeviceStatus, "status2")
.groupBy("status2.mac")
}, "status3", 'status.created = "status3"."lastdate"')
}, "status", 'device.mac = "status"."mac"')
.where('device.id = :id AND "device"."userId" = :user', {id: id, user: user.id})
.getOne();

You can achieve this by joining status 2 times.
The second status join (next_status) condition should compare created date and be after the first joined status.
Then just check if next_status is null, that means that there is no status younger than the first join
Code is probably a better explanation:
const device: Device = await this.deviceRepository
.createQueryBuilder("device")
.leftJoinAndSelect("device.status", "status")
.leftJoin("device.status", "next_status", "status.created < next_status.created")
.where("next_status.id IS NULL")
// device.status is the latest status

Related

Show additional data in the response from another table that is connected via a many-to-many relation by TypeORM in NestJS

I have four tables users, ingredients, recipes and a connection table ingredients_ingrec_recipes that TypeORM created automatically. This connection table consists of two columns: ingredientsId and recipesId.
In the ingredient.entity.ts file I have defined #ManyToMany relation with { eager:true } between ingredients and recipes. When I make a Get-Request for a specific ingredient (via Postman), I also see the associated recipes in the response automatically, thanks to the magic of { eager: true } and TypeORM+NestJS.
Now I need exactly the same, but for recipes (other side). In the recipe response, the corresponding ingredients should also be displayed. Unfortunately, according to TypeORM, I can only use #ManyToMany with { eager: true } in one of the two entities. In order to achieve the same with recipes, I have to do this somehow via leftJoinAndSelect(). However, I don't know how to do this via this connection table ingredients_ingrec_recipes in my code.
Here is my code:
ingredient.entitiy.ts
#Entity({ name: 'ingredients' })
export class IngredientEntity {
#PrimaryGeneratedColumn()
id: number;
#Column()
slug: string;
#Column()
name: string;
#Column({ default: '' })
description: string;
// more fields
#ManyToMany(() => RecipeEntity, { eager: true })
#JoinTable()
ingrec: RecipeEntity[];
#ManyToOne(() => UserEntity, (user) => user.recipes, { eager: true })
author: UserEntity;
}
recipe.entity.ts
#Entity({ name: 'recipes' })
export class RecipeEntity {
#PrimaryGeneratedColumn()
id: number;
#Column()
slug: string;
#Column()
title: string;
#Column({ default: '' })
description: string;
#Column({ default: '' })
body: string;
// more fields
#ManyToOne(() => UserEntity, (user) => user.recipes, { eager: true })
author: UserEntity;
}
recipe.service.ts
async findAll(query: any) {
const queryBuilder = getRepository(RecipeEntity)
.createQueryBuilder('recipes')
.leftJoinAndSelect('recipes.author', 'author');
queryBuilder.orderBy('recipes.createdAt', 'DESC');
if (query.author) {
const author = await this.userRepository.findOne({
username: query.author,
});
queryBuilder.andWhere('recipes.authorId = :id', {
id: author.id,
});
}
const recipes = await queryBuilder.getMany();
return { recipes };
}
I have already used leftJoinAndSelect() with the author, but in a #OneToMany relationship. How can I adjust my findAll() service function so that I can still see in my recipe-response the data from my user (author) table and also the associated data from the ingredients?
ingredientsId
recipesId
3
1
1
2
3
3
3
4
If I add following to the RecipeEntity:
#ManyToMany(() => IngredientEntity, { eager: true })
ingrec: IngredientEntity[];
I get an server error and [Nest] 230 - 05/17/2022, 3:02:11 PM ERROR [ExceptionsHandler] Maximum call stack size exceeded
Without { eager: true } I don't get any errors but I also don't see any data from the ingredients table.

TypeORM insert objects including relationships

I got two entities with some relationships going on. The problem is that the EntityManager only saves the main entity and sets the foreign keys to NULL. I have read a similar question that OP seems to have solved himself, but his answer leaves me clueless.
I'm used to ORMs being able to insert nested entities in one call. Does have TypeORM have such functionality?
The entities are at the bottom of the question to save space here. The objects are built up like this:
const whiteFlourEntity = new Ingredient();
whiteFlourEntity.name = "white flour";
whiteFlourEntity.percentage = 100;
const flourEntities: Ingredient[] = [whiteFlourEntity];
const waterEntity = new Ingredient();
waterEntity.name = "water";
waterEntity.percentage = 68;
waterEntity.waterPercentage = 100;
const yeastEntity = new Ingredient();
yeastEntity.name = "yeast";
yeastEntity.percentage = 1;
const saltEntity = new Ingredient();
saltEntity.name = "salt";
saltEntity.percentage = 2;
const formulaEntity = new Formula();
formulaEntity.name = "test formula";
formulaEntity.flours = flourEntities;
formulaEntity.water = waterEntity;
formulaEntity.yeast = yeastEntity;
formulaEntity.salt = saltEntity;
When I save all nested entities first and after that the main entity all relationships are set up right. But when I only save the main entity, the sub-entities are not saved.
createConnection({
/** */
"entities": [Ingredient, Formula]
}).then(async connection => {
// WITH THIS LINES ACTIVE ALL RELATIONSHIPS ARE SAVED CORRECTLY
// flourEntities.forEach((flour: Ingredient) => {
// connection.manager.save(flour);
// });
// await connection.manager.save(waterEntity );
// await connection.manager.save(yeastEntity );
// await connection.manager.save(saltEntity );
await connection.manager.save(formulaEntity);
}).catch(error => console.log(error));
#Entity()
export default class Ingredient {
#PrimaryGeneratedColumn()
id?: number;
#Column()
name!: string;
#Column()
percentage!: number;
#Column()
waterPercentage!: number = 0;
}
#Entity()
export class Formula {
#PrimaryGeneratedColumn()
id?: number;
#Column()
name!: string;
#ManyToMany(() => Formula)
#JoinTable()
flours!: Ingredient[];
#OneToOne(() => Ingredient)
#JoinColumn()
water!: Ingredient;
#OneToOne(() => Ingredient)
#JoinColumn()
yeast!: Ingredient;
#OneToOne(() => Ingredient)
#JoinColumn()
salt!: Ingredient;
#ManyToMany(() => Ingredient)
#JoinTable()
extras?: Ingredient[];
#ManyToMany(() => Formula)
#JoinTable()
subFormulas?: Formula[];
}

Mikro-orm calling remove() on a collection removes everything instead of just one

I'm currently implementing an M:M relationship between a user and a product that uses a custom join table for implementing a shopping cart. Here is the custom join table:
#Entity({ tableName: "users_in_cart_products" })
export class UserInCartProducts {
#ManyToOne(() => User, { primary: true, nullable: true })
user: User;
#ManyToOne(() => Product, { primary: true, nullable: true })
product: Product;
#Property()
amount: number;
}
Parts of the user entity (similar to Product)
#Entity({ tableName: "users", customRepository: () => UserRepository })
export class User {
#PrimaryKey()
id: string = v4();
/* some properties */
#OneToMany(() => UserInCartProducts, (userInCartProducts) => userInCartProducts.user)
userInCartProducts = new Collection<UserInCartProducts>(this);
}
I'm currently implementing a functionality where a product will be deleted from the shopping cart. However, when I call user.userInCartProducts.remove(), instead of just removing that element, it removes everything from user.userInCartProducts, leaving an empty array. Here's the code that removes the product from a user's cart:
async removeCartItem(userId: string, productId: string) {
const user = await this.userRepository.findOneOrFail({ id: userId }, [
"userInCartProducts",
"userInCartProducts.product",
]);
for (const e of user.userInCartProducts) {
if (e.product.id === productId) user.userInCartProducts.remove(e);
}
await this.userRepository.persistAndFlush(user);
return user;
}
I've checked the SQL generated from Mikro-orm, and somehow it sets user_id to NULL for everything inside the users_in_cart_products join table:
[query] update "users_in_cart_products" set "user_id" = NULL [took 2808 ms]
So how can I solve this problem? I just want the collection to remove that one item, but not every single item. Thanks!

How can i post a DTO that contain an array of entities?

I have a couple of questions about NestJS and TypeOrm.
First, how to pass an array of strings to DTO? I tried just to use :string[] type, but the compiler gives an error.
This is my Post entity:
#Entity('posts')
export class Post {
#PrimaryGeneratedColumn()
id: number;
#ManyToOne(() => User, user => user.posts, { cascade: true })
author: number;
#Column({ type: 'timestamp' })
date: Date;
#Column()
text: string;
#Column({ default: 0 })
likes: number;
#OneToMany(() => Photo, photo => photo.post, { cascade: true })
photos: Photo[];
}
And CreatePostDto:
export class CreatePostDto {
authorId: number;
date: Date;
text?: string;
// photos?: string[];
}
And the second question: How can i save to the repository every photo (keeping the connection with post), posts to the posts repo and update user by adding new post binded to him.
I tried something like this, but it won't work obviously.
async create(createPostDto: CreatePostDto) {
const post = this.postsRepository.create(createPostDto);
const user = await this.usersRepository.findOne(createPostDto.authorId);
return this.postsRepository.save({author: user, date: createPostDto.date, text: createPostDto.text});
}
What you missed here is saving photos before bind them with the post, here's an example:
async create(createPostDto: CreatePostDto) {
let photos:Array<Photo> = [] ; array of type photo entity
for(let urlPhoto of createPostDto.photos)
{
let photo = await this.imageRepository.save({url : urlPhoto }); you must save the photos first
photos.push(photo);
}
const user = await this.usersRepository.findOne(createPostDto.authorId);
return this.postsRepository.save({author: user, date: createPostDto.date, text:
createPostDto.text,photos:photos});
}
#Entity()
export class User {
#PrimaryGeneratedColumn()
id: number
#Column("simple-array")
names: string[]
}

Is it possible to use subquery in leftJoinAndSelect in TypeORM

So i've got a project on nestjs and using TypeORM. And my situation is i have a User which have One-to-May relation with UserSubscrption. Default find query added all subscriptions to User as it should be. So now i want to find one specific subscription, for example last added. So i build this query:
const query = this.createQueryBuilder("user")
.leftJoinAndSelect("user.settings", "settings")
.leftJoinAndSelect(
subQuery => {
return subQuery
.select()
.from(UserSubscription, "subscription")
.where("subscription.userId = :id AND subscription.isConfirmed = :isConfirmed", { id, isConfirmed: true })
.orderBy('"createdDate"', 'DESC')
.limit(1);
}, 'subscription', '"subscription"."userId" = "user"."id"')
.where('user.id = :id', { id });
const result = await query.getOne(); // await query.execute()
First, i try just execute the query and it works fine but all the data not structured
[
{
user_id: 1,
user_username: 'name',
...
settings_id: 1,
settings_ifOnline: true,
...
subscriptions_id: 1,
subscriptions_subscriptionId: 1,
...
}
]
So it's not good.
Then i try query.getOne() but it wont work as i want, it's lost all subscription data
User {
id: 1,
username: 'name',
...
settings: UserNotificationSettings {
id: 1,
ifOnline: true,
...
}
}
Also, i tried to add virtual field subscription to user entity and try to use leftJoinAndMapOne:
...
.leftJoinAndSelect("user.settings", "settings")
.leftJoinAndMapOne("user.subscription",
subQuery => {
return subQuery
.select()
.from(UserSubscription, "subscription")
.where("subscription.userId = :id AND subscription.isConfirmed = :isConfirmed", { id, isConfirmed: true })
.orderBy('"createdDate"', 'DESC')
.limit(1);
}, 'subscription', '"subscription"."userId" = "user"."id"')
...
But with no luck. In docs it's said that "Subqueries are supported in FROM, WHERE and JOIN expressions." but no example is provided. So, i have no idea how to handle this i belive pretty much simple query with TypeORM. Any suggestions? i'm kinda stuck with this. May be there is some better way than buildibg queries? Thanks.
User.entity
export class User extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
...
// subscription: UserSubscription;
#OneToMany(type => UserSubscription, subscriptions => subscriptions.user, { cascade:true })
subscriptions: UserSubscription[];
#OneToOne(type => UserNotificationSettings, settings => settings.user, { cascade:true })
settings: UserNotificationSettings;
}
UserSubscription.entity
export class UserSubscription extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
#OneToOne(type => Subscription)
#JoinColumn()
subscription: Subscription
#Column()
subscriptionId: number
#ManyToOne(type => User, user => user.subscriptions)
user: User
#Column({type: 'integer'})
userId: number
#Column({ type: 'boolean', default: false })
isConfirmed: boolean
...
}
Using the SubQueryFactory option does not automatically create the on clause as the condition as it cannot know what the underlying query's alias is that you're trying to join with. It doesn't have the context afforded by the metadata of .leftJoinAndSelect('user.photos', 'photos') because of how it's been written.
Instead, you have to explicitly define the condition. In your case:
const result = await userRepo
.createQueryBuilder('user')
.leftJoinAndSelect(
qb => qb
.select()
.from(UserPhotos, 'p')
.orderBy({ 'p.updatedAt': 'ASC' })
.limit(5),
'photos',
'photos.userId = user.id' // the answer
)
.getRawMany() // .getMany() seems not working
Here is the actual answer:
https://github.com/typeorm/typeorm/issues/6767
UPDATE
I saw that .getMany() is not working when using SubQueryFactory. Insted you can use .getRawMany().

Resources