How to allow circular relation with nullable: false? - typeorm

I want to store who is the "creator" and "updater" of every record as a reference to the "User" table in my database. This applies to every table, including the "User" table itself. This is because it can happen that a record is created either by the user that is registering or by some other already existing user.
On the "User" table I have a DB trigger that fills the "creator" and "updater" fields to the same value as the newly generated "id" by default, hence I would like to use a NOT NULL constraint on my "creator" and "updater" column. Unfortunately, this results in the following error: "Circular relations detected: User -> User. To resolve this issue you need to set nullable: false somewhere in this dependency structure."
Is there a way for me to have a circular relation with a not null constraint using TypeORM?

I was actually able to make it work. I'm going to share my solution in case anyone is looking for an answer.
Before I was trying to auto generate the "created_by" column in the following way:
#ManyToOne(type => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE', nullable: false })
#JoinColumn({ name: 'created_by' })
creator: User
All I had to do to make it work was to remove the nullable: false from there and move it to a new #Column field so that in total I would have:
#Column({ nullable: false })
created_by: number
#ManyToOne(type => User, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
#JoinColumn({ name: 'created_by' })
creator: User

I had a similar issue. I have a scenario where I have foreign key in the same table.
If I explain my scenario a bit more. I was working on storing conditional statements in an application.
condition#1 a > 4
condition#2 a + b < 0
Now I wanted to give user the ability to put a logical operator in b/w condition#1 and condition#2 . Let say user selected OR operator in b/w.
That would look like
(condition#1 OR condition#2)
Now comes the interesting part where I felt the need to have FK in the same. What if user wants to add another condition with OR operator before or after #1 and #2.
condition#3 OR (condition#1 OR condition#2)
So for storing these conditions I created relationships.
I stored 2 records for this scenario
1st for storing (condition#1 OR condition#2) and 2nd record for storing
condition#3 OR (condition#1 OR condition#2)
Now come to the point how I fixed it.
Previously it was something like :
export class ConditionLogicalOperator {
#PrimaryGeneratedColumn({
type: "int",
name: "ConditionLogicalOperatorID"
})
ConditionLogicalOperatorID: number;
#Column("int", {
nullable: false,
name: "LogicalOperatorID"
})
LogicalOperatorID: number;
#ManyToOne(() => ConditionLogicalOperator, (ConditionLogicalOperator: ConditionLogicalOperator) => ConditionLogicalOperator.conditionLogicalOperators)
#JoinColumn({ name: 'ConditionID1' })
conditionLogicalOperator1: ConditionLogicalOperator | null;
#ManyToOne(() => ConditionLogicalOperator, (ConditionLogicalOperator: ConditionLogicalOperator) => ConditionLogicalOperator.conditionLogicalOperators2)
#JoinColumn({ name: 'ConditionID2' })
conditionLogicalOperator2: ConditionLogicalOperator | null;
....
....
}
The trick was to add an empty object { } to Relationship. Please notice { } at the end of #ManyToOne line in each property. It worked for me.
export class ConditionLogicalOperator {
#PrimaryGeneratedColumn({
type: "int",
name: "ConditionLogicalOperatorID"
})
ConditionLogicalOperatorID: number;
#Column("int", {
nullable: false,
name: "LogicalOperatorID"
})
LogicalOperatorID: number;
#ManyToOne(() => ConditionLogicalOperator, (ConditionLogicalOperator: ConditionLogicalOperator) => ConditionLogicalOperator.conditionLogicalOperators, { })
#JoinColumn({ name: 'ConditionID1' })
conditionLogicalOperator1: ConditionLogicalOperator | null;
#ManyToOne(() => ConditionLogicalOperator, (ConditionLogicalOperator: ConditionLogicalOperator) => ConditionLogicalOperator.conditionLogicalOperators2, { })
#JoinColumn({ name: 'ConditionID2' })
conditionLogicalOperator2: ConditionLogicalOperator | null;
....
....
}

Related

Return ONLY selected fields within a TypeORM find request

I'm struggling in returning only selected fields in my TypeORM find request.
Assuming the following request
const data = await AppDataSource.manager.find(User, {
select: {
id: true,
hash: true,
firstname: true,
lastname: false,
},
take: 10, // Just here to shrink dataset
});
The script works pretty well excepted that it return every field of my model, with default value initialized.
[
User {
prefix: 'usr',
hash: 'usr_835b0ad2-XXXXXX',
email: undefined,
accountValidated: false,
role: 'free',
myKeyOne: true,
myKeyTwo: false,
gender: 'unspecified',
lastConnexion: 2023-01-19T10:11:02.733Z,
pendingDeletion: false,
deletionDate: undefined,
firstname: 'Clément',
lastname: undefined,
password: undefined,
facebookId: undefined,
googleId: undefined,
id: 158
},
...
]
Of course, it's not usable as it, because I have extensive relations, and thus the payload would be extremely heavy.
Are you aware of a method / a way to remove all unnecessary fields ?
i.e. I'm expecting
[
User {
id: 124,
hash: 'urs_XXXX',
firstname: 'Clément',
},
...
]
In older versions of typeorm I think you need to select with an array of strings, try:
select: ["id", "hash", "firstname"],
See this older version of the docs: https://github.com/typeorm/typeorm/blob/bc60dd559ba42af083ddea17f01205c78c83c7e0/docs/find-options.md
After hours of researches I've finally found out why it behaved like this.
TypeORM relies on class definitions and typescript so...
if you have typescript default values OR if you have rewrite your constructor, all the "default" properties are injected.
Assuming a User model
❌ You should not do
#Entity({ name: 'users' })
class User {
#Column()
firstname?: string;
#Column({ nullable: true })
lastname?: string;
#Column({ unique: true, nullable: false })
email!: string;
#Column({ name: 'account_validated', nullable: false})
accountValidated?: boolean = false
//Your other fields...
}
✅ You should do
#Entity({ name: 'users' })
class User {
#Column()
firstname?: string;
#Column({ nullable: true })
lastname?: string;
#Column({ unique: true, nullable: false })
email!: string;
// Use default argument of the decorator
#Column({ name: 'account_validated', nullable: false, default: false})
accountValidated?: boolean
//Your other fields...
}
And if you need in some way to init a default, then create a public static method which return the Entity instead of using the constructor.
#Entity({ name: 'users' })
class User {
//...fields
public static init(...params): User {
let _user = new User()
//...populate your object
return _user
}
}

Typeorm bulk insert

I have the following Group entity
export class Group {
#PrimaryGeneratedColumn('increment')
id: number;
#Column({ type: 'varchar', unique: true })
name: string;
}
I am trying to implement a bulk insert where only groups with new names would get inserted, while existing one would just get returned without any operation performed on them.
I managed to get this working as follows:
await this
.createQueryBuilder()
.insert()
.into(Group)
.values(groups)
.orUpdate({
conflict_target: ['name'],
overwrite: ['name'],
})
.execute();
But it can not work.why?
When I set unique key in db.It is work.

TypeORM relation through third table won't work

Following the TypeORM tutorial (this part - https://typeorm.io/#/many-to-many-relations/many-to-many-relations-with-custom-properties) i created the models in my app:
#Entity({ name: 'organizations'})
export class OrganizationEntity extends AbstractEntity {
#Column()
name: string;
#Column({ nullable: true })
description: string;
#Column({ nullable: true })
logo: string;
#ManyToOne(() => UserEntity, { nullable: true })
mainContactUser: UserEntity;
#OneToOne(() => LicenseEntity, license => license.organization)
license: LicenseEntity;
#OneToMany(() => UserRoleEntity, userRole => userRole.user)
users!: UserRoleEntity[]
}
#Entity({ name: 'users' })
export class UserEntity extends AbstractEntityWithDates {
#Column({ nullable: false })
firstName: string;
#Column({ nullable: false})
lastName: string;
#Column({ nullable: true })
jobTitle: string;
#Column({ unique: true })
email: string;
#Column({ nullable: true })
phone: string;
#Column({ nullable: true })
avatar: string;
#Column({ nullable: true })
oAuthIdentityId: string;
#OneToMany(() => UserRoleEntity, userRole => userRole.user)
userRoles: UserRoleEntity[]
}
and finally:
#Entity({ name: 'user_roles' })
export class UserRoleEntity extends AbstractEntity {
#ManyToOne(() => UserEntity, user => user.userRoles)
user: UserEntity;
#ManyToOne(() => OrganizationEntity, organization => organization.users, { nullable: true })
organization: OrganizationEntity;
#ManyToOne(() => ProjectEntity, { nullable: true })
project: ProjectEntity;
#Column({
type: "enum",
enum: RoleType,
nullable: true
})
role: string;
}
Then in a service i'm performing a query where i want to fetch all organizations with at least related user roles:
const found = await this.organizationRepository
.createQueryBuilder("organization")
.leftJoinAndSelect("organization.users", "users")
.getMany();
No errors but the query generated by TypeORM is as follows:
SELECT "organization"."id" AS "organization_id", "organization"."name" AS "organization_name", "organization"."description" AS "organization_description", "organization"."logo" AS "organization_logo", "organization"."main_contact_user_id" AS "organization_main_contact_user_id", "users"."id" AS "users_id", "users"."role" AS "users_role", "users"."user_id" AS "users_user_id", "users"."organization_id" AS "users_organization_id", "users"."project_id" AS "users_project_id"
FROM "organizations" "organization"
LEFT JOIN "user_roles" "users" ON "users"."user_id"="organization"."id"
This part:
...ON "users"."user_id"="organization"."id"
is obviously incorrect but i can't see why?
Any help would be appreciated.
Your OrganizationEntity has an error in the OneToMany config; You set userRole.user, but it should be userRole.organization, which is the field in that table that links to this field.
#OneToMany(() => UserRoleEntity, userRole => userRole.organization)
users!: UserRoleEntity[]
Also, you should add primary keys to your tables. A safe approach by default is to use a Generated column. You can add it to your 3 tables. For the many-to-many table you could create a compound key, or simply an index with user_id, and company_id, but with tradeoffs - so just use an id on every table to get started.
#PrimaryGeneratedColumn()
id: string;

How to expose foreign key column in the entity in TypeORM

I have failed to expose the foreign key column in the entity which I feel weird to not be able to.
If I am not eager loading the relation, at least I should be able to to see the imageId to have some clue of the existence of the relation. so when I do a userRepository.findOne({email: emailaddress}), even I know that I can not eager load the image this way. But at least I could see the imageId.
#Column('datetime', { nullable: true, name: 'last_login' })
lastLogin: string;
#OneToOne(() => UserSetting)
#JoinColumn({ name: 'setting_id' })
setting: UserSetting;
#OneToOne(() => UserImage, { onDelete: 'SET NULL' })
#JoinColumn({ name: 'image_id' })
image: UserImage;
imageUrl: { preview: string, thumbnail: string };
#OneToMany(() => Contact, contact => contact.user)
contacts: Contact[];
#OneToMany(() => Notification, notification => notification.user)
notifications: Notification[];
As you can see, there is no imageId defined. I tried to put it like this. The database just cannot be synced up and it wiped out all my image data too.
#Column({name: 'image_id' })
imageId: string;
#OneToOne(() => UserImage, { onDelete: 'SET NULL' })
#JoinColumn({ name: 'image_id' })
image: UserImage;
Am I missing something simple here?
Your User Entity should look like this:
#Column({ nullable: true })
imageId: string;
#OneToOne(() => UserImage, userImage=> userImage.user, { onDelete: 'SET NULL' })
#JoinColumn({ name: 'image_id' })
image: UserImage;
and in your UserImage Entity:
#OneToOne(() => User, user=> user.image)
user: User;

typeorm - how to add extra field to "many to many" table?

for example:
3 tables
user
user_business_lines_business_line
business_line
those created by typeorm with the declaration in User
#ManyToMany(type => BusinessLine)
#JoinTable()
businessLines: BusinessLine[]
then, how to add columns fields like
#CreateDateColumn({ type: 'timestamp' })
createdAt: Date
#UpdateDateColumn({ type: 'timestamp' })
updatedAt: Date
to user_business_lines_business_line
It is not possible to add custom column in the auto created many-to-many bridge table. So create another table and give one-to-many and many-to-one relationship between them.
for example:
Three tables
User -> Table 1
BusinessLine -> Table 2
UserBusinessLine -> Bridge Table between User table and BusinessLine table
UserBusinessLine table will contain the foreign key of both parent tables and also we can add custom columns into it.
In User Table
#OneToMany(() => UserBusinessLine, (userBusinessLine) => userBusinessLine.user)
public userBusinessLines: UserBusinessLine[];
In BusinessLine Table
#OneToMany(() => UserBusinessLine, (userBusinessLine) => userBusinessLine.businessLine)
public userBusinessLines: UserBusinessLine[];
In UserBusinessLine Table
#ManyToOne(() => User, (user) => user.userBusinessLines)
public user: User;
#ManyToOne(() => BusinessLine, (businessLine) => businessLine.userBusinessLines)
public businessLine: BusinessLine;
// Custom Colums
#CreateDateColumn({ type: 'timestamp' })
createdAt: Date;
#UpdateDateColumn({ type: 'timestamp' })
updatedAt: Date;
So now the custom table has the foreign keys of User table and BusinessLine table. Also the CreateddateColumn and UpdatedDateColumn
You can specify custom-join table for ManyToMany relation
#Entity()
export class User {
#PrimaryGeneratedColumn()
id: number
#Column()
name: string
#ManyToMany(type => BusinessLine, businessLine => businessLine.users)
#JoinTable({
name: 'user_business_line',
joinColumn: {
name: 'userId',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'businessLineId',
referencedColumnName: 'id',
},
})
businessLines: BusinessLine[]
}
#Entity('user_business_line')
export class UserBusinessLine {
#CreateDateColumn({ type: 'timestamp' })
createdAt: Date
#UpdateDateColumn({ type: 'timestamp' })
updatedAt: Date
#Column()
#IsNotEmpty()
#PrimaryColumn()
userId: number;
#Column()
#IsNotEmpty()
#PrimaryColumn()
businessLineId: number;
}
#Entity()
export class BusinessLine {
#PrimaryGeneratedColumn()
id: number
#Column()
name: string
#ManyToMany(type => User, user => user.businessLines)
users: User[]
}
If it is just for a createdAt timestamp for example, you could just add an extra column to your user_business_lines_business_line table using migrations like this (I am using Postgres):
ALTER TABLE "user_business_lines_business_line" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT LOCALTIMESTAMP

Resources