mongoose-plugin-autoinc
Version:
This plugin allows you to auto-increment any field on any mongoose schema that you wish (forked mongoose-auto-increment in 2018).
342 lines (299 loc) • 10.5 kB
Flow
// @flow
import mongoose, {
type MongooseConnection,
type MongooseSchema,
type MongooseDocument,
type MongooseModel,
} from 'mongoose';
export type AutoIncSettings = {|
migrate?: boolean, // If this is to be run on a migration for existing records. Only set this on migration processes.
model: string, // The model to configure the plugin for.
field: string, // The field the plugin should track.
groupingField: string, // The field by which to group documents, allowing for each grouping to be incremented separately.
startAt: number, // The number the count should start at.
incrementBy: number, // The number by which to increment the count each time.
unique?: boolean, // Should we create a unique index for the field,
outputFilter?: (count: number) => number, // function that modifies the output of the counter.
|};
export type AutoIncOptions = string | $Shape<AutoIncSettings>;
declare class IdentityCounterDoc extends Mongoose$Document {
model: string;
field: string;
groupingField: string;
count: number;
constructor(data?: $Shape<this>): IdentityCounterModel;
}
export type IdentityCounterModel = typeof IdentityCounterDoc;
const counterSchema = new mongoose.Schema({
model: { type: String, required: true },
field: { type: String, required: true },
groupingField: { type: String, default: '' },
count: { type: Number, default: 0 },
});
counterSchema.index(
{
field: 1,
groupingField: 1,
model: 1,
},
{
unique: true,
}
);
export function initialize(): void {
console.log(
`MongooseAutoIncrement.initialize() method is deprecated. ` +
`Just remove this method, it not required anymore.`
);
}
function isMongoDuplicateError(e: any): boolean {
return e.code * 1 === 11000;
}
// Initialize plugin by creating counter collection in database.
function initIdentityCounter(connection: MongooseConnection): IdentityCounterModel {
let identityCounter;
try {
identityCounter = connection.model('IdentityCounter');
} catch (ex) {
if (ex.name === 'MissingSchemaError') {
// Create model using new schema.
identityCounter = connection.model('IdentityCounter', counterSchema);
} else {
throw ex;
}
}
return identityCounter;
}
async function createCounterIfNotExist(
IC: IdentityCounterModel,
settings: AutoIncSettings,
doc: MongooseDocument
): Promise<void> {
const groupingField = doc.get(settings.groupingField) || '';
let existedCounter: IdentityCounterDoc = (await IC.findOne({
model: settings.model,
field: settings.field,
groupingField,
}).exec(): any);
try {
if (!existedCounter) {
// Check old record format without `groupingField`,
// convert old record to the new format
existedCounter = (await IC.findOne({
model: settings.model,
field: settings.field,
groupingField: { $exists: false },
}): any);
if (existedCounter) {
existedCounter.groupingField = '';
await existedCounter.save();
}
}
if (!existedCounter) {
// If no counter exists then create one and save it.
existedCounter = (new IC({
model: settings.model,
field: settings.field,
groupingField,
count: settings.startAt - settings.incrementBy,
}): any);
await existedCounter.save();
}
} catch (e) {
if (isMongoDuplicateError(e)) {
return;
}
throw e; // other unhandled errors
}
}
async function preSave(
IC: IdentityCounterModel,
settings: AutoIncSettings,
doc: MongooseDocument,
next: Function,
attempts: number = 0
) {
try {
// it is a first run
if (!attempts) {
await createCounterIfNotExist(IC, settings, doc);
}
if (typeof doc.get(settings.field) === 'number') {
// check that a number has already been provided, and update the counter
// to that number if it is greater than the current count
await IC.findOneAndUpdate(
{
model: settings.model,
field: settings.field,
groupingField: doc.get(settings.groupingField) || '',
count: { $lt: doc.get(settings.field) },
},
{
count: doc.get(settings.field),
}
).exec();
} else {
// Find the counter collection entry for this model and field and update it.
// Should be done via atomic mongodb operation, cause parallel processes may change value
const updatedCounter = await IC.findOneAndUpdate(
{
model: settings.model,
field: settings.field,
groupingField: doc.get(settings.groupingField) || '',
},
{
// Increment the count by `incrementBy`.
$inc: { count: settings.incrementBy },
},
{
// new:true specifies that the callback should get the counter AFTER it is updated (incremented).
new: true,
}
).exec();
if (!updatedCounter) {
throw new Error(`MongooseAutoInc cannot update counter for ${settings.model}`);
}
let { count } = updatedCounter;
// if an output filter was provided, apply it.
if (typeof settings.outputFilter === 'function') {
count = settings.outputFilter(count);
}
// If there are no errors then go ahead and set the document's field to the current count.
doc.set(settings.field, count);
// $FlowFixMe
doc.__maiRanOnce = true; // eslint-disable-line
}
next();
} catch (err) {
if (isMongoDuplicateError(err) && attempts * 1 < 10) {
setTimeout(() => preSave(IC, settings, doc, next, attempts + 1), 5);
} else {
next(err);
}
}
}
// Declare a function to get the next counter for the model/schema.
async function nextCount(
IC: IdentityCounterModel,
settings: AutoIncSettings,
groupingFieldValue?: string
): Promise<number> {
const counter = await IC.findOne({
model: settings.model,
field: settings.field,
groupingField: groupingFieldValue || '',
}).exec();
return !counter ? settings.startAt : counter.count + settings.incrementBy;
}
// Declare a function to reset counter at the start value - increment value.
async function resetCount(
IC: IdentityCounterModel,
settings: AutoIncSettings,
groupingFieldValue?: string
): Promise<number> {
await IC.findOneAndUpdate(
{ model: settings.model, field: settings.field, groupingField: groupingFieldValue || '' },
{ count: settings.startAt - settings.incrementBy },
{ new: true } // new: true specifies that the callback should get the updated counter.
).exec();
return settings.startAt;
}
// The function to use when invoking the plugin on a custom schema.
export function autoIncrement(
schema: MongooseSchema<MongooseDocument>,
options: AutoIncOptions
): void {
const compoundIndex = {};
let _IC_: IdentityCounterModel;
function getIC(connection: MongooseConnection) {
if (!_IC_) {
_IC_ = initIdentityCounter(connection);
}
return _IC_;
}
// Default settings and plugin scope variables.
let settings: $Shape<AutoIncSettings> = {
migrate: false,
model: undefined,
field: '_id',
groupingField: '',
startAt: 0,
incrementBy: 1,
unique: true,
outputFilter: undefined,
};
switch (typeof options) {
// If string, the user chose to pass in just the model name.
case 'string':
settings.model = options;
break;
// If object, the user passed in a hash of options.
case 'object':
settings = ({ ...settings, ...options }: any);
break;
default:
}
if (typeof settings.model !== 'string') {
throw new Error('model must be set');
}
if (settings.field === '_id' && settings.groupingField.length) {
throw new Error('Cannot use a grouping field with _id, choose a different field name.');
}
if (!schema.path(settings.field) || settings.field === '_id') {
schema.path(settings.field, Number);
}
if (settings.groupingField.length) {
// If a groupingField is specified, create a compound unique index.
compoundIndex[settings.field] = 1;
compoundIndex[settings.groupingField] = 1;
schema.index(compoundIndex, { unique: settings.unique });
} else if (settings.field !== '_id') {
// Otherwise, add the unique index directly to the custom field.
schema.path(settings.field).index({ unique: settings.unique });
}
// Add nextCount as both a method on documents and a static on the schema for convenience.
schema.method('nextCount', function(groupingFieldValue?: string) {
const doc: MongooseDocument = this;
const IC = getIC(doc.collection.conn);
return nextCount(IC, settings, groupingFieldValue);
});
// $FlowFixMe
schema.static('nextCount', function(groupingFieldValue?: string) {
const model: MongooseModel = this;
const IC = getIC(model.collection.conn);
return nextCount(IC, settings, groupingFieldValue);
});
// Add resetCount as both a method on documents and a static on the schema for convenience.
schema.method('resetCount', function(groupingFieldValue?: string) {
const doc: MongooseDocument = this;
const IC = getIC(doc.collection.conn);
return resetCount(IC, settings, groupingFieldValue);
});
// $FlowFixMe
schema.static('resetCount', function(groupingFieldValue?: string) {
const model: MongooseModel = this;
const IC = getIC(model.collection.conn);
return resetCount(IC, settings, groupingFieldValue);
});
// Every time documents in this schema are saved, run this logic.
schema.pre('validate', function(next: Function) {
// Get reference to the document being saved.
const doc: MongooseDocument = this;
// $FlowFixMe
const alreadyGetId = doc.__maiRanOnce === true;
// Only do this if it is a new document & the field doesn't have
// a value set (see http://mongoosejs.com/docs/api.html#document_Document-isNew)
if ((doc.isNew && !alreadyGetId) || settings.migrate) {
const IC = getIC(doc.collection.conn);
preSave(IC, settings, doc, next);
} else {
// If the document does not have the field we're interested in or that field isn't a number AND the user did
// not specify that we should increment on updates, then just continue the save without any increment logic.
next();
}
});
}
export function plugin(schema: MongooseSchema<MongooseDocument>, options: AutoIncOptions): void {
return autoIncrement(schema, options);
}
export default autoIncrement;