From 61b4468dac7360568f1e292e6e9de46d79f8fb68 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 17 Feb 2016 19:00:17 -0800 Subject: [PATCH] Implement DELETE /schemas/:className --- spec/schemas.spec.js | 101 +++++++++++++++++++++++++++++++++++++++++++ src/Schema.js | 3 +- src/schemas.js | 85 ++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index fd136df466..1a6a30696e 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -1,6 +1,9 @@ var Parse = require('parse/node').Parse; var request = require('request'); var dd = require('deep-diff'); +var Config = require('../src/Config'); + +var config = new Config('test'); var hasAllPODobject = () => { 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); + }); + }); }); diff --git a/src/Schema.js b/src/Schema.js index a07018bf8a..0d60144937 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -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 return database.dropCollection(prefix + '_Join:' + fieldName + ':' + className) //Save the _SCHEMA object @@ -714,6 +714,7 @@ function getObjectType(obj) { module.exports = { load: load, classNameIsValid: classNameIsValid, + invalidClassNameMessage: invalidClassNameMessage, mongoSchemaFromFieldsAndClassName: mongoSchemaFromFieldsAndClassName, schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType, buildMergedSchemaObject: buildMergedSchemaObject, diff --git a/src/schemas.js b/src/schemas.js index cd8b92ecfa..9fb191c82a 100644 --- a/src/schemas.js +++ b/src/schemas.js @@ -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/:className', getOneSchema); router.route('POST', '/schemas', createSchema); router.route('POST', '/schemas/:className', createSchema); router.route('PUT', '/schemas/:className', modifySchema); +router.route('DELETE', '/schemas/:className', deleteSchema); module.exports = router;