Implement DELETE /schemas/:className

This commit is contained in:
Drew Gross
2016-02-17 19:00:17 -08:00
parent 7376c5aa26
commit 61b4468dac
3 changed files with 188 additions and 1 deletions

View File

@@ -1,6 +1,9 @@
var Parse = require('parse/node').Parse; var Parse = require('parse/node').Parse;
var request = require('request'); var request = require('request');
var dd = require('deep-diff'); var dd = require('deep-diff');
var Config = require('../src/Config');
var config = new Config('test');
var hasAllPODobject = () => { var hasAllPODobject = () => {
var obj = new Parse.Object('HasAllPOD'); var obj = new Parse.Object('HasAllPOD');
@@ -633,4 +636,102 @@ describe('schemas', () => {
}); });
}); });
}); });
it('requires the master key to delete schemas', done => {
request.del({
url: 'http://localhost:8378/1/schemas/DoesntMatter',
headers: noAuthHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized');
done();
});
});
it('refuses to delete non-empty collection', done => {
var obj = hasAllPODobject();
obj.save()
.then(() => {
request.del({
url: 'http://localhost:8378/1/schemas/HasAllPOD',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(255);
expect(body.error).toEqual('class HasAllPOD not empty, contains 1 objects, cannot drop schema');
done();
});
});
});
it('fails when deleting collections with invalid class names', done => {
request.del({
url: 'http://localhost:8378/1/schemas/_GlobalConfig',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(body.error).toEqual('Invalid classname: _GlobalConfig, classnames can only have alphanumeric characters and _, and must start with an alpha character ');
done();
})
});
it('does not fail when deleting nonexistant collections', done => {
request.del({
url: 'http://localhost:8378/1/schemas/Missing',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(body).toEqual({});
done();
});
});
it('deletes collections including join tables', done => {
var obj = new Parse.Object('MyClass');
obj.set('data', 'data');
obj.save()
.then(() => {
var obj2 = new Parse.Object('MyOtherClass');
var relation = obj2.relation('aRelation');
relation.add(obj);
return obj2.save();
})
.then(obj2 => obj2.destroy())
.then(() => {
request.del({
url: 'http://localhost:8378/1/schemas/MyOtherClass',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
expect(response.statusCode).toEqual(200);
expect(response.body).toEqual({});
config.database.db.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) => {
// Expect data table to be gone
expect(err).not.toEqual(null);
request.get({
url: 'http://localhost:8378/1/schemas/MyOtherClass',
headers: masterKeyHeaders,
json: true,
}, (error, response, body) => {
//Expect _SCHEMA entry to be gone.
expect(response.statusCode).toEqual(400);
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
expect(body.error).toEqual('class MyOtherClass does not exist');
done();
});
});
});
});
}, error => {
fail(error);
});
});
}); });

View File

@@ -521,7 +521,7 @@ Schema.prototype.deleteField = function(fieldName, className, database, prefix)
}); });
} }
if (schema.data[className][fieldName].startsWith('relation')) { if (schema.data[className][fieldName].startsWith('relation<')) {
//For relations, drop the _Join table //For relations, drop the _Join table
return database.dropCollection(prefix + '_Join:' + fieldName + ':' + className) return database.dropCollection(prefix + '_Join:' + fieldName + ':' + className)
//Save the _SCHEMA object //Save the _SCHEMA object
@@ -714,6 +714,7 @@ function getObjectType(obj) {
module.exports = { module.exports = {
load: load, load: load,
classNameIsValid: classNameIsValid, classNameIsValid: classNameIsValid,
invalidClassNameMessage: invalidClassNameMessage,
mongoSchemaFromFieldsAndClassName: mongoSchemaFromFieldsAndClassName, mongoSchemaFromFieldsAndClassName: mongoSchemaFromFieldsAndClassName,
schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType, schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType,
buildMergedSchemaObject: buildMergedSchemaObject, buildMergedSchemaObject: buildMergedSchemaObject,

View File

@@ -183,10 +183,95 @@ function modifySchema(req) {
}); });
} }
// A helper function that removes all join tables for a schema. Returns a promise.
var removeJoinTables = (database, prefix, mongoSchema) => {
return Promise.all(Object.keys(mongoSchema)
.filter(field => mongoSchema[field].startsWith('relation<'))
.map(field => {
var joinCollectionName = prefix + '_Join:' + field + ':' + mongoSchema._id;
return new Promise((resolve, reject) => {
database.dropCollection(joinCollectionName, (err, results) => {
if (err) {
reject(err);
} else {
resolve();
}
})
});
})
);
};
function deleteSchema(req) {
if (!req.auth.isMaster) {
return masterKeyRequiredResponse();
}
if (!Schema.classNameIsValid(req.params.className)) {
return Promise.resolve({
status: 400,
response: {
code: Parse.Error.INVALID_CLASS_NAME,
error: Schema.invalidClassNameMessage(req.params.className),
}
});
}
return req.config.database.collection(req.params.className)
.then(coll => new Promise((resolve, reject) => {
coll.count((err, count) => {
if (err) {
reject(err);
} else if (count > 0) {
resolve({
status: 400,
response: {
code: 255,
error: 'class ' + req.params.className + ' not empty, contains ' + count + ' objects, cannot drop schema',
}
});
} else {
coll.drop((err, reply) => {
if (err) {
reject(err);
} else {
// We've dropped the collection now, so delete the item from _SCHEMA
// and clear the _Join collections
req.config.database.collection('_SCHEMA')
.then(coll => new Promise((resolve, reject) => {
coll.findAndRemove({ _id: req.params.className }, [], (err, doc) => {
if (err) {
reject(err);
} else if (doc.value === null) {
//tried to delete non-existant class
resolve({ response: {}});
} else {
removeJoinTables(req.config.database.db, req.config.database.collectionPrefix, doc.value)
.then(resolve, reject);
}
});
}))
.then(resolve.bind(undefined, {response: {}}), reject);
}
});
}
});
}))
.catch(error => {
if (error.message == 'ns not found') {
// If they try to delete a non-existant class, thats fine, just let them.
return Promise.resolve({ response: {} });
} else {
return Promise.reject(error);
}
});
}
router.route('GET', '/schemas', getAllSchemas); router.route('GET', '/schemas', getAllSchemas);
router.route('GET', '/schemas/:className', getOneSchema); router.route('GET', '/schemas/:className', getOneSchema);
router.route('POST', '/schemas', createSchema); router.route('POST', '/schemas', createSchema);
router.route('POST', '/schemas/:className', createSchema); router.route('POST', '/schemas/:className', createSchema);
router.route('PUT', '/schemas/:className', modifySchema); router.route('PUT', '/schemas/:className', modifySchema);
router.route('DELETE', '/schemas/:className', deleteSchema);
module.exports = router; module.exports = router;