Relations
Relations work a little differently in typegoose
when compared to other relational ORMs such as sequelize
or
typeorm
. You can read more about relations (references) in typegoose
[here](https://mongoosejs.com/docs/populate
.html)
There are multiple ways to set of references in Typegoose. These are intended as a starting point.
Filtering on references is not supported by Typegoose.
One to Many/Many To One Example
To set up a one to many/many to one relationship in Typegoose, you will store a reference in your document.
For example, lets add sub tasks to our todo items by storing a todoItem
ref on our subTask
and an array of sub-tasks on our todoItem
entity.
- TodoItemEntity
- SubTaskEntity
import { Prop, modelOptions } from '@typegoose/typegoose';
import { Base } from '@typegoose/typegoose/lib/defaultClasses';
@modelOptions({ schemaOptions: { timestamps: true } })
export class TodoItemEntity extends Base {
@Prop({ required: true })
title!: string;
@Prop()
description?: string;
@Prop({ required: true })
completed!: boolean;
@Prop({ default: Date.now })
createdAt!: Date;
@Prop({ default: Date.now })
updatedAt!: Date;
@Prop({ default: 0 })
priority!: number;
@Prop({ ref: () => SubTaskEntity })
subTasks: Ref<SubTaskEntity>[];
@Prop()
createdBy?: string;
@Prop()
updatedBy?: string;
}
import { Prop, modelOptions, Ref } from '@typegoose/typegoose';
import { Base } from '@typegoose/typegoose/lib/defaultClasses';
@modelOptions({ schemaOptions: { timestamps: true } })
export class SubTaskEntity extends Base {
@Prop({ required: true })
title!: string;
@Prop()
description?: string;
@Prop({ required: true })
completed!: boolean;
@Prop({ ref: () => TodoItemEntity, required: true })
todoItem!: Ref<TodoItemEntity>;
@Prop()
createdAt!: Date;
@Prop()
updatedAt!: Date;
@Prop()
createdBy?: string;
@Prop()
updatedBy?: string;
}
Now that we have the relationships defined, we can add the @Relation
and @Connection
to our DTOs
- TodoItemDTO
- SubTaskDTO
import { FilterableField, IDField, KeySet, Connection } from '@ptc-org/nestjs-query-graphql';
import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql';
import { SubTaskDTO } from '../../sub-task/dto/sub-task.dto';
@ObjectType('TodoItem')
@KeySet(['id'])
// disable the remove because mongoose does not support removing a virtual
@Connection('subTasks', () => SubTaskDTO, { update: { enabled: true } })
export class TodoItemDTO {
@IDField(() => ID)
id!: string;
@FilterableField()
title!: string;
@FilterableField({ nullable: true })
description?: string;
@FilterableField()
completed!: boolean;
@FilterableField(() => GraphQLISODateTime)
createdAt!: Date;
@FilterableField(() => GraphQLISODateTime)
updatedAt!: Date;
@Field()
age!: number;
@FilterableField()
priority!: number;
@FilterableField({ nullable: true })
createdBy?: string;
@FilterableField({ nullable: true })
updatedBy?: string;
}
import { FilterableField, IDField, KeySet, Relation } from '@ptc-org/nestjs-query-graphql';
import { ObjectType, ID, GraphQLISODateTime } from '@nestjs/graphql';
import { TodoItemDTO } from '../../todo-item/dto/todo-item.dto';
@ObjectType('SubTask')
@KeySet(['id'])
// disable the remove because a sub task cannot exist without a todoitem
@Relation('todoItem', () => TodoItemDTO, { update: { enabled: true } })
export class SubTaskDTO {
@IDField(() => ID)
id!: string;
@FilterableField()
title!: string;
@FilterableField({ nullable: true })
description?: string;
@FilterableField()
completed!: boolean;
@FilterableField(() => GraphQLISODateTime)
createdAt!: Date;
@FilterableField(() => GraphQLISODateTime)
updatedAt!: Date;
@FilterableField({ nullable: true })
createdBy?: string;
@FilterableField({ nullable: true })
updatedBy?: string;
}
Many To Many Example
In this example, we'll add tags
to todoItems
by storing an array of tag
references on the todoItems
.
- TodoItemEntity
- TagEntity
import { Prop, modelOption, Ref } from '@typegoose/typegoose';
import { Base } from '@typegoose/typegoose/lib/defaultClasses';
@modelOptions({ schemaOptions: { timestamps: true } })
export class TodoItemEntity extends Base {
@Prop({ required: true })
title!: string;
@Prop()
description?: string;
@Prop({ required: true })
completed!: boolean;
@Prop({ default: Date.now })
createdAt!: Date;
@Prop({ default: Date.now })
updatedAt!: Date;
@Prop({ ref: () => TagEntity })
tags!: Ref<TagEntity>[];
@Prop({ default: 0 })
priority!: number;
@Prop()
createdBy?: string;
@Prop()
updatedBy?: string;
public get id(): string {
// eslint-disable-next-line no-underscore-dangle
return this._id.toHexString();
}
}
import { Base } from '@typegoose/typegoose/lib/defaultClasses';
import { Prop, modelOptions, Ref } from '@typegoose/typegoose';
import { Types } from 'mongoose';
import { TodoItemEntity } from '../todo-item/todo-item.entity';
@modelOptions({
schemaOptions: {
timestamps: true,
collection: 'tags',
toObject: { virtuals: true },
},
})
export class TagEntity implements Base {
_id!: Types.ObjectId;
id!: string;
@Prop({ required: true })
name!: string;
@Prop()
createdAt!: Date;
@Prop()
updatedAt!: Date;
@Prop()
createdBy?: string;
@Prop()
updatedBy?: string;
@Prop({
ref: 'TodoItemEntity',
localField: '_id',
foreignField: 'tags',
})
todoItems?: Ref<TodoItemEntity>[];
}
Now that we have the relationship defined, we can add the @Connection
to our DTOS
- TodoItemDTO
- TagDTO
import { FilterableField, IDField, KeySet, Connection } from '@ptc-org/nestjs-query-graphql';
import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql';
import { TagDTO } from '../../tag/dto/tag.dto';
@ObjectType('TodoItem')
@KeySet(['id'])
@Connection('tags', () => TagDTO)
export class TodoItemDTO {
@IDField(() => ID)
id!: string;
@FilterableField()
title!: string;
@FilterableField({ nullable: true })
description?: string;
@FilterableField()
completed!: boolean;
@FilterableField(() => GraphQLISODateTime)
createdAt!: Date;
@FilterableField(() => GraphQLISODateTime)
updatedAt!: Date;
@Field()
age!: number;
@FilterableField()
priority!: number;
@FilterableField({ nullable: true })
createdBy?: string;
@FilterableField({ nullable: true })
updatedBy?: string;
}
import { FilterableField, IDField, KeySet, Connection } from '@ptc-org/nestjs-query-graphql';
import { ObjectType, ID, GraphQLISODateTime } from '@nestjs/graphql';
import { TodoItemDTO } from '../../todo-item/dto/todo-item.dto';
@ObjectType('Tag')
@KeySet(['id'])
// disable update and remove since it is a virtual in the entity
@Connection('todoItems', () => TodoItemDTO)
export class TagDTO {
@IDField(() => ID)
id!: string;
@FilterableField()
name!: string;
@FilterableField(() => GraphQLISODateTime)
createdAt!: Date;
@FilterableField(() => GraphQLISODateTime)
updatedAt!: Date;
@FilterableField({ nullable: true })
createdBy?: string;
@FilterableField({ nullable: true })
updatedBy?: string;
}
Discriminators
Typegoose supports mongoose discriminators. nestjs-query
provides support for them through the NestjsQueryTypegooseModule
.
To use discriminators you need to define them in your NestjsQueryTypegooseModule.forFeature
call.
When working with discriminators, nestjs-query
requires you to provide a service class for each discriminated entity. This is necessary for the auto-generated resolvers to be created correctly. These service classes are also the perfect place to add any custom business logic for your discriminated entities.
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypegooseModule, TypegooseQueryService } from '@ptc-org/nestjs-query-typegoose';
import { Module } from '@nestjs/common';
import { TodoItemEntity } from './entities/todo-item.entity';
import { TodoTaskEntity } from './entities/todo-task.entity';
import { TodoAppointmentEntity } from './entities/todo-appointment.entity';
import { TodoItemDTO } from './dto/todo-item.dto';
import { TodoTaskDTO } from './dto/todo-task.dto';
import { TodoAppointmentDTO } from './dto/todo-appointment.dto';
import { CreateTodoTaskInput } from './dto/create-todo-task.input';
import { CreateTodoAppointmentInput } from './dto/create-todo-appointment.input';
import { InjectModel } from '@m8a/nestjs-typegoose';
import { ReturnModelType } from '@typegoose/typegoose';
const typegooseModule = NestjsQueryTypegooseModule.forFeature([
{
typegooseClass: TodoItemEntity,
discriminators: [
{ typegooseClass: TodoTaskEntity },
{ typegooseClass: TodoAppointmentEntity }
]
}
]);
class TodoTaskEntityService extends TypegooseQueryService<TodoTaskEntity> {
constructor(@InjectModel(TodoTaskEntity) readonly model: ReturnModelType<typeof TodoTaskEntity>) {
super(model)
}
}
class TodoAppointmentService extends TypegooseQueryService<TodoAppointmentEntity> {
constructor(@InjectModel(TodoAppointmentEntity) readonly model: ReturnModelType<typeof TodoAppointmentEntity>) {
super(model)
}
}
@Module({
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [typegooseModule],
resolvers: [
{
DTOClass: TodoItemDTO,
EntityClass: TodoItemEntity,
create: {disabled: true}, // Disable create for the base entity
update: {disabled: true} // Disable update for the base entity
},
{
DTOClass: TodoTaskDTO,
EntityClass: TodoTaskEntity,
// Tell the resolver to use our custom input for the create operation
CreateDTOClass: CreateTodoTaskInput,
ServiceClass: TodoTaskEntityService
},
{
DTOClass: TodoAppointmentDTO,
EntityClass: TodoAppointmentEntity,
// Tell the resolver to use our custom input for the create operation
CreateDTOClass: CreateTodoAppointmentInput,
ServiceClass: TodoAppointmentService
}
],
services: [
TodoTaskEntityService,
TodoAppointmentService
]
})
]
})
export class TodoItemModule {}