diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js new file mode 100644 index 0000000000..cb721db00e --- /dev/null +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -0,0 +1,53 @@ + +let mongodb = require('mongodb'); +let Collection = mongodb.Collection; + +export default class MongoCollection { + _mongoCollection:Collection; + + constructor(mongoCollection:Collection) { + this._mongoCollection = mongoCollection; + } + + // Does a find with "smart indexing". + // Currently this just means, if it needs a geoindex and there is + // none, then build the geoindex. + // This could be improved a lot but it's not clear if that's a good + // idea. Or even if this behavior is a good idea. + find(query, { skip, limit, sort } = {}) { + return this._rawFind(query, { skip, limit, sort }) + .catch(error => { + // Check for "no geoindex" error + if (error.code != 17007 || + !error.message.match(/unable to find index for .geoNear/)) { + throw error; + } + // Figure out what key needs an index + let key = error.message.match(/field=([A-Za-z_0-9]+) /)[1]; + if (!key) { + throw error; + } + + var index = {}; + index[key] = '2d'; + //TODO: condiser moving index creation logic into Schema.js + return this._mongoCollection.createIndex(index) + // Retry, but just once. + .then(() => this._rawFind(query, { skip, limit, sort })); + }); + } + + _rawFind(query, { skip, limit, sort } = {}) { + return this._mongoCollection + .find(query, { skip, limit, sort }) + .toArray(); + } + + count(query, { skip, limit, sort } = {}) { + return this._mongoCollection.count(query, { skip, limit, sort }); + } + + drop() { + return this._mongoCollection.drop(); + } +} diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 742420c5b3..201388b2ff 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -1,4 +1,6 @@ +import MongoCollection from './MongoCollection'; + let mongodb = require('mongodb'); let MongoClient = mongodb.MongoClient; @@ -30,6 +32,12 @@ export class MongoStorageAdapter { }); } + adaptiveCollection(name: string) { + return this.connect() + .then(() => this.database.collection(name)) + .then(rawCollection => new MongoCollection(rawCollection)); + } + collectionExists(name: string) { return this.connect().then(() => { return this.database.listCollections({ name: name }).toArray(); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 99ed564838..a7d26245a7 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -38,6 +38,10 @@ DatabaseController.prototype.collection = function(className) { return this.rawCollection(className); }; +DatabaseController.prototype.adaptiveCollection = function(className) { + return this.adapter.adaptiveCollection(this.collectionPrefix + className); +}; + DatabaseController.prototype.collectionExists = function(className) { return this.adapter.collectionExists(this.collectionPrefix + className); }; @@ -340,9 +344,8 @@ DatabaseController.prototype.create = function(className, object, options) { // to avoid Mongo-format dependencies. // Returns a promise that resolves to a list of items. DatabaseController.prototype.mongoFind = function(className, query, options = {}) { - return this.collection(className).then((coll) => { - return coll.find(query, options).toArray(); - }); + return this.adaptiveCollection(className) + .then(collection => collection.find(query, options)); }; // Deletes everything in the database matching the current collectionPrefix @@ -378,23 +381,17 @@ function keysForQuery(query) { // Returns a promise for a list of related ids given an owning id. // className here is the owning className. DatabaseController.prototype.relatedIds = function(className, key, owningId) { - var joinTable = '_Join:' + key + ':' + className; - return this.collection(joinTable).then((coll) => { - return coll.find({owningId: owningId}).toArray(); - }).then((results) => { - return results.map(r => r.relatedId); - }); + return this.adaptiveCollection(joinTableName(className, key)) + .then(coll => coll.find({owningId : owningId})) + .then(results => results.map(r => r.relatedId)); }; // Returns a promise for a list of owning ids given some related ids. // className here is the owning className. DatabaseController.prototype.owningIds = function(className, key, relatedIds) { - var joinTable = '_Join:' + key + ':' + className; - return this.collection(joinTable).then((coll) => { - return coll.find({relatedId: {'$in': relatedIds}}).toArray(); - }).then((results) => { - return results.map(r => r.owningId); - }); + return this.adaptiveCollection(joinTableName(className, key)) + .then(coll => coll.find({ relatedId: { '$in': relatedIds } })) + .then(results => results.map(r => r.owningId)); }; // Modifies query so that it no longer has $in on relation fields, or @@ -443,38 +440,6 @@ DatabaseController.prototype.reduceRelationKeys = function(className, query) { } }; -// Does a find with "smart indexing". -// Currently this just means, if it needs a geoindex and there is -// none, then build the geoindex. -// This could be improved a lot but it's not clear if that's a good -// idea. Or even if this behavior is a good idea. -DatabaseController.prototype.smartFind = function(coll, where, options) { - return coll.find(where, options).toArray() - .then((result) => { - return result; - }, (error) => { - // Check for "no geoindex" error - if (!error.message.match(/unable to find index for .geoNear/) || - error.code != 17007) { - throw error; - } - - // Figure out what key needs an index - var key = error.message.match(/field=([A-Za-z_0-9]+) /)[1]; - if (!key) { - throw error; - } - - var index = {}; - index[key] = '2d'; - //TODO: condiser moving index creation logic into Schema.js - return coll.createIndex(index).then(() => { - // Retry, but just once. - return coll.find(where, options).toArray(); - }); - }); -}; - // Runs a query on the database. // Returns a promise that resolves to a list of items. // Options: @@ -528,8 +493,8 @@ DatabaseController.prototype.find = function(className, query, options = {}) { }).then(() => { return this.reduceInRelation(className, query, schema); }).then(() => { - return this.collection(className); - }).then((coll) => { + return this.adaptiveCollection(className); + }).then(collection => { var mongoWhere = transform.transformWhere(schema, className, query); if (!isMaster) { var orParts = [ @@ -542,9 +507,9 @@ DatabaseController.prototype.find = function(className, query, options = {}) { mongoWhere = {'$and': [mongoWhere, {'$or': orParts}]}; } if (options.count) { - return coll.count(mongoWhere, mongoOptions); + return collection.count(mongoWhere, mongoOptions); } else { - return this.smartFind(coll, mongoWhere, mongoOptions) + return collection.find(mongoWhere, mongoOptions) .then((mongoResults) => { return mongoResults.map((r) => { return this.untransformObject( @@ -555,4 +520,8 @@ DatabaseController.prototype.find = function(className, query, options = {}) { }); }; +function joinTableName(className, key) { + return `_Join:${key}:${className}`; +} + module.exports = DatabaseController; diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index 1ed606b136..007625f3c4 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -38,11 +38,10 @@ function mongoSchemaToSchemaAPIResponse(schema) { } function getAllSchemas(req) { - return req.config.database.collection('_SCHEMA') - .then(coll => coll.find({}).toArray()) - .then(schemas => ({response: { - results: schemas.map(mongoSchemaToSchemaAPIResponse) - }})); + return req.config.database.adaptiveCollection('_SCHEMA') + .then(collection => collection.find({})) + .then(schemas => schemas.map(mongoSchemaToSchemaAPIResponse)) + .then(schemas => ({ response: { results: schemas }})); } function getOneSchema(req) { @@ -152,7 +151,7 @@ function deleteSchema(req) { throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, Schema.invalidClassNameMessage(req.params.className)); } - return req.config.database.collection(req.params.className) + return req.config.database.adaptiveCollection(req.params.className) .then(collection => { return collection.count() .then(count => { @@ -161,19 +160,19 @@ function deleteSchema(req) { } return collection.drop(); }) - .then(() => { - // We've dropped the collection now, so delete the item from _SCHEMA - // and clear the _Join collections - return req.config.database.collection('_SCHEMA') - .then(coll => coll.findAndRemove({_id: req.params.className}, [])) - .then(doc => { - if (doc.value === null) { - //tried to delete non-existent class - return Promise.resolve(); - } - return removeJoinTables(req.config.database, doc.value); - }); - }) + }) + .then(() => { + // We've dropped the collection now, so delete the item from _SCHEMA + // and clear the _Join collections + return req.config.database.collection('_SCHEMA') + .then(coll => coll.findAndRemove({_id: req.params.className}, [])) + .then(doc => { + if (doc.value === null) { + //tried to delete non-existent class + return Promise.resolve(); + } + return removeJoinTables(req.config.database, doc.value); + }); }) .then(() => { // Success