diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js new file mode 100644 index 0000000000..3c55e1dd1b --- /dev/null +++ b/spec/DatabaseController.spec.js @@ -0,0 +1,17 @@ +'use strict'; + +let DatabaseController = require('../src/Controllers/DatabaseController'); +let MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); + +describe('DatabaseController', () => { + it('can be constructed', done => { + let adapter = new MongoStorageAdapter('mongodb://localhost:27017/test'); + let databaseController = new DatabaseController(adapter, { + collectionPrefix: 'test_' + }); + databaseController.connect().then(done, error => { + console.log('error', error.stack); + fail(); + }); + }); +}); diff --git a/spec/ExportAdapter.spec.js b/spec/ExportAdapter.spec.js deleted file mode 100644 index a4f3f9b6ec..0000000000 --- a/spec/ExportAdapter.spec.js +++ /dev/null @@ -1,15 +0,0 @@ -var ExportAdapter = require('../src/ExportAdapter'); - -describe('ExportAdapter', () => { - it('can be constructed', (done) => { - var database = new ExportAdapter('mongodb://localhost:27017/test', - { - collectionPrefix: 'test_' - }); - database.connect().then(done, (error) => { - console.log('error', error.stack); - fail(); - }); - }); - -}); diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index ca42779279..6a02009feb 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -520,11 +520,11 @@ describe('Schema', () => { return obj2.save(); }) .then(() => { - config.database.db.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => { + config.database.adapter.database.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => { expect(err).toEqual(null); config.database.loadSchema() - .then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database.db, 'test_')) - .then(() => config.database.db.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => { + .then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database.adapter.database, 'test_')) + .then(() => config.database.adapter.database.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => { expect(err).not.toEqual(null); done(); })) @@ -538,7 +538,7 @@ describe('Schema', () => { var obj2 = hasAllPODobject(); var p = Parse.Object.saveAll([obj1, obj2]) .then(() => config.database.loadSchema()) - .then(schema => schema.deleteField('aString', 'HasAllPOD', config.database.db, 'test_')) + .then(schema => schema.deleteField('aString', 'HasAllPOD', config.database.adapter.database, 'test_')) .then(() => new Parse.Query('HasAllPOD').get(obj1.id)) .then(obj1Reloaded => { expect(obj1Reloaded.get('aString')).toEqual(undefined); @@ -568,7 +568,7 @@ describe('Schema', () => { expect(obj1.get('aPointer').id).toEqual(obj1.id); }) .then(() => config.database.loadSchema()) - .then(schema => schema.deleteField('aPointer', 'NewClass', config.database.db, 'test_')) + .then(schema => schema.deleteField('aPointer', 'NewClass', config.database.adapter.database, 'test_')) .then(() => new Parse.Query('NewClass').get(obj1.id)) .then(obj1 => { expect(obj1.get('aPointer')).toEqual(undefined); diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 1a6a30696e..36ba763787 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -710,10 +710,10 @@ describe('schemas', () => { }, (error, response, body) => { expect(response.statusCode).toEqual(200); expect(response.body).toEqual({}); - config.database.db.collection('test__Join:aRelation:MyOtherClass', { strict: true }, (err, coll) => { + config.database.adapter.database.collection('test__Join:aRelation:MyOtherClass', { strict: true }, (err, coll) => { //Expect Join table to be gone expect(err).not.toEqual(null); - config.database.db.collection('test_MyOtherClass', { strict: true }, (err, coll) => { + config.database.adapter.database.collection('test_MyOtherClass', { strict: true }, (err, coll) => { // Expect data table to be gone expect(err).not.toEqual(null); request.get({ diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js index a1d5955ff4..2ff9fdb24a 100644 --- a/src/Adapters/Files/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -8,7 +8,7 @@ // * getFileLocation(config, request, filename) // // Default is GridStoreAdapter, which requires mongo -// and for the API server to be using the ExportAdapter +// and for the API server to be using the DatabaseController with Mongo // database adapter. export class FilesAdapter { diff --git a/src/Adapters/Files/GridStoreAdapter.js b/src/Adapters/Files/GridStoreAdapter.js index 21934c9a5a..00fd37bcc0 100644 --- a/src/Adapters/Files/GridStoreAdapter.js +++ b/src/Adapters/Files/GridStoreAdapter.js @@ -11,7 +11,7 @@ export class GridStoreAdapter extends FilesAdapter { // Returns a promise createFile(config, filename, data) { return config.database.connect().then(() => { - let gridStore = new GridStore(config.database.db, filename, 'w'); + let gridStore = new GridStore(config.database.adapter.database, filename, 'w'); return gridStore.open(); }).then((gridStore) => { return gridStore.write(data); @@ -22,7 +22,7 @@ export class GridStoreAdapter extends FilesAdapter { deleteFile(config, filename) { return config.database.connect().then(() => { - let gridStore = new GridStore(config.database.db, filename, 'w'); + let gridStore = new GridStore(config.database.adapter.database, filename, 'w'); return gridStore.open(); }).then((gridStore) => { return gridStore.unlink(); @@ -33,9 +33,9 @@ export class GridStoreAdapter extends FilesAdapter { getFileData(config, filename) { return config.database.connect().then(() => { - return GridStore.exist(config.database.db, filename); + return GridStore.exist(config.database.adapter.database, filename); }).then(() => { - let gridStore = new GridStore(config.database.db, filename, 'r'); + let gridStore = new GridStore(config.database.adapter.database, filename, 'r'); return gridStore.open(); }).then((gridStore) => { return gridStore.read(); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js new file mode 100644 index 0000000000..914d9bb030 --- /dev/null +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -0,0 +1,49 @@ + +let mongodb = require('mongodb'); +let MongoClient = mongodb.MongoClient; + +export class MongoStorageAdapter { + // Private + _uri: string; + // Public + connectionPromise; + database; + + constructor(uri: string) { + this._uri = uri; + } + + connect() { + if (this.connectionPromise) { + return this.connectionPromise; + } + + this.connectionPromise = MongoClient.connect(this._uri).then(database => { + this.database = database; + }); + return this.connectionPromise; + } + + collection(name: string) { + return this.connect().then(() => { + return this.database.collection(name); + }); + } + + // Used for testing only right now. + collectionsContaining(match: string) { + return this.connect().then(() => { + return this.database.collections(); + }).then(collections => { + return collections.filter(collection => { + if (collection.namespace.match(/\.system\./)) { + return false; + } + return (collection.collectionName.indexOf(match) == 0); + }); + }); + } +} + +export default MongoStorageAdapter; +module.exports = MongoStorageAdapter; // Required for tests diff --git a/src/ExportAdapter.js b/src/Controllers/DatabaseController.js similarity index 85% rename from src/ExportAdapter.js rename to src/Controllers/DatabaseController.js index 821c69cdca..954b1a6f96 100644 --- a/src/ExportAdapter.js +++ b/src/Controllers/DatabaseController.js @@ -2,18 +2,17 @@ // Parse database. var mongodb = require('mongodb'); -var MongoClient = mongodb.MongoClient; var Parse = require('parse/node').Parse; -var Schema = require('./Schema'); -var transform = require('./transform'); +var Schema = require('./../Schema'); +var transform = require('./../transform'); // options can contain: // collectionPrefix: the string to put in front of every collection name. -function ExportAdapter(mongoURI, options = {}) { - this.mongoURI = mongoURI; +function DatabaseController(adapter, { collectionPrefix } = {}) { + this.adapter = adapter; - this.collectionPrefix = options.collectionPrefix; + this.collectionPrefix = collectionPrefix; // We don't want a mutable this.schema, because then you could have // one request that uses different schemas for different parts of @@ -25,25 +24,13 @@ function ExportAdapter(mongoURI, options = {}) { // Connects to the database. Returns a promise that resolves when the // connection is successful. -// this.db will be populated with a Mongo "Db" object when the -// promise resolves successfully. -ExportAdapter.prototype.connect = function() { - if (this.connectionPromise) { - // There's already a connection in progress. - return this.connectionPromise; - } - - this.connectionPromise = Promise.resolve().then(() => { - return MongoClient.connect(this.mongoURI); - }).then((db) => { - this.db = db; - }); - return this.connectionPromise; +DatabaseController.prototype.connect = function() { + return this.adapter.connect(); }; // Returns a promise for a Mongo collection. // Generally just for internal use. -ExportAdapter.prototype.collection = function(className) { +DatabaseController.prototype.collection = function(className) { if (!Schema.classNameIsValid(className)) { throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className); @@ -51,10 +38,8 @@ ExportAdapter.prototype.collection = function(className) { return this.rawCollection(className); }; -ExportAdapter.prototype.rawCollection = function(className) { - return this.connect().then(() => { - return this.db.collection(this.collectionPrefix + className); - }); +DatabaseController.prototype.rawCollection = function(className) { + return this.adapter.collection(this.collectionPrefix + className); }; function returnsTrue() { @@ -64,7 +49,7 @@ function returnsTrue() { // Returns a promise for a schema object. // If we are provided a acceptor, then we run it on the schema. // If the schema isn't accepted, we reload it at most once. -ExportAdapter.prototype.loadSchema = function(acceptor = returnsTrue) { +DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) { if (!this.schemaPromise) { this.schemaPromise = this.collection('_SCHEMA').then((coll) => { @@ -88,8 +73,8 @@ ExportAdapter.prototype.loadSchema = function(acceptor = returnsTrue) { // Returns a promise for the classname that is related to the given // classname through the key. -// TODO: make this not in the ExportAdapter interface -ExportAdapter.prototype.redirectClassNameForKey = function(className, key) { +// TODO: make this not in the DatabaseController interface +DatabaseController.prototype.redirectClassNameForKey = function(className, key) { return this.loadSchema().then((schema) => { var t = schema.getExpectedType(className, key); var match = t.match(/^relation<(.*)>$/); @@ -105,7 +90,7 @@ ExportAdapter.prototype.redirectClassNameForKey = function(className, key) { // Returns a promise that resolves to the new schema. // This does not update this.schema, because in a situation like a // batch request, that could confuse other users of the schema. -ExportAdapter.prototype.validateObject = function(className, object, query) { +DatabaseController.prototype.validateObject = function(className, object, query) { return this.loadSchema().then((schema) => { return schema.validateObject(className, object, query); }); @@ -113,7 +98,7 @@ ExportAdapter.prototype.validateObject = function(className, object, query) { // Like transform.untransformObject but you need to provide a className. // Filters out any data that shouldn't be on this REST-formatted object. -ExportAdapter.prototype.untransformObject = function( +DatabaseController.prototype.untransformObject = function( schema, isMaster, aclGroup, className, mongoObject) { var object = transform.untransformObject(schema, className, mongoObject); @@ -138,7 +123,7 @@ ExportAdapter.prototype.untransformObject = function( // acl: a list of strings. If the object to be updated has an ACL, // one of the provided strings must provide the caller with // write permissions. -ExportAdapter.prototype.update = function(className, query, update, options) { +DatabaseController.prototype.update = function(className, query, update, options) { var acceptor = function(schema) { return schema.hasKeys(className, Object.keys(query)); }; @@ -196,7 +181,7 @@ ExportAdapter.prototype.update = function(className, query, update, options) { // Returns a promise that resolves successfully when these are // processed. // This mutates update. -ExportAdapter.prototype.handleRelationUpdates = function(className, +DatabaseController.prototype.handleRelationUpdates = function(className, objectId, update) { var pending = []; @@ -243,7 +228,7 @@ ExportAdapter.prototype.handleRelationUpdates = function(className, // Adds a relation. // Returns a promise that resolves successfully iff the add was successful. -ExportAdapter.prototype.addRelation = function(key, fromClassName, +DatabaseController.prototype.addRelation = function(key, fromClassName, fromId, toId) { var doc = { relatedId: toId, @@ -258,7 +243,7 @@ ExportAdapter.prototype.addRelation = function(key, fromClassName, // Removes a relation. // Returns a promise that resolves successfully iff the remove was // successful. -ExportAdapter.prototype.removeRelation = function(key, fromClassName, +DatabaseController.prototype.removeRelation = function(key, fromClassName, fromId, toId) { var doc = { relatedId: toId, @@ -277,7 +262,7 @@ ExportAdapter.prototype.removeRelation = function(key, fromClassName, // acl: a list of strings. If the object to be updated has an ACL, // one of the provided strings must provide the caller with // write permissions. -ExportAdapter.prototype.destroy = function(className, query, options = {}) { +DatabaseController.prototype.destroy = function(className, query, options = {}) { var isMaster = !('acl' in options); var aclGroup = options.acl || []; @@ -320,7 +305,7 @@ ExportAdapter.prototype.destroy = function(className, query, options = {}) { // Inserts an object into the database. // Returns a promise that resolves successfully iff the object saved. -ExportAdapter.prototype.create = function(className, object, options) { +DatabaseController.prototype.create = function(className, object, options) { var schema; var isMaster = !('acl' in options); var aclGroup = options.acl || []; @@ -346,7 +331,7 @@ ExportAdapter.prototype.create = function(className, object, options) { // This should only be used for testing - use 'find' for normal code // to avoid Mongo-format dependencies. // Returns a promise that resolves to a list of items. -ExportAdapter.prototype.mongoFind = function(className, query, options = {}) { +DatabaseController.prototype.mongoFind = function(className, query, options = {}) { return this.collection(className).then((coll) => { return coll.find(query, options).toArray(); }); @@ -355,19 +340,13 @@ ExportAdapter.prototype.mongoFind = function(className, query, options = {}) { // Deletes everything in the database matching the current collectionPrefix // Won't delete collections in the system namespace // Returns a promise. -ExportAdapter.prototype.deleteEverything = function() { +DatabaseController.prototype.deleteEverything = function() { this.schemaPromise = null; - return this.connect().then(() => { - return this.db.collections(); - }).then((colls) => { - var promises = []; - for (var coll of colls) { - if (!coll.namespace.match(/\.system\./) && - coll.collectionName.indexOf(this.collectionPrefix) === 0) { - promises.push(coll.drop()); - } - } + return this.adapter.collectionsContaining(this.collectionPrefix).then(collections => { + let promises = collections.map(collection => { + return collection.drop(); + }); return Promise.all(promises); }); }; @@ -390,7 +369,7 @@ function keysForQuery(query) { // Returns a promise for a list of related ids given an owning id. // className here is the owning className. -ExportAdapter.prototype.relatedIds = function(className, key, owningId) { +DatabaseController.prototype.relatedIds = function(className, key, owningId) { var joinTable = '_Join:' + key + ':' + className; return this.collection(joinTable).then((coll) => { return coll.find({owningId: owningId}).toArray(); @@ -401,7 +380,7 @@ ExportAdapter.prototype.relatedIds = function(className, key, owningId) { // Returns a promise for a list of owning ids given some related ids. // className here is the owning className. -ExportAdapter.prototype.owningIds = function(className, key, relatedIds) { +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(); @@ -414,7 +393,7 @@ ExportAdapter.prototype.owningIds = function(className, key, relatedIds) { // equal-to-pointer constraints on relation fields. // Returns a promise that resolves when query is mutated // TODO: this only handles one of these at a time - make it handle more -ExportAdapter.prototype.reduceInRelation = function(className, query, schema) { +DatabaseController.prototype.reduceInRelation = function(className, query, schema) { // Search for an in-relation or equal-to-relation for (var key in query) { if (query[key] && @@ -442,7 +421,7 @@ ExportAdapter.prototype.reduceInRelation = function(className, query, schema) { // Modifies query so that it no longer has $relatedTo // Returns a promise that resolves when query is mutated -ExportAdapter.prototype.reduceRelationKeys = function(className, query) { +DatabaseController.prototype.reduceRelationKeys = function(className, query) { var relatedTo = query['$relatedTo']; if (relatedTo) { return this.relatedIds( @@ -461,7 +440,7 @@ ExportAdapter.prototype.reduceRelationKeys = function(className, query) { // 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. -ExportAdapter.prototype.smartFind = function(coll, where, options) { +DatabaseController.prototype.smartFind = function(coll, where, options) { return coll.find(where, options).toArray() .then((result) => { return result; @@ -502,7 +481,7 @@ ExportAdapter.prototype.smartFind = function(coll, where, options) { // TODO: make userIds not needed here. The db adapter shouldn't know // anything about users, ideally. Then, improve the format of the ACL // arg to work like the others. -ExportAdapter.prototype.find = function(className, query, options = {}) { +DatabaseController.prototype.find = function(className, query, options = {}) { var mongoOptions = {}; if (options.skip) { mongoOptions.skip = options.skip; @@ -568,4 +547,4 @@ ExportAdapter.prototype.find = function(className, query, options = {}) { }); }; -module.exports = ExportAdapter; +module.exports = DatabaseController; diff --git a/src/DatabaseAdapter.js b/src/DatabaseAdapter.js index 5c95741890..47b4dbca22 100644 --- a/src/DatabaseAdapter.js +++ b/src/DatabaseAdapter.js @@ -13,11 +13,12 @@ // * destroy(className, query, options) // * This list is incomplete and the database process is not fully modularized. // -// Default is ExportAdapter, which uses mongo. +// Default is MongoStorageAdapter. -var ExportAdapter = require('./ExportAdapter'); +import DatabaseController from './Controllers/DatabaseController'; +import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter'; -var adapter = ExportAdapter; +let adapter = MongoStorageAdapter; var dbConnections = {}; var databaseURI = 'mongodb://localhost:27017/parse'; var appDatabaseURIs = {}; @@ -46,10 +47,11 @@ function getDatabaseConnection(appId: string, collectionPrefix: string) { } var dbURI = (appDatabaseURIs[appId] ? appDatabaseURIs[appId] : databaseURI); - dbConnections[appId] = new adapter(dbURI, { + + let storageAdapter = new adapter(dbURI); + dbConnections[appId] = new DatabaseController(storageAdapter, { collectionPrefix: collectionPrefix }); - dbConnections[appId].connect(); return dbConnections[appId]; } diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index d738815869..a748ad14fe 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -164,7 +164,7 @@ function modifySchema(req) { .then(() => schema.deleteField( submittedFieldName, className, - req.config.database.db, + req.config.database.adapter.database, req.config.database.collectionPrefix )); deletionPromises.push(promise); @@ -246,7 +246,7 @@ function deleteSchema(req) { //tried to delete non-existant class resolve({ response: {}}); } else { - removeJoinTables(req.config.database.db, req.config.database.collectionPrefix, doc.value) + removeJoinTables(req.config.database.adapter.database, req.config.database.collectionPrefix, doc.value) .then(resolve, reject); } }); diff --git a/src/Schema.js b/src/Schema.js index 63dc6f376e..7f7d4701a7 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -10,7 +10,7 @@ // keeping it this way for now. // // In API-handling code, you should only use the Schema class via the -// ExportAdapter. This will let us replace the schema logic for +// DatabaseController. This will let us replace the schema logic for // different databases. // TODO: hide all schema logic inside the database adapter. diff --git a/src/index.js b/src/index.js index dae92ee4b6..5062b6b03a 100644 --- a/src/index.js +++ b/src/index.js @@ -45,7 +45,7 @@ addParseCloud(); // ParseServer works like a constructor of an express app. // The args that we understand are: -// "databaseAdapter": a class like ExportAdapter providing create, find, +// "databaseAdapter": a class like DatabaseController providing create, find, // update, and delete // "filesAdapter": a class like GridStoreAdapter providing create, get, // and delete