# Mongoose

This tutorial shows you how you can use mongoose package with Ts.ED.

# Features

Currently, @tsed/mongoose (opens new window) allows you to:

  • Configure one or more MongoDB database connections via the @ServerSettings configuration. All databases will be initialized when the server starts during the server's OnInit phase.
  • Declare a Model from a class with annotation,
  • Add a plugin, PreHook method and PostHook on your model
  • Inject a Model to a Service, Controller, Middleware, etc.
  • Create and manage multiple connections

Note

@tsed/mongoose uses the JsonSchema and its decorators to generate the mongoose schema.

# Installation

Before using the @tsed/mongoose package, we need to install the mongoose (opens new window) module.

npm install --save mongoose
npm install --save @tsed/mongoose
npm install --save-dev @tsed/testing-mongoose
1
2
3

WARNING

Since mongoose v5.11.0, the module expose his own file definition and can broke your build! To solve it, install @tsed/mongoose v6.14.1 and remove the @types/mongoose dependencies.

Then import @tsed/mongoose in your Configuration:

    # MongooseService

    MongooseService lets you to retrieve an instance of Mongoose.Connection.

    import {Service} from "@tsed/common";
    import {MongooseService} from "@tsed/mongoose";
    
    @Service()
    export class MyService {
      constructor(mongooseService: MongooseService) {
        const default = mongooseService.get(); // OR mongooseService.get("default");
        // GET Other connection
        const db2 = mongooseService.get('db2');
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    # Decorators

    Ts.ED gives some decorators and services to write your code:

    You can also use the common decorators to describe model (See models documentation):

    # Declaring a Mongoose object (schema or model)

    # Declaring a Model

    @tsed/mongoose works with models which must be explicitly declared.

    import {Property} from "@tsed/schema";
    import {Model, ObjectID} from "@tsed/mongoose";
    
    @Model()
    export class MyModel {
      @ObjectID("id")
      _id: string;
    
      @Property()
      unique: string;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    # Declaring a Model to a specific connection

    import {Property} from "@tsed/schema";
    import {Model, ObjectID} from "@tsed/mongoose";
    
    @Model({
      connection: "db2"
    })
    export class MyModel {
      @ObjectID("id")
      _id: string;
    
      @Property()
      unique: string;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    # Declaring a Schema

    @tsed/mongoose supports subdocuments which must be explicitly declared.

    import {Property} from "@tsed/schema";
    import {Schema} from "@tsed/mongoose";
    
    @Schema()
    export class MyModel {
      @Property()
      unique: string;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

    TIP

    Schema decorator accepts a second parameter to configure the Schema (See Mongoose Schema (opens new window))

    # Declaring Properties

    By default, @tsed/mongoose reuses the metadata stored by the decorators dedicated to describe a JsonSchema. These decorators come from the @tsed/common package.

    import {Default, Enum, Format, Ignore, Maximum, MaxLength, Minimum, MinLength, Pattern, Required} from "@tsed/schema";
    import {Indexed, Model, ObjectID, Unique} from "@tsed/mongoose";
    
    enum Categories {
      CAT1 = "cat1",
      CAT2 = "cat2"
    }
    
    @Model()
    export class MyModel {
      @Ignore() // exclude _id from mongoose in the generated schema
      _id: string;
    
      @ObjectID("id") // Or rename _id by id (for response sent to the client)
      _id: string;
    
      @Unique()
      @Required()
      unique: string;
    
      @Indexed()
      @MinLength(3)
      @MaxLength(50)
      indexed: string;
    
      @Minimum(0)
      @Maximum(100)
      @Default(0)
      rate: Number = 0;
    
      @Enum(Categories)
        // or @Enum("type1", "type2")
      category: Categories;
    
      @Pattern(/[a-z]/) // equivalent of match field in mongoose
      pattern: String;
    
      @Format("date-time")
      @Default(Date.now)
      dateCreation: Date = new Date();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41

    TIP

    It isn't necessary to use Property decorator on property when you use one of these decorators:

    These decorators call automatically the Property decorator.

    # Collections

    Mongoose and @tsed/mongoose support both lists and maps.

    import {CollectionOf} from "@tsed/schema";
    import {Model} from "@tsed/mongoose";
    
    @Model()
    export class MyModel {
      @CollectionOf(String)
      list: string[];
    
      @CollectionOf(String)
      map: Map<string, string>; // key must be a string.
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    # Subdocuments

    @tsed/mongoose supports mongoose subdocuments as long as they are defined schemas. Therefore, subdocuments must be decorated by @Schema().

    import {CollectionOf, Property} from "@tsed/schema";
    import {Model, ObjectID, Schema} from "@tsed/mongoose";
    
    @Schema()
    export class MySchema {
      @ObjectID("id")
      _id: string;
    
      @Property()
      name: string;
    }
    
    @Model()
    export class MyModel {
      @ObjectID("id")
      _id: string;
    
      @Property()
      schema: MySchema;
    
      @CollectionOf(MySchema)
      schemes: MySchema[];
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    # References

    @tsed/mongoose supports mongoose references between defined models.

    import {Model, ObjectID, Ref} from "@tsed/mongoose";
    
    @Model()
    export class MyRef {
      @ObjectID("id")
      _id: string;
    }
    
    @Model()
    export class MyModel {
      @Ref(MyRef)
      ref: Ref<MyRef>;
    
      @Ref(MyRef)
      refs: Ref<MyRef>[];
    
      @Ref(MyRef)
      refs: Map<string, MyRef>;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    # Circular References

    @tsed/mongoose supports mongoose circular references between defined models. When you have models that either both refer to each other, or refer to themselves there is a slightly different way to declare this inside those models.

    In this example a Customer has many Contracts and each Contract has a reference back to the Customer. This is declared using an arrow function. () => ModelName

    import {Required, CollectionOf} from "@tsed/schema";
    import {Model, ObjectID} from "@tsed/mongoose";
    
    
    @Model()
    export class Customer {
      @ObjectID('id')
      _id: string;
    
      @Property()
      name: string;
    
      @Ref(() => Contract)
      @CollectionOf(() => Contract)
      contracts?: Ref<Contract>[];
    }
    
    @Model()
    export class Contract {
      
      @ObjectID('id')
      _id: string;
    
      @Ref(() => Customer)
      customer: Ref<Customer>
    
      @Required()
      contractName: string;
    
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31

    # Virtual References

    @tsed/mongoose supports mongoose virtual references between defined models.

    The same rules for Circular References apply (See above);

    import {Property} from "@tsed/schema";
    import {Model, Ref, VirtualRef, VirtualRefs} from "@tsed/mongoose";
    
    @Model()
    class Person {
      @Property()
      name: string;
    
      @Property()
      band: string;
    }
    
    @Model()
    class Band {
      @VirtualRef({
        ref: Person, // The model to use
        localField: "name",  // Find people where `localField`
        foreignField: "band", // is equal to `foreignField`
        // If `justOne` is true, 'members' will be a single doc as opposed to
        // an array. `justOne` is false by default.
        justOne: false,
        options: {} // Query options, see http://bit.ly/mongoose-query-options
      })
      members: VirtualRefs<Person>;
    }
    
    @Model()
    export class MyRef {
      @VirtualRef({ref: "MyModel", justOne: true})
      virtual: VirtualRef<MyModel>;
    
      @VirtualRef("MyModel")
      virtuals: VirtualRefs<MyModel>;
    }
    
    @Model()
    export class MyModel {
      @Ref(MyRef)
      ref: Ref<MyRef>;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40

    # Dynamic References

    @tsed/mongoose supports mongoose dynamic references between defined models.

    This works by having a field with the referenced object model's name and a field with the referenced field.

    import {DynamicRef, Model, Ref} from "@tsed/mongoose";
    import {Enum} from "@tsed/schema";
    import {ModelA} from "./modelA";
    import {ModelB} from "./ModelB";
    
    @Model()
    export class MyModel {
      @DynamicRef("type")
      dynamicRef: Ref<ModelA | ModelB>;
    
      @Enum("Mode lA", "ModelB")
      type: string; // This field has to match the referenced model's name
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    # Register hook

    Mongoose allows the developer to add pre and post hooks / middlewares (opens new window) to the schema. With this it is possible to add document transformations and observations before or after validation, save and more.

    Ts.ED provides class decorator to register middlewares on the pre and post hook.

    # Pre hook

    We can simply attach a PreHook decorator to the model class and define the hook function like we would normally do in Mongoose.

    import {Required} from "@tsed/schema";
    import {Model, ObjectID, PreHook} from "@tsed/mongoose";
    
    @Model()
    @PreHook("save", (car: CarModel, next: any) => {
      if (car.model === "Tesla") {
        car.isFast = true;
      }
      next();
    })
    export class CarModel {
      @ObjectID("id")
      _id: string;
    
      @Required()
      model: string;
    
      @Required()
      isFast: boolean;
    
      // or Prehook on static method
      @PreHook("save")
      static preSave(car: CarModel, next: any) {
        if (car.model === "Tesla") {
          car.isFast = true;
        }
        next();
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29

    This will execute the pre-save hook each time a CarModel document is saved.

    # Post hook

    We can simply attach a PostHook decorator to the model class and define the hook function like we would normally do in Mongoose.

    import {Required} from "@tsed/schema";
    import {Model, ObjectID, PostHook} from "@tsed/mongoose";
    
    @Model()
    @PostHook("save", (car: CarModel) => {
      if (car.topSpeedInKmH > 300) {
        console.log(car.model, "is fast!");
      }
    })
    export class CarModel {
      @ObjectID("id")
      _id: string;
    
      @Required()
      model: string;
    
      @Required()
      isFast: boolean;
    
      // or Prehook on static method
      @PostHook("save")
      static postSave(car: CarModel) {
        if (car.topSpeedInKmH > 300) {
          console.log(car.model, "is fast!");
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    This will execute the post-save hook each time a CarModel document is saved.

    # Plugin

    Using the Unable to find something: symbolName === "Plugin" decorator enables the developer to attach various Mongoose plugins to the schema. Just like the regular schema.plugin() call, the decorator accepts 1 or 2 parameters: the plugin itself, and an optional configuration object. Multiple plugin decorator can be used for a single model class.

    import {Inject, Service} from "@tsed/common";
    import {Model, MongooseModel, MongoosePlugin} from "@tsed/mongoose";
    import * as findOrCreate from "mongoose-findorcreate";
    import {User} from "./User";
    
    @Model()
    @MongoosePlugin(findOrCreate)
    class UserModel {
      // this isn't the complete method signature, just an example
      static findOrCreate(condition: InstanceType<User>):
        Promise<{doc: InstanceType<User>, created: boolean}>;
    }
    
    @Service()
    class UserService {
      constructor(@Inject(UserModel) userModel: MongooseModel<UserModel>) {
        userModel
          .findOrCreate({...})
          .then(findOrCreateResult => {
            // ...
          });
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    # Inject model

    It's possible to inject a model into a Service (or Controller, Middleware, etc...):

    import {Inject, Injectable} from "@tsed/common";
    import {MongooseModel} from "@tsed/mongoose";
    import {MyModel} from "./models/MyModel";
    
    @Injectable()
    export class MyRepository {
      @Inject(MyModel) private model: MongooseModel<MyModel>;
    
      async save(obj: MyModel): Promise<MongooseModel<MyModel>> {
        const doc = new this.model(obj);
        await doc.save();
    
        return doc;
      }
    
      async find(query: any) {
        const list = await this.model.find(query).exec();
    
        console.log(list);
    
        return list;
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    TIP

    You can find a working example on Mongoose here (opens new window).

    # Caveat v6.14.4

    Mongoose doesn't return a real instance of your class. If you inspected the returned item by one of mongoose's methods, you'll see that the instance is as Model type instead of the expected class:

    import {Inject, Injectable} from "@tsed/common";
    import {MongooseModel} from "@tsed/mongoose";
    import {Product} from "./models/Product";
    
    @Injectable()
    export class MyRepository {
      @Inject(Product) 
      private model: MongooseModel<Product>;
    
      async find(query: any) {
        const list = await this.model.find(query).exec();
    
        console.log(list[0]); // Model { Product }
    
        return list;
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    There is no proper solution currently to have the expected instance without transforming the current instance to the class with the deserialize function.

    To simplify this, Ts.ED adds a toClass method to the MongooseModel to find, if necessary, an instance of type Product.

    import {Inject, Injectable} from "@tsed/common";
    import {MongooseModel} from "@tsed/mongoose";
    import {Product} from "./models/Product";
    
    @Injectable()
    export class MyRepository {
      @Inject(Product) 
      private model: MongooseModel<Product>;
    
      async find(query: any) {
        const list = await this.model.find(query).exec();
    
        console.log(list[0]); // Model { Product }
        console.log(list[0].toClass()); // Product {}
    
        return list;
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    # Testing beta

    The package @tsed/testing-mongoose (opens new window) allows you to test your server with a memory database.

    TIP

    This package uses the amazing mongodb-memory-server (opens new window) to mock the mongo database.

    # Testing API

    This example shows you how you can test your Rest API with superagent and a mocked Mongo database:

      TIP

      To increase mocha timeout from 2000ms to 10000ms use option --timeout 10000.

      # Jest additional setup

      Add a script to close connection after all unit test. In your jest configuration file add the following line:

      {
        "globalTeardown": "./scripts/jest/teardown.js"
      }
      
      1
      2
      3

      And create the script with the following content:

      module.exports = async () => {
        await global.__MONGOD__ && global.__MONGOD__.stop();
      };
      
      1
      2
      3

      # Testing Model

      This example shows you how can test the model:

        # Author

          # Maintainers

            Last Updated: 4/14/2021, 10:01:59 AM

            Other topics