Remove direct mongo access from SchemaRouter.modify, Schema.deleteField.

This commit is contained in:
Nikita Lutsenko
2016-02-29 17:41:09 -08:00
parent 028ef2a7b2
commit 6893895aea
4 changed files with 93 additions and 153 deletions

View File

@@ -483,7 +483,7 @@ describe('Schema', () => {
.then(schema => schema.deleteField('installationId', '_Installation')) .then(schema => schema.deleteField('installationId', '_Installation'))
.catch(error => { .catch(error => {
expect(error.code).toEqual(136); expect(error.code).toEqual(136);
expect(error.error).toEqual('field installationId cannot be changed'); expect(error.message).toEqual('field installationId cannot be changed');
done(); done();
}); });
}); });
@@ -493,7 +493,7 @@ describe('Schema', () => {
.then(schema => schema.deleteField('field', 'NoClass')) .then(schema => schema.deleteField('field', 'NoClass'))
.catch(error => { .catch(error => {
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(error.error).toEqual('class NoClass does not exist'); expect(error.message).toEqual('Class NoClass does not exist.');
done(); done();
}); });
}); });
@@ -504,7 +504,7 @@ describe('Schema', () => {
.then(schema => schema.deleteField('missingField', 'HasAllPOD')) .then(schema => schema.deleteField('missingField', 'HasAllPOD'))
.fail(error => { .fail(error => {
expect(error.code).toEqual(255); expect(error.code).toEqual(255);
expect(error.error).toEqual('field missingField does not exist, cannot delete'); expect(error.message).toEqual('Field missingField does not exist, cannot delete.');
done(); done();
}); });
}); });
@@ -523,11 +523,11 @@ describe('Schema', () => {
config.database.adapter.database.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); expect(err).toEqual(null);
config.database.loadSchema() config.database.loadSchema()
.then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database.adapter.database, 'test_')) .then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database))
.then(() => config.database.adapter.database.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => { .then(() => config.database.adapter.database.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => {
expect(err).not.toEqual(null); expect(err).not.toEqual(null);
done(); done();
})) }));
}); });
}) })
}); });
@@ -538,7 +538,7 @@ describe('Schema', () => {
var obj2 = hasAllPODobject(); var obj2 = hasAllPODobject();
var p = Parse.Object.saveAll([obj1, obj2]) var p = Parse.Object.saveAll([obj1, obj2])
.then(() => config.database.loadSchema()) .then(() => config.database.loadSchema())
.then(schema => schema.deleteField('aString', 'HasAllPOD', config.database.adapter.database, 'test_')) .then(schema => schema.deleteField('aString', 'HasAllPOD', config.database))
.then(() => new Parse.Query('HasAllPOD').get(obj1.id)) .then(() => new Parse.Query('HasAllPOD').get(obj1.id))
.then(obj1Reloaded => { .then(obj1Reloaded => {
expect(obj1Reloaded.get('aString')).toEqual(undefined); expect(obj1Reloaded.get('aString')).toEqual(undefined);
@@ -568,7 +568,7 @@ describe('Schema', () => {
expect(obj1.get('aPointer').id).toEqual(obj1.id); expect(obj1.get('aPointer').id).toEqual(obj1.id);
}) })
.then(() => config.database.loadSchema()) .then(() => config.database.loadSchema())
.then(schema => schema.deleteField('aPointer', 'NewClass', config.database.adapter.database, 'test_')) .then(schema => schema.deleteField('aPointer', 'NewClass', config.database))
.then(() => new Parse.Query('NewClass').get(obj1.id)) .then(() => new Parse.Query('NewClass').get(obj1.id))
.then(obj1 => { .then(obj1 => {
expect(obj1.get('aPointer')).toEqual(undefined); expect(obj1.get('aPointer')).toEqual(undefined);

View File

@@ -369,7 +369,7 @@ describe('schemas', () => {
}, (error, response, body) => { }, (error, response, body) => {
expect(response.statusCode).toEqual(400); expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(body.error).toEqual('class NoClass does not exist'); expect(body.error).toEqual('Class NoClass does not exist.');
done(); done();
}); });
}); });
@@ -390,7 +390,7 @@ describe('schemas', () => {
}, (error, response, body) => { }, (error, response, body) => {
expect(response.statusCode).toEqual(400); expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(255); expect(body.code).toEqual(255);
expect(body.error).toEqual('field aString exists, cannot update'); expect(body.error).toEqual('Field aString exists, cannot update.');
done(); done();
}); });
}) })
@@ -412,7 +412,7 @@ describe('schemas', () => {
}, (error, response, body) => { }, (error, response, body) => {
expect(response.statusCode).toEqual(400); expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(255); expect(body.code).toEqual(255);
expect(body.error).toEqual('field nonExistentKey does not exist, cannot delete'); expect(body.error).toEqual('Field nonExistentKey does not exist, cannot delete.');
done(); done();
}); });
}); });

View File

@@ -1,8 +1,8 @@
// schemas.js // schemas.js
var express = require('express'), var express = require('express'),
Parse = require('parse/node').Parse, Parse = require('parse/node').Parse,
Schema = require('../Schema'); Schema = require('../Schema');
import PromiseRouter from '../PromiseRouter'; import PromiseRouter from '../PromiseRouter';
@@ -49,10 +49,10 @@ function getAllSchemas(req) {
return masterKeyRequiredResponse(); return masterKeyRequiredResponse();
} }
return req.config.database.collection('_SCHEMA') return req.config.database.collection('_SCHEMA')
.then(coll => coll.find({}).toArray()) .then(coll => coll.find({}).toArray())
.then(schemas => ({response: { .then(schemas => ({response: {
results: schemas.map(mongoSchemaToSchemaAPIResponse) results: schemas.map(mongoSchemaToSchemaAPIResponse)
}})); }}));
} }
function getOneSchema(req) { function getOneSchema(req) {
@@ -60,15 +60,15 @@ function getOneSchema(req) {
return masterKeyRequiredResponse(); return masterKeyRequiredResponse();
} }
return req.config.database.collection('_SCHEMA') return req.config.database.collection('_SCHEMA')
.then(coll => coll.findOne({'_id': req.params.className})) .then(coll => coll.findOne({'_id': req.params.className}))
.then(schema => ({response: mongoSchemaToSchemaAPIResponse(schema)})) .then(schema => ({response: mongoSchemaToSchemaAPIResponse(schema)}))
.catch(() => ({ .catch(() => ({
status: 400, status: 400,
response: { response: {
code: 103, code: 103,
error: 'class ' + req.params.className + ' does not exist', error: 'class ' + req.params.className + ' does not exist',
} }
})); }));
} }
function createSchema(req) { function createSchema(req) {
@@ -91,12 +91,12 @@ function createSchema(req) {
}); });
} }
return req.config.database.loadSchema() return req.config.database.loadSchema()
.then(schema => schema.addClassIfNotExists(className, req.body.fields)) .then(schema => schema.addClassIfNotExists(className, req.body.fields))
.then(result => ({ response: mongoSchemaToSchemaAPIResponse(result) })) .then(result => ({ response: mongoSchemaToSchemaAPIResponse(result) }))
.catch(error => ({ .catch(error => ({
status: 400, status: 400,
response: error, response: error,
})); }));
} }
function modifySchema(req) { function modifySchema(req) {
@@ -112,75 +112,48 @@ function modifySchema(req) {
var className = req.params.className; var className = req.params.className;
return req.config.database.loadSchema() return req.config.database.loadSchema()
.then(schema => { .then(schema => {
if (!schema.data[className]) { if (!schema.data[className]) {
return Promise.resolve({ throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${req.params.className} does not exist.`);
status: 400, }
response: {
code: Parse.Error.INVALID_CLASS_NAME, let existingFields = schema.data[className];
error: 'class ' + req.params.className + ' does not exist', Object.keys(submittedFields).forEach(name => {
let field = submittedFields[name];
if (existingFields[name] && field.__op !== 'Delete') {
throw new Parse.Error(255, `Field ${name} exists, cannot update.`);
}
if (!existingFields[name] && field.__op === 'Delete') {
throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
} }
}); });
}
var existingFields = schema.data[className];
for (var submittedFieldName in submittedFields) { let newSchema = Schema.buildMergedSchemaObject(existingFields, submittedFields);
if (existingFields[submittedFieldName] && submittedFields[submittedFieldName].__op !== 'Delete') { let mongoObject = Schema.mongoSchemaFromFieldsAndClassName(newSchema, className);
return Promise.resolve({ if (!mongoObject.result) {
status: 400, throw new Parse.Error(mongoObject.code, mongoObject.error);
response: {
code: 255,
error: 'field ' + submittedFieldName + ' exists, cannot update',
}
});
} }
if (!existingFields[submittedFieldName] && submittedFields[submittedFieldName].__op === 'Delete') { // Finally we have checked to make sure the request is valid and we can start deleting fields.
return Promise.resolve({ // Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
status: 400, let deletionPromises = [];
response: { Object.keys(submittedFields).forEach(submittedFieldName => {
code: 255, if (submittedFields[submittedFieldName].__op === 'Delete') {
error: 'field ' + submittedFieldName + ' does not exist, cannot delete', let promise = schema.deleteField(submittedFieldName, className, req.config.database);
} deletionPromises.push(promise);
}); }
}
}
var newSchema = Schema.buildMergedSchemaObject(existingFields, submittedFields);
var mongoObject = Schema.mongoSchemaFromFieldsAndClassName(newSchema, className);
if (!mongoObject.result) {
return Promise.resolve({
status: 400,
response: mongoObject,
}); });
}
// Finally we have checked to make sure the request is valid and we can start deleting fields. return Promise.all(deletionPromises)
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions. .then(() => new Promise((resolve, reject) => {
var deletionPromises = [] schema.collection.update({_id: className}, mongoObject.result, {w: 1}, (err, docs) => {
Object.keys(submittedFields).forEach(submittedFieldName => { if (err) {
if (submittedFields[submittedFieldName].__op === 'Delete') { reject(err);
var promise = req.config.database.connect() }
.then(() => schema.deleteField( resolve({ response: mongoSchemaToSchemaAPIResponse(mongoObject.result)});
submittedFieldName, })
className, }));
req.config.database.adapter.database,
req.config.database.collectionPrefix
));
deletionPromises.push(promise);
}
}); });
return Promise.all(deletionPromises)
.then(() => new Promise((resolve, reject) => {
schema.collection.update({_id: className}, mongoObject.result, {w: 1}, (err, docs) => {
if (err) {
reject(err);
}
resolve({ response: mongoSchemaToSchemaAPIResponse(mongoObject.result)});
})
}));
});
} }
// A helper function that removes all join tables for a schema. Returns a promise. // A helper function that removes all join tables for a schema. Returns a promise.

View File

@@ -500,80 +500,47 @@ Schema.prototype.validateField = function(className, key, type, freeze) {
// Passing the database and prefix is necessary in order to drop relation collections // Passing the database and prefix is necessary in order to drop relation collections
// and remove fields from objects. Ideally the database would belong to // and remove fields from objects. Ideally the database would belong to
// a database adapter and this fuction would close over it or access it via member. // a database adapter and this function would close over it or access it via member.
Schema.prototype.deleteField = function(fieldName, className, database, prefix) { Schema.prototype.deleteField = function(fieldName, className, database) {
if (!classNameIsValid(className)) { if (!classNameIsValid(className)) {
return Promise.reject({ throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className));
code: Parse.Error.INVALID_CLASS_NAME,
error: invalidClassNameMessage(className),
});
} }
if (!fieldNameIsValid(fieldName)) { if (!fieldNameIsValid(fieldName)) {
return Promise.reject({ throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${fieldName}`);
code: Parse.Error.INVALID_KEY_NAME,
error: 'invalid field name: ' + fieldName,
});
} }
//Don't allow deleting the default fields. //Don't allow deleting the default fields.
if (!fieldNameIsValidForClass(fieldName, className)) { if (!fieldNameIsValidForClass(fieldName, className)) {
return Promise.reject({ throw new Parse.Error(136, `field ${fieldName} cannot be changed`);
code: 136,
error: 'field ' + fieldName + ' cannot be changed',
});
} }
return this.reload() return this.reload()
.then(schema => { .then(schema => {
return schema.hasClass(className) return schema.hasClass(className)
.then(hasClass => { .then(hasClass => {
if (!hasClass) { if (!hasClass) {
return Promise.reject({ throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
code: Parse.Error.INVALID_CLASS_NAME, }
error: 'class ' + className + ' does not exist', if (!schema.data[className][fieldName]) {
}); throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`);
} }
if (!schema.data[className][fieldName]) { if (schema.data[className][fieldName].startsWith('relation<')) {
return Promise.reject({ //For relations, drop the _Join table
code: 255, return database.dropCollection(`_Join:${fieldName}:${className}`);
error: 'field ' + fieldName + ' does not exist, cannot delete', }
});
}
if (schema.data[className][fieldName].startsWith('relation<')) { // for non-relations, remove all the data.
//For relations, drop the _Join table // This is necessary to ensure that the data is still gone if they add the same field.
return database.dropCollection(prefix + '_Join:' + fieldName + ':' + className) return database.collection(className)
//Save the _SCHEMA object .then(collection => {
var mongoFieldName = schema.data[className][fieldName].startsWith('*') ? '_p_' + fieldName : fieldName;
return collection.update({}, { "$unset": { [mongoFieldName] : null } }, { multi: true });
});
})
// Save the _SCHEMA object
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }})); .then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }}));
} else {
//for non-relations, remove all the data. This is necessary to ensure that the data is still gone
//if they add the same field.
return new Promise((resolve, reject) => {
database.collection(prefix + className, (err, coll) => {
if (err) {
reject(err);
} else {
var mongoFieldName = schema.data[className][fieldName].startsWith('*') ?
'_p_' + fieldName :
fieldName;
return coll.update({}, {
"$unset": { [mongoFieldName] : null },
}, {
multi: true,
})
//Save the _SCHEMA object
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }}))
.then(resolve)
.catch(reject);
}
});
});
}
}); });
}); };
}
// Given a schema promise, construct another schema promise that // Given a schema promise, construct another schema promise that
// validates this field once the schema loads. // validates this field once the schema loads.