diff --git a/README.md b/README.md index f8704a1..6f79901 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,58 @@ user.setNext('inhabitant_seq', function(err, user){ Of course this example is a bit forced and this is for sure not the perfect use case. The fields `country` and `city` have to be present and must not change during the life of the document because no automatic hooks are set on the change of those values. But there are situations when you want a similar behavior. +### Shared counters + +Let say we want to share counter between User and Person document. +The schema is like this: + +```js +UserSchema = mongoose.Schema({ + name: String, + country: String, + city: String, + inhabitant_number: Number +}); + +PersonSchema = mongoose.Schema({ + name: String, + country: String, + city: String, + inhabitant_number: Number +}); +``` + +Every time a new User or Person is added, we want the inhabitant_number to be incremented and to be shared between the two collection. We do so by setting duplicate and super field. + +```js +UserSchema.plugin(AutoIncrement, {id: 'inhabitant_seq', inc_field: 'inhabitant_number', reference_fields: ['country'] , duplicate: true, super: true}); + +PersonSchema.plugin(AutoIncrement, {id: 'inhabitant_seq', inc_field: 'inhabitant_number', reference_fields: ['country'] , duplicate: true, super: false}); +``` + +Notice that we have to set super to only one of the Schema, and duplicate to all the schemas for we want a shared counter. + +Now save a new user and a person: +```js +var user = new User({ + name: 'Patrice', + country: 'France', + city: 'Paris' +}); +user.save(); + +var person = new PersonSchema({ + name: 'Patrice', + country: 'France', + city: 'Paris' +}); +person.save(); +``` + +This user will have the `inhabitant_number` counter set to 1 and person will have the `inhabitant_number` counter set to 1 . + +Important Note: For the shared counter to work as inteneded, reference field property must be same and have type in all the models. + ### Reset a counter It's possible to programmatically reset a counter through the Model's static method `counterReset(id, reference, callback)`. The method takes these parameters: diff --git a/incrementScopePrep.js b/incrementScopePrep.js new file mode 100644 index 0000000..42e5dba --- /dev/null +++ b/incrementScopePrep.js @@ -0,0 +1,26 @@ +const MongoClient = require('mongodb').MongoClient; +const url = "mongodb://localhost:27017"; +const dbName = 'test'; +const CounterCollection = "counters"; + +const id = 'member_id'; +const suffix = '_old'; + +try { + MongoClient.connect(url, async(err, client) => { + if (err) console.log(err); + + const db = client.db(dbName); + const Counter = db.collection(CounterCollection); + + const counters = await Counter.find({id}).toArray(); + + for (let index = 0; index < counters.length; index++) { + const counter = counters[index]; + await Counter.update({_id: counter._id}, {$set: {id : `${counter.id}_${suffix}`}}); + } + console.log('done') + }); +} catch (error) { + console.log(error) +} diff --git a/lib/sequence.js b/lib/sequence.js index e85c888..3a756c2 100644 --- a/lib/sequence.js +++ b/lib/sequence.js @@ -6,18 +6,18 @@ var _ = require('lodash'), Sequence; /** - * Sequence plugin constructor - * @class Sequence - * @param {string} schema the schema object - * @param {object} options A set of options for this plugin - * @param {string} [options.inc_field='_id'] The field to increment - * @param {string} [options.id='same as inc_field'] The id of this sequence. Mandatory only if the sequence use reference fields - * @param {string|string[]} [options.reference_fields=['_id']] Any field to consider as reference for the counter - * @param {boolean} [options.disable_hooks] If true any hook will be disabled - * @param {string} [options.collection_name='counters'] A name for the counter collection - * @throws {Error} If id is missing for counter which referes other fields - * @throws {Error} If A counter collide with another because of same id - */ +* Sequence plugin constructor +* @class Sequence +* @param {string} schema the schema object +* @param {object} options A set of options for this plugin +* @param {string} [options.inc_field='_id'] The field to increment +* @param {string} [options.id='same as inc_field'] The id of this sequence. Mandatory only if the sequence use reference fields +* @param {string|string[]} [options.reference_fields=['_id']] Any field to consider as reference for the counter +* @param {boolean} [options.disable_hooks] If true any hook will be disabled +* @param {string} [options.collection_name='counters'] A name for the counter collection +* @throws {Error} If id is missing for counter which referes other fields +* @throws {Error} If A counter collide with another because of same id +*/ Sequence = function(schema, options) { var defaults = { id: null, @@ -51,35 +51,46 @@ Sequence = function(schema, options) { }; /** - * Create an instance for a sequence - * - * @method getInstance - * @param {Object} schema A mongoose Schema - * @param {object} options Options as accepted by A sequence - * constructor - * @return {Sequence} A sequence - * - * @static - */ +* Create an instance for a sequence +* +* @method getInstance +* @param {Object} schema A mongoose Schema +* @param {object} options Options as accepted by A sequence +* constructor +* @return {Sequence} A sequence +* +* @static +*/ Sequence.getInstance = function(schema, options) { var sequence = new Sequence(schema, options), id = sequence.getId(); - if (sequenceArchive.existsSequence(id)){ + if (!options.duplicate && sequenceArchive.existsSequence(id)){ throw new Error('Counter already defined for field "'+id+'"'); } - sequence.enable(); + + if (options.duplicate && !options.super) { + sequence.enable(options); + return sequence; + } + + sequence.enable(options); sequenceArchive.addSequence(id, sequence); return sequence; }; /** - * Enable the sequence creating all the necessary models - * - * @method enable - */ -Sequence.prototype.enable = function(){ - this._counterModel = this._createCounterModel(); +* Enable the sequence creating all the necessary models +* +* @method enable +*/ +Sequence.prototype.enable = function(options){ + if (options.duplicate) { + const key = 'Counter_' + this._options.id; + this._counterModel = sequenceArchive.getSequenceModel(key) || this._createCounterModel(); + } else { + this._counterModel = this._createCounterModel(); + } this._createSchemaKeys(); @@ -91,33 +102,41 @@ Sequence.prototype.enable = function(){ }; /** - * Return the id of the sequence - * - * @method getId - * @return {String} The id of the sequence - */ +* Return the id of the sequence +* +* @method getId +* @return {String} The id of the sequence +*/ Sequence.prototype.getId = function() { return this._options.id; }; /** - * Given a mongoose document, retrieve the values of the fields set as reference - * for the sequence. - * - * @method _getCounterReferenceField - * @param {object} doc A mongoose document - * @return {Array} An array of strings which represent the value of the - * reference - */ -Sequence.prototype._getCounterReferenceField = function(doc) { +* Given a mongoose document, retrieve the values of the fields set as reference +* for the sequence. +* +* @method _getCounterReferenceField +* @param {object} doc A mongoose document +* @param {Boolean} incrScope If set, returns reference fields, used to find old counter +* @return {Array} An array of strings which represent the value of the +* reference +*/ +Sequence.prototype._getCounterReferenceField = function(doc, incrScope) { var reference = {}; + if (incrScope) { + for (var index in this._options.old_reference_fields) { + reference[this._options.reference_fields[index]] = doc[this._options.reference_fields[index]]; + } + return reference; + } + if (this._useReference === false) { reference = null; } else { for (var i in this._options.reference_fields) { reference[this._options.reference_fields[i]] = doc[this._options.reference_fields[i]]; - // reference.push(JSON.stringify(doc[this._options.reference_fields[i]])); + // reference.push(JSON.stringify(doc[this._options.reference_fields[i]])); } } @@ -125,10 +144,10 @@ Sequence.prototype._getCounterReferenceField = function(doc) { }; /** - * Enrich the schema with keys needed by this sequence - * - * @method _createSchemaKeys - */ +* Enrich the schema with keys needed by this sequence +* +* @method _createSchemaKeys +*/ Sequence.prototype._createSchemaKeys = function() { var schemaKey = this._schema.path(this._options.inc_field); if (_.isUndefined(schemaKey)) { @@ -143,11 +162,11 @@ Sequence.prototype._createSchemaKeys = function() { }; /** - * Create a model for the counter handled by this sequence - * - * @method _createCounterModel - * @return {Mongoose~Model} A mongoose model - */ +* Create a model for the counter handled by this sequence +* +* @method _createCounterModel +* @return {Mongoose~Model} A mongoose model +*/ Sequence.prototype._createCounterModel = function() { var CounterSchema; @@ -163,23 +182,25 @@ Sequence.prototype._createCounterModel = function() { versionKey: false, _id: false } - ); + ); CounterSchema.index({id: 1, reference_value: 1}, {unique: true}); - /* Unused. Enable when is useful */ - // CounterSchema.static('getNext', function(id, referenceValue, callback) { - // this.findOne({ id: id, reference_value: referenceValue }, callback); - // }); - - return mongoose.model('Counter_' + this._options.id, CounterSchema); +/* Unused. Enable when is useful */ +// CounterSchema.static('getNext', function(id, referenceValue, callback) { +// this.findOne({ id: id, reference_value: referenceValue }, callback); +// }); + const modal = mongoose.model('Counter_' + this._options.id, CounterSchema); + sequenceArchive.setSequenceModel('Counter_' + this._options.id, modal); + + return modal; }; /** - * Set and handler for some hooks on the schema referenced by this sequence - * - * @method _setHooks - */ +* Set and handler for some hooks on the schema referenced by this sequence +* +* @method _setHooks +*/ Sequence.prototype._setHooks = function() { this._schema.pre('save', true, (function(sequence){ return function(next, done) { @@ -198,17 +219,17 @@ Sequence.prototype._setHooks = function() { }; /** - * Set some useful methods on the schema - * - * @method _setMethods - */ +* Set some useful methods on the schema +* +* @method _setMethods +*/ Sequence.prototype._setMethods = function() { - // this._schema.static('getNext', function(id, referenceValue, callback) { - // this._counterModel.getNext(id, referenceValue, function(err, counter) { - // if (err) return callback(err); - // return callback(null, ++counter.seq); - // }); - // }.bind(this)); +// this._schema.static('getNext', function(id, referenceValue, callback) { +// this._counterModel.getNext(id, referenceValue, function(err, counter) { +// if (err) return callback(err); +// return callback(null, ++counter.seq); +// }); +// }.bind(this)); this._schema.method('setNext', function(id, callback) { var sequence = sequenceArchive.getSequence(id); @@ -216,7 +237,7 @@ Sequence.prototype._setMethods = function() { if (_.isNull(sequence)) { return callback(new Error('Trying to increment a wrong sequence using the id ' + id)); } - // sequence = sequence.sequence; + // sequence = sequence.sequence; sequence._setNextCounter(this, function(err, seq) { if (err) return callback(err); @@ -242,28 +263,60 @@ Sequence.prototype._resetCounter = function(id, reference, callback){ }; /** - * Utility function to increment a counter in a transaction - * - * @method _setNextCounter - * @param {object} doc A mongoose model which need to receive the - * increment - * @param {Function} callback Called with the sequence counter - */ +* Utility function to increment a counter in a transaction +* +* @method _setNextCounter +* @param {object} doc A mongoose model which need to receive the +* increment +* @param {Function} callback Called with the sequence counter +*/ Sequence.prototype._setNextCounter = function(doc, callback) { var retriable = function(callback) { - var id = this.getId(); - var referenceValue = this._getCounterReferenceField(doc); - this._counterModel.findOneAndUpdate( - { id: id, reference_value: referenceValue }, - { $inc: { seq: 1 } }, - { new: true, upsert: true }, - function(err, counter) { - if (err) return callback(err); - return callback(null, counter.seq); - } - - ); + var that = this; + var id = that.getId(); + var referenceValue = that._getCounterReferenceField(doc); + if (that._options.old_reference_fields) { + that._counterModel.findOne({id : id, reference_value: referenceValue}, function (err, counter) { + if (err) return callback(err); + if (counter) { + that._counterModel.findOneAndUpdate( + { id: id, reference_value: referenceValue }, + { $inc: { seq: 1 } }, + { new: true, upsert: true, passRawResult: true }, + function(err, counter) { + if (err) return callback(err); + return callback(null, counter.seq); + } + ); + } else { + const oldReferenceID = `${id}_${ that._options.old_ref_field_suffix|| 'old'}`; + that._counterModel.findOne({id : oldReferenceID, reference_value: that._getCounterReferenceField(doc, true)}, function (err, counter) { + if (err) return callback(err); + var seq = counter ? counter.seq + 1 : 1; + that._counterModel.findOneAndUpdate( + { id: id, reference_value: referenceValue }, + { seq } , + { new: true, upsert: true, passRawResult: true }, + function(err, counter) { + if (err) return callback(err); + return callback(null, counter.seq); + } + ); + }); + } + }); + } else { + that._counterModel.findOneAndUpdate( + { id: id, reference_value: referenceValue }, + { $inc: { seq: 1 } }, + { new: true, upsert: true, passRawResult: true }, + function(err, counter) { + if (err) return callback(err); + return callback(null, counter.seq); + } + ); + } }; async.retry(0, retriable.bind(this), callback); diff --git a/lib/sequence_archive.js b/lib/sequence_archive.js index 68fa42f..7a79729 100644 --- a/lib/sequence_archive.js +++ b/lib/sequence_archive.js @@ -9,6 +9,7 @@ var _ = require('lodash'), */ SequenceArchive = function() { this.sequences = []; + this.models = {}; }; /** @@ -47,6 +48,29 @@ SequenceArchive.prototype.getSequence = function(id) { return null; }; + +/** + * Get a sequenceModel by id + * + * @method getSequence + * @param {string} id An id for the sequenceModel + * @return {object|null} Return the found sequenceModel or null + */ +SequenceArchive.prototype.getSequenceModel = function(id) { + return this.models[id]; +}; + +/** + * Get a sequenceModel by id + * + * @method getSequence + * @param {string} id An id for the sequenceModel + * @return {object|null} Return the found sequenceModel or null + */ +SequenceArchive.prototype.setSequenceModel = function(id, model) { + this.models[id] = model; +}; + /** * Check if a sequence already exists *