Skip to content

Implement DELETE /schemas/:className #474

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions spec/schemas.spec.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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);
});
});
});
3 changes: 2 additions & 1 deletion src/Schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -714,6 +714,7 @@ function getObjectType(obj) {
module.exports = {
load: load,
classNameIsValid: classNameIsValid,
invalidClassNameMessage: invalidClassNameMessage,
mongoSchemaFromFieldsAndClassName: mongoSchemaFromFieldsAndClassName,
schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType,
buildMergedSchemaObject: buildMergedSchemaObject,
Expand Down
85 changes: 85 additions & 0 deletions src/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;