UNPKG

mongoose-patch-history

Version:

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

300 lines (239 loc) 10.8 kB
<p align="center"><img title="redux-active" src="docs/mongoose-patch-history.png" width="519" style="margin-top:20px;"></p> [![npm version](https://badge.fury.io/js/mongoose-patch-history.svg)](https://badge.fury.io/js/mongoose-patch-history) [![Build Status](https://travis-ci.org/codepunkt/mongoose-patch-history.svg?branch=master)](https://travis-ci.org/codepunkt/mongoose-patch-history) [![Greenkeeper badge](https://badges.greenkeeper.io/codepunkt/mongoose-patch-history.svg)](https://greenkeeper.io/) [![Known Vulnerabilities](https://snyk.io/test/github/codepunkt/mongoose-patch-history/badge.svg)](https://snyk.io/test/github/codepunkt/mongoose-patch-history:package.json?targetFile=package.json) [![Coverage Status](https://coveralls.io/repos/github/codepunkt/mongoose-patch-history/badge.svg?branch=master)](https://coveralls.io/github/codepunkt/mongoose-patch-history?branch=master) Mongoose Patch History is a mongoose plugin that saves a history of [JSON Patch](http://jsonpatch.com/) operations for all documents belonging to a schema in an associated "patches" collection. ## Installation $ npm install mongoose-patch-history ## Usage To use **mongoose-patch-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-patch-history with default options: ```javascript import mongoose, { Schema } from 'mongoose' import patchHistory from 'mongoose-patch-history' /* or the following if not running your app with babel: const patchHistory = require('mongoose-patch-history').default; const mongoose = require('mongoose'); const Schema = mongoose.Schema; */ const PostSchema = new Schema({ title: { type: String, required: true }, comments: Array, }) PostSchema.plugin(patchHistory, { mongoose, name: 'postPatches' }) const Post = mongoose.model('Post', PostSchema) ``` **mongoose-patch-history** will define a schema that has a `ref` field containing the `ObjectId` of the original document, a `ops` array containing all json patch operations and a `date` field storing the date where the patch was applied. ### Storing a new document Continuing the previous example, a new patch is added to the associated patch collection whenever a new post is added to the posts collection: ```javascript Post.create({ title: 'JSON patches' }) .then(post => post.patches.findOne({ ref: post.id })) .then(console.log) // { // _id: ObjectId('4edd40c86762e0fb12000003'), // ref: ObjectId('4edd40c86762e0fb12000004'), // ops: [ // { value: 'JSON patches', path: '/title', op: 'add' }, // { value: [], path: '/comments', op: 'add' } // ], // date: new Date(1462360838107), // __v: 0 // } ``` ### Updating an existing document **mongoose-patch-history** also adds a static field `Patches` to the model that can be used to access the patch model associated with the model, for example to query all patches of a document. Whenever a post is edited, a new patch that reflects the update operation is added to the associated patch collection: ```javascript const data = { title: 'JSON patches with mongoose', comments: [{ message: 'Wow! Such Mongoose! Very NoSQL!' }], } Post.create({ title: 'JSON patches' }) .then(post => post.set(data).save()) .then(post => post.patches.find({ ref: post.id })) .then(console.log) // [{ // _id: ObjectId('4edd40c86762e0fb12000003'), // ref: ObjectId('4edd40c86762e0fb12000004'), // ops: [ // { value: 'JSON patches', 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 patches with mongoose', path: '/title', op: 'replace' } // ], // "date": new Date(1462361848742), // "__v": 0 // }] ``` ### Rollback to a specific patch ```javascript rollback(ObjectId, data, save) ``` Documents have a `rollback` method that accepts the _ObjectId_ of a patch doc and sets the document to the state of that patch, adding a new patch to the history. ```javascript 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.patches .find({ ref: post.id }) .then(patches => post.rollback(patches[1].id)) }) .then(console.log) // { // _id: ObjectId('4edd40c86762e0fb12000006'), // title: 'Second version', // __v: 0 // } ``` #### Injecting data Further the `rollback` method accepts a _data_ object which is injected into the document. ```javascript post.rollback(patches[1].id, { name: 'merged' }) // { // _id: ObjectId('4edd40c86762e0fb12000006'), // title: 'Second version', // name: 'merged', // __v: 0 // } ``` #### Rollback without saving To `rollback` the document to a specific patch but without saving it back to the database call the method with an empty _data_ object and the save flag set to false. ```javascript post.rollback(patches[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 patch of the document - the latest patch of the document ## Options ```javascript PostSchema.plugin(patchHistory, { mongoose, name: 'postPatches', }) ``` - `mongoose` :pushpin: _required_ <br/> The mongoose instance to work with - `name` :pushpin: _required_ <br/> String where the names of both patch model and patch collection are generated from. By default, model name is the pascalized version and collection name is an undercore separated version - `removePatches` <br/> Removes patches when origin document is removed. Default: `true` - `transforms` <br/> An array of two functions that generate model and collection name based on the `name` option. Default: An array of [humps](https://github.com/domchristie/humps).pascalize and [humps](https://github.com/domchristie/humps).decamelize - `includes` <br/> Property definitions that will be included in the patch schema. Read more about includes in the next chapter of the documentation. Default: `{}` - `excludes` <br/> Property paths that will be excluded in patches. Read more about excludes in the [excludes chapter of the documentation](https://github.com/codepunkt/mongoose-patch-history#excludes). Default: `[]` - `trackOriginalValue` <br/> If enabled, the original value will be stored in the change patches under the attribute `originalValue`. Default: `false` ### Includes ```javascript PostSchema.plugin(patchHistory, { mongoose, name: 'postPatches', includes: { title: { type: String, required: true }, }, }) ``` This will add a `title` property to the patch schema. All options that are available in mongoose's schema property definitions such as `required`, `default` or `index` can be used. ```javascript Post.create({ title: 'Included in every patch' }) .then((post) => post.patches.findOne({ ref: post.id }) .then((patch) => { console.log(patch.title) // 'Included in every patch' }) ``` The value of the patch documents properties is read from the versioned documents property of the same name. #### Reading from virtuals There is an additional option that allows storing information in the patch documents that is not stored in the versioned documents. To do so, you can use a combination of [virtual type setters](http://mongoosejs.com/docs/guide.html#virtuals) on the versioned document and an additional `from` property in the include options of **mongoose-patch-history**: ```javascript // save user as _user in versioned documents PostSchema.virtual('user').set(function (user) { this._user = user }) // read user from _user in patch documents PostSchema.plugin(patchHistory, { mongoose, name: 'postPatches', 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.patches.findOne({ ref: post.id }) }) .then(patch => { console.log(patch.user) // 4edd40c86762e0fb12000012 }) ``` In case of a rollback in this scenario, the `rollback` method accepts an [object as its second parameter](https://github.com/codepunkt/mongoose-patch-history#injecting-data) where additional data can be injected: ```javascript Post.create({ title: 'v1', user: mongoose.Types.ObjectId() }) .then(post => post .set({ title: 'v2', user: mongoose.Types.ObjectId(), }) .save() ) .then(post => { return post.patches.find({ ref: post.id }).then(patches => post.rollback(patches[0].id, { user: mongoose.Types.ObjectId(), }) ) }) ``` #### Reading from query options In situations where you are running Mongoose queries directly instead of via a document, you can specify the extra fields in the query options: ```javascript Post.findOneAndUpdate( { _id: '4edd40c86762e0fb12000012' }, { title: 'Why is hiring broken? (updated)' }, { _user: mongoose.Types.ObjectId() } ) ``` ### Excludes ```javascript PostSchema.plugin(patchHistory, { mongoose, name: 'postPatches', 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 patch. - Arrays work a little different. Since json-patch-operations work on the array index, array elements that are `{}` or `undefined` are still added to the patch. This brings support for later `remove` or `replace` operations to work as intended.<br/> 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!