import _ = require('underscore');
import mongoose = require('mongoose'); // only for typing
import each = require('each');
import IManager = require('../IManager');
import ModelBuilder = require('./ModelBuilder');
import IEntityStatic = require('../../Entity/IEntityStatic');
import IEntityListStatic = require('../../Entity/IEntityListStatic');
import Converter = require('../../Entity/Converter');
import EntityMapper = require('../../Mapping/EntityMapper');
import Updater = require('../../Entity/Updater');
import List = require('../../Entity/List');
import Exception = require('../../../Error/Exception');
// import SequenceManager = require('./Sequence/SequenceManager'); // circular require dependency
import ISequenceObject = require('./Sequence/ISequenceObject');
import Sequence = require('./Sequence/Sequence');
// TODO move it to mongoose.d.ts & DefinitelyTyped repo
interface IMongooseCollection {
insert(docs: any[], callback: (e: Error, docs?: mongoose.Document[]) => void): void;
}
export = Manager;
class Manager> implements IManager {
private converter: Converter;
private entityMapper: EntityMapper;
private entityUpdater: Updater;
private model: mongoose.Model;
private sequenceManager: Manager>;
get Model() { return this.model; }
get Converter() { return this.converter; }
constructor(
private EntityStatic: IEntityStatic,
private EntityListStatic: IEntityListStatic,
private connection: mongoose.Mongoose
) {
this.converter = new Converter(EntityStatic);
this.entityMapper = new EntityMapper(EntityStatic);
this.entityUpdater = new Updater(this.entityMapper);
var modelBuilder = new ModelBuilder(this.entityMapper, this.connection);
this.model = modelBuilder.create();
if (this.entityMapper.getIdType().SequenceAllowed) {
this.sequenceManager = new (require('./Sequence/SequenceManager'))(this.connection);
}
}
private autoIncrementIdsOfList(
entityList: List,
callback: (e: Error, entityList?: List) => void)
: void {
this.getNextIds(entityList.count(), (e: Error, nextIds?: number[]) => {
if (e) return callback(e);
entityList.forEach((entity: Entity) => {
var idKey = this.entityMapper.getIdKey();
var id = this.entityUpdater.get(entity, idKey);
var nextId = nextIds.shift();
if (!nextId) {
throw new Exception('No next id is available for current got set');
}
if (id === null) {
this.entityUpdater.set(entity, idKey, nextId);
} else {
throw new Exception('Id tried to autoincrement is not null');
}
});
callback(null, entityList);
});
}
insertList(entityList: List, callback: (e: Error, entityList?: List) => void): IManager {
this.autoIncrementIdsOfList(entityList, (e: Error, entityList?: List) => {
if (e) return callback(e);
var objects = entityList.toArray((entity: Entity) => this.converter.toObject(entity));
(this.model.collection).insert(
objects,
(e: Error, docs?: mongoose.Document[]) => {
if (e) return callback(e);
callback(null, entityList);
});
});
return this;
}
private getIds(entityList: List) {
var idKey = this.entityMapper.getIdKey();
var ids = entityList.map((entity: Entity) => {
return this.entityUpdater.get(entity, idKey);
}).filter((id: number) => {
return id !== null;
}).toArray();
return ids;
}
updateList(entityList: List, callback: (e: Error, entityList?: List) => void): IManager {
var idKey = this.entityMapper.getIdKey();
var idName = this.entityMapper.getIdName();
var ids = this.getIds(entityList);
var condition: any = {};
condition[idName] = { $in: ids };
this.model.find(condition)
.exec((e: Error, docs?: mongoose.Document[]) => {
if (e) return callback(e);
if (docs.length != entityList.count()) {
return callback(new Error('Not found all entities to update. Use insertOrUpdate.'));
}
var indexedById = entityList.index((entity: Entity) => {
return this.entityUpdater.get(entity, idKey);
});
each(docs)
.on('item', (doc: mongoose.Document, next: (e?: Error) => void) => {
var loadedEntity = this.converter.fromRow(doc.toObject());
var loadedEntityId = this.entityUpdater.get(loadedEntity, idKey);
var entity = indexedById[loadedEntityId];
this.updateDocument(entity, doc, next);
})
.on('error', (e: Error) => {
callback(e);
})
.on('end', () => {
callback(null, entityList);
});
});
return this;
}
insertOrUpdateList(
entityList: List,
callback: (e: Error, entityList?: List) => void)
: IManager {
var idKey = this.entityMapper.getIdKey();
var idName = this.entityMapper.getIdName();
var ids = this.getIds(entityList);
var condition: any = {};
condition[idName] = { $in: ids };
this.model.find(condition)
.exec((e: Error, docs?: mongoose.Document[]) => {
if (e) return callback(e);
var loadedIds = _.map(docs, (doc: mongoose.Document) => {
var loadedEntity = this.converter.fromRow(doc.toObject());
return this.entityUpdater.get(loadedEntity, idKey);
});
var listToInsert = entityList.filter((entity: Entity) => {
var id = this.entityUpdater.get(entity, idKey);
return loadedIds.indexOf(id) === -1;
});
var listToUpdate = entityList.filter((entity: Entity) => {
var id = this.entityUpdater.get(entity, idKey);
return loadedIds.indexOf(id) !== -1;
});
var processCallbacks = [];
if (listToInsert.count()) {
processCallbacks.push((next: () => void) => {
this.insertList(listToInsert, next);
});
}
if (listToUpdate.count()) {
processCallbacks.push((next: () => void) => {
this.updateList(listToUpdate, next);
});
}
each(processCallbacks)
.on('item', (
process: (next: (e?: Error) => void) => void,
next: (e?: Error) => void
) => {
process(next);
})
.on('error', (e: Error) => {
callback(e);
})
.on('end', () => {
callback(null, entityList);
});
});
return this;
}
insertOrUpdate(entity: Entity, callback: (e: Error, entity?: Entity) => void): IManager {
var idKey = this.entityMapper.getIdKey();
var idName = this.entityMapper.getIdName();
var id = this.entityUpdater.get(entity, idKey);
if (id) {
var condition: any = {};
condition[idName] = id;
this.model.findOne(condition)
.exec((e: Error, doc: mongoose.Document) => {
if (e) return callback(e);
if (doc) {
this.updateDocument(entity, doc, callback);
} else {
this.insert(entity, callback);
}
});
} else {
this.insert(entity, callback);
}
return this;
}
update(entity: Entity, callback: (e: Error, entity?: Entity) => void): IManager {
var idKey = this.entityMapper.getIdKey();
var idName = this.entityMapper.getIdName();
var id = this.entityUpdater.get(entity, idKey);
var condition: any = {};
condition[idName] = id;
this.model.findOne(condition)
.exec((e: Error, doc: mongoose.Document) => {
if (e) return callback(e);
if (!doc) {
return callback(new Error('Entity not found'));
}
this.updateDocument(entity, doc, callback);
});
return this;
}
private updateDocument(
entity: Entity,
doc: mongoose.Document,
callback: (e: Error, entity?: Entity) => void
) {
var row = this.converter.toRow(entity);
doc.update(row, {}, (e: Error, affectedRows: number) => {
if (e) return callback(e);
callback(null, entity);
});
}
insert(entity: Entity, callback: (e: Error, entity?: Entity) => void): IManager {
var idKey = this.entityMapper.getIdKey();
var id = this.entityUpdater.get(entity, idKey);
if (id) {
this.insertWithId(entity, callback);
} else {
this.getNextId((e: Error, id?: number) => {
if (e) return callback(e);
this.entityUpdater.set(entity, idKey, id);
this.insertWithId(entity, callback);
});
}
return this;
}
private insertWithId(entity: Entity, callback: (e: Error, entity?: Entity) => void) {
var row = this.converter.toRow(entity);
var doc = new this.Model(row);
doc.update(row, { upsert: true }, (e: Error, affectedRows: number) => {
if (e) return callback(e);
callback(null, entity);
});
}
private getNextId(callback: (e: Error, id?: number) => void) {
this.getNextIds(1, (e: Error, ids: number[]) => callback(e, ids ? ids.pop() : null));
}
private getNextIds(count: number, callback: (e: Error, ids?: number[]) => void) {
var idType = this.entityMapper.getIdType();
if (!idType.SequenceAllowed) {
throw new Exception('For collection is not allowed sequence. Id must be specified not null');
}
if (count < 0) {
throw new Exception('Count of next ids must be positive integer');
}
this.sequenceManager.Model.findOneAndUpdate({
id: this.model.modelName
}, {
$inc: { seq: count }
}, (e: Error, doc?: mongoose.Document) => {
if (e) return callback(e);
var sequence: Sequence;
if (doc) {
sequence = this.sequenceManager.Converter.fromObject(doc.toObject());
callback(null, _.range(sequence.Seq - count, sequence.Seq));
} else {
sequence = this.sequenceManager.Converter.fromObject({ id: this.model.modelName, seq: 1 + count });
this.sequenceManager.insert(sequence, () => {
callback(null, _.range(1, 1 + count));
});
}
});
}
}