TypeORM How to access reference relation field without loading relation entity - sql

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;
}

Related

createQueryBuilder with getOne doesn't return #JoinColumns inside table Typeorm

I have a project written by nestjs and typeorm. In my project I have tables chat and user so I want to get ManyToOne relationships. Here is the chat table
export class Chat extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
#ManyToOne(() => User, (user) => user.chatsCreater, { nullable: false })
#JoinColumn({ name: 'creatorId' })
creator: User;
#ManyToOne(() => User, (user) => user.chatsCompanion, { nullable: false })
#JoinColumn({ name: 'companionId' })
companion: User;
}
and chunk from user table
...
#OneToMany(() => Chat, (chat) => chat.creator)
chatsCreater: Chat[];
#OneToMany(() => Chat, (chat) => chat.companion)
chatsCompanion: Chat[];
...
When I query data from chat I expect to get the hole table {id, companionId, creatorI} not only value of chat.id. Here is my query
.createQueryBuilder('chat')
.where('chat.creatorId = :creatorId AND chat.companionId = :companionId', { creatorId, companionId })
.getOne()
and the result {id: 1}
So what I want is to get values of companionId and creatorId too when I query from chat.
If I change getOne() to getRaw() I get the desired output. But in case of more complex queries (with multiple joins) it becomes a mess with getRaw so is there a way to get all columns using getOne ?
I was able to do it by using leftJoinAndSelect
.createQueryBuilder('chat')
.leftJoinAndSelect('chat.creator', 'creator')
.leftJoinAndSelect('chat.companion', 'companion')
.where('chat.creatorId = :creatorId AND chat.companionId = :companionId', { creatorId, companionId })
.getOne();
In case if hole table is not needed it's also possible to use leftJoin without select and later add to query addSelect(['creator.id','companion.id'])

Create query builder that the source table (FROM) is a join table in TypeORM

I'm trying to implement the following SQL in TypeORM using QueryBuilder:
SELECT
user_places.user_id,
place.mpath
FROM
public.user_root_places_place user_places
INNER JOIN
public.place place
ON place.id = user_places.place_id
The entities are:
#Entity()
export class User {
#Column({ unique: true, primary: true })
id: string;
#ManyToMany(() => Place)
#JoinTable()
rootPlaces: Place[];
}
#Entity()
export class Place {
#PrimaryGeneratedColumn()
id: number;
#Column()
mpath: string;
}
When you create a query builder you have to use some entity or table but the join table is "hidden" by TypeORM
I know I can replace the inner join table order and it will solve the problem but I'm looking for when the source table is the join table
If you don't want to use the generated name just specify explicitly the join table name
#Entity()
export class User {
#Column({ unique: true, primary: true })
id: string;
#ManyToMany(() => Place)
#JoinTable({
name: 'user_places' // <---
})
rootPlaces: Place[];
}
And then:
createQueryBuilder('user_places')
.select(['user_places.userId', 'place.mpath'])
.innerJoin(Place, 'place', 'place.id = user_places.place_id')
.getMany();

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.

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

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;}

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