diff --git a/src/MongooseIdAssigner.ts b/src/MongooseIdAssigner.ts index c770d9f..be860fc 100644 --- a/src/MongooseIdAssigner.ts +++ b/src/MongooseIdAssigner.ts @@ -4,18 +4,14 @@ import { Collection } from 'mongodb'; import { Document, Model, Schema } from 'mongoose'; import { AssignerOptions, FieldConfig } from './assigner.interfaces'; import { localStateStore, SchemaState } from './LocalStateStore'; -import { - initialiseOptions, - normaliseOptions, - throwPluginError, - waitPromise, -} from './utils'; +import { initialiseOptions, normaliseOptions, throwPluginError } from './utils'; import { refreshOptions } from './utils/assign-fields-ids'; import { configureSchema } from './utils/configure-schema'; export interface NormalisedOptions { modelName: string; network: boolean; + timestamp?: number | null; fields?: Map; } @@ -53,7 +49,6 @@ export class MongooseIdAssigner extends EventEmitter { this.options = normaliseOptions(options); this.modelName = this.options.modelName; this._saveState(); - this._modelNameIndex(); configureSchema(this); } @@ -110,20 +105,4 @@ export class MongooseIdAssigner extends EventEmitter { idAssigner: this, }); } - - private _modelNameIndex() { - this.on('ready', async () => { - if (!this.options.fields) { - return; - } - - try { - await waitPromise(1); // nextTick - - await this.collection.createIndex('modelName'); - } catch (e) { - throw e; - } - }); - } } diff --git a/src/__mocks__/mongoose.config.ts b/src/__mocks__/mongoose.config.ts index 72b1749..10ec6d3 100644 --- a/src/__mocks__/mongoose.config.ts +++ b/src/__mocks__/mongoose.config.ts @@ -40,12 +40,18 @@ export function getMongoose() { return mongoose; } else { jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - mongoose.connect( - 'mongodb://localhost:27017/test_ia', - { + /*const originalConnect = mongoose.connect; + + (mongoose as any).connect = () => { + originalConnect.bind(mongoose)('mongodb://localhost:27017/test_ia', { useNewUrlParser: true, - }, + }); + };*/ + mongoose.connect( + 'mongodb://localhost:27017/demoDB', + { useNewUrlParser: true }, ); + return mongoose; } } diff --git a/src/utils/__tests__/initialise-options.spec.ts b/src/utils/__tests__/initialise-options.spec.ts new file mode 100644 index 0000000..bcb870b --- /dev/null +++ b/src/utils/__tests__/initialise-options.spec.ts @@ -0,0 +1,30 @@ +import { AssignerOptions, FieldConfigTypes } from '../../assigner.interfaces'; +import { checkAndUpdateOptions } from '../initialise-options'; +import { normaliseOptions } from '../normalise-options'; + +describe('initialise-options ->', () => { + const options: AssignerOptions = { + modelName: 'Person', + fields: { + _id: true, + clientId: true, + withUUID: 'UUID', + String: '5555', + Number: 5555, + last: { + type: FieldConfigTypes.Number, + nextId: 5641, + }, + }, + }; + + const normalised = normaliseOptions(options); + + describe('checkAndUpdateOptions()', () => { + it('should return current option if freshUnAvailable', async () => { + expect(checkAndUpdateOptions(normalised, '' as any).options).toEqual( + normalised, + ); + }); + }); +}); diff --git a/src/utils/get-next-ids/get-next-id-number.ts b/src/utils/get-next-ids/get-next-id-number.ts index a5f51dd..2bee757 100644 --- a/src/utils/get-next-ids/get-next-id-number.ts +++ b/src/utils/get-next-ids/get-next-id-number.ts @@ -1,6 +1,6 @@ import { NumberFieldConfig } from '../../assigner.interfaces'; import { MongooseIdAssigner } from '../../MongooseIdAssigner'; -import { waitPromise } from '../index'; +import { throwPluginError, waitPromise } from '../index'; export async function getNextIdNumber( field: string, @@ -31,7 +31,10 @@ export async function getNextIdNumber( modelName: idAssigner.modelName, [updateField]: nextId, }, - { $set: { [updateField]: afterNextId } }, + { + $set: { [updateField]: afterNextId }, + $currentDate: { timestamp: true }, + }, { projection: { value: 1 } }, ); @@ -40,6 +43,12 @@ export async function getNextIdNumber( await waitPromise(idAssigner.retryMillis * multiplier); await idAssigner.refreshOptions(); return getNextIdNumber(field, idAssigner, fieldConfig, ++retries); + } else if (!update.value && retries > idAssigner.retryTime) { + throwPluginError( + `Maximum retryTime to set value attained!`, + idAssigner.modelName, + field, + ); } } catch (e) { return Promise.reject(e); diff --git a/src/utils/get-next-ids/get-next-id-string.ts b/src/utils/get-next-ids/get-next-id-string.ts index f1bb4a8..5120da8 100644 --- a/src/utils/get-next-ids/get-next-id-string.ts +++ b/src/utils/get-next-ids/get-next-id-string.ts @@ -1,6 +1,6 @@ import { StringFieldConfig } from '../../assigner.interfaces'; import { MongooseIdAssigner } from '../../MongooseIdAssigner'; -import { waitPromise } from '../index'; +import { throwPluginError, waitPromise } from '../index'; import { stringIncrementer } from './utils/string-incrementer'; export async function getNextIdString( @@ -31,7 +31,10 @@ export async function getNextIdString( modelName: idAssigner.modelName, [updateField]: nextId, }, - { $set: { [updateField]: afterNextId } }, + { + $set: { [updateField]: afterNextId }, + $currentDate: { timestamp: true }, + }, { projection: { value: 1 } }, ); @@ -40,6 +43,12 @@ export async function getNextIdString( await waitPromise(idAssigner.retryMillis * multiplier); await idAssigner.refreshOptions(); return getNextIdString(field, idAssigner, fieldConfig, ++retries); + } else if (!update.value && retries > idAssigner.retryTime) { + throwPluginError( + `Maximum retryTime to set value attained!`, + idAssigner.modelName, + field, + ); } } catch (e) { return Promise.reject(e); diff --git a/src/utils/initialise-options.ts b/src/utils/initialise-options.ts index bfaa27f..8bc693e 100644 --- a/src/utils/initialise-options.ts +++ b/src/utils/initialise-options.ts @@ -2,38 +2,57 @@ import * as eventToPromise from 'event-to-promise'; import { Document, Model } from 'mongoose'; import { localStateStore } from '../LocalStateStore'; import { MongooseIdAssigner, NormalisedOptions } from '../MongooseIdAssigner'; +import { throwPluginError, waitPromise } from './others'; import { isNumber, isString } from './type-guards'; +interface OptionsCheckResults { + abort?: boolean; // if no fresh, no options + replace?: boolean; // if there are replace to from fresh config + delete?: boolean; // if freshOptions exist, but local config doesn't need it. + options: NormalisedOptions; +} + // checks if fieldConfigs changed, update field nextIds only -// checks if AssignerOptions contains no field configs, changes = false +// checks if AssignerOptions contains no field configs, replace = false export function checkAndUpdateOptions( options: NormalisedOptions, - dbOptions?: NormalisedOptions, -): { changes?: boolean; options: NormalisedOptions } { - if (!dbOptions || !dbOptions.fields) { + freshOptions?: NormalisedOptions, +): OptionsCheckResults { + if (!freshOptions || !freshOptions.fields) { if (!options || !options.fields) { - return { changes: false, options }; + return { abort: true, options }; + } else { + options.timestamp = null; + return { replace: true, options }; } - return { changes: true, options }; } + // set timestamp + options.timestamp = freshOptions.timestamp; + + // delete old options if new doesn't need it if (!options || !options.fields) { - return { changes: false, options }; + return { delete: true, options }; } - const rObject = { config: false, options }; + const rObject = { replace: false, options }; for (const [field, config] of options.fields.entries()) { - const oldConfig = (dbOptions as any).fields[field]; - if (isNumber(config) && oldConfig && isNumber(oldConfig)) { + const oldConfig = (freshOptions as any).fields[field]; + + if (!oldConfig) { + rObject.replace = true; + } + + if (isNumber(config) && isNumber(oldConfig)) { if (oldConfig && config.nextId !== oldConfig.nextId) { - rObject.config = true; + rObject.replace = true; config.nextId = oldConfig.nextId; } } - if (isString(config) && oldConfig && isString(oldConfig)) { + if (isString(config) && isString(oldConfig)) { if (oldConfig && config.nextId !== oldConfig.nextId) { - rObject.config = true; + rObject.replace = true; config.nextId = oldConfig.nextId; } } @@ -42,54 +61,102 @@ export function checkAndUpdateOptions( return rObject; } -async function dbInitialiseLogic( +async function refreshDBOptions( mongooseModel: Model, assignId: MongooseIdAssigner, + retries = 0, ): Promise { const options = assignId.options; - try { - const oldOptions = await mongooseModel.db + const freshOptions = await mongooseModel.db .collection(localStateStore.getCollName()) .findOne({ modelName: options.modelName }); - const mergedOptions = checkAndUpdateOptions(options, oldOptions); + const mergedOptions = checkAndUpdateOptions(options, freshOptions); + + if (mergedOptions.abort) { + assignId.appendState({ + modelName: options.modelName, + readyState: 1, + model: mongooseModel, + }); + + return 1; + } + + let update; - if (mergedOptions.changes) { - const update = await mongooseModel.db + if (mergedOptions.replace) { + update = await mongooseModel.db .collection(localStateStore.getCollName()) .findOneAndReplace( - { modelName: options.modelName }, + { + modelName: options.modelName, + timestamp: mergedOptions.options.timestamp, + }, mergedOptions.options, { upsert: true, }, ); - - if (update.ok) { - assignId.appendState({ - modelName: options.modelName, - readyState: 1, - model: mongooseModel, - }); - return 1; - } else { - assignId.appendState({ + } else if (mergedOptions.delete) { + update = await mongooseModel.db + .collection(localStateStore.getCollName()) + .findOneAndDelete({ modelName: options.modelName, - error: new Error(`AssignId Initialise Error!', ${options.modelName}`), - readyState: 3, + timestamp: mergedOptions.options.timestamp, }); - return 3; + + // new options requests deletion of old options + // but those options have been updated by another process + if (!update || !update.ok) { + throwPluginError( + 'Error at initialisation, cannot delete old options, Still in use!', + options.modelName, + ); } } - assignId.appendState({ - modelName: options.modelName, - readyState: 1, - model: mongooseModel, - }); + if (update && update.ok) { + assignId.appendState({ + readyState: 1, + model: mongooseModel, + }); + return 1; + } else { + throwPluginError(`Initialisation error ${update}`, options.modelName); + return 3; + } + } catch (e) { + if (e.code === 11000) { + if (retries > 30) { + throwPluginError( + 'Initialisation error, maximum retries attained', + options.modelName, + ); + } + return refreshDBOptions(mongooseModel, assignId, ++retries); + } + return Promise.reject(e); + } +} - return 1; +async function dbInitialiseLogic( + mongooseModel: Model, + assignId: MongooseIdAssigner, +): Promise { + const options = assignId.options; + + try { + // create index, ensures no duplicates during upserts + await mongooseModel.db + .collection(localStateStore.getCollName()) + .createIndex('modelName', { + unique: true, + background: false, + }); + + return await refreshDBOptions(mongooseModel, assignId); } catch (e) { assignId.appendState({ modelName: options.modelName, @@ -103,6 +170,7 @@ async function dbInitialiseLogic( export async function initialiseOptions( mongooseModel: Model, assignId: MongooseIdAssigner, + retries = 0, ): Promise { const options = assignId.options; @@ -125,6 +193,23 @@ export async function initialiseOptions( } else if (mongooseModel.db.readyState === 1) { return await dbInitialiseLogic(mongooseModel, assignId); } - return 0; - // disconnecting, disconnected + + if (retries < 10) { + try { + // 3 - disconnecting, wait more + // 0 - disconnected, wait less as connection can be back anytime. + await waitPromise( + (mongooseModel.db.readyState === 3 ? 500 : 100) * retries, + ); + + return initialiseOptions(mongooseModel, assignId, ++retries); + } catch (e) { + return Promise.reject(e); + } + } else { + throwPluginError( + 'Initialisation failed, cannot establish db connection not established!', + ); + return 0; + } }