@compwright/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
225 lines (181 loc) • 8.32 kB
Markdown
<p align="center"><img title="redux-active" src="docs/mongoose-patch-history.png" width="519" style="margin-top:20px;"></p>
[](https://badge.fury.io/js/mongoose-patch-history) [](https://travis-ci.org/compwright/mongoose-patch-history) [](https://greenkeeper.io/) [](https://snyk.io/test/github/compwright/mongoose-patch-history:package.json?targetFile=package.json) [](https://coveralls.io/github/compwright/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 @compwright/mongoose-patch-history
## Usage
To use **@compwright/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 @compwright/mongoose-patch-history with default options:
```javascript
import mongoose, { Schema } from 'mongoose'
import patchHistory from '@compwright/mongoose-patch-history'
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
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
// }
```
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: `{}`
* `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 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() }
)
```