Embedded

You can declare a property as embedded to embed a document inside another document.

@Definition()
export class Book {
  @Id()
  id: ObjectId;
 
  @Property()
  name: string;
}
 
@Definition()
export class Author {
  @Id()
  id: ObjectId;
 
  @Property()
  name: string;
 
  @Embedded(() => Book)
  books: Book[];
}
💡

You should not include embedded definitions in the definitions array of the DryerModule.register method.

// app.module.ts
@Module({
  imports: [
    // will be other imports as well
    DryerModule.register({
      definitions: [
        {
          definition: Author,
          embeddedConfigs: [
            {
              property: 'books',
              allowedApis: ['create', 'update', 'remove', 'findOne', 'findAll'],
            }
          ],
        }
      ],
    }),
  ],
})
export class AppModule {}

Generated APIs

Create API

You will be able to create an author with books.

mutation {
  createAuthor(input: {
    name: "Joanne Rowling"
    books: [
      { name: "The Philosopher's Stone" }
      { name: "The Chamber of Secrets" }
    ]
  }) {
    id
    name
    books {
      id
      name
    }
  }
}

Update API

You will be able to update author and books as well.

mutation {
  updateAuthor(input: {
    id: "000000000000000000000000",
    name: "J. K. Rowling"
    books: [
      {
        id: "111111111111111111111111",
        name: "Harry Potter and the Philosopher's Stone"
      }
    ]
  }) {
    id
    name
    books {
      id
      name
    }
  }
}
💡

If there are more books in the database than the ones you are sending, the extra books will be removed. To avoid this behavior you can include all the books in the update or use the updateAuthorBooks API.

Create embedded API

mutation {
  createAuthorBooks(
    authorId: "000000000000000000000000"
    inputs: [
      { name: "The Prisoner of Azkaban" }
      { name: "The Goblet of Fire" }
    ]
  ) {
    id
    name
  }
}
💡

Only newly created books will be returned.

Update embedded API

mutation {
  updateAuthorBooks(
    authorId: "000000000000000000000000"
    inputs: [
      { id: "111111111111111111111111", name: "Harry Potter and the Prisoner of Azkaban" }
      { id: "222222222222222222222222", name: "Harry Potter and the Goblet of Fire" }
    ]
  ) {
    id
    name
  }
}
💡

Only books with the listed IDs will be returned, other books will remain unchanged but won't be included in the response.

Remove embedded API

mutation {
  removeAuthorBooks(
    authorId: "000000000000000000000000"
    ids: [
      "111111111111111111111111",
      "222222222222222222222222"
    ]
  ) {
    success
  }
}

Find one embedded API

query {
  authorBook(
    authorId: "000000000000000000000000"
    id: "111111111111111111111111"
  ) {
    id
    name
  }
}

Find all embedded API

query {
  authorBooks(
    authorId: "000000000000000000000000"
  ) {
    id
    name
  }
}

Object embedded

You can also embed an object. There will be no embedded APIs for this case, but you will be able to create and update the object from the parent document.

@Definition()
class Brand {
  @Property()
  name: string;
}
 
@Definition()
export class Car {
  @Id()
  id: ObjectId;
 
  @Property()
  name: string;
 
  @Embedded(() => Brand)
  brand: Brand;
}

You can disable _id for the embedded object by passing schemaOptions: { _id: false } to the @Definition decorator.

@Definition({ schemaOptions: { _id: false } })
export class Money {
  @Property({ type: () => graphql.GraphQLInt })
  amount: number;
 
  @Property()
  currency: string;
}

Multi-level Embedded

You can embed documents inside other embedded documents but the embedded APIs will only be generated for the first level. As on the example below, Author will have embedded Books and Books will have embedded Reviews.

@Definition()
export class Review {
  @Id()
  id: ObjectId;
 
  @Property()
  content: string;
}
 
@Definition()
export class Book {
  @Id()
  id: ObjectId;
 
  @Property()
  name: string;
 
  @Embedded(() => Review)
  reviews: Review[];
}
 
@Definition()
export class Author {
  @Id()
  id: ObjectId;
 
  @Property()
  name: string;
 
  @Embedded(() => Book)
  books: Book[];
}

Authentication

💡

You should read the Authentication section before this one.

You can add any decorators for embedded APIs not just Authentication related decorators.

@Injectable()
export class MustBeUser implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    return (
      GqlExecutionContext.create(context)
        .getContext()
        .req.header('user-role') === 'user'
    );
  }
}
 
@Definition()
export class Author {
  @Embedded(() => Book)
  books: Book[];
}
 
@Module({
  imports: [
    // will be other imports as well
    DryerModule.register({
      definitions: [
        {
          definition: Author,
          embeddedConfigs: [
            {
              allowedApis: ['create', 'update', 'remove', 'findOne', 'findAll'],
              decorators: { remove: UseGuards(MustBeUser) },
              property: 'books',
            },
          ],
        }
      ],
    }),
  ],
})
export class AppModule {}

The example above will add the @UseGuards(MustBeUser) decorator to the removeAuthorBooks resolver. You can add decorators for other APIs as well. The decorators object has the following structure:

decorators?: {
  default?: MethodDecorator | MethodDecorator[];
  write?: MethodDecorator | MethodDecorator[];
  read?: MethodDecorator | MethodDecorator[];
  findOne?: MethodDecorator | MethodDecorator[];
  findAll?: MethodDecorator | MethodDecorator[];
  remove?: MethodDecorator | MethodDecorator[];
  update?: MethodDecorator | MethodDecorator[];
  create?: MethodDecorator | MethodDecorator[];
};

You can use default to add decorators to all APIs, write to add decorators to all write APIs (create, update and remove), read to add decorators to all read APIs (findOne and findAll). There is a priority order for the fields. For example, remove will have priority over write and write will have priority over default.

Custom Schema

There is a onSchema option that you can use to customize the schema of the embedded document.

import { UseGuards } from '@nestjs/common';
 
@Definition()
export class Author {
  @Embedded(() => Book, {
    onSubSchema: (schema) => {
      schema.virtual('fullname').get(function() {
        return this.name;
      });
    }
  })
  books: Book[];
}

Override Default Options

You can use overridePropertyOptions to override the property options of the embedded property.

@Definition()
export class Car {
  @Id()
  id: ObjectId;
 
  @Property()
  name: string;
 
  @Embedded(() => Brand, {
    overridePropertyOptions: {
      create: { nullable: false },
      output: { nullable: false },
      db: { required: true },
    },
  })
  brand: Brand;
}

By default, the embedded property is nullable for create and output and not required for db. With the example above, the embedded property will be required for create and output and db.