Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

hydrate-mongodb

Package Overview
Dependencies
Maintainers
1
Versions
71
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

hydrate-mongodb

An Object Document Mapper (ODM) for MongoDB.

  • 0.5.0
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
13
increased by18.18%
Maintainers
1
Weekly downloads
 
Created
Source

Build Status

Hydrate

An Object/Document Mapping (ODM) framework for Node.js and MongodDB

Hydrate provides a means for developers to map Node.js classes to documents stored in a MongoDB database. Developers can work normally with objects and classes, and Hydrate takes care of the onerous details such as serializing classes to documents, validation, mapping of class inheritance, optimistic locking, fetching of references between database collections, change tracking, and managing of persistence through bulk operations.

Hydrate is inspired by other projects including JPA, Hibernate, and Doctrine.

NOTICE: Hydrate is an experimental project and is not recommended for production systems. It is also possible that breaking changes may be introduced in the API until version 1.0.0 is reached.

Idiomatic Javascript

Hydrate has no requirements for how persistent classes are declared. Developers can work with standard JavasScript idioms, such as constructor functions and ES6 classes. Furthermore, no base class is required for creating persistent classes.

TypeScript support

TypeScript is a superset of JavaScript that includes type information and compiles to regular JavaScript. If you choose to use TypeScript in your projects, this type information can be used to create the mappings between your classes and the MongoDB documents, reducing duplicate work. However, TypeScript is not required and you can use Hydrate with plain JavaScript.

Decorator support

Decorators are a method of annotating classes and properties in JavaScript at design time. There is currently a proposal to include decorators as a standard part of JavaScript in ES7. In the meantime, several popular transpilers including Babel and TypeScript make decorators available for use now. Hydrate gives developers the option to leverages decorators as simple means to describe persistent classes.

Familiar API

Hydrate uses a session-based approach to the persistence API similar to Hibernate ORM. Developers familiar with this approach should feel at home with Hydrate. Furthermore, Hydrate's query API is kept as similar as possible to the MongoDB native Node.js driver.

High performance

MongoDB bulk write operations are used to synchronize changes with the database, which can result in significant performance gains.

Installation

Hydrate requires a minimum of MongoDB 2.6 and Node 4.0. Once these dependencies are installed, Hydrate can be installed using npm:

$ npm install hydrate-mongodb --save

Getting Started

For brevity, the example here is only given in TypeScript. JavaScript examples coming soon.

Defining a Model

In this example we'll model a task list. We create a file, model.ts, defining entities Task and Person. We also define an enumeration used to indicate the status of a task on the task list.

model.ts:

import {Entity, Field} from "hydrate-mongodb";

export enum TaskStatus {

    Pending,
    Completed,
    Archived
}

@Entity()
export class Person {

    @Field()
    name: string;
    
    constructor(name: string) {

        this.name = name;
    }
}

@Entity()
export class Task {

    @Field()
    text: string;

    @Field()
    status: TaskStatus;

    @Field()
    created: Date;
    
    @Field()
    assigned: Person;

    constructor(text: string) {

        this.created = new Date();
        this.status = TaskStatus.Pending;
        this.text = text;
    }

    archive(): boolean {

        if(this.status == TaskStatus.Completed) {
            this.status = TaskStatus.Archived;
            return true;
        }
        return false;
    }
}

Configuring Hydrate

Once our model is defined, we need to tell Hydrate about it. We do this by adding the model to an AnnotationMappingProvider, then adding the mapping provider to the Configuration.

server.ts:

import {MongoClient} from "mongodb";
import {Configuration, AnnotationMappingProvider} from "hydrate-mongodb";
import * as model from "./model";

var config = new Configuration();
config.addMapping(new AnnotationMappingProvider(model));

Connecting to MongoDB

We use the standard MongoDB native driver to establish a connection to MongoDB. Once the connection is open, we create a SessionFactory using the MongoDB connection and the previously defined Configuration.

server.ts (con't):


MongoClient.connect('mongodb://localhost/mydatabase', (err, db) => {
    if(err) throw err;
    
    config.createSessionFactory(db, (err, sessionFactory) => {        
        ...
    });
});

Creating a Session

A Hydrate Session should not be confused with the web-server session. The Hydrate Session is analogous to JPA's EntityManager, and is responsible for managing the lifecycle of persistent entities.

Typically the SessionFactory is created once at server startup and then used to create a Session for each connection to the server. For example, using a Session in an Express route might look something like this:


app.get('/', function (req, res, next) {
    
    var session = sessionFactory.createSession();

    ...
   
    session.close(next); 
}); 

Calling close on the Session persists any changes to the database and closes the Session. Call flush instead to persist any changes without closing the Session.

Working with Persistent Objects

In order to create a new Task we instantiate the task and then add it to the Session by calling save.

var task = new Task("Take out the trash.");
session.save(task);

To find a task by identifier we use find.

session.find(Task, id, (err, task) => {
    ...
});

To find all tasks that have not yet been completed, we can use the query method.

session.query(Task).findAll({ status: TaskStatus.Pending }, (err, tasks) => {
    ...
});

Below is an example of finding all tasks assigned to a specific person. Note that even though person is an instance of the Person entity which is serialized as an ObjectId in the task collection, there is no need to pass the identifier of the person directly to the query.


session.find(Person, personId, (err, person) => {
    ...
    
    session.query(Task).findAll({ assigned: person }, (err, tasks) => {
        ...
    });
});    

Hydrate provides a mechanism to retrieve references between persistent entities. We do this using fetch. Note that fetch uses the same dot notation that MongoDB uses for queries.

For example, say we wanted to fetch the Person that a Task is assigned to.

session.fetch(task, "assigned", (err) => {
        
    console.log(task.assigned.name); // prints the name of the Person
});

The fetch method can be used in conjunction with queries as well.

session.find(Task, id).fetch("assigned", (err, task) => {
    ...
});

Modeling

In TypeScript, the emitDecoratorMetadata and experimentalDecorators options must be enabled on the compiler.

Entities

Entities are classes that map to a document in a MongoDB collection.

@Entity()
export class Person {

    @Field()
    name: string;
    
    constructor(name: string) {

        this.name = name;
    }
}
  • The entity must be a class
  • The entity must be decorated with the Entity decorator
  • The entity is not required to have a parameterless constructor, which is different than JPA and Hibernate. This allows for entities to enforce required parameters for construction. When an entity is deserialized from the database, the constructor is not called. This means the internal state of an entity must fully represented by it's serialized fields.
  • An identifier is assigned to the entity when it is saved.

Collections

If a name for the collection is not given, an entity is mapped to a collection in MongoDB based on the name of the class. The collectionNamingStrategy in the Configuration is used to determine the name of the collection. The default naming strategy is CamelCase. Alternatively, a name for the collection can be specified using the Collection decorator.

@Entity()
@Collection("people")
export class Person {

    @Field()
    name: string;
    
    constructor(name: string) {

        this.name = name;
    }
}

Fields

Fields are mapped on an opt-in basis. Only fields that are decorated are mapped. The name for the field in the document can optionally be specified using the Field decorator.

@Entity()
export class User {

    @Field("u")
    username: string;
}

If the name for the field is not specified, the fieldNamingStrategy on the Configuration is used to determine the name of the field. The default naming strategy is CamelCase.

Identity

The identityGenerator on the Configuration is used to generate an identifier for an entity. The default identity generator is the ObjectIdGenerator. This is the only generator that ships with Hydrate. Composite identifiers are not supported. Natural identifiers are not supported.

By default the identifier is not exposed as a public member on an entity. The identifier can be retrieved as a string using the getIdentifier function.

import {getIdentifier} from "hydrate-mongodb";

...

session.query(Task).findAll({ status: TaskStatus.Pending }, (err, tasks) => {
    ...    
    var id = getIdentifier(tasks[0]);
    ...
});

Alternatively, the identifier can be exposed on an entity as a string using the Id decorator.

@Entity()
export class User {

    @Id()
    id: string;
    
    @Field()
    username: string;
}

Embeddables

Embeddables are classes that map to nested subdocuments within entities, arrays, or other embeddables.

@Embeddable()
export class HumanName {
 
    @Field()
    last: string;
    
    @Field()
    first: string;
    
    @Field()
    name: string;
    
    constructor(last: string, first?: string) {

        this.last = last;
        this.first = first;
        
        this.name = last;
        if(first) {
            this.name += ", " + first;
        }
    }
}

@Entity()
export class Person {

    @Field()
    name: HumanName;
    
    constructor(name: HumanName) {

        this.name = name;
    }
}
  • The embeddable must be a class
  • The embeddable must be decorated with the Embeddable decorator
  • Like an entity, an embeddable is not required to have a parameterless constructor. When an embeddable is deserialized from the database, the constructor is not called. This means the internal state of an embeddable must fully represented by it's serialized fields.

Types

When using TypeScript, the type of a field is automatically provided. The following types are supported:

  • Number
  • String
  • Boolean
  • Date
  • RegExp
  • Buffer
  • Array
  • Enum
  • Embeddables
  • Entities

Type Decorator

When a property is an embeddable or a reference to an entity, sometimes the type of the property cannot be determined because of circular references of import statements. In this case the Type decorator should be used with the name of the type.

@Entity()
export class Person {

    @Type("HumanName")
    name: HumanName;
    
    constructor(name: HumanName) {

        this.name = name;
    }
}

Arrays

TypeScript does not provide the type of an array element, so the type of the array element must be indicate with the ElementType decorator.

@Entity()
export class Organization {

    @ElementType(Address)
    addresses: Address[];
}

This is true for primitive types as well.

@Entity()
export class Person {

    @ElementType(String)
    aliases: string[];
}

Enums

By default enums are serialized as numbers. Use the Enumerated decorator to serialize enums as strings.

export enum TaskStatus {

    Pending,
    Completed,
    Archived
}

@Entity()
export class Task {

    @Field()
    text: string;

    @Enumerated(TaskStatus)
    status: TaskStatus;
}

Inheritance

Standard prototypical inheritance is supported for both entities and embeddables.

@Entity()
class Party {
    ...
}

@Entity()
class Person extends Party {
    ...
}

@Entity()
class Organization extends Party {
    ...
}

All entities within an inheritance hierarchy are stored in the same collection. If the Collection decorator is used, it is only valid on the root of an inheritance hierarchy.

Mapped Superclass

Entities stored in separate collections may share a common superclass that is not mapped to a collection. In the example, below Patient (stored in patient collection) and Document (stored in document collection) share a common superclass Asset that defines the field owner.

class Asset {

    @Field()
    owner: Organization;

    constructor(owner: Organization) {
        this.owner = owner;
    }
}

@Entity()
class Patient extends Asset {
    ...
}

@Entity()
class Document extends Asset {
    ...
}

If Asset was decorated with Entity then Patient and Document would instead both be stored in a collection called asset.

Discriminators

If an inheritance hierarchy is defined, a discriminator field is added to the serialized document to indicate the type when deserializing the entity or embeddable. By default, the discriminatorField on the Configuration is used to determine the name of the field to store the discriminator. Optionally, the discriminator field can be specified on the root of an inheritance hierarchy using the DiscriminatorField decorator.

@Entity()
@DiscriminatorField("type")
class Party {
    ...
}

The class discriminator can be specified using the DiscriminatorValue decorator.

@Entity()
@DiscriminatorValue("P")
class Person extends Party {
    ...
}

@Entity()
@DiscriminatorValue("O")
class Organization extends Party {
    ...
}

If the discriminator value is not explicitly specified for a class, it is determined using the discriminatorNamingStrategy on the Configuration. By default, the name of the class is used.

Lifecycle Callbacks

Hydrate provides callbacks that can be called on an entity during various stages of the entity lifecycle similar to JPA Lifecycle Callbacks. Callbacks are indicated by adding a decorator to a class method. A few important restrictions:

  • If the method is parameterless, it is executed synchronously.
  • If the method has a single parameter, it is executed as an asynchronous method and passed a callback for it to call when finished. Any errors passed to the callback by the method get returned on the Session operation that triggered the callback.
  • The method must not have more than one parameter.
  • The method must not modify any entities other than the entity that the method is called on; otherwise, results are unpredictable.
  • The method should avoid accessing the Session. Because of the way operations are queued on the session, executing an operation on the session during a lifecycle callback could cause the callback to hang.

Lifecycle callbacks are defined by adding the corresponding decorator to the class method as follows:

@Entity()
class Person {

    @Field()
    modified: Date;
       
    @PreUpdate()
    private _beforeUpdate(): void {
    
        // set the modified date on the entity before it is saved to the database.
        this.modified = new Date();
    }   
}

Lifecycle callbacks can be used to validate entities.

@Entity()
class Document {

    @Field()
    owner: Person;
       
    @PrePersist()
    @PreUpdate()
    validate(callback: Callback): void {

        if(!this.owner) {
            return callback(new Error("A document must have an owner.");
        }
        
        callback();
    }   
}

The following decorators are available for lifecycle callbacks:

  • PrePersist: Call method before a new entity is saved to the database.
  • PostPersist: Call method after a new entity is saved to the database.
  • PostLoad: Call method after an entity is loaded from the database.
  • PreUpdate: Call method before modifications to an entity are saved to the database.
  • PostUpdate: Call method after modifications to an entity are saved to the database.
  • PreRemove: Call method before an entity is deleted from the database.
  • PostRemove: Call method after an entity is deleted from the database.

Keywords

FAQs

Package last updated on 28 Jun 2016

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc