codepunkt/mongoose--history

Repository files navigation

npm versionBuild StatusGreenkeeper badgeKnown VulnerabilitiesCoverage Status

Mongoose History is a mongoose plugin that saves a history of JSON operations for all documents belonging to a schema in an associated "es" collection.

$ npm install mongoose--history

To use mongoose--history for an existing mongoose schema you can simply plug it in. As an example, the following schema definition defines a Post schema, and uses mongoose--history with default options:

import mongoose, { Schema } from 'mongoose'
import History from 'mongoose--history'

/* or the following if not running your app with babel:
const History = require('mongoose--history').default;
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
*/

const PostSchema = new Schema({
  title: { type: String, required: true },
  comments: Array,
})

PostSchema.plugin(History, { mongoose, name: 'postes' })
const Post = mongoose.model('Post', PostSchema)

mongoose--history will define a schema that has a ref field containing the ObjectId of the original document, a ops array containing all json operations and a date field storing the date where the was applied.

Continuing the previous example, a new is added to the associated collection whenever a new post is added to the posts collection:

Post.create({ title: 'JSON es' })
  .then(post => post.es.findOne({ ref: post.id }))
  .then(console.log)

// {
//   _id: ObjectId('4edd40c86762e0fb12000003'),
//   ref: ObjectId('4edd40c86762e0fb12000004'),
//   ops: [
//     { value: 'JSON es', path: '/title', op: 'add' },
//     { value: [], path: '/comments', op: 'add' }
//   ],
//   date: new Date(1462360838107),
//   __v: 0
// }

mongoose--history also adds a static field es to the model that can be used to access the model associated with the model, for example to query all es of a document. Whenever a post is edited, a new that reflects the update operation is added to the associated collection:

const data = {
  title: 'JSON es with mongoose',
  comments: [{ message: 'Wow! Such Mongoose! Very NoSQL!' }],
}

Post.create({ title: 'JSON es' })
  .then(post => post.set(data).save())
  .then(post => post.es.find({ ref: post.id }))
  .then(console.log)

// [{
//   _id: ObjectId('4edd40c86762e0fb12000003'),
//   ref: ObjectId('4edd40c86762e0fb12000004'),
//   ops: [
//     { value: 'JSON es', path: '/title', op: 'add' },
//     { value: [], path: '/comments', op: 'add' }
//   ],
//   date: new Date(1462360838107),
//   __v: 0
// }, {
//   _id: ObjectId('4edd40c86762e0fb12000005'),
//   ref: ObjectId('4edd40c86762e0fb12000004'),
//   ops: [
//     { value: { message: 'Wow! Such Mongoose! Very NoSQL!' }, path: '/comments/0', op: 'add' },
//     { value: 'JSON es with mongoose', path: '/title', op: 'replace' }
//   ],
//   "date": new Date(1462361848742),
//   "__v": 0
// }]
rollback(ObjectId, data, save)

Documents have a rollback method that accepts the ObjectId of a doc and sets the document to the state of that , adding a new to the history.

Post.create({ title: 'First version' })
  .then(post => post.set({ title: 'Second version' }).save())
  .then(post => post.set({ title: 'Third version' }).save())
  .then(post => {
    return post.es
      .find({ ref: post.id })
      .then(es => post.rollback(es[1].id))
  })
  .then(console.log)

// {
//   _id: ObjectId('4edd40c86762e0fb12000006'),
//   title: 'Second version',
//   __v: 0
// }

Further the rollback method accepts a data object which is injected into the document.

post.rollback(es[1].id, { name: 'merged' })

// {
//   _id: ObjectId('4edd40c86762e0fb12000006'),
//   title: 'Second version',
//   name: 'merged',
//   __v: 0
// }

To rollback the document to a specific but without saving it back to the database call the method with an empty data object and the save flag set to false.

post.rollback(es[1].id, {}, false)

// Returns the document without saving it back to the db.
// {
//   _id: ObjectId('4edd40c86762e0fb12000006'),
//   title: 'Second version',
//   __v: 0
// }

The rollback method will throw an Error when invoked with an ObjectId that is

  • not a of the document
  • the latest of the document
PostSchema.plugin(History, {
  mongoose,
  name: 'postes',
})
  • mongoose ๐Ÿ“Œ required
    The mongoose instance to work with
  • name ๐Ÿ“Œ required
    String where the names of both model and collection are generated from. By default, model name is the pascalized version and collection name is an undercore separated version
  • removees
    Removes es when origin document is removed. Default: true
  • transforms
    An array of two functions that generate model and collection name based on the name option. Default: An array of humps.pascalize and humps.decamelize
  • includes
    Property definitions that will be included in the schema. Read more about includes in the next chapter of the documentation. Default: {}
  • excludes
    Property paths that will be excluded in es. Read more about excludes in the excludes chapter of the documentation. Default: []
  • trackOriginalValue
    If enabled, the original value will be stored in the change es under the attribute originalValue. Default: false
PostSchema.plugin(History, {
  mongoose,
  name: 'postes',
  includes: {
    title: { type: String, required: true },
  },
})

This will add a title property to the schema. All options that are available in mongoose's schema property definitions such as required, default or index can be used.

Post.create({ title: 'Included in every ' })
  .then((post) => post.es.findOne({ ref: post.id })
  .then(() => {
    console.log(.title) // 'Included in every '
  })

The value of the documents properties is read from the versioned documents property of the same name.

There is an additional option that allows storing information in the documents that is not stored in the versioned documents. To do so, you can use a combination of virtual type setters on the versioned document and an additional from property in the include options of mongoose--history:

// save user as _user in versioned documents
PostSchema.virtual('user').set(function (user) {
  this._user = user
})

// read user from _user in  documents
PostSchema.plugin(History, {
  mongoose,
  name: 'postes',
  includes: {
    user: { type: Schema.Types.ObjectId, required: true, from: '_user' },
  },
})

// create post, pass in user information
Post.create({
  title: 'Why is hiring broken?',
  user: mongoose.Types.ObjectId(),
})
  .then(post => {
    console.log(post.user) // undefined
    return post.es.findOne({ ref: post.id })
  })
  .then( => {
    console.log(.user) // 4edd40c86762e0fb12000012
  })

In case of a rollback in this scenario, the rollback method accepts an object as its second parameter where additional data can be injected:

Post.create({ title: 'v1', user: mongoose.Types.ObjectId() })
  .then(post =>
    post
      .set({
        title: 'v2',
        user: mongoose.Types.ObjectId(),
      })
      .save()
  )
  .then(post => {
    return post.es.find({ ref: post.id }).then(es =>
      post.rollback(es[0].id, {
        user: mongoose.Types.ObjectId(),
      })
    )
  })

In situations where you are running Mongoose queries directly instead of via a document, you can specify the extra fields in the query options:

Post.findOneAndUpdate(
  { _id: '4edd40c86762e0fb12000012' },
  { title: 'Why is hiring broken? (updated)' },
  { _user: mongoose.Types.ObjectId() }
)
PostSchema.plugin(History, {
  mongoose,
  name: 'postes',
  excludes: [
    '/path/to/hidden/property',
    '/path/into/array/*/property',
    '/path/to/one/array/1/element',
  ],
})

// Properties
// /path/to/hidden:                   included
// /path/to/hidden/property:          excluded
// /path/to/hidden/property/nesting:  excluded

// Array element properties
// /path/into/array/0:                included
// /path/into/array/345345/property:  excluded
// /path/to/one/array/0/element:      included
// /path/to/one/array/1/element:      excluded

This will exclude the given properties and all nested paths. Excluding / however will not work, since then you can just disable the plugin.

  • If a property is {} or undefined after processing all excludes statements, it will not be included in the .
  • Arrays work a little different. Since json--operations work on the array index, array elements that are {} or undefined are still added to the . This brings support for later remove or replace operations to work as intended.
    The ARRAY_WILDCARD * matches every array element.

If there are any bugs experienced with the excludes feature please write an issue so we can fix it!

About

Mongoose plugin that saves a history of JSON operations for all documents belonging to a schema in an associated 'es' collection

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 10