setting up a one to many relationship for a self referencing table - sql

I have a Project entity with a non-autogenerated id field and a successor field. This successor is the project that follows next. But maybe there is no following project so this might be null.
#Entity()
export class Project extends BaseEntity {
#PrimaryColumn({ unique: true })
public id: string;
#OneToMany(() => Project, project => project.id, { nullable: true })
public successorId?: string;
}
When creating a new project via
public createProject(id: string, successorId?: string): Promise<Project> {
const project: Project = new Project();
project.id = id;
project.successorId = successorId;
return project.save();
}
there are multiple cases I have to take care for.
Passing in an id that already exists:
This will not throw an error. It just overrides the existing entity.
Passing in undefined for the successorId:
The code works fine then but it does not create a successorId column with null then. The column simply does not exist in the database.
Passing in the same id for id and successorId (this should be possible):
TypeORM throws the error
TypeError: Cannot read property 'joinColumns' of undefined
Passing in a successorId of another existing project:
I'm getting the same error as above
Passing in a successorId of a project that doesn't exist:
I'm getting the same error as above
So how can I fix that? I think my entity design seems to be wrong. Basically it should be
One project might have one successor
A project can be the successor of many projects
Would be awesome if someone could help!
Update
I also tried this
#OneToMany(() => Project, project => project.successorId, { nullable: true })
#Column()
public successorId?: string;
but whenever I want to call the createProject method I'm getting this error
QueryFailedError: null value in column "successorId" violates not-null
constraint
and this
#OneToMany(() => Project, project => project.successorId, { nullable: true })
public successorId?: string;
but then I'm getting this error
TypeError: relatedEntities.forEach is not a function

Please try this solution
#Entity()
export class Project extends BaseEntity {
#PrimaryColumn({ unique: true })
public id: string;
#Column({ nullable: true })
public successorId?: string;
#ManyToOne(() => Project, project => project.id)
#JoinColumn({ name: "successorId" })
public successor?: Project;
}
public successor?: Project; - property is used for building relation between entities (same entity in this case). Related entity must be specified as a property type, because TypeORM uses this type to determine target entity and build relation metadata. You can read more about TypeORM relations here
public successorId?: string; - property is just an "extracted" join column. When you use ManyToOne relation, TypeORM automatically creates a column in the database named propertyName + referencedColumnName (successorId in this case). But you cannot use this column in your code, because it is defined only in table and not in your class. And if you need this column defined in class (for further saving or displaying) you can create a property and mark it with a #Column decorator. Property name must be the same as the join column name in the table. Described in more detail here
Creating an entity with the same id just overrides the existing one
this is an expected behaviour. When you trying to save entity with an existing Id, TypeORM recognizes this as an update, not a create

You can try this
export class RolesPermission {
#PrimaryGeneratedColumn('uuid')
#PrimaryColumn({ type: 'varchar', length: 36, default: 'UUID()' })
entityId?: string;
#Column({ unique: true })
name: string;
#OneToMany(() => RolesPermission, (rolePermission) => rolePermission.parent)
rolePermissions?: RolesPermission[];
#ManyToOne(() => RolesPermission, (rolePermission) => rolePermission.rolePermissions, { nullable: true, createForeignKeyConstraints: false })
parent?: RolesPermission;
#Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
createdAt?: Date;
#Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP' })
updatedAt?: Date;}

Related

Typeorm - Converting SQL Left Join one to one relation into typeorm query builder

I have following entities in a postgresql database using Typeorm:
#Entity('issuer')
export class Issuer {
#PrimaryColumn()
issuer_id: string
#OneToOne(() => UserData, { cascade: true })
#JoinColumn({ name: 'issuer_id', referencedColumnName: 'public_address' })
u_data: UserData
#BeforeInsert()
newid() {
this.issuer_id = this.u_data.public_address
}
...remaining columns...
}
#Entity('user_data')
export class UserData {
#PrimaryColumn({ type: 'varchar', unique: true })
email: string
#Column({ type: 'varchar', nullable: false, unique: true })
public_address: string
...remaining columns...
}
Above in the Issuer entity, I am doing a small trick to be able to make a key both primary and foreign, issuer_id column, which is primary key of Issuer and foreign key of UserData which refers to public_address column of UserData. I wanna join both entities, and I am able to do it with the following query:
SELECT *
FROM user_data
LEFT OUTER JOIN issuer ON issuer.issuer_id = user_data.public_address
WHERE user_data.email = $1
I am unable to convert this simple SQL code into Typeorm query builder. Here is my failed attempt:
await this.userRepository
.createQueryBuilder('user')
.leftJoin('user.public_address', 'issuer')
.where('user.email = :email', { email })
.getOne()
Here is the error I am getting:
TypeORMError: Relation with property path public_address in entity was not found.
It seems when trying to left join (right join doesn't exist on typeorm) from an entity that has no direct relation to its relative, leftJoinAndSelect function should be used with a condition:
return await this.userRepo
.createQueryBuilder('user')
.leftJoinAndSelect(Issuer, 'issuer', 'user.public_address = issuer.issuer_id')
.where('user.email = :email', { email })
.getRawOne()

TypeORM How to access reference relation field without loading relation entity

As we know, to create ManyToOne/OneToMany relation we have to use #ManyToOne/#OneToMany decorators on a field.
In my project I have two entities: Project and Position.
This is how I created a relation:
#Entity('positions')
export class Position {
#ManyToOne(() => Project, {
nullable: false,
eager: true,
})
#JoinColumn()
project: Project;
}
TypeORM documentation says this code will create projectId FOREIGN KEY column in the database and store a project id in it.
Then when we trying to access project property TypeORM loads a project by the id stored in projectId field.
QUESTION
How can I access that pojectId field without loading a relational entity?
The property projectId does not exists by default in Position entity and if I manually create it it is not populated by projectId column value.
I have tried this way:
#ManyToOne(() => Project, {
nullable: false,
eager: false,
})
#JoinColumn()
project: Project;
projectId: string;
You can use the #RelationId decorator exported by typeorm. Using your example:
import {
Column,
Entity,
ManyToOne,
RelationId,
JoinColumn,
} from 'typeorm'
#Entity()
export class Position {
#ManyToOne(() => Project, {
nullable: false,
eager: false,
})
#JoinColumn()
project: Project;
#Column()
#RelationId((position: Position) => position.project)
projectId: string;
}

GraphQL & Sequelize: Users and followers/following

I'm attempting to set up my User GraphQL model to have followers and following attributes to query on. However I'm having trouble setting up the relationship in Sequelize. I'm trying to use a Follower model as a Join Table and setup a BelongsToMany association, but haven't been able to get it working. Can anyone suggest what to do or point out what I'm doing wrong?
I've come up with a temporary solution by manually querying, which you can see in my User.model.ts, but I believe there is a better way to do it using proper configuration.
I'm using typescript wrappers around GraphQL and Sequelize, TypeGraphQL and sequelize-typescript respectively, as well as PostgreSQL.
User.model.ts
// VENDOR
import { ObjectType, Field, ID } from 'type-graphql';
import { Model, Table, Column, PrimaryKey, Unique, IsUUID, HasMany, DefaultScope, AllowNull, DataType, BelongsToMany } from 'sequelize-typescript';
// APP
import Post from '../post/post.types';
import Follower from '../follower/follower.types';
/** User model for GraphQL & Database */
#Table({ timestamps: false, tableName: 'users' }) // tell sequelize to treat class as table model
#DefaultScope(() => ({ include: [{ model: Post.scope(), as: 'posts' }] })) // tell sequelize to include posts in its default queries
#ObjectType() // tell GraphQL to treat class as GraphQL model
export default class User extends Model<User>{
#PrimaryKey
#Unique
#AllowNull(false)
#IsUUID(4)
#Column(DataType.UUID)
#Field(() => ID)
id: string;
#Unique
#AllowNull(false)
#Column
#Field()
ci_username: string;
#AllowNull(false)
#Column
#Field()
username: string;
#AllowNull(false)
#Column
#Field()
first_name: string;
#Column
#Field()
last_name: string;
#Column
#Field({ nullable: true })
profile_picture?: string;
// #BelongsToMany(() => User, { otherKey: 'user_id', as: 'followers', through: () => Follower })
// #Field(() => [User])
// followers: User[];
// MY TEMPORARY SOLUTION USING MANUAL QUERYING
#Field(() => [User])
get followers(): Promise<User[]> {
return Follower.findAll({ where: { user_id: this.id } })
.then(records => records.map(record => record.follower_id))
.then((follower_ids: string[]) => {
return User.findAll({ where: { id: follower_ids }});
})
}
// DOES NOT WORK, BUT I BELIEVE COULD POTENTIALLY LEAD TO BETTER SOLUTION
#BelongsToMany(() => User, { otherKey: 'follower_id', as: 'following', through: () => Follower })
#Field(() => [User])
following: User[];
#HasMany(() => Post)
#Field(() => [Post])
posts: Post[];
}
Follower.model.ts
// VENDOR
import { Model, Table, Column, PrimaryKey, Unique, IsUUID, AllowNull, DataType, Index, ForeignKey, AutoIncrement } from 'sequelize-typescript';
// APP
import User from '../user/user.types';
/** Follower model for Database */
#Table({ timestamps: false, tableName: 'followers' }) // tell sequelize to treat class as table model
export default class Follower extends Model<Follower>{
#PrimaryKey
#AutoIncrement
#Unique
#AllowNull(false)
#Column
id: number;
#AllowNull(false)
#IsUUID(4)
#Index
#ForeignKey(() => User)
#Column(DataType.UUID)
user_id: string;
#AllowNull(false)
#IsUUID(4)
#Index
#ForeignKey(() => User)
#Column(DataType.UUID)
follower_id: string;
}
GraphQL Query
{
users: allUsers {
id
username
first_name
last_name
following {
username
id
}
}
}
GraphQL Response / Error
{
"errors": [
{
"message": "Cannot return null for non-nullable field User.following.",
"locations": [
{
"line": 7,
"column": 5
}
],
"path": [
"users",
0,
"following"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"Error: Cannot return null for non-nullable field User.following.",
" at completeValue (/Users/jsainz237/Projects/trueview/trueview-api/node_modules/graphql/execution/execute.js:560:13)",
" at /Users/jsainz237/Projects/trueview/trueview-api/node_modules/graphql/execution/execute.js:492:16"
]
}
}
}
],
"data": null
}
Any help is appreciated.
You need to write a #FieldResolver manually that will resolve the relation and return proper data.
Another solution is to rely on ORM capabilities and lazy relations - when the returned base entity contains a promise as a field, so when .then() is called, it automatically fetches the relation for the database.

TypeORM: how to implement bidirectional relationship, multiple fields --> one entity type

I've created a 'document' entity:
e.g.
#Entity()
export class Document {
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#Column()
path: string;
...
}
Multiple documents can be related to different entity types: post, userProfile etc
in the post entity for example, I have several fields which all specify document relationships.
#OneToOne(type => DocumentEntity)
#JoinColumn({ name: 'default_document' })
defaultDocument: DocumentEntity;
#OneToOne(type => DocumentEntity)
#JoinColumn({ name: 'featured_document' })
featuredDocument: DocumentEntity;
#OneToMany(type => DocumentEntity, document => document.post)
#JoinColumn({ name: 'other_documents' })
otherDocs: DocumentEntity[];
I'm unclear how to make the document relationships bidirectional.
I had hoped to have a single field on document like:
#ManyToOne(type => abstractEntity, entity => entity.document)
parentEntity: abstractEntity;
This way if I'm querying document entities for their parent relationships,
I would have a result like:
documents: [
{
id: 1,
name: 'document 1',
path: 'https://image.hosted.service/1.jpg',
parentEntityId: 23
},
{
id: 2
name: 'document 2',
path: 'https://image.hosted.service/2.jpg'
parentEntityId: 27
}
]
But Typeorm seems to want me to define an exact matching field for each parent relationship field on documentEntity like:
#Entity()
export class Document {
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#Column()
path: string;
...
#OneToOne(type => PostEntity, post => post.defaultDocument)
postEntityDefaultDoc: PostEntity;
#OneToOne(type => PostEntity, post => post.featuredDocument)
postEntityFeaturedDoc: PostEntity;
#ManyToOne(type => PostEntity, post => post.otherDocs)
otherDocs: PostEntity[];
}
For the sake of simplicity in this example, there are no M:N relationships: document can have at most one parent.
It doesn't seem correct that I would have to define a new field on document entity, for every possible instance where a parent entity field references a document.
A query on document would not return a list with one field defining the parent entity, instead I have to parse/aggregate an arbitrary number of fields.
I can't seem to find any tutorials/examples in which a single entity has many fields each referencing the same other entity, which is making me think my basic approach is flawed.
The secret ingridient is leftJoinAndMapMany which allows you to join abitrary entities and map it onto attributes.
Here is what I would do in your case. The DocumentEntity would look like that:
#Entity()
class DocumentEntity {
#PrimaryGeneratedColumn()
public id!: number;
#Column()
public entity!: string;
#Column({
name: 'entity_id',
})
public entityId!: string;
#Column()
public name!: string;
}
Your PostEntity would look like that:
#Entity()
class PostEntity {
#PrimaryGeneratedColumn()
public id!: number;
#Column()
public name: string;
public documents?: DocumentEntity[];
}
As you might notice, the documents on the post has no anotation. Thats because we will do the join with the aforementioned method. Your query would look something like that:
connection
.getRepository(PostEntity)
.createQueryBuilder('p')
.leftJoinAndMapMany(
'p.documents',
DocumentEntity,
'p__d',
'(p.id = md.entityId AND md.entity = :documentEntity)',
{
documentEntity: PostEntity.name,
},
)
.getMany()
These methods are available for joining these entities:
leftJoinAndMapMany
innerJoinAndMapMany
leftJoinAndMapOne
innerJoinAndMapOne

TypeORM getRepository.find() does not include Foreign Key Fields

I am trying to fetch all the columns included on my entity, but I only able to fetch the columns that does not have any relationship from the other entity.
I use this block of codes to fetch the all the rows to this repository.
private translationTextRepository = getRepository(TranslationText);
async all(request: Request, response: Response, next: NextFunction) {
return this.translationTextRepository.find();
}
And here's the entity for this repository.
#Entity('TranslationText')
export class TranslationText {
#PrimaryGeneratedColumn()
ID: number;
#Column()
CreatedBy: string;
#Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
CreatedDate: Date;
#Column()
Status: boolean;
#Column({ nullable: true, default: null })
ModifiedBy: string;
#Column({ type: 'timestamp', nullable: true, default: null })
ModifiedDate: Date;
#Column()
Text: string;
#ManyToOne((type) => Locale, (locale) => locale.ID)
#JoinColumn({ name: 'LocaleID' })
LocaleID: Locale;
#ManyToOne((type) => TranslationTitle, (translationTitle) => translationTitle.ID)
#JoinColumn({ name: 'TranslationTitleID' })
TranslationTitleID: TranslationTitle;
}
But I was only able to fetch all the columns except the LocaleID and the TranslationTitleID.
How can I achieve this?
Check this document:
https://typeorm.io/#/relations-faq/how-to-use-relation-id-without-joining-relation
solution:
define new column:
#column()
LocaleID: number
rename old one to : Locale
But typeOrm cannot sync your table due to foreign key problem.
use eager option in #ManyToOne({eager: true})
The search result will contain relation Locale object, you can take id from it.
Can you try to specify the relations like that:
async all(request: Request, response: Response, next: NextFunction) {
return this.translationTextRepository.find({
relations:["LocaleID","TranslationTitleID"]
});
}
Because you have to make explicit that you want your relations on the query.