Introduction

Vitamin provides a simple and easy to use Data Mapper implementation for working with your relational database.

Based on knex, it supports Postgres, MySQL, MariaDB, SQLite3, and Oracle databases, featuring both promise based and traditional callback interfaces, providing lazy and eager relationships loading, and support for one-to-one, one-to-many, and many-to-many relations.


Installation

$ npm install --save vitamin

# Then add one of the supported database drivers
$ npm install pg
$ npm install sqlite3
$ npm install mysql
$ npm install mysql2
$ npm install mariasql
$ npm install strong-oracle
$ npm install oracle

Vitamin is initialized by passing an initialized Knex client instance. The knex documentation provides a number of examples for different use cases.

var knex = require('knex')({
  client: 'mysql',
  connection: {
    host     : '127.0.0.1',
    user     : 'your_database_user',
    password : 'your_database_password',
    database : 'your_database_name',
    charset  : 'utf8'
  }
})

module.exports = require('vitamin')(knex)

Getting started

Defining data mappers

To get started, let's define a user data mapper by specifying both, the primary key name, and the table name

// using the previous initialized vitamin object,
// we define a mapper called `user` using `mapper` method of vitamin object
module.exports = vitamin.model('user', {

  // the primary key, default to `id`
  primaryKey: 'user_id',

  // the table name
  tableName: 'users',

  // add default attributes
  defaults: {
    active: true,
    verified: false
  }

})

model() also accepts a mapper instance passed as a second argument instead of a config object

CRUD : Reading and writing data

You can use the standard Node.js style callbacks by calling .asCallback(function (error, result) {}) on any promise method

1 - Create

To create a new record in the database, simply create a new model instance, set its attributes, then call the save method

// access the model generated by the mapper via vitamin's `model` method
var User = vitamin.model('user')

// we create a new instance of User model with `make` method
var user = User.make({ name: "John", occupation: "Developer" })
// or simply with the new operator
var user = new User({ name: "John", occupation: "Developer" })

// then we save it
user.save()
.then(function (result) {
  assert.equal(result, user)
  assert.instanceOf(result, User)
})
.catch(function (error) {
  ...
})

Another shorthand to create and save a new user is the create static method

var data = { name: "John", occupation: "Developer" }

User.create(data).then(function (result) {
  assert.instanceOf(result, User)
})

2 - Read

Below a few examples of different data access methods provided by Vitamin.

// get a collection of all users
User.query().fetch().then(
  function (result) {
    assert.instanceOf(result, Collection)
  },
  function (error) {
    ...
  }
)

The fetch method will return all the rows in the users table as a collection of User models. But, if you may also add constraints to queries, you can use the where method, which returns a query builder instance

User.query().where('role', "guest").offset(10).limit(15).fetch().then(
  function (result) {
    assert.instanceOf(result, Collection)
  },
  function (error) {
    ...
  }
)
// find a user by its primary key
User.query().find(123).then(
  function (result) {
    assert.instanceOf(result, User)
  },
  function (error) {
    ...
  }
)

To retrieve the first model matching the query constraints, use first

// fetch the `id` and `email` of the first admin user
User.query().where('is_admin', true).first('id', 'email').then(
  function (result) {
    assert.instanceOf(result, User)
  },
  function (error) {
    ...
  }
)

3 - Update

The save method may also be used to update a single model that already exists in the database. To update a model, you should retrieve it, set any attributes you wish to update, and then call the save method.

// post model is retrieved from `posts` table
// then we modify the status attribute and save it
var Post = vitamin.model('post')

Post.query().find(1).then(function (post) {
  return post.set('status', "draft").save()
})

In case you have many attributes to edit, you may use the update method directly:

var data = { 'status': "published", 'published_at': new Date }

post.update(data).then(
  function (result) {
    ...
  },
  function (error) {
    ...
  }
)

4 - Delete

Likewise, once retrieved, a model can be destroyed which removes it from the database. To delete a model, call the destroy method on an existing model instance:

Post.make({ id : 45 }).destroy().then(
  function (result) {
    assert.equal(result, post)
  },
  function (error) {
    ...
  }
)

Of course, you may also run a delete query on a set of models.

// we will delete all posts that are marked as draft
Post.query().where('status', 'draft').destroy().then(
  function (result) {
    ...
  },
  function (error) {
    ...
  }
)

Events

Model events allow you to attach code to certain events in the lifecycle of yours models. This enables you to add behaviors to your models when those built-in events ready, creating, created, saving, saved, updating, updated, deleting or deleted occur.

Events can be defined when you register the model

vitamin.model('user', {

  ...

  events: {

    'creating': _.noop,

    'saved': [
      handler1,
      handler2,
    ]
  }

})

Or, later with the static method on

// attach a listener for `created` event
User.on('created', function (user) {
  assert.instanceOf(user, User)
})

// Events `saving - creating - created - saved` are fired in order when we create a new model
>>> User.create({ name: "John", occupation: "Developer" })

You can also attach the same handler for many events separated by a white space

Post.on('creating updating', updateTimestamps)

The built-in events are fired automatically by the mapper, but you can trigger manually those events, or any custom ones

Post.make().emit('saving')

Order.make().emit('purchased', ...arguments)

Associations

Vitamin makes managing and working with relationships easy, and supports several types of relations:

Defining relations

One to One

Let's define a relation one to one between Person and Phone.

var Phone = vitamin.model('phone', {

  tableName: 'phones',

  relations: {

    owner: function () {
      // we refer to`Person` mapper by its name
      // `owner_id` is the foreign key of `users` in `phones` table
      return this.belongsTo('person', 'owner_id', 'id')
    }

  }

})

var Person = vitamin.model('person', {

  tableName: 'people',

  relations: {

    phone: function () {
      // the first argument is the target mapper name
      // the second is the foreign key in phones table
      // the third parameter is optional, it corresponds to the primary key of person model
      return this.hasOne('phone', 'owner_id', 'id')
    }

  }

})

One To Many

An example for this type, is the relation between blog post and its author

var User = vitamin.model('user', {

  tableName: 'users',

  relations: {

    posts: function () {
      // if the foreign key is not provided, 
      // vitamin will use the parent mapper name suffixed by '_id',
      // as a foreign key in the `posts` table, in this case `author_id`
      return this.hasMany('post')
    }

  }

})

var Post = vitamin.model('post', {

  tableName: 'posts',

  relations: {

    author: function () {
      return this.belongsTo('author', 'author_id')
    }

  }

})

Many To Many

This relation is more complicated than the previous. An example of that is the relation between Product and Category, when a product has many categories, and the same category is assigned to many products. A pivot table product_categories is used and contains the relative keys product_id and category_id

vitamin.model('product', {

  tableName: 'products',

  relations: {

    categories: function () {
      return belongsToMany('category', 'product_categories', 'category_id', 'product_id')
    }

  }

})

vitamin.model('category', {

  tableName: 'categories',

  relations: {

    products: function () {
      return belongsToMany('product', 'product_categories', 'product_id', 'category_id')
    }

  }

})

Querying relations

Lazy loading

We will use the relations defined below, to lazy load the related models

// load the related phone model of the person with the id 123
// we access the relation via `phone()` which return a HasOne relation instance
var person = Person.make({ id: 123 })

person.load(['posts']).then(function (model) {
  assert.equal(model, person)
  assert.instanceOf(person.getRelated('posts'), Collection)
})

Eager loading

To load a model and its relationships in one call, you can use the query method withRelated

// fetch the first article and its author
Post.query().withRelated('author').first().then(function (post) {
  assert.instanceOf(post.getRelated('author'), User)
})

// load all authors with their posts
User.query().withRelated('posts').fetch().asCallback((error, authors) => {
  assert.instanceOf(authors, Collection)

  authors.forEach(function (author) {
    assert.instanceOf(author.getRelated('posts'), Collection)
    assert.instanceOf(author.getRelated('posts').first(), Post)
  })
})

Saving related models

Instead of manually setting the foreign keys, Vitamin provides many methods to save the related models.

save() and saveMany()

var comment = Comment.make({ body: "Hello World !!" })

// saving and attach one comment
post.comments().save(comment).then(
  function (model) {
    assert.equal(model, comment)
  },
  function (error) {
    ...
  }
)

// saving many events
post.comments().saveMany([
  Comment.make({ body: "first comment" }),
  Comment.make({ body: "second comment" })
]).then(function (result) {
  assert.instanceOf(result, Collection)
  assert.instanceOf(result.first(), Comment)
})

create() and createMany()

In addition to the save and saveMany methods, you may also use the create method, which accepts an array of attributes, creates a model, and inserts it into the database.

// create and attach a post comment
post.comments().create({ body: "Hello World !!" }).then(function (error, model) {
  assert.instanceOf(model, Comment)
})

// create and attach the post comments
post.comments().createMany([
  { body: "first comment" }, { body: "second comment" }
]).then(function (result) {
  assert.instanceOf(result, Collection)
  assert.instanceOf(result.first(), Comment)
})

associate() and dissociate()

When updating a belongsTo relationship, you may use the associate method.

var john = Person.make({ id: 123 })

// set the foreign key `owner_id` and save the phone model
phone.owner().associate(john).save().then(...)

// unset the foreign key, then save
phone.owner().dissociate().save().then(...)

attach(), detach() and updatePivot()

When working with many-to-many relationships, Vitamin provides a few additional helper methods to make working with related models more convenient.

// to attach a role to a user by inserting a record in the joining table
user.roles().attach(roleId).then(
  function (result) {
    ...
  },
  function (error) {
    ...
  }
)

To remove a many-to-many relationship record, use the detach method.

// detach all roles of the loaded user
user.roles().detach().asCallback(function (error, result) {
  ...
})

// detach only the role with the given id
user.roles().detach(roleId).then(...)

// detach all roles with the given ids
user.roles().detach([1, 2, 3]).then(...)

If you need to update an existing row in your pivot table, you may use updatePivot method

user.roles().updatePivot(roleId, pivotAttributes).then(...)

sync()

You may also use the sync method to construct many-to-many associations. The sync method accepts an array of IDs to place on the intermediate table. Any IDs that are not in the given array will be removed from the intermediate table. So, after this operation is complete, only the IDs in the array will exist in the intermediate table:

// using callbacks
user.roles().sync([1, 2, 3], function (error, result) {
  ...
})

// using promises
user.roles().sync([1, 2, 3]).then(...)

You may also pass additional intermediate table values with the IDs:

user.roles().sync([[1, { 'expires': true }], 2]).then(...)