Skip to content

Commit 6294162

Browse files
committed
Merge pull request #353 from drew-gross/delete-field
Delete field function in Schema.js
2 parents 8d89838 + 92e9db9 commit 6294162

File tree

3 files changed

+254
-3
lines changed

3 files changed

+254
-3
lines changed

spec/Schema.spec.js

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
1-
// These tests check that the Schema operates correctly.
21
var Config = require('../src/Config');
32
var Schema = require('../src/Schema');
43
var dd = require('deep-diff');
54

65
var config = new Config('test');
76

7+
var hasAllPODobject = () => {
8+
var obj = new Parse.Object('HasAllPOD');
9+
obj.set('aNumber', 5);
10+
obj.set('aString', 'string');
11+
obj.set('aBool', true);
12+
obj.set('aDate', new Date());
13+
obj.set('aObject', {k1: 'value', k2: true, k3: 5});
14+
obj.set('aArray', ['contents', true, 5]);
15+
obj.set('aGeoPoint', new Parse.GeoPoint({latitude: 0, longitude: 0}));
16+
obj.set('aFile', new Parse.File('f.txt', { base64: 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=' }));
17+
var objACL = new Parse.ACL();
18+
objACL.setPublicWriteAccess(false);
19+
obj.setACL(objACL);
20+
return obj;
21+
};
22+
823
describe('Schema', () => {
924
it('can validate one object', (done) => {
1025
config.database.loadSchema().then((schema) => {
@@ -406,4 +421,153 @@ describe('Schema', () => {
406421
done();
407422
});
408423
});
424+
425+
it('can check if a class exists', done => {
426+
config.database.loadSchema()
427+
.then(schema => {
428+
return schema.addClassIfNotExists('NewClass', {})
429+
.then(() => {
430+
schema.hasClass('NewClass')
431+
.then(hasClass => {
432+
expect(hasClass).toEqual(true);
433+
done();
434+
})
435+
.catch(fail);
436+
437+
schema.hasClass('NonexistantClass')
438+
.then(hasClass => {
439+
expect(hasClass).toEqual(false);
440+
done();
441+
})
442+
.catch(fail);
443+
})
444+
.catch(error => {
445+
fail('Couldn\'t create class');
446+
fail(error);
447+
});
448+
})
449+
.catch(error => fail('Couldn\'t load schema'));
450+
});
451+
452+
it('refuses to delete fields from invalid class names', done => {
453+
config.database.loadSchema()
454+
.then(schema => schema.deleteField('fieldName', 'invalid class name'))
455+
.catch(error => {
456+
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
457+
done();
458+
});
459+
});
460+
461+
it('refuses to delete invalid fields', done => {
462+
config.database.loadSchema()
463+
.then(schema => schema.deleteField('invalid field name', 'ValidClassName'))
464+
.catch(error => {
465+
expect(error.code).toEqual(Parse.Error.INVALID_KEY_NAME);
466+
done();
467+
});
468+
});
469+
470+
it('refuses to delete the default fields', done => {
471+
config.database.loadSchema()
472+
.then(schema => schema.deleteField('installationId', '_Installation'))
473+
.catch(error => {
474+
expect(error.code).toEqual(136);
475+
expect(error.error).toEqual('field installationId cannot be changed');
476+
done();
477+
});
478+
});
479+
480+
it('refuses to delete fields from nonexistant classes', done => {
481+
config.database.loadSchema()
482+
.then(schema => schema.deleteField('field', 'NoClass'))
483+
.catch(error => {
484+
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
485+
expect(error.error).toEqual('class NoClass does not exist');
486+
done();
487+
});
488+
});
489+
490+
it('refuses to delete fields that dont exist', done => {
491+
hasAllPODobject().save()
492+
.then(() => config.database.loadSchema())
493+
.then(schema => schema.deleteField('missingField', 'HasAllPOD'))
494+
.fail(error => {
495+
expect(error.code).toEqual(255);
496+
expect(error.error).toEqual('field missingField does not exist, cannot delete');
497+
done();
498+
});
499+
});
500+
501+
it('drops related collection when deleting relation field', done => {
502+
var obj1 = hasAllPODobject();
503+
obj1.save()
504+
.then(savedObj1 => {
505+
var obj2 = new Parse.Object('HasPointersAndRelations');
506+
obj2.set('aPointer', savedObj1);
507+
var relation = obj2.relation('aRelation');
508+
relation.add(obj1);
509+
return obj2.save();
510+
})
511+
.then(() => {
512+
config.database.db.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => {
513+
expect(err).toEqual(null);
514+
config.database.loadSchema()
515+
.then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database.db, 'test_'))
516+
.then(() => config.database.db.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => {
517+
expect(err).not.toEqual(null);
518+
done();
519+
}))
520+
});
521+
})
522+
});
523+
524+
it('can delete string fields and resave as number field', done => {
525+
Parse.Object.disableSingleInstance();
526+
var obj1 = hasAllPODobject();
527+
var obj2 = hasAllPODobject();
528+
var p = Parse.Object.saveAll([obj1, obj2])
529+
.then(() => config.database.loadSchema())
530+
.then(schema => schema.deleteField('aString', 'HasAllPOD', config.database.db, 'test_'))
531+
.then(() => new Parse.Query('HasAllPOD').get(obj1.id))
532+
.then(obj1Reloaded => {
533+
expect(obj1Reloaded.get('aString')).toEqual(undefined);
534+
obj1Reloaded.set('aString', ['not a string', 'this time']);
535+
obj1Reloaded.save()
536+
.then(obj1reloadedAgain => {
537+
expect(obj1reloadedAgain.get('aString')).toEqual(['not a string', 'this time']);
538+
return new Parse.Query('HasAllPOD').get(obj2.id);
539+
})
540+
.then(obj2reloaded => {
541+
expect(obj2reloaded.get('aString')).toEqual(undefined);
542+
done();
543+
Parse.Object.enableSingleInstance();
544+
});
545+
})
546+
});
547+
548+
it('can delete pointer fields and resave as string', done => {
549+
Parse.Object.disableSingleInstance();
550+
var obj1 = new Parse.Object('NewClass');
551+
obj1.save()
552+
.then(() => {
553+
obj1.set('aPointer', obj1);
554+
return obj1.save();
555+
})
556+
.then(obj1 => {
557+
expect(obj1.get('aPointer').id).toEqual(obj1.id);
558+
})
559+
.then(() => config.database.loadSchema())
560+
.then(schema => schema.deleteField('aPointer', 'NewClass', config.database.db, 'test_'))
561+
.then(() => new Parse.Query('NewClass').get(obj1.id))
562+
.then(obj1 => {
563+
expect(obj1.get('aPointer')).toEqual(undefined);
564+
obj1.set('aPointer', 'Now a string');
565+
return obj1.save();
566+
})
567+
.then(obj1 => {
568+
expect(obj1.get('aPointer')).toEqual('Now a string');
569+
done();
570+
Parse.Object.enableSingleInstance();
571+
});
572+
});
409573
});

src/ExportAdapter.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ ExportAdapter.prototype.connect = function() {
4343

4444
// Returns a promise for a Mongo collection.
4545
// Generally just for internal use.
46-
var joinRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
47-
var otherRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
4846
ExportAdapter.prototype.collection = function(className) {
4947
if (!Schema.classNameIsValid(className)) {
5048
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME,

src/Schema.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,88 @@ Schema.prototype.validateField = function(className, key, type, freeze) {
409409
});
410410
};
411411

412+
// Delete a field, and remove that data from all objects. This is intended
413+
// to remove unused fields, if other writers are writing objects that include
414+
// this field, the field may reappear. Returns a Promise that resolves with
415+
// no object on success, or rejects with { code, error } on failure.
416+
417+
// Passing the database and prefix is necessary in order to drop relation collections
418+
// and remove fields from objects. Ideally the database would belong to
419+
// a database adapter and this fuction would close over it or access it via member.
420+
Schema.prototype.deleteField = function(fieldName, className, database, prefix) {
421+
if (!classNameIsValid(className)) {
422+
return Promise.reject({
423+
code: Parse.Error.INVALID_CLASS_NAME,
424+
error: invalidClassNameMessage(className),
425+
});
426+
}
427+
428+
if (!fieldNameIsValid(fieldName)) {
429+
return Promise.reject({
430+
code: Parse.Error.INVALID_KEY_NAME,
431+
error: 'invalid field name: ' + fieldName,
432+
});
433+
}
434+
435+
//Don't allow deleting the default fields.
436+
if (!fieldNameIsValidForClass(fieldName, className)) {
437+
return Promise.reject({
438+
code: 136,
439+
error: 'field ' + fieldName + ' cannot be changed',
440+
});
441+
}
442+
443+
return this.reload()
444+
.then(schema => {
445+
return schema.hasClass(className)
446+
.then(hasClass => {
447+
if (!hasClass) {
448+
return Promise.reject({
449+
code: Parse.Error.INVALID_CLASS_NAME,
450+
error: 'class ' + className + ' does not exist',
451+
});
452+
}
453+
454+
if (!schema.data[className][fieldName]) {
455+
return Promise.reject({
456+
code: 255,
457+
error: 'field ' + fieldName + ' does not exist, cannot delete',
458+
});
459+
}
460+
461+
if (schema.data[className][fieldName].startsWith('relation')) {
462+
//For relations, drop the _Join table
463+
return database.dropCollection(prefix + '_Join:' + fieldName + ':' + className)
464+
//Save the _SCHEMA object
465+
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }}));
466+
} else {
467+
//for non-relations, remove all the data. This is necessary to ensure that the data is still gone
468+
//if they add the same field.
469+
return new Promise((resolve, reject) => {
470+
database.collection(prefix + className, (err, coll) => {
471+
if (err) {
472+
reject(err);
473+
} else {
474+
var mongoFieldName = schema.data[className][fieldName].startsWith('*') ?
475+
'_p_' + fieldName :
476+
fieldName;
477+
return coll.update({}, {
478+
"$unset": { [mongoFieldName] : null },
479+
}, {
480+
multi: true,
481+
})
482+
//Save the _SCHEMA object
483+
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }}))
484+
.then(resolve)
485+
.catch(reject);
486+
}
487+
});
488+
});
489+
}
490+
});
491+
});
492+
}
493+
412494
// Given a schema promise, construct another schema promise that
413495
// validates this field once the schema loads.
414496
function thenValidateField(schemaPromise, className, key, type) {
@@ -477,6 +559,13 @@ Schema.prototype.getExpectedType = function(className, key) {
477559
return undefined;
478560
};
479561

562+
// Checks if a given class is in the schema. Needs to load the
563+
// schema first, which is kinda janky. Hopefully we can refactor
564+
// and make this be a regular value.
565+
Schema.prototype.hasClass = function(className) {
566+
return this.reload().then(newSchema => !!newSchema.data[className]);
567+
}
568+
480569
// Helper function to check if a field is a pointer, returns true or false.
481570
Schema.prototype.isPointer = function(className, key) {
482571
var expected = this.getExpectedType(className, key);

0 commit comments

Comments
 (0)