Skip to content
/ type-mongodb Public

A simple & very fast decorator based MongoDB class mapper.

License

Notifications You must be signed in to change notification settings

j/type-mongodb

Folders and files

NameName
Last commit message
Last commit date
Aug 23, 2021
Nov 30, 2022
Nov 30, 2022
Nov 30, 2022
May 26, 2021
Jun 30, 2020
Feb 10, 2020
Oct 9, 2023
Aug 23, 2021
Oct 9, 2023
Dec 1, 2022
Jun 1, 2021
Jun 1, 2021
Jun 1, 2021

Repository files navigation

🔗 type-mongodb

A simple @decorator based MongoDB ODM.

type-mongodb makes it easy to map classes to MongoDB documents and back using @decorators.

Features

  • Extremely simply @Decorator() based document mapping
  • Very fast 🚀! (thanks to JIT compilation)
  • RAW. MongoDB is already extremely easy to use. It's best to use the driver as it's intended. No validation, no change-set tracking, no magic -- just class mapping
  • Custom Repositories
  • Event Subscribers
  • Transaction Support
  • Discriminator Mapping
  • & more!

How to use

type-orm allows you to create a base document class for common functionality. Notice that we don't enforce strict types. MongoDB is "schema-less", so we've decided to just support their main types and not do anything fancy. Again, we wanted to keep it as close to the core driver as possible.

import { Id, Field } from 'type-mongodb';
import { ObjectId } from 'mongodb';

abstract class BaseDocument {
  @Id()
  _id: ObjectId;

  get id(): string {
    return this._id.toHexString();
  }

  @Field()
  createdAt: Date = new Date();

  @Field()
  updatedAt: Date = new Date();
}

Now create our document class with some fields.

import { Document, Field } from 'type-mongodb';
import { BaseDocument, Address, Pet } from './models';

@Document()
class User extends BaseDocument {
  @Field()
  name: string;

  @Field(() => Address)
  address: Address; // single embedded document

  @Field(() => [Address])
  addresses: Address[] = []; // array of embedded documents

  @Field(() => [Pet])
  pets: Pet[] = []; // array of discriminator mapped documents

  @Field(() => [Pet])
  favoritePet: Pet = []; // single discriminator mapped document
}

And here's the embedded Address document.

import { Field } from 'type-mongodb';

class Address {
  @Field()
  city: string;

  @Field()
  state: string;
}

type-mongodb also has support for discriminator mapping (polymorphism). You do this by creating a base class mapped by @Discriminator({ property: '...' }) with a @Field() with the name of the "property". Then decorate discriminator types with @Discriminator({ value: '...' }) and type-mongodb takes care of the rest.

import { Discriminator, Field } from 'type-mongodb';

@Discriminator({ property: 'type' })
abstract class Pet {
  @Field()
  abstract type: string;

  @Field()
  abstract sound: string;

  speak(): string {
    return this.sound;
  }
}

@Discriminator({ value: 'dog' })
class Dog extends Pet {
  type: string = 'dog';
  sound: string = 'ruff';

  // dog specific fields & methods
}

@Discriminator({ value: 'cat' })
class Cat extends Pet {
  type: string = 'cat';
  sound: string = 'meow';

  // cat specific fields & methods
}

And now, lets see the magic!

import { DocumentManager } from 'type-mongodb';
import { User } from './models';

async () => {
  const dm = await DocumentManager.create({
    connection: {
      uri: process.env.MONGO_URI,
      database: process.env.MONGO_DB
    },
    documents: [User]
  });

  const repository = dm.getRepository(User);

  await repository.create({
    name: 'John Doe',
    address: {
      city: 'San Diego',
      state: 'CA'
    },
    addresses: [
      {
        city: 'San Diego',
        state: 'CA'
      }
    ],
    pets: [{ type: 'dog', sound: 'ruff' }],
    favoritePet: { type: 'dog', sound: 'ruff' }
  });

  const users = await repository.find().toArray();
};

What about custom repositories? Well, that's easy too:

import { Repository } from 'type-mongodb';
import { User } from './models';

export class UserRepository extends Repository<User> {
  async findJohnDoe(): Promise<User> {
    return this.findOneOrFail({ name: 'John Doe' });
  }
}

Then register this repository with the User class:

import { DocumentRepository } from 'type-mongodb';
import { UserRepository } from './repositories';
// ...

@Document({ repository: () => UserRepository })
class User extends BaseDocument {
  // for type inference when using `getRepository`
  [DocumentRepository]: UserRepository;
  
  // ...
}

... and finally, to use:

const repository = dm.getRepository(User); // repository typed as UserRepository

What about custom IDs? You can either create your own type that extends Type, or use our built-ins:

import { Id, Field, UUIDType } from 'type-mongodb';

@Document()
class User {
  @Id({ type: UUIDType })
  _id: string;

  // fields can also be a "UUID" type.
  @Field({
    type: UUIDType /* create: true (pass this to auto-generate the uuid, otherwise, omit) */
  })
  uuid: string;
}

What about events? We want the base class to have createdAt and updatedAt be mapped correctly.

import {
  EventSubscriber,
  DocumentManager,
  InsertEvent,
  UpdateEvent
} from 'type-mongodb';
import { BaseDocument } from './models';

export class TimestampableSubscriber implements EventSubscriber<BaseDocument> {
  // Find all documents that extend BaseDocument
  getSubscribedDocuments?(dm: DocumentManager): any[] {
    return dm
      .filterMetadata(
        (meta) => meta.DocumentClass.prototype instanceof BaseDocument
      )
      .map((meta) => meta.DocumentClass);
  }

  beforeInsert(e: InsertEvent<BaseDocument>) {
    if (!e.model.updatedAt) {
      e.model.updatedAt = new Date();
    }

    if (!e.model.createdAt) {
      e.model.createdAt = new Date();
    }
  }

  beforeUpdate(e: UpdateEvent<BaseDocument>) {
    this.prepareUpdate(e);
  }

  beforeUpdateMany(e: UpdateEvent<BaseDocument>) {
    this.prepareUpdate(e);
  }

  prepareUpdate(e: UpdateEvent<BaseDocument>) {
    e.update.$set = {
      updatedAt: new Date(),
      ...(e.update.$set || {})
    };

    e.update.$setOnInsert = {
      createdAt: new Date(),
      ...(e.update.$setOnInsert || {})
    };
  }
}

...then register TimestampableSubscriber:

const dm = await DocumentManager.create({
  /// ...,
  subscribers: [TimestampableSubscriber]
});

Other Common Features

// custom collection and database
@Document({ database: 'app', collection: 'users' })

// using internal hydration methods
dm.toDB(User, user);
dm.fromDB(User, { /* document class */ });
dm.init(User, { /* user props */ });
dm.merge(User, user, { /* user props */ });

For more advanced usage and examples, check out the tests.

About

A simple & very fast decorator based MongoDB class mapper.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages