hydrate-mongodb
Version:
An Object Document Mapper (ODM) for MongoDB.
819 lines (818 loc) • 32.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var async = require("async");
var callback_1 = require("./core/callback");
var queryKind_1 = require("./query/queryKind");
var criteriaBuilder_1 = require("./query/criteriaBuilder");
var updateDocumentBuilder_1 = require("./query/updateDocumentBuilder");
var readContext_1 = require("./mapping/readContext");
var writeContext_1 = require("./mapping/writeContext");
var persistenceError_1 = require("./persistenceError");
var timerUtil_1 = require("./core/timerUtil");
var PersisterImpl = (function () {
function PersisterImpl(session, mapping, collection) {
this._session = session;
this._mapping = mapping;
this._collection = collection;
var inheritanceRoot = mapping.inheritanceRoot;
this.changeTracking = inheritanceRoot.changeTracking;
this.identity = inheritanceRoot.identity;
this._versioned = inheritanceRoot.versioned;
this._traceEnabled = this._session.factory.logger != null;
this._defaultFields = mapping.getDefaultFields();
}
PersisterImpl.prototype.dirtyCheck = function (batch, entity, originalDocument) {
var document = this._dirtyCheck(batch, entity, originalDocument);
if (!document) {
return null;
}
else {
if (this._versioned) {
var version = this._mapping.getDocumentVersion(originalDocument);
this._mapping.setDocumentVersion(document, (version || 0) + 1);
}
this._getCommand(batch).addReplace(document, version);
return document;
}
};
PersisterImpl.prototype.areDocumentsEqual = function (context, entity, originalDocument) {
return this._dirtyCheck(context, entity, originalDocument) == null;
};
PersisterImpl.prototype._dirtyCheck = function (context, entity, originalDocument) {
var writeContext = new writeContext_1.WriteContext();
var document = this._mapping.write(writeContext, entity);
if (writeContext.hasErrors) {
context.error = new persistenceError_1.PersistenceError("Error serializing document:\n" + writeContext.getErrorMessage());
return null;
}
if (this._mapping.areDocumentsEqual(originalDocument, document)) {
return null;
}
else {
return document;
}
};
PersisterImpl.prototype.addInsert = function (batch, entity) {
var context = new writeContext_1.WriteContext();
var document = this._mapping.write(context, entity);
if (context.hasErrors) {
batch.error = new persistenceError_1.PersistenceError("Error serializing document:\n" + context.getErrorMessage());
return null;
}
if (this._versioned) {
this._mapping.setDocumentVersion(document, 1);
}
this._getCommand(batch).addInsert(document);
return document;
};
PersisterImpl.prototype.addRemove = function (batch, entity) {
this._getCommand(batch).addRemove(entity["_id"]);
};
PersisterImpl.prototype.refresh = function (entity, callback) {
var _this = this;
this.findOneById(entity["_id"], function (err, document) {
if (err)
return callback(err);
_this._refreshFromDocument(entity, document, callback);
});
};
PersisterImpl.prototype.watch = function (value, observer) {
this._mapping.watchEntity(value, observer);
};
PersisterImpl.prototype._refreshFromDocument = function (entity, document, callback) {
var context = new readContext_1.ReadContext(this._session);
this._mapping.refresh(context, entity, document);
if (context.hasErrors) {
return callback(new persistenceError_1.PersistenceError("Error deserializing document:\n" + context.getErrorMessage()));
}
callback(null, document);
};
PersisterImpl.prototype.fetch = function (entity, path, callback) {
if (typeof path !== "string") {
return callback(new persistenceError_1.PersistenceError("Path must be a string."));
}
this._mapping.fetch(this._session, undefined, entity, path.split("."), 0, callback);
};
PersisterImpl.prototype.fetchPropertyValue = function (entity, property, callback) {
var _this = this;
var fields = {
_id: 0
};
property.setFieldValue(fields, 1);
var version;
if (this._versioned) {
version = this._session.getVersion(entity);
this._mapping.setDocumentVersion(fields, 1);
}
var criteria = {
_id: entity["_id"]
};
var handleCallback = callback;
if (this._traceEnabled) {
handleCallback = this._createTraceableCallback({ kind: queryKind_1.QueryKind[queryKind_1.QueryKind.FindOne], criteria: criteria, fields: fields }, callback);
}
this._collection.findOne(criteria, fields, function (err, document) {
if (err)
return callback(err);
if (_this._versioned) {
if (_this._mapping.getDocumentVersion(document) != version) {
return callback(new Error("Cannot fetch property value because document version has changed."));
}
}
var readContext = new readContext_1.ReadContext(_this._session);
readContext.path = property.name;
var propertyValue = property.mapping.read(readContext, property.getFieldValue(document));
if (readContext.hasErrors) {
return callback(new persistenceError_1.PersistenceError("Error deserializing property value: " + readContext.getErrorMessage()));
}
handleCallback(null, propertyValue);
});
};
PersisterImpl.prototype.findInverseOf = function (entity, path, callback) {
var criteria = this._prepareInverseQuery(entity, path, callback);
if (criteria) {
this.findAll(criteria, callback);
}
};
PersisterImpl.prototype.findOneInverseOf = function (entity, path, callback) {
var criteria = this._prepareInverseQuery(entity, path, callback);
if (criteria) {
this.findOne(criteria, callback);
}
};
PersisterImpl.prototype._prepareInverseQuery = function (entity, path, callback) {
var property = this._mapping.getProperty(path);
if (property === undefined) {
callback(new persistenceError_1.PersistenceError("Missing property '" + path + "'."));
return null;
}
var id = entity._id;
if (id === undefined) {
callback(new persistenceError_1.PersistenceError("Missing identifier on entity that is the inverse side of a relationship."));
return null;
}
var query = {};
property.setFieldValue(query, id);
return query;
};
PersisterImpl.prototype.findOneById = function (id, callback) {
var entity = this._session.getObject(id);
if (entity !== undefined) {
process.nextTick(function () { return callback(null, entity); });
return;
}
(this._findQueue || (this._findQueue = new FindQueue(this))).add(id, callback);
};
PersisterImpl.prototype.findOne = function (criteria, callback) {
var query = {
criteria: criteria,
fields: this._defaultFields
};
var handleCallback = callback;
if (this._traceEnabled) {
query.kind = queryKind_1.QueryKind[queryKind_1.QueryKind.FindOne];
handleCallback = this._createTraceableCallback(query, callback);
}
this._findOne(query, handleCallback);
};
PersisterImpl.prototype._findOne = function (query, callback) {
var _this = this;
this._collection.findOne(query.criteria, { projection: query.fields }, function (err, document) {
if (err)
return callback(err);
_this._loadOne(document, query.isLazy, callback);
});
};
PersisterImpl.prototype.findAll = function (criteria, callback) {
var query = {
criteria: criteria,
fields: this._defaultFields
};
var handleCallback = callback;
if (this._traceEnabled) {
query.kind = queryKind_1.QueryKind[queryKind_1.QueryKind.FindAll];
handleCallback = this._createTraceableCallback(query, callback);
}
this._findAll(query, handleCallback);
};
PersisterImpl.prototype._findAll = function (query, callback) {
var cursor = this._prepareFind(query), self = this, entities = [];
next();
function next(err) {
if (err)
return error(err);
cursor.next(function (err, item) {
if (err)
return error(err);
if (item == null) {
return callback(null, entities);
}
self._loadOne(item, query.isLazy, function (err, value) {
if (err)
return error(err);
if (value !== null) {
entities.push(value);
}
next();
});
});
}
function error(err) {
cursor.close(null);
callback(err);
callback = function () { };
}
};
PersisterImpl.prototype.executeQuery = function (query, callback) {
var _this = this;
if (query.criteria) {
query.criteria = (this._criteriaBuilder || (this._criteriaBuilder = new criteriaBuilder_1.CriteriaBuilder(this._mapping)))
.build(query.criteria);
if (this._criteriaBuilder.error) {
return callback(this._criteriaBuilder.error);
}
}
if (query.isLazy) {
query.fields = { _id: 1 };
}
else {
query.fields = this._defaultFields;
}
if (query.updateDocument) {
query.updateDocument = (this._updateDocumentBuilder || (this._updateDocumentBuilder = new updateDocumentBuilder_1.UpdateDocumentBuilder(this._mapping)))
.build(query.updateDocument);
if (this._updateDocumentBuilder.error) {
return callback(this._updateDocumentBuilder.error);
}
if (this._versioned) {
var fields = query.updateDocument["$inc"];
if (!fields) {
fields = query.updateDocument["$inc"] = {};
}
this._mapping.setDocumentVersion(fields, 1);
}
}
if (query.sortValue) {
this._prepareOrderDocument(query.sortValue, function (err, preparedOrder) {
if (err)
return callback(err);
query.orderDocument = preparedOrder;
_this._executeQuery(query, callback);
});
}
else {
this._executeQuery(query, callback);
}
};
PersisterImpl.prototype._executeQuery = function (query, callback) {
var handleCallback = callback;
if (this._traceEnabled && query.kind != queryKind_1.QueryKind.FindOneById) {
handleCallback = this._createTraceableCallback(query.toObject(), callback);
}
switch (query.kind) {
case queryKind_1.QueryKind.FindOne:
this._findOne(query, this._fetchOne(query, handleCallback));
break;
case queryKind_1.QueryKind.FindOneById:
this.findOneById(query.id, this._fetchOne(query, handleCallback));
break;
case queryKind_1.QueryKind.FindAll:
this._findAll(query, this._fetchAll(query, handleCallback));
break;
case queryKind_1.QueryKind.FindEach:
this._findEach(query, handleCallback);
break;
case queryKind_1.QueryKind.FindEachSeries:
this._findEachSeries(query, handleCallback);
break;
case queryKind_1.QueryKind.FindCursor:
this._findCursor(query, handleCallback);
break;
case queryKind_1.QueryKind.FindOneAndRemove:
case queryKind_1.QueryKind.FindOneAndUpdate:
case queryKind_1.QueryKind.FindOneAndUpsert:
this._findOneAndModify(query, this._fetchOne(query, handleCallback));
break;
case queryKind_1.QueryKind.RemoveOne:
this._removeOne(query, handleCallback);
break;
case queryKind_1.QueryKind.RemoveAll:
this._removeAll(query, handleCallback);
break;
case queryKind_1.QueryKind.UpdateOne:
case queryKind_1.QueryKind.Upsert:
this._updateOne(query, handleCallback);
break;
case queryKind_1.QueryKind.UpdateAll:
this._updateAll(query, handleCallback);
break;
case queryKind_1.QueryKind.Distinct:
this._distinct(query, handleCallback);
break;
case queryKind_1.QueryKind.Count:
this._count(query, handleCallback);
break;
default:
handleCallback(new persistenceError_1.PersistenceError("Unknown query type '" + query.kind + "'."));
}
};
PersisterImpl.prototype._createTraceableCallback = function (query, callback) {
var _this = this;
var start = process.hrtime();
return function (err, result) {
if (err)
return callback(err);
_this._session.factory.logger.trace({
collection: _this._mapping.inheritanceRoot.collectionName,
query: query,
duration: timerUtil_1.getDuration(start)
}, "[Hydrate] Executed query.");
callback(null, result);
};
};
PersisterImpl.prototype._prepareOrderDocument = function (sorting, callback) {
var order = [];
for (var i = 0; i < sorting.length; i++) {
var value = sorting[i];
var context = this._mapping.resolve(value[0]);
if (context.error) {
return callback(context.error);
}
order.push([context.resolvedPath, value[1]]);
}
callback(null, order);
};
PersisterImpl.prototype._findEach = function (query, callback) {
var cursor = this._prepareFind(query);
var iterator = this._fetchIterator(query);
var completed = 0, started = 0, finished = false, self = this;
replenish();
function replenish() {
cursor.next(function (err, item) {
if (err)
return error(err);
if (item == null) {
if (completed >= started) {
callback();
}
finished = true;
return;
}
process(err, item);
while (cursor.bufferedCount() > 0) {
cursor.next(process);
}
});
}
function process(err, item) {
if (err)
return error(err);
started++;
self._loadOne(item, query.isLazy, function (err, value) {
if (err)
return error(err);
if (value === null) {
done(null);
}
else {
iterator(value, callback_1.onlyOnce(done));
}
});
}
function done(err) {
if (err)
return error(err);
completed++;
if (cursor.bufferedCount() == 0 && completed >= started) {
if (finished)
return callback();
replenish();
}
}
function error(err) {
cursor.close(null);
callback(err);
callback = function () { };
}
};
PersisterImpl.prototype._findEachSeries = function (query, callback) {
var cursor = this._prepareFind(query), iterator = this._fetchIterator(query), self = this;
(function next(err) {
if (err)
return error(err);
cursor.next(function (err, item) {
if (err)
return error(err);
if (item == null) {
return callback();
}
self._loadOne(item, query.isLazy, function (err, value) {
if (err)
return error(err);
if (value === null) {
next();
}
else {
iterator(value, next);
}
});
});
})();
function error(err) {
cursor.close(null);
callback(err);
callback = function () { };
}
};
PersisterImpl.prototype._findCursor = function (query, callback) {
var _this = this;
callback(null, new CursorImpl(this._prepareFind(query), function (document, callback) {
_this._loadOne(document, query.isLazy, function (err, entity) {
if (err)
return callback(err);
if (!query.fetchPaths) {
callback(null, entity);
return;
}
_this._session.fetchInternal(entity, query.fetchPaths, callback);
});
}));
};
PersisterImpl.prototype._prepareFind = function (query) {
var cursor = this._collection.find(query.criteria);
if (query.fields) {
cursor.project(query.fields);
}
if (query.orderDocument !== undefined) {
cursor.sort(query.orderDocument);
}
if (query.skipCount !== undefined) {
cursor.skip(query.skipCount);
}
if (query.limitCount !== undefined) {
cursor.limit(query.limitCount);
}
if (query.collationOptions !== undefined) {
cursor.collation(query.collationOptions);
}
if (query.batchSizeValue !== undefined) {
cursor.batchSize(query.batchSizeValue);
}
return cursor;
};
PersisterImpl.prototype._findOneAndModify = function (query, callback) {
var options = {
returnOriginal: !query.wantsUpdated,
upsert: query.kind == queryKind_1.QueryKind.FindOneAndUpsert,
sort: query.orderDocument
};
var self = this;
if (query.kind == queryKind_1.QueryKind.FindOneAndRemove) {
this._collection.findOneAndDelete(query.criteria, options, handleCallback);
}
else {
this._collection.findOneAndUpdate(query.criteria, query.updateDocument, options, handleCallback);
}
function handleCallback(err, response) {
if (err)
return callback(err);
var document = response.value;
if (!document)
return callback(null);
var entity = self._session.getObject(document["_id"]);
if (entity !== undefined) {
var alreadyLoaded = true;
finish();
}
else {
self._loadOne(document, query.isLazy, function (err, value) {
if (err)
return callback(err);
entity = value;
finish();
});
}
function finish() {
if (entity) {
if (query.kind == queryKind_1.QueryKind.FindOneAndRemove) {
self._session.notifyRemoved(entity);
}
else if (!options.returnOriginal && alreadyLoaded) {
self._refreshFromDocument(entity, document, function (err) {
if (err)
return callback(err);
callback(null, entity);
});
return;
}
}
callback(null, entity);
}
}
};
PersisterImpl.prototype._removeOne = function (query, callback) {
this._collection.deleteOne(query.criteria, function (err, response) {
if (err)
return callback(err);
callback(null, response.deletedCount);
});
};
PersisterImpl.prototype._removeAll = function (query, callback) {
this._collection.deleteMany(query.criteria, function (err, response) {
if (err)
return callback(err);
callback(null, response.deletedCount);
});
};
PersisterImpl.prototype._updateOne = function (query, callback) {
this._collection.updateOne(query.criteria, query.updateDocument, { upsert: query.kind == queryKind_1.QueryKind.Upsert }, function (err, response) {
if (err)
return callback(err);
callback(null, response.modifiedCount);
});
};
PersisterImpl.prototype._updateAll = function (query, callback) {
this._collection.updateMany(query.criteria, query.updateDocument, function (err, response) {
if (err)
return callback(err);
callback(null, response.modifiedCount);
});
};
PersisterImpl.prototype._distinct = function (query, callback) {
var _this = this;
var context = this._mapping.resolve(query.key);
if (context.error) {
return callback(context.error);
}
this._collection.distinct(context.resolvedPath, query.criteria, undefined, function (err, results) {
if (err)
return callback(err);
var readContext = new readContext_1.ReadContext(_this._session);
for (var i = 0, l = results.length; i < l; i++) {
readContext.path = context.resolvedPath;
results[i] = context.resolvedMapping.read(readContext, results[i]);
if (readContext.hasErrors) {
return callback(new persistenceError_1.PersistenceError("Error deserializing distinct values for: " + readContext.getErrorMessage()));
}
}
callback(null, results);
});
};
PersisterImpl.prototype._count = function (query, callback) {
var options = {
limit: query.limitCount,
skip: query.skipCount
};
this._collection.count(query.criteria, options, function (err, result) {
if (err)
return callback(err);
callback(null, result);
});
};
PersisterImpl.prototype._fetchOne = function (query, callback) {
var _this = this;
if (!query.fetchPaths) {
return callback;
}
return function (err, entity) {
if (err)
return callback(err);
if (!entity)
return callback(null);
_this._session.fetchInternal(entity, query.fetchPaths, callback);
};
};
PersisterImpl.prototype._fetchAll = function (query, callback) {
var _this = this;
if (!query.fetchPaths) {
return callback;
}
return function (err, entities) {
if (err)
return callback(err);
async.each(entities, function (entity, done) { return _this._session.fetchInternal(entity, query.fetchPaths, done); }, function (err) {
if (err)
return callback(err);
callback(null, entities);
});
};
};
PersisterImpl.prototype._fetchIterator = function (query) {
var _this = this;
if (!query.fetchPaths) {
return query.iterator;
}
return function (entity, done) {
if (!entity)
return done();
_this._session.fetchInternal(entity, query.fetchPaths, function (err, result) {
if (err)
return done(err);
query.iterator(result, done);
});
};
};
PersisterImpl.prototype._loadOne = function (document, reference, callback) {
var entity;
if (!document) {
entity = null;
}
else if (reference) {
entity = this._session.getReferenceInternal(this._mapping, document["_id"]);
}
else {
entity = this._session.getObject(document["_id"]);
if (entity === undefined) {
var context = new readContext_1.ReadContext(this._session);
entity = this._mapping.read(context, document);
if (context.hasErrors) {
return callback(new persistenceError_1.PersistenceError("Error deserializing document:\n" + context.getErrorMessage()));
}
this._session.registerManaged(this, entity, document);
if (context.fetches) {
this._session.fetchInternal(entity, context.fetches, callback);
return;
}
}
}
process.nextTick(function () { return callback(null, entity); });
};
PersisterImpl.prototype._getCommand = function (batch) {
var id = this._mapping.inheritanceRoot.id;
var command = batch.getCommand(id);
if (!command) {
command = new BulkOperationCommand(batch, this._collection, this._mapping);
batch.addCommand(id, command);
}
return command;
};
return PersisterImpl;
}());
exports.PersisterImpl = PersisterImpl;
var BulkOperationCommand = (function () {
function BulkOperationCommand(batch, collection, mapping) {
this._mapping = mapping;
this._batch = batch;
this.priority = mapping.inheritanceRoot.flushPriority;
this.collectionName = collection.collectionName;
this.operation = collection.initializeUnorderedBulkOp();
this.inserted = this.updated = this.removed = 0;
}
BulkOperationCommand.prototype.addInsert = function (document) {
this.inserted++;
try {
this.operation.insert(document);
}
catch (err) {
this._batch.error = new persistenceError_1.PersistenceError("Error adding insert to batch: " + err.message, err);
}
};
BulkOperationCommand.prototype.addReplace = function (document, version) {
var query = {
_id: document["_id"]
};
if (version != null) {
this._mapping.setDocumentVersion(query, version);
}
this.updated++;
try {
this.operation.find(query).replaceOne(document);
}
catch (err) {
this._batch.error = new persistenceError_1.PersistenceError("Error adding replace to batch: " + err.message, err);
}
};
BulkOperationCommand.prototype.addRemove = function (id) {
var query = {
_id: id
};
this.removed++;
try {
this.operation.find(query).removeOne();
}
catch (err) {
this._batch.error = new persistenceError_1.PersistenceError("Error adding remove to batch: " + err.message, err);
}
};
BulkOperationCommand.prototype.execute = function (callback) {
var _this = this;
this.operation.execute(function (err, result) {
if (err)
return callback(err);
if ((result.nInserted || 0) != _this.inserted) {
return callback(new persistenceError_1.PersistenceError("Flush failed for collection '" + _this.collectionName + "'. Expected to insert " + _this.inserted + " documents but only inserted " + (result.nInserted || 0) + "."));
}
if ((result.nModified || 0) != _this.updated) {
return callback(new persistenceError_1.PersistenceError("Flush failed for collection '" + _this.collectionName + "'. Expected to update " + _this.updated + " documents but only updated " + (result.nModified || 0) + "."));
}
if ((result.nRemoved || 0) != _this.removed) {
return callback(new persistenceError_1.PersistenceError("Flush failed for collection '" + _this.collectionName + "'. Expected to remove " + _this.removed + " documents but only removed " + (result.nRemoved || 0) + "."));
}
callback();
});
};
return BulkOperationCommand;
}());
var FindQueue = (function () {
function FindQueue(persister) {
this._persister = persister;
}
FindQueue.prototype.add = function (id, callback) {
var _this = this;
if (!this._ids) {
this._ids = [];
this._callbacksMap = new Map();
process.nextTick(function () { return _this._process(); });
}
var normalizedId = this._normalizeIdentifier(id);
if (normalizedId == null) {
callback(new persistenceError_1.PersistenceError("'" + id + "' is not a valid identifier."));
return;
}
var key = normalizedId.toString();
var callbacks = this._callbacksMap.get(key);
if (callbacks === undefined) {
callbacks = [];
this._ids.push(normalizedId);
this._callbacksMap.set(key, callbacks);
}
callbacks.push(callback);
};
FindQueue.prototype._normalizeIdentifier = function (id) {
if (typeof id === "string") {
return this._persister.identity.fromString(id);
}
else if (this._persister.identity.validate(id)) {
return id;
}
return null;
};
FindQueue.prototype._process = function () {
var callbacksMap = this._callbacksMap, ids = this._ids;
this._ids = this._callbacksMap = undefined;
if (ids.length == 1) {
var id = ids[0];
this._persister.findOne({ _id: id }, function (err, entity) {
if (err)
return handleCallback(id, err);
if (!entity) {
return handleCallback(id, new persistenceError_1.EntityNotFoundError("Unable to find document with identifier '" + id.toString() + "'."));
}
handleCallback(id, null, entity);
});
return;
}
this._persister.findAll({ _id: { $in: ids } }, function (err, entities) {
if (!err) {
for (var i = 0, l = entities.length; i < l; i++) {
var entity = entities[i];
handleCallback(entity["_id"], null, entity);
}
}
callbacksMap.forEach(function (callbacks, id) { return callCallbacks(callbacks, err || new persistenceError_1.EntityNotFoundError("Unable to find document with identifier '" + id + "'.")); });
});
function handleCallback(id, err, entity) {
var key = id.toString(), callbacks = callbacksMap.get(key);
callbacksMap.delete(key);
callCallbacks(callbacks, err, entity);
}
function callCallbacks(callbacks, err, entity) {
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](err, entity);
}
}
};
return FindQueue;
}());
var CursorImpl = (function () {
function CursorImpl(cursor, loader) {
this._cursor = cursor;
this._loader = loader;
}
CursorImpl.prototype.next = function (callback) {
var _this = this;
this._cursor.next(function (err, item) {
if (err) {
_this.close();
callback(err);
return;
}
if (item == null) {
callback(null, null);
return;
}
_this._loader(item, function (err, result) {
if (err) {
_this.close();
callback(err);
return;
}
callback(null, result);
});
});
};
CursorImpl.prototype.close = function (callback) {
this._cursor.close(callback);
};
return CursorImpl;
}());