doug-martin / nestjs-query

Easy CRUD for GraphQL.
https://doug-martin.github.io/nestjs-query
MIT License
822 stars 142 forks source link

Nested Relations #800

Closed h4rm closed 3 years ago

h4rm commented 3 years ago

Hi @doug-martin , thanks a lot for this amazing library. I was recently looking into the @Relation decorator and I would like to use it for querying a reference in a subfield without creating it's own collection (as it is possible with pure mongoose):

import { Connection, FilterableField, KeySet, Relation } from '@nestjs-query/query-graphql';
import { ObjectType, Field, InputType } from '@nestjs/graphql';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { SchemaTypes, Types } from 'mongoose';
import { BaseDTO, BaseEntityMongoose } from '../BaseEntity';
import { Document } from '../document/document.entity';

@Schema({ timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } })
export class ColorEntity extends Document {
  @Prop({ required: true })
  name: string;
}
export const ColorSchema = SchemaFactory.createForClass(ColorEntity);

@ObjectType('Color')
@InputType('ColorInput')
@KeySet(['id'])
export class ColorDTO {
  @FilterableField(() => String)
  id!: string;
  @Field()
  name: string;
}

@Schema({ timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } })
export class Wall extends Document {
  @Prop({ required: true })
  name: string;
  @Prop({ type: SchemaTypes.ObjectId, ref: 'Color', required: false })
  colorId: Types.ObjectId;
}
export const WallSchema = SchemaFactory.createForClass(Wall);

@ObjectType('Wall')
@InputType('WallInput')
@KeySet(['id'])
@Relation('color', () => ColorDTO, {
  disableRemove: true,
  relationName: 'colorId',
  nullable: true,
})
export class WallDTO {
  @FilterableField(() => String)
  id!: string;
  @Field()
  name: string;
  @Field()
  colorId: string;
}

@Schema({ timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } })
export class Room extends Document {
  @Prop({ type: [WallSchema], default: [] })
  walls: Wall[];
}
export const RoomSchema = SchemaFactory.createForClass(Room);

@ObjectType('Room')
@KeySet(['id'])
export class RoomDTO {
  @FilterableField(() => String)
  id!: string;
  @Field(() => [WallDTO])
  walls: WallDTO[];
}

And the following module:

import { Module } from '@nestjs/common';
import { NestjsQueryGraphQLModule, PagingStrategies } from '@nestjs-query/query-graphql';
import { NestjsQueryMongooseModule } from '@nestjs-query/query-mongoose';
import { ColorDTO, Room, RoomDTO, RoomSchema, Wall, WallDTO, ColorEntity, WallSchema, ColorSchema } from './test.entity';

@Module({
  imports: [
    NestjsQueryGraphQLModule.forFeature({
      // import the NestjsQueryTypeOrmModule to register the entity with typeorm
      // and provide a QueryService
      imports: [
        NestjsQueryMongooseModule.forFeature([
          { document: Room, name: Room.name, schema: RoomSchema },
          { document: Wall, name: Wall.name, schema: WallSchema },
          { document: ColorEntity, name: ColorEntity.name, schema: ColorSchema },
        ]),
      ],
      // describe the resolvers you want to expose
      resolvers: [
        { DTOClass: RoomDTO, EntityClass: Room, pagingStrategy: PagingStrategies.NONE },
        { DTOClass: WallDTO, EntityClass: Wall, pagingStrategy: PagingStrategies.NONE },
        { DTOClass: ColorDTO, EntityClass: ColorEntity, pagingStrategy: PagingStrategies.NONE },
      ],
    }),
  ],

  providers: [],
})
export class RoomModule {}

The main problem is, that the nested relation of WallDTO (i.e. color) is not populated when querying for the RoomDTO although all GraphQL schemes are correctly generated. I can manually write a service that populates the subfields walls.colorId via mongoose but I initially expected the @Relation decorator also to work for nested schemas.

Maybe I am missing something and it is already working with the current features. I appreciate any feedback. Cheers!

Edit: I know that it would be possible to put Wall into its own collection but here I am seeking to explicitly leave it in theRoom collection as a subfield.

h4rm commented 3 years ago

Hey guys,

just a little update from my testing around. I think the @Relation decorator only works on the top-most level. The workaround I found so war is to have a custom Service that overrides the Mongoose query and model the Relation as a simple @Field:

Here are the entities:

import { Connection, FilterableField, KeySet, Relation } from '@nestjs-query/query-graphql';
import { ObjectType, Field, InputType } from '@nestjs/graphql';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { SchemaTypes, Types } from 'mongoose';
import { BaseDTO, BaseEntityMongoose } from '../BaseEntity';
import { Document } from '../document/document.entity';

@Schema({ timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } })
export class ColorEntity extends Document {
  @Prop({ required: true })
  name: string;
}
export const ColorSchema = SchemaFactory.createForClass(ColorEntity);

@ObjectType('Color')
@InputType('ColorInput')
@KeySet(['id'])
export class ColorDTO extends BaseDTO {
  @Field()
  name: string;
}

@Schema({ timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } })
export class Wall extends Document {
  @Prop({ required: true })
  name: string;
  @Prop({ type: SchemaTypes.ObjectId, ref: 'ColorEntity', required: false })
  colorId: Types.ObjectId;
}
export const WallSchema = SchemaFactory.createForClass(Wall);
WallSchema.virtual('color', {
  ref: 'ColorEntity', // The model to use
  localField: 'colorId', // Find people where `localField`
  foreignField: '_id', // is equal to `foreignField`,
  justOne: true,
});
@ObjectType('Wall')
@InputType('WallInput')
@KeySet(['id'])
// @Relation('color', () => ColorDTO, {
//   disableRemove: true,
//   relationName: 'color',
//   nullable: true,
// })
export class WallDTO extends BaseDTO {
  @Field()
  name: string;
  @Field({ nullable: true })
  colorId?: string;
  @Field(() => ColorDTO, { nullable: true })
  color?: ColorDTO;
}

@Schema({ timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } })
export class Room extends Document {
  @Prop({ type: [WallSchema], default: [] })
  walls: Wall[];
}
export const RoomSchema = SchemaFactory.createForClass(Room);

@ObjectType('Room')
@KeySet(['id'])
export class RoomDTO extends BaseDTO {
  @Field(() => [WallDTO])
  walls: WallDTO[];
}

Here is the Module:

import { Module } from '@nestjs/common';
import { NestjsQueryGraphQLModule, PagingStrategies } from '@nestjs-query/query-graphql';
import { NestjsQueryMongooseModule } from '@nestjs-query/query-mongoose';
import { ColorDTO, Room, RoomDTO, RoomSchema, Wall, WallDTO, ColorEntity, WallSchema, ColorSchema } from './test.entity';
import { RoomService } from './test.service';

@Module({
  imports: [
    NestjsQueryGraphQLModule.forFeature({
      imports: [
        NestjsQueryMongooseModule.forFeature([
          { document: Room, name: Room.name, schema: RoomSchema },
          { document: Wall, name: Wall.name, schema: WallSchema },
          { document: ColorEntity, name: ColorEntity.name, schema: ColorSchema },
        ]),
      ],
      resolvers: [
        { DTOClass: RoomDTO, EntityClass: Room, ServiceClass: RoomService, pagingStrategy: PagingStrategies.NONE },
        { DTOClass: WallDTO, EntityClass: Wall, pagingStrategy: PagingStrategies.NONE },
        { DTOClass: ColorDTO, EntityClass: ColorEntity, pagingStrategy: PagingStrategies.NONE },
      ],
      services: [RoomService],
    }),
  ],

  providers: [],
})
export class RoomModule {}

And here is the Service

import { QueryService } from '@nestjs-query/core';
import { InjectModel } from '@nestjs/mongoose';
import { MongooseQueryService } from '@nestjs-query/query-mongoose';
import { Model, Query } from 'mongoose';
import { Room } from './test.entity';

@QueryService(Room)
export class RoomService extends MongooseQueryService<Room> {
  constructor(@InjectModel(Room.name) private model: Model<Room>) {
    super(model);
  }

  async query(query: any): Promise<Room[]> {
    const res = await this.model.find({}).populate('walls.color').exec();
    return res;
  }
}
smolinari commented 3 years ago

@h4rm - The auto-populate plugin might be of interest to you.

https://plugins.mongoosejs.io/plugins/autopopulate

Scott

h4rm commented 3 years ago

Edit: Added the plugin to the schema but it's not automatically populating so far.

@Schema({ timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } })
export class Wall extends Document {
  @Prop({ required: true })
  name: string;
  @Prop({ type: SchemaTypes.ObjectId, ref: 'ColorEntity', required: false })
  colorId: Types.ObjectId;
}
export const WallSchema = SchemaFactory.createForClass(Wall);
WallSchema.virtual('color', {
  ref: 'ColorEntity', // The model to use
  localField: 'colorId', // Find people where `localField`
  foreignField: '_id', // is equal to `foreignField`,
  justOne: true,
  autopopulate: true,
});
// eslint-disable-next-line @typescript-eslint/no-var-requires
WallSchema.plugin(require('mongoose-autopopulate'));
h4rm commented 3 years ago

It seems that the autopopulate plugin is not helping here because it also cannot work with nested Schemas as stated here: https://github.com/mongodb-js/mongoose-autopopulate/issues/26

smolinari commented 3 years ago

Ah. Bummer that is. Sorry for sending you on a goose chase. @h4rm

Scott

h4rm commented 3 years ago

So I think the service approach works best for now and I am happy with the solution.

smolinari commented 3 years ago

@h4rm - Can you share some code that shows what you mean?

Scott