244 lines
7.6 KiB
JavaScript
244 lines
7.6 KiB
JavaScript
var _ = require('../util').lodash,
|
|
PropertyBase = require('./property-base').PropertyBase,
|
|
|
|
/**
|
|
* Primitive mutation types.
|
|
*
|
|
* @private
|
|
* @constant
|
|
* @type {Object}
|
|
*/
|
|
PRIMITIVE_MUTATIONS = {
|
|
SET: 'set',
|
|
UNSET: 'unset'
|
|
},
|
|
|
|
/**
|
|
* Detects if the mutation is a primitive mutation type. A primitive mutation is the simplified mutation structure.
|
|
*
|
|
* @private
|
|
* @param {MutationTracker.mutation} mutation -
|
|
* @returns {Boolean}
|
|
*/
|
|
isPrimitiveMutation = function (mutation) {
|
|
return mutation && mutation.length <= 2;
|
|
},
|
|
|
|
/**
|
|
* Applies a single mutation on a target.
|
|
*
|
|
* @private
|
|
* @param {*} target -
|
|
* @param {MutationTracker.mutation} mutation -
|
|
*/
|
|
applyMutation = function applyMutation (target, mutation) {
|
|
// only `set` and `unset` instructions are supported
|
|
// for non primitive mutations, the instruction would have to be extracted from mutation
|
|
/* istanbul ignore if */
|
|
if (!isPrimitiveMutation(mutation)) {
|
|
return;
|
|
}
|
|
|
|
// extract instruction from the mutation
|
|
var operation = mutation.length > 1 ? PRIMITIVE_MUTATIONS.SET : PRIMITIVE_MUTATIONS.UNSET;
|
|
|
|
// now hand over applying mutation to the target
|
|
target.applyMutation(operation, ...mutation);
|
|
},
|
|
|
|
MutationTracker;
|
|
|
|
/**
|
|
* A JSON representation of a mutation on an object. Here objects mean instances of postman-collection classes.
|
|
* This captures the instruction and the parameters of the instruction so that it can be replayed on a different object.
|
|
* Mutations can be any change on an object. For example setting a key or unsetting a key.
|
|
*
|
|
* For example, the mutation to set `name` on an object to 'Bruce Wayne' would look like ['name', 'Bruce Wayne']. Where
|
|
* the first item is the key path and second item is the value. To add a property `punchLine` to the object it would be
|
|
* the same as updating the property i.e. ['punchLine', 'I\'m Batman']. To remove a property `age` the mutation would
|
|
* look like ['age'].
|
|
*
|
|
* This format of representing changes is derived from
|
|
* {@link http://json-delta.readthedocs.io/en/latest/philosophy.html}.
|
|
*
|
|
* The `set` and `unset` are primitive instructions and can be derived from the mutation without explicitly stating the
|
|
* instruction. For more complex mutation the instruction would have to be explicitly stated.
|
|
*
|
|
* @typedef {Array} MutationTracker.mutation
|
|
*/
|
|
|
|
/**
|
|
* A JSON representation of the MutationTracker.
|
|
*
|
|
* @typedef MutationTracker.definition
|
|
*
|
|
* @property {Array} stream contains the stream mutations tracked
|
|
* @property {Object} compacted contains a compacted version of the mutations
|
|
* @property {Boolean} [autoCompact=false] when set to true, all new mutations would be compacted immediately
|
|
*/
|
|
_.inherit((
|
|
|
|
/**
|
|
* A MutationTracker allows to record mutations on any of object and store them. This stored mutations can be
|
|
* transported for reporting or to replay on similar objects.
|
|
*
|
|
* @constructor
|
|
* @extends {PropertyBase}
|
|
*
|
|
* @param {MutationTracker.definition} definition serialized mutation tracker
|
|
*/
|
|
MutationTracker = function MutationTracker (definition) {
|
|
// this constructor is intended to inherit and as such the super constructor is required to be executed
|
|
MutationTracker.super_.call(this, definition);
|
|
|
|
definition = definition || {};
|
|
|
|
// initialize options
|
|
this.autoCompact = Boolean(definition.autoCompact);
|
|
|
|
// restore mutations
|
|
this.stream = Array.isArray(definition.stream) ? definition.stream : [];
|
|
this.compacted = _.isPlainObject(definition.compacted) ? definition.compacted : {};
|
|
}), PropertyBase);
|
|
|
|
_.assign(MutationTracker.prototype, /** @lends MutationTracker.prototype */ {
|
|
|
|
/**
|
|
* Records a new mutation.
|
|
*
|
|
* @private
|
|
* @param {MutationTracker.mutation} mutation -
|
|
*/
|
|
addMutation (mutation) {
|
|
// bail out for empty or unsupported mutations
|
|
if (!(mutation && isPrimitiveMutation(mutation))) {
|
|
return;
|
|
}
|
|
|
|
// if autoCompact is set, we need to compact while adding
|
|
if (this.autoCompact) {
|
|
this.addAndCompact(mutation);
|
|
|
|
return;
|
|
}
|
|
|
|
// otherwise just push to the stream of mutations
|
|
this.stream.push(mutation);
|
|
},
|
|
|
|
/**
|
|
* Records a mutation compacting existing mutations for the same key path.
|
|
*
|
|
* @private
|
|
* @param {MutationTracker.mutation} mutation -
|
|
*/
|
|
addAndCompact (mutation) {
|
|
// for `set` and `unset` mutations the key to compact with is the `keyPath`
|
|
var key = mutation[0];
|
|
|
|
// convert `keyPath` to a string
|
|
key = Array.isArray(key) ? key.join('.') : key;
|
|
|
|
this.compacted[key] = mutation;
|
|
},
|
|
|
|
/**
|
|
* Track a mutation.
|
|
*
|
|
* @param {String} instruction the type of mutation
|
|
* @param {...*} payload mutation parameters
|
|
*/
|
|
track (instruction, ...payload) {
|
|
// invalid call
|
|
if (!(instruction && payload)) {
|
|
return;
|
|
}
|
|
|
|
// unknown instruction
|
|
if (!(instruction === PRIMITIVE_MUTATIONS.SET || instruction === PRIMITIVE_MUTATIONS.UNSET)) {
|
|
return;
|
|
}
|
|
|
|
// for primitive mutations the arguments form the mutation object
|
|
// if there is more complex mutation, we have to use a processor to create a mutation for the instruction
|
|
this.addMutation(payload);
|
|
},
|
|
|
|
/**
|
|
* Compacts the recorded mutations removing duplicate mutations that apply on the same key path.
|
|
*/
|
|
compact () {
|
|
// for each of the mutation, add to compacted list
|
|
this.stream.forEach(this.addAndCompact.bind(this));
|
|
|
|
// reset the `stream`, all the mutations are now recorded in the `compacted` storage
|
|
this.stream = [];
|
|
},
|
|
|
|
/**
|
|
* Returns the number of mutations tracked so far.
|
|
*
|
|
* @returns {Number}
|
|
*/
|
|
count () {
|
|
// the total count of mutations is the sum of
|
|
// mutations in the stream
|
|
var mutationCount = this.stream.length;
|
|
|
|
// and the compacted mutations
|
|
mutationCount += Object.keys(this.compacted).length;
|
|
|
|
return mutationCount;
|
|
},
|
|
|
|
/**
|
|
* Applies all the recorded mutations on a target object.
|
|
*
|
|
* @param {*} target Target to apply mutations. Must implement `applyMutation`.
|
|
*/
|
|
applyOn (target) {
|
|
if (!(target && target.applyMutation)) {
|
|
return;
|
|
}
|
|
|
|
var applyIndividualMutation = function applyIndividualMutation (mutation) {
|
|
applyMutation(target, mutation);
|
|
};
|
|
|
|
// mutations move from `stream` to `compacted`, so we apply the compacted mutations first
|
|
// to ensure FIFO of mutations
|
|
|
|
// apply the compacted mutations first
|
|
_.forEach(this.compacted, applyIndividualMutation);
|
|
|
|
// apply the mutations in the stream
|
|
_.forEach(this.stream, applyIndividualMutation);
|
|
}
|
|
});
|
|
|
|
_.assign(MutationTracker, /** @lends MutationTracker */ {
|
|
/**
|
|
* Defines the name of this property for internal use.
|
|
*
|
|
* @private
|
|
* @readOnly
|
|
* @type {String}
|
|
*/
|
|
_postman_propertyName: 'MutationTracker',
|
|
|
|
/**
|
|
* Check whether an object is an instance of {@link MutationTracker}.
|
|
*
|
|
* @param {*} obj -
|
|
* @returns {Boolean}
|
|
*/
|
|
isMutationTracker: function (obj) {
|
|
return Boolean(obj) && ((obj instanceof MutationTracker) ||
|
|
_.inSuperChain(obj.constructor, '_postman_propertyName', MutationTracker._postman_propertyName));
|
|
}
|
|
});
|
|
|
|
module.exports = {
|
|
MutationTracker
|
|
};
|