Skip to content

Commit 2e6173f

Browse files
committed
Merge pull request #474 from drew-gross/schemas-delete
Implement DELETE /schemas/:className
2 parents d1476ec + 61b4468 commit 2e6173f

File tree

3 files changed

+188
-1
lines changed

3 files changed

+188
-1
lines changed

spec/schemas.spec.js

+101
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
var Parse = require('parse/node').Parse;
22
var request = require('request');
33
var dd = require('deep-diff');
4+
var Config = require('../src/Config');
5+
6+
var config = new Config('test');
47

58
var hasAllPODobject = () => {
69
var obj = new Parse.Object('HasAllPOD');
@@ -633,4 +636,102 @@ describe('schemas', () => {
633636
});
634637
});
635638
});
639+
640+
it('requires the master key to delete schemas', done => {
641+
request.del({
642+
url: 'http://localhost:8378/1/schemas/DoesntMatter',
643+
headers: noAuthHeaders,
644+
json: true,
645+
}, (error, response, body) => {
646+
expect(response.statusCode).toEqual(403);
647+
expect(body.error).toEqual('unauthorized');
648+
done();
649+
});
650+
});
651+
652+
it('refuses to delete non-empty collection', done => {
653+
var obj = hasAllPODobject();
654+
obj.save()
655+
.then(() => {
656+
request.del({
657+
url: 'http://localhost:8378/1/schemas/HasAllPOD',
658+
headers: masterKeyHeaders,
659+
json: true,
660+
}, (error, response, body) => {
661+
expect(response.statusCode).toEqual(400);
662+
expect(body.code).toEqual(255);
663+
expect(body.error).toEqual('class HasAllPOD not empty, contains 1 objects, cannot drop schema');
664+
done();
665+
});
666+
});
667+
});
668+
669+
it('fails when deleting collections with invalid class names', done => {
670+
request.del({
671+
url: 'http://localhost:8378/1/schemas/_GlobalConfig',
672+
headers: masterKeyHeaders,
673+
json: true,
674+
}, (error, response, body) => {
675+
expect(response.statusCode).toEqual(400);
676+
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
677+
expect(body.error).toEqual('Invalid classname: _GlobalConfig, classnames can only have alphanumeric characters and _, and must start with an alpha character ');
678+
done();
679+
})
680+
});
681+
682+
it('does not fail when deleting nonexistant collections', done => {
683+
request.del({
684+
url: 'http://localhost:8378/1/schemas/Missing',
685+
headers: masterKeyHeaders,
686+
json: true,
687+
}, (error, response, body) => {
688+
expect(response.statusCode).toEqual(200);
689+
expect(body).toEqual({});
690+
done();
691+
});
692+
});
693+
694+
it('deletes collections including join tables', done => {
695+
var obj = new Parse.Object('MyClass');
696+
obj.set('data', 'data');
697+
obj.save()
698+
.then(() => {
699+
var obj2 = new Parse.Object('MyOtherClass');
700+
var relation = obj2.relation('aRelation');
701+
relation.add(obj);
702+
return obj2.save();
703+
})
704+
.then(obj2 => obj2.destroy())
705+
.then(() => {
706+
request.del({
707+
url: 'http://localhost:8378/1/schemas/MyOtherClass',
708+
headers: masterKeyHeaders,
709+
json: true,
710+
}, (error, response, body) => {
711+
expect(response.statusCode).toEqual(200);
712+
expect(response.body).toEqual({});
713+
config.database.db.collection('test__Join:aRelation:MyOtherClass', { strict: true }, (err, coll) => {
714+
//Expect Join table to be gone
715+
expect(err).not.toEqual(null);
716+
config.database.db.collection('test_MyOtherClass', { strict: true }, (err, coll) => {
717+
// Expect data table to be gone
718+
expect(err).not.toEqual(null);
719+
request.get({
720+
url: 'http://localhost:8378/1/schemas/MyOtherClass',
721+
headers: masterKeyHeaders,
722+
json: true,
723+
}, (error, response, body) => {
724+
//Expect _SCHEMA entry to be gone.
725+
expect(response.statusCode).toEqual(400);
726+
expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
727+
expect(body.error).toEqual('class MyOtherClass does not exist');
728+
done();
729+
});
730+
});
731+
});
732+
});
733+
}, error => {
734+
fail(error);
735+
});
736+
});
636737
});

src/Schema.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ Schema.prototype.deleteField = function(fieldName, className, database, prefix)
521521
});
522522
}
523523

524-
if (schema.data[className][fieldName].startsWith('relation')) {
524+
if (schema.data[className][fieldName].startsWith('relation<')) {
525525
//For relations, drop the _Join table
526526
return database.dropCollection(prefix + '_Join:' + fieldName + ':' + className)
527527
//Save the _SCHEMA object
@@ -714,6 +714,7 @@ function getObjectType(obj) {
714714
module.exports = {
715715
load: load,
716716
classNameIsValid: classNameIsValid,
717+
invalidClassNameMessage: invalidClassNameMessage,
717718
mongoSchemaFromFieldsAndClassName: mongoSchemaFromFieldsAndClassName,
718719
schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType,
719720
buildMergedSchemaObject: buildMergedSchemaObject,

src/schemas.js

+85
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,95 @@ function modifySchema(req) {
183183
});
184184
}
185185

186+
// A helper function that removes all join tables for a schema. Returns a promise.
187+
var removeJoinTables = (database, prefix, mongoSchema) => {
188+
return Promise.all(Object.keys(mongoSchema)
189+
.filter(field => mongoSchema[field].startsWith('relation<'))
190+
.map(field => {
191+
var joinCollectionName = prefix + '_Join:' + field + ':' + mongoSchema._id;
192+
return new Promise((resolve, reject) => {
193+
database.dropCollection(joinCollectionName, (err, results) => {
194+
if (err) {
195+
reject(err);
196+
} else {
197+
resolve();
198+
}
199+
})
200+
});
201+
})
202+
);
203+
};
204+
205+
function deleteSchema(req) {
206+
if (!req.auth.isMaster) {
207+
return masterKeyRequiredResponse();
208+
}
209+
210+
if (!Schema.classNameIsValid(req.params.className)) {
211+
return Promise.resolve({
212+
status: 400,
213+
response: {
214+
code: Parse.Error.INVALID_CLASS_NAME,
215+
error: Schema.invalidClassNameMessage(req.params.className),
216+
}
217+
});
218+
}
219+
220+
return req.config.database.collection(req.params.className)
221+
.then(coll => new Promise((resolve, reject) => {
222+
coll.count((err, count) => {
223+
if (err) {
224+
reject(err);
225+
} else if (count > 0) {
226+
resolve({
227+
status: 400,
228+
response: {
229+
code: 255,
230+
error: 'class ' + req.params.className + ' not empty, contains ' + count + ' objects, cannot drop schema',
231+
}
232+
});
233+
} else {
234+
coll.drop((err, reply) => {
235+
if (err) {
236+
reject(err);
237+
} else {
238+
// We've dropped the collection now, so delete the item from _SCHEMA
239+
// and clear the _Join collections
240+
req.config.database.collection('_SCHEMA')
241+
.then(coll => new Promise((resolve, reject) => {
242+
coll.findAndRemove({ _id: req.params.className }, [], (err, doc) => {
243+
if (err) {
244+
reject(err);
245+
} else if (doc.value === null) {
246+
//tried to delete non-existant class
247+
resolve({ response: {}});
248+
} else {
249+
removeJoinTables(req.config.database.db, req.config.database.collectionPrefix, doc.value)
250+
.then(resolve, reject);
251+
}
252+
});
253+
}))
254+
.then(resolve.bind(undefined, {response: {}}), reject);
255+
}
256+
});
257+
}
258+
});
259+
}))
260+
.catch(error => {
261+
if (error.message == 'ns not found') {
262+
// If they try to delete a non-existant class, thats fine, just let them.
263+
return Promise.resolve({ response: {} });
264+
} else {
265+
return Promise.reject(error);
266+
}
267+
});
268+
}
269+
186270
router.route('GET', '/schemas', getAllSchemas);
187271
router.route('GET', '/schemas/:className', getOneSchema);
188272
router.route('POST', '/schemas', createSchema);
189273
router.route('POST', '/schemas/:className', createSchema);
190274
router.route('PUT', '/schemas/:className', modifySchema);
275+
router.route('DELETE', '/schemas/:className', deleteSchema);
191276

192277
module.exports = router;

0 commit comments

Comments
 (0)