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

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

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