UNPKG

@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

312 lines (249 loc) 9.77 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.RollbackError = undefined; exports.default = function (schema, opts) { var options = (0, _lodash.merge)({}, defaultOptions, opts); // get _id type from schema options._idType = schema.tree._id.type; // validate parameters (0, _assert2.default)(options.mongoose, '`mongoose` option must be defined'); (0, _assert2.default)(options.name, '`name` option must be defined'); (0, _assert2.default)(!schema.methods.data, 'conflicting instance method: `data`'); (0, _assert2.default)(options._idType, 'schema is missing an `_id` property'); // used to compare instance data snapshots. depopulates instance, // removes version key and object id schema.methods.data = function () { return this.toObject({ depopulate: true, versionKey: false, transform: function transform(doc, ret, options) { delete ret._id; // if timestamps option is set on schema, ignore timestamp fields if (schema.options.timestamps) { delete ret[schema.options.timestamps.createdAt || 'createdAt']; delete ret[schema.options.timestamps.updatedAt || 'updatedAt']; } } }); }; // roll the document back to the state of a given patch id() schema.methods.rollback = function (patchId, data) { var _this = this; return this.patches.find({ ref: this.id }).sort({ date: 1 }).exec().then(function (patches) { return new _bluebird2.default(function (resolve, reject) { // patch doesn't exist if (!~(0, _lodash.map)(patches, 'id').indexOf(patchId)) { return reject(new RollbackError("patch doesn't exist")); } // get all patches that should be applied var apply = (0, _lodash.dropRightWhile)(patches, function (patch) { return patch.id !== patchId; }); // if the patches that are going to be applied are all existing patches, // the rollback attempts to rollback to the latest patch if (patches.length === apply.length) { return reject(new RollbackError('rollback to latest patch')); } // apply patches to `state` var state = {}; apply.forEach(function (patch) { _fastJsonPatch2.default.applyPatch(state, patch.ops, true); }); // save new state and resolve with the resulting document _this.set((0, _lodash.merge)(data, state)).save().then(resolve).catch(reject); }); }); }; // create patch model, enable static model access via `Patches` and // instance method access through an instances `patches` property var Patches = createPatchModel(options); schema.statics.Patches = Patches; schema.virtual('patches').get(function () { return Patches; }); // after a document is initialized or saved, fresh snapshots of the // documents data are created var snapshot = function snapshot() { this._original = toJSON(this.data()); }; schema.post('init', snapshot); schema.post('save', snapshot); // when a document is removed and `removePatches` is not set to false , // all patch documents from the associated patch collection are also removed function deletePatches(document) { var ref = document._id; return document.patches.find({ ref: document._id }).then(function (patches) { return (0, _bluebird.join)(patches.map(function (patch) { return patch.remove(); })); }); } schema.pre('remove', function (next) { if (!options.removePatches) { return next(); } deletePatches(this).then(function () { return next(); }).catch(next); }); // when a document is saved, the json patch that reflects the changes is // computed. if the patch consists of one or more operations (meaning the // document has changed), a new patch document reflecting the changes is // added to the associated patch collection function createPatch(document) { var queryOptions = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var ref = document._id; var ops = _fastJsonPatch2.default.compare(document.isNew ? {} : document._original || {}, toJSON(document.data())); // don't save a patch when there are no changes to save if (!ops.length) { return _bluebird2.default.resolve(); } // track original values if enabled if (options.trackOriginalValue) { ops.map(function (entry) { var path = (0, _lodash.tail)(entry.path.split('/')).join('.'); entry.originalValue = (0, _lodash.get)(document.isNew ? {} : document._original, path); }); } // assemble patch data var data = { ops: ops, ref: ref }; (0, _lodash.each)(options.includes, function (type, name) { data[name] = document[type.from || name] || queryOptions[type.from || name]; }); return document.patches.create(data); } schema.pre('save', function (next) { createPatch(this).then(function () { return next(); }).catch(next); }); schema.pre('findOneAndRemove', function (next) { if (!options.removePatches) { return next(); } this.model.findOne(this._conditions).then(function (original) { return deletePatches(original); }).then(function () { return next(); }).catch(next); }); schema.pre('findOneAndUpdate', preUpdateOne); function preUpdateOne(next) { var _this2 = this; this.model.findOne(this._conditions).then(function (original) { original = original || new _this2.model({}); _this2._original = toJSON(original.data()); }).then(function () { return next(); }).catch(next); } schema.post('findOneAndUpdate', function (doc, next) { if (!this.options.new) { return postUpdateOne.call(this, {}, next); } doc._original = this._original; createPatch(doc, this.options).then(function () { return next(); }).catch(next); }); function postUpdateOne(result, next) { var _this3 = this; if (result.nModified === 0) return; var conditions = Object.assign({}, this._conditions, this._update.$set || (0, _lodash.omitBy)(this._update, function (value, key) { return key[0] === '$'; })); this.model.findOne(conditions).then(function (doc) { doc._original = _this3._original; return createPatch(doc, _this3.options); }).then(function () { return next(); }).catch(next); } schema.pre('updateOne', preUpdateOne); schema.post('updateOne', postUpdateOne); function preUpdateMany(next) { var _this4 = this; this.model.find(this._conditions).then(function () { var originals = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; _this4._originals = [].concat(originals).map(function (original) { return original || new _this4.model({}); }).map(function (original) { return toJSON(original.data()); }); }).then(function () { return next(); }).catch(next); } function postUpdateMany(result, next) { var _this5 = this; if (result.nModified === 0) return; var conditions = Object.assign({}, this._conditions, this._update.$set || (0, _lodash.omitBy)(this._update, function (value, key) { return key[0] === '$'; })); this.model.find(conditions).then(function (docs) { return _bluebird2.default.all(docs.map(function (doc, i) { doc._original = _this5._originals[i]; return createPatch(doc, _this5.options); })); }).then(function () { return next(); }).catch(next); } schema.pre('updateMany', preUpdateMany); schema.post('updateMany', postUpdateMany); schema.pre('update', function (next) { if (this.options.multi) { preUpdateMany.call(this, next); } else { preUpdateOne.call(this, next); } }); schema.post('update', function (result, next) { if (this.options.many) { postUpdateMany.call(this, result, next); } else { postUpdateOne.call(this, result, next); } }); }; var _assert = require('assert'); var _assert2 = _interopRequireDefault(_assert); var _mongoose = require('mongoose'); var _bluebird = require('bluebird'); var _bluebird2 = _interopRequireDefault(_bluebird); var _fastJsonPatch = require('fast-json-patch'); var _fastJsonPatch2 = _interopRequireDefault(_fastJsonPatch); var _humps = require('humps'); var _lodash = require('lodash'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var RollbackError = exports.RollbackError = function RollbackError(message, extra) { Error.captureStackTrace(this, this.constructor); this.name = 'RollbackError'; this.message = message; }; require('util').inherits(RollbackError, Error); var createPatchModel = function createPatchModel(options) { var def = { date: { type: Date, required: true, default: Date.now }, ops: { type: [], required: true }, ref: { type: options._idType, required: true, index: true } }; (0, _lodash.each)(options.includes, function (type, name) { def[name] = (0, _lodash.omit)(type, 'from'); }); var PatchSchema = new _mongoose.Schema(def); return options.mongoose.model(options.transforms[0]('' + options.name), PatchSchema, options.transforms[1]('' + options.name)); }; var defaultOptions = { includes: {}, removePatches: true, transforms: [_humps.pascalize, _humps.decamelize], trackOriginalValue: false // used to convert bson to json - especially ObjectID references need // to be converted to hex strings so that the jsonpatch `compare` method // works correctly };var toJSON = function toJSON(obj) { return JSON.parse(JSON.stringify(obj)); };