Cannot query across one-to-many for property in NestJS Postgresql - node.js

That's the updateEntity.ts
import { IsNotEmpty } from 'class-validator'
import { BaseEntity, Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
import { Company } from './company.entity'
#Entity('countries')
export class Country extends BaseEntity {
#PrimaryGeneratedColumn('uuid')
id: string
#IsNotEmpty()
#Column({ unique: true })
name: string
#ManyToOne(() => Company, (company) => company.locations, { nullable: true })
#JoinColumn({ name: 'company_id' })
countryId: Company[]
}
CompanyEntity.ts with the location field
#OneToMany(() => Country, (country) => country.countryId, { eager: true })
locations: Array<Country>
and here is the function where I want to update the properties
async update(id: number, updateCompanyDto: UpdateCompanyDto) {
const newLocations = updateCompanyDto.locations.map((location) => Country.create(location))
updateCompanyDto.locations = newLocations
const status = await Company.update(id, updateCompanyDto)
if (status.affected <= 0) {
throw new HttpException('This company does not exist', HttpStatus.NOT_FOUND)
}
return status
}
First time working with OneToMany and ManyToMany, and if I have the request body like this
"locations": [{"name":"Paris"}]
I'm getting an error "Cannot query across one-to-many for property locations"
I just want to be able to update the companies

It's okay because if you need to update,create or delete data from Location entity,you need to query using that entity not from joined entity

Related

How to implement many-to-many query in typeorm

I'm working with typeORM with nestJS.
I have bi-directional many-to-many relationship between 2 tables: A and B (means a entity of A can be assign to many entities of B, and vice versa)
A.entity.ts:
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm';
import { B } from './B.entity';
#Entity('A')
export class A {
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#ManyToMany(() => B,
(b)=>(b.AObjects))
BObjects: B[];
}
B.entity.ts:
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm';
import { A} from './A.entity';
#Entity('B')
export class B{
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#ManyToMany(() => A,
(a) => a.BObjects,
{eager:true})
#JoinTable({
name: 'AB',
inverseJoinColumn : { name: 'Aid', referencedColumnName: 'id'},
joinColumn: { name: 'Bid', referencedColumnName: 'id'},
})
AObjects: A[];
}
In the module service I want to implement a function, which receives a given id of B entity, and retreive all A's objects which refers to B's id
I want to write a typeORM query which implements the following sql query, for a given_B_id (which will be supplied as a parameter):
SELECT A.*
from A, AB
where AB.Bid = given_B_id and A.id = AB.Aid
Will appreciate your help
I finally find a workaround solution.
But still will appreciate your feedback and advices about the best way to implement many-to-many request with constraint with TypeORM.
My workaround based on queryRunner of DataSource
in the service constructor: add private member :
#Injectable()
export class AService {
constructor(
#InjectRepository(A)
private workerRepository: Repository<A>,
private dataSource: DataSource
){}
// .....
}
and in the "#GET" route handler using
async getByB(given_B_id: number): Promise<A[]> {
let AObjects :Promise<A[]>
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
try {
AObjects = await queryRunner.query(
` SELECT A.*
FROM A, AB
WHERE AB.Bid = ${given_B_id}
AND A.id = AB.Aid`
)
} catch (ex) {
throw ex;
} finally {
await queryRunner.release();
}
return(AObjects)
};
You can use find method as below
const res = this.BRepository.find({
where: { id: given_B_id },
relations: ['A']
});
Or use queryBuilder:
const res = this.BRepository
.createQueryBuilder('b')
.where('id = :b_id', { b_id: given_B_id })
.leftJoinAndSelect('B.A', 'A')
.getMany();
For more information check out the official TypeORM Docs.

How do I cause n + 1 problems with NestJs TypeOrm?

I was studying TypeOrm and I'm trying to create an N+1 problem, but it's not happening properly. Company and employee have a 1:N relationship.
Could you tell me why N + 1 is not causing any problems? I've tried setting up Lazy and setting up Eager, but I've been doing left join continuously so that n + 1 doesn't cause problems.
entity
#Entity('COMPANY')
export class Company extends TimeStamped {
#PrimaryGeneratedColumn('increment')
companyId: number;
#Column({ type: 'varchar' })
companyName: string;
#OneToMany(() => Employee, (employee) => employee.company, {
onDelete: 'CASCADE'
})
employee: Employee[];
}
#Entity('EMPLOYEE')
export class Employee extends TimeStamped {
#PrimaryGeneratedColumn('increment')
employeeId: number;
#Column({ type: 'varchar' })
employeeName: string;
#ManyToOne(() => Company, (company) => company.employee)
#JoinColumn([{ name: 'companyId', referencedColumnName: 'companyId' }])
company: Company;
}
crud
#Injectable()
export class CompanyService {
constructor(
#InjectRepository(Company)
private readonly companyRepository: Repository<Company>
) {}
getAllCompany() {
return this.companyRepository.find({ relations: ['employee'] });
}
getCompany(companyId: number) {
return this.companyRepository.findOne(companyId, {
relations: ['employee']
});
}
setCompany(setComanyDto: SetCompanyDto) {
return this.companyRepository.save(setComanyDto);
}
}
#Injectable()
export class EmployeeService {
constructor(
#InjectRepository(Employee)
private readonly employeeRepository: Repository<Employee>,
#InjectRepository(Company)
private readonly companyRepository: Repository<Company>
) {}
getAllEmployee() {
return this.employeeRepository.find({
relations: ['company']
});
}
getEmployee(employeeId: number) {
return this.employeeRepository.findOne(employeeId, {
relations: ['company']
});
}
async setEmployee(setEmployeeDto: SetEmployeeDto) {
const employee: Employee = new Employee();
employee.employeeName = setEmployeeDto.employeeName;
employee.company = await this.companyRepository.findOne(
setEmployeeDto.companyId
);
return this.employeeRepository.save(employee);
}
}
I believe you have a good idea about what N+1 problem is. You can check this question if you need to understand it more clearly.
If you use eager loading, you will not see the N+1 problem anyway since it joins the related entity and return both entities in one query.
If you specify relations as you've done below, again you will not see the N+1 problem since it creates a join query and returns all in 1 single query.
this.companyRepository.find({ relations: ['employee'] });
To create the N+1 problem,
Update your Company entity like below:
#Entity('COMPANY')
export class Company extends TimeStamped {
#PrimaryGeneratedColumn('increment')
companyId: number;
#Column({ type: 'varchar' })
companyName: string;
#OneToMany(() => Employee, (employee) => employee.company, {
onDelete: 'CASCADE',
lazy: true
})
employee: Promise<Employee[]>
}
In your CompanyService, create a new function to simulate the N+1 problem like below:
#Injectable()
export class CompanyService {
async createNPlus1Problem() {
// Query all companies (let's say you have N number of companies)
// SELECT * FROM "COMPANY";
const companies = this.companyRepository.find();
// The following `for` loop, loops through all N number of
// companies to get the employee data of each
for(company of companies) {
// Query employees of each company
// SELECT * FROM "EMPLOYEE" WHERE "companyId"=?;
const employees = await company.employee;
}
}
}
So in the above example, you have 1 query to get the company data. And N queries to get the employee data. Hence the N+1 problem.
Hope this clarifies your problem. Cheers 🍻 !!!
You can try to use this library https://github.com/Adrinalin4ik/Nestjs-Graphql-Tools it allows to overcome n+1 with the simple decorator. And it has minimum deps.
You can use leftJoinAndSelect method with query builder.
https://orkhan.gitbook.io/typeorm/docs/select-query-builder#joining-relations
const user = await createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.where("user.name = :name", { name: "Timber" })
.andWhere("photo.isRemoved = :isRemoved", { isRemoved: false })
.getOne()
SELECT user.*, photo.* FROM users user
LEFT JOIN photos photo ON photo.user = user.id AND photo.isRemoved = FALSE
WHERE user.name = 'Timber'

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!

Nestjs Response Serialization with array of objects

I want to serialize a controller response by the nestjs serialization technique. I didn't find any approach and my solution is as follows:
User Entity
export type UserRoleType = "admin" | "editor" | "ghost";
#Entity()
export class User {
#PrimaryGeneratedColumn() id: number;
#Column('text')
username: string;
#Column('text')
password: string;
#Column({
type: "enum",
enum: ["admin", "editor", "ghost"],
default: "ghost"
})
roles: UserRoleType;
#Column({ nullable: true })
profileId: number;
}
User Response Classes
import { Exclude } from 'class-transformer';
export class UserResponse {
id: number;
username: string;
#Exclude()
roles: string;
#Exclude()
password: string;
#Exclude()
profileId: number;
constructor(partial: Partial<UserResponse>) {
Object.assign(this, partial);
}
}
import { Exclude, Type } from 'class-transformer';
import { User } from 'src/_entities/user.entity';
import { UserResponse } from './user.response';
export class UsersResponse {
#Type(() => UserResponse)
users: User[]
constructor() { }
}
Controller
#Controller('user')
export class UsersController {
constructor(
private readonly userService: UserService
) {
}
#UseInterceptors(ClassSerializerInterceptor)
#Get('all')
async findAll(
): Promise<UsersResponse> {
let users = await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
let rsp =new UsersResponse()
rsp.users = users
return rsp
}
It works, but I must explicitly assign the db query result to the response users member.
Is there a better way? Thanks a lot
Here the actual Response and wanted result, for a better explanation.
Result in this Approach
{
"users": [
{
"id": 1,
"username": "a"
},
{
"id": 2,
"username": "bbbbbb"
}
]
}
Result Wanted
{
{
"id": 1,
"username": "a"
},
{
"id": 2,
"username": "bbbbbb"
}
}
I would recommend to directly put the #Exclude decorators on your entity class User instead of duplicating the properties in UserResponse. The following answer assumes you have done so.
Flat Response
If you have a look at the code of the ClassSerializerInterceptor, you can see that it automatically handles arrays:
return isArray
? (response as PlainLiteralObject[]).map(item =>
this.transformToPlain(item, options),
)
: this.transformToPlain(response, options);
However, it will only transform them, if you directly return the array, so return users instead of return {users: users}:
#UseInterceptors(ClassSerializerInterceptor)
#Get('all')
async findAll(): Promise<User> {
return this.userService.findAll()
}
Nested Response
If you need the nested response, then your way is a good solution.
Alternatively, you can call class-transformer's serialize directly instead of using the ClassSerializerInterceptor. It also handles arrays automatically:
import { serialize } from 'class-transformer';
#Get('all')
async findAll(): Promise<UsersResponse> {
const users: User[] = await this.userService.findAll();
return {users: serialize(users)};
}
Wow, what easy, if i know! Perfect, this solves my problem. Also your recommendation for the User Entity with the class-transformer #Exclue() decorator.
And i know that i do not need a custom UsersResponse class in this use case.
This solution was that what i was looking for, but i overjump this quite easy way
Thank you so much for your superfast answer and the problem solution.
Greetings to Berlin from Rostock :)
Here my final approach:
Controller
#UseInterceptors(ClassSerializerInterceptor)
#Get('all')
async findAll(
): Promise<User> {
return await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
}
User Entitiy
import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn, OneToMany } from 'typeorm';
import { Profile } from './profile.entity';
import { Photo } from './photo.entity';
import { Album } from './album.entity';
import { Exclude } from 'class-transformer';
export type UserRoleType = "admin" | "editor" | "ghost";
#Entity()
export class User {
#PrimaryGeneratedColumn() id: number;
#Column('text')
username: string;
#Exclude()
#Column('text')
password: string;
#Column({
type: "enum",
enum: ["admin", "editor", "ghost"],
default: "ghost"
})
roles: UserRoleType;
#Exclude()
#Column({ nullable: true })
profileId: number;
#OneToMany(type => Photo, photo => photo.user)
photos: Photo[];
#OneToMany(type => Album, albums => albums.user)
albums: Album[];
#OneToOne(type => Profile, profile => profile.user)
#JoinColumn()
profile: Profile;
}
Response Result
[
{
"id": 1,
"username": "a",
"roles": "admin"
},
{
"id": 2,
"username": "bbbbbb",
"roles": "ghost"
}
]
I have alternative way for your problem.
you can remove #UseInterceptors(ClassSerializerInterceptor) from your Controller. Instead use serialize and deserialize function.
import { serialize, deserialize } from 'class-transformer';
import { User } from './users.entity';
#Get('all')
async findAll() {
const users = serialize(await this.userService.findAll());
return {
status: 200,
message: 'ok',
users: deserialize(User, users)
};
}
it's work too for single data
import { Param } from '#nestjs/common';
import { serialize, deserialize } from 'class-transformer';
import { User } from './users.entity';
#Get(':id')
async findById(#Param('id') id: number) {
const user = serialize(await this.userService.findById(id));
return {
status: 200,
message: 'ok',
user: deserialize(User, user)
};
}
Your approach is recommended by nestjs but that has a fault. You are excluding some properties from being exposed to the client. What if, you work in a project that has an admin and admin wants to see all the data about the users or products. If you exclude fields in the entities, your admin won't see those fields either. Instead, leave the entities as it is, and write dto's for each controller or for each request handler and in this dto's just list the properties you want to expose.
Then write a custom interceptor and create specific dto for ecah entity. For example in your example, you create a userDto:
import { Expose } from 'class-transformer';
// this is a serizalization dto
export class UserDto {
#Expose()
id: number;
#Expose()
roles: UserRoleType;
#Expose()
albums: Album[];
// Basically you list what you wanna expose here
}
custom interceptor is a little messy:
import {
UseInterceptors,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '#nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';
// Normally user entity goes into the interceptor and nestjs turns it into the JSON. But we we ill turn it to User DTO which will have all the serialization rules.then nest will take dto and turn it to the json and send it back as response
export class SerializerInterceptor implements NestInterceptor {
// dto is the variable. so you can use this class for different entities
constructor(private dto:any){
}
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
// you can write some code to run before request is handled
return handler.handle().pipe(
// data is the incoming user entity
map((data: any) => {
return plainToClass(this.dto, data, {
// this takes care of everything. this will expose things that are set in the UserDto
excludeExtraneousValues: true,
});
}),
);
}
}
Now you use this in the controller:
// See we passed UserDto. for different entities, we would just write a new dto for that entity and our custom interceptor would stay reusable
#UseInterceptors(new SerializerInterceptor(UserDto))
#Get('all')
async findAll(
): Promise<UsersResponse> {
let users = await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
let rsp =new UsersResponse()
rsp.users = users
return rsp
}

How can i do the join of two table in loopback4

I have created below mention controller,model and repository in my code. Please have look.
I have developed below mention code but still not able to perform the join operation.
I am going to join two table that is person and info table.
- Info table having one foreign key which is belong to person table.
- Person table: id, name, status
- Info table : id, person_id , name , status
I have also create repository,model and controller file for info and person.
Person Repository ( person.repository.ts)
) {
super(Person, dataSource);
this.infos = this._createHasOneRepositoryFactoryFor(
'info',
getInfoRepository,
);
}
Person Module ( person.module.ts)
#hasOne(() => Info)
infos?: Info;
constructor(data?: Partial<Person>) {
super(data);
}
Info Module (info.module.ts)
#belongsTo(() => Person)
personId: number;
constructor(data?: Partial<Info>) {
super(data);
}
It show me error like this
Unhandled error in GET /people/fetchfromtwotable?filter[offset]=0&filter[limit]=10&filter[skip]=0: 500 TypeError: Cannot read property 'target' of undefined
Is there any idea about join?
drp, Thanks for sharing your models. My post got deleted because I am just starting out and needed to ask for more info which seems strange. ANYWAY, Try to change this line:
this.infos = this._createHasOneRepositoryFactoryFor(
'info',
getInfoRepository
);
to
this.infos = this._createHasOneRepositoryFactoryFor(
'infos',
getInfoRepository,
);
The framework cannot find the 'info' relation on the model because you called the property 'infos'
Here is my example that currently works for me (running latest lb4 and postgres):
User.model.ts
import { model, property, hasOne, Entity } from '#loopback/repository';
import { Address } from './address.model';
#model()
export class User extends Entity {
constructor(data?: Partial<User>) {
super(data);
}
#property({ id: true })
id: number;
#property()
email: string;
#property()
isMember: boolean;
#hasOne(() => Address, {})
address?: Address;
}
Address.model.ts:
import { model, property, belongsTo, Entity } from '#loopback/repository';
import { User } from '../models/user.model';
#model()
export class Address extends Entity {
constructor(data?: Partial<Address>) {
super(data);
}
#property({ id: true })
id: number;
#property()
street1: string;
#property()
street2: string;
#property()
city: string;
#property()
state: string;
#property()
zip: string;
#belongsTo(() => User)
userId: number;
}
User.repository.ts:
import { HasOneRepositoryFactory, DefaultCrudRepository, juggler, repository } from '#loopback/repository';
import { User, Address } from '../models';
import { PostgresDataSource } from '../datasources';
import { inject, Getter } from '#loopback/core';
import { AddressRepository } from '../repositories'
export class UserRepository extends DefaultCrudRepository<
User,
typeof User.prototype.id
> {
public readonly address: HasOneRepositoryFactory<Address, typeof User.prototype.id>;
constructor(
#inject('datasources.postgres')
dataSource: PostgresDataSource,
#repository.getter('AddressRepository')
protected getAccountRepository: Getter<AddressRepository>,
) {
super(User, dataSource);
this.address = this._createHasOneRepositoryFactoryFor('address', getAccountRepository);
} // end ctor
}
User.controller.ts (abridged for length):
#get('/users/{id}/address')
async getAddress(
#param.path.number('id') userId: typeof User.prototype.id,
#param.query.object('filter', getFilterSchemaFor(Address)) filter?: Filter,
): Promise<Address> {
return await this.userRepository
.address(userId).get(filter);
}
Hope this helps.
Good luck!

Resources