Merge pull request #943 from ParsePlatform/nlutsenko.schemaCollection

Add new schema collection type and replace all usages of direct mongo collection for schema operations.
This commit is contained in:
Nikita Lutsenko
2016-03-09 16:27:36 -08:00
6 changed files with 90 additions and 32 deletions

View File

@@ -76,17 +76,6 @@ export default class MongoCollection {
return this._mongoCollection.updateMany(query, update); return this._mongoCollection.updateMany(query, update);
} }
// Atomically find and delete an object based on query.
// The result is the promise with an object that was in the database before deleting.
// Postgres Note: Translates directly to `DELETE * FROM ... RETURNING *`, which will return data after delete is done.
findOneAndDelete(query) {
// arguments: query, sort
return this._mongoCollection.findAndRemove(query, []).then(document => {
// Value is the object where mongo returns multiple fields.
return document.value;
});
}
deleteOne(query) { deleteOne(query) {
return this._mongoCollection.deleteOne(query); return this._mongoCollection.deleteOne(query);
} }

View File

@@ -0,0 +1,58 @@
import MongoCollection from './MongoCollection';
function _mongoSchemaQueryFromNameQuery(name: string, query) {
return _mongoSchemaObjectFromNameFields(name, query);
}
function _mongoSchemaObjectFromNameFields(name: string, fields) {
let object = { _id: name };
if (fields) {
Object.keys(fields).forEach(key => {
object[key] = fields[key];
});
}
return object;
}
export default class MongoSchemaCollection {
_collection: MongoCollection;
constructor(collection: MongoCollection) {
this._collection = collection;
}
getAllSchemas() {
return this._collection._rawFind({});
}
findSchema(name: string) {
return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(results => {
return results[0];
});
}
// Atomically find and delete an object based on query.
// The result is the promise with an object that was in the database before deleting.
// Postgres Note: Translates directly to `DELETE * FROM ... RETURNING *`, which will return data after delete is done.
findAndDeleteSchema(name: string) {
// arguments: query, sort
return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []).then(document => {
// Value is the object where mongo returns multiple fields.
return document.value;
});
}
addSchema(name: string, fields) {
let mongoObject = _mongoSchemaObjectFromNameFields(name, fields);
return this._collection.insertOne(mongoObject);
}
updateSchema(name: string, update) {
return this._collection.updateOne(_mongoSchemaQueryFromNameQuery(name), update);
}
upsertSchema(name: string, query: string, update) {
return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update);
}
}

View File

@@ -1,9 +1,12 @@
import MongoCollection from './MongoCollection'; import MongoCollection from './MongoCollection';
import MongoSchemaCollection from './MongoSchemaCollection';
let mongodb = require('mongodb'); let mongodb = require('mongodb');
let MongoClient = mongodb.MongoClient; let MongoClient = mongodb.MongoClient;
const MongoSchemaCollectionName = '_SCHEMA';
export class MongoStorageAdapter { export class MongoStorageAdapter {
// Private // Private
_uri: string; _uri: string;
@@ -38,6 +41,12 @@ export class MongoStorageAdapter {
.then(rawCollection => new MongoCollection(rawCollection)); .then(rawCollection => new MongoCollection(rawCollection));
} }
schemaCollection(collectionPrefix: string) {
return this.connect()
.then(() => this.adaptiveCollection(collectionPrefix + MongoSchemaCollectionName))
.then(collection => new MongoSchemaCollection(collection));
}
collectionExists(name: string) { collectionExists(name: string) {
return this.connect().then(() => { return this.connect().then(() => {
return this.database.listCollections({ name: name }).toArray(); return this.database.listCollections({ name: name }).toArray();

View File

@@ -33,6 +33,10 @@ DatabaseController.prototype.adaptiveCollection = function(className) {
return this.adapter.adaptiveCollection(this.collectionPrefix + className); return this.adapter.adaptiveCollection(this.collectionPrefix + className);
}; };
DatabaseController.prototype.schemaCollection = function() {
return this.adapter.schemaCollection(this.collectionPrefix);
};
DatabaseController.prototype.collectionExists = function(className) { DatabaseController.prototype.collectionExists = function(className) {
return this.adapter.collectionExists(this.collectionPrefix + className); return this.adapter.collectionExists(this.collectionPrefix + className);
}; };
@@ -59,7 +63,7 @@ DatabaseController.prototype.validateClassName = function(className) {
DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) { DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) {
if (!this.schemaPromise) { if (!this.schemaPromise) {
this.schemaPromise = this.adaptiveCollection('_SCHEMA').then(collection => { this.schemaPromise = this.schemaCollection().then(collection => {
delete this.schemaPromise; delete this.schemaPromise;
return Schema.load(collection); return Schema.load(collection);
}); });
@@ -70,7 +74,7 @@ DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) {
if (acceptor(schema)) { if (acceptor(schema)) {
return schema; return schema;
} }
this.schemaPromise = this.adaptiveCollection('_SCHEMA').then(collection => { this.schemaPromise = this.schemaCollection().then(collection => {
delete this.schemaPromise; delete this.schemaPromise;
return Schema.load(collection); return Schema.load(collection);
}); });

View File

@@ -15,23 +15,22 @@ function classNameMismatchResponse(bodyClass, pathClass) {
} }
function getAllSchemas(req) { function getAllSchemas(req) {
return req.config.database.adaptiveCollection('_SCHEMA') return req.config.database.schemaCollection()
.then(collection => collection.find({})) .then(collection => collection.getAllSchemas())
.then(schemas => schemas.map(Schema.mongoSchemaToSchemaAPIResponse)) .then(schemas => schemas.map(Schema.mongoSchemaToSchemaAPIResponse))
.then(schemas => ({ response: { results: schemas } })); .then(schemas => ({ response: { results: schemas } }));
} }
function getOneSchema(req) { function getOneSchema(req) {
const className = req.params.className; const className = req.params.className;
return req.config.database.adaptiveCollection('_SCHEMA') return req.config.database.schemaCollection()
.then(collection => collection.find({ '_id': className }, { limit: 1 })) .then(collection => collection.findSchema(className))
.then(results => { .then(mongoSchema => {
if (results.length != 1) { if (!mongoSchema) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`); throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
} }
return results[0]; return { response: Schema.mongoSchemaToSchemaAPIResponse(mongoSchema) };
}) });
.then(schema => ({ response: Schema.mongoSchemaToSchemaAPIResponse(schema) }));
} }
function createSchema(req) { function createSchema(req) {
@@ -142,8 +141,8 @@ function deleteSchema(req) {
.then(() => { .then(() => {
// We've dropped the collection now, so delete the item from _SCHEMA // We've dropped the collection now, so delete the item from _SCHEMA
// and clear the _Join collections // and clear the _Join collections
return req.config.database.adaptiveCollection('_SCHEMA') return req.config.database.schemaCollection()
.then(coll => coll.findOneAndDelete({ _id: req.params.className })) .then(coll => coll.findAndDeleteSchema(req.params.className))
.then(document => { .then(document => {
if (document === null) { if (document === null) {
//tried to delete non-existent class //tried to delete non-existent class

View File

@@ -184,7 +184,7 @@ class Schema {
reloadData() { reloadData() {
this.data = {}; this.data = {};
this.perms = {}; this.perms = {};
return this._collection.find({}).then(results => { return this._collection.getAllSchemas().then(results => {
for (let obj of results) { for (let obj of results) {
let className = null; let className = null;
let classData = {}; let classData = {};
@@ -231,7 +231,7 @@ class Schema {
return Promise.reject(mongoObject); return Promise.reject(mongoObject);
} }
return this._collection.insertOne(mongoObject.result) return this._collection.addSchema(className, mongoObject.result)
.then(result => result.ops[0]) .then(result => result.ops[0])
.catch(error => { .catch(error => {
if (error.code === 11000) { //Mongo's duplicate key error if (error.code === 11000) { //Mongo's duplicate key error
@@ -268,7 +268,7 @@ class Schema {
'schema is frozen, cannot add: ' + className); 'schema is frozen, cannot add: ' + className);
} }
// We don't have this class. Update the schema // We don't have this class. Update the schema
return this._collection.insertOne({ _id: className }).then(() => { return this._collection.addSchema(className).then(() => {
// The schema update succeeded. Reload the schema // The schema update succeeded. Reload the schema
return this.reloadData(); return this.reloadData();
}, () => { }, () => {
@@ -288,14 +288,13 @@ class Schema {
// Sets the Class-level permissions for a given className, which must exist. // Sets the Class-level permissions for a given className, which must exist.
setPermissions(className, perms) { setPermissions(className, perms) {
var query = {_id: className};
var update = { var update = {
_metadata: { _metadata: {
class_permissions: perms class_permissions: perms
} }
}; };
update = {'$set': update}; update = {'$set': update};
return this._collection.updateOne(query, update).then(() => { return this._collection.updateSchema(className, update).then(() => {
// The update succeeded. Reload the schema // The update succeeded. Reload the schema
return this.reloadData(); return this.reloadData();
}); });
@@ -353,12 +352,12 @@ class Schema {
// We don't have this field. Update the schema. // We don't have this field. Update the schema.
// Note that we use the $exists guard and $set to avoid race // Note that we use the $exists guard and $set to avoid race
// conditions in the database. This is important! // conditions in the database. This is important!
var query = { _id: className }; let query = {};
query[key] = { '$exists': false }; query[key] = { '$exists': false };
var update = {}; var update = {};
update[key] = type; update[key] = type;
update = {'$set': update}; update = {'$set': update};
return this._collection.upsertOne(query, update).then(() => { return this._collection.upsertSchema(className, query, update).then(() => {
// The update succeeded. Reload the schema // The update succeeded. Reload the schema
return this.reloadData(); return this.reloadData();
}, () => { }, () => {
@@ -428,7 +427,7 @@ class Schema {
}); });
}) })
// Save the _SCHEMA object // Save the _SCHEMA object
.then(() => this._collection.updateOne({ _id: className }, { $unset: { [fieldName]: null } })); .then(() => this._collection.updateSchema(className, { $unset: { [fieldName]: null } }));
}); });
} }