Merge pull request #353 from drew-gross/delete-field
Delete field function in Schema.js
This commit is contained in:
@@ -1,10 +1,25 @@
|
|||||||
// These tests check that the Schema operates correctly.
|
|
||||||
var Config = require('../src/Config');
|
var Config = require('../src/Config');
|
||||||
var Schema = require('../src/Schema');
|
var Schema = require('../src/Schema');
|
||||||
var dd = require('deep-diff');
|
var dd = require('deep-diff');
|
||||||
|
|
||||||
var config = new Config('test');
|
var config = new Config('test');
|
||||||
|
|
||||||
|
var hasAllPODobject = () => {
|
||||||
|
var obj = new Parse.Object('HasAllPOD');
|
||||||
|
obj.set('aNumber', 5);
|
||||||
|
obj.set('aString', 'string');
|
||||||
|
obj.set('aBool', true);
|
||||||
|
obj.set('aDate', new Date());
|
||||||
|
obj.set('aObject', {k1: 'value', k2: true, k3: 5});
|
||||||
|
obj.set('aArray', ['contents', true, 5]);
|
||||||
|
obj.set('aGeoPoint', new Parse.GeoPoint({latitude: 0, longitude: 0}));
|
||||||
|
obj.set('aFile', new Parse.File('f.txt', { base64: 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=' }));
|
||||||
|
var objACL = new Parse.ACL();
|
||||||
|
objACL.setPublicWriteAccess(false);
|
||||||
|
obj.setACL(objACL);
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
describe('Schema', () => {
|
describe('Schema', () => {
|
||||||
it('can validate one object', (done) => {
|
it('can validate one object', (done) => {
|
||||||
config.database.loadSchema().then((schema) => {
|
config.database.loadSchema().then((schema) => {
|
||||||
@@ -406,4 +421,153 @@ describe('Schema', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can check if a class exists', done => {
|
||||||
|
config.database.loadSchema()
|
||||||
|
.then(schema => {
|
||||||
|
return schema.addClassIfNotExists('NewClass', {})
|
||||||
|
.then(() => {
|
||||||
|
schema.hasClass('NewClass')
|
||||||
|
.then(hasClass => {
|
||||||
|
expect(hasClass).toEqual(true);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(fail);
|
||||||
|
|
||||||
|
schema.hasClass('NonexistantClass')
|
||||||
|
.then(hasClass => {
|
||||||
|
expect(hasClass).toEqual(false);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(fail);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
fail('Couldn\'t create class');
|
||||||
|
fail(error);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => fail('Couldn\'t load schema'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses to delete fields from invalid class names', done => {
|
||||||
|
config.database.loadSchema()
|
||||||
|
.then(schema => schema.deleteField('fieldName', 'invalid class name'))
|
||||||
|
.catch(error => {
|
||||||
|
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses to delete invalid fields', done => {
|
||||||
|
config.database.loadSchema()
|
||||||
|
.then(schema => schema.deleteField('invalid field name', 'ValidClassName'))
|
||||||
|
.catch(error => {
|
||||||
|
expect(error.code).toEqual(Parse.Error.INVALID_KEY_NAME);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses to delete the default fields', done => {
|
||||||
|
config.database.loadSchema()
|
||||||
|
.then(schema => schema.deleteField('installationId', '_Installation'))
|
||||||
|
.catch(error => {
|
||||||
|
expect(error.code).toEqual(136);
|
||||||
|
expect(error.error).toEqual('field installationId cannot be changed');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses to delete fields from nonexistant classes', done => {
|
||||||
|
config.database.loadSchema()
|
||||||
|
.then(schema => schema.deleteField('field', 'NoClass'))
|
||||||
|
.catch(error => {
|
||||||
|
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||||
|
expect(error.error).toEqual('class NoClass does not exist');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses to delete fields that dont exist', done => {
|
||||||
|
hasAllPODobject().save()
|
||||||
|
.then(() => config.database.loadSchema())
|
||||||
|
.then(schema => schema.deleteField('missingField', 'HasAllPOD'))
|
||||||
|
.fail(error => {
|
||||||
|
expect(error.code).toEqual(255);
|
||||||
|
expect(error.error).toEqual('field missingField does not exist, cannot delete');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops related collection when deleting relation field', done => {
|
||||||
|
var obj1 = hasAllPODobject();
|
||||||
|
obj1.save()
|
||||||
|
.then(savedObj1 => {
|
||||||
|
var obj2 = new Parse.Object('HasPointersAndRelations');
|
||||||
|
obj2.set('aPointer', savedObj1);
|
||||||
|
var relation = obj2.relation('aRelation');
|
||||||
|
relation.add(obj1);
|
||||||
|
return obj2.save();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
config.database.db.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => {
|
||||||
|
expect(err).toEqual(null);
|
||||||
|
config.database.loadSchema()
|
||||||
|
.then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database.db, 'test_'))
|
||||||
|
.then(() => config.database.db.collection('test__Join:aRelation:HasPointersAndRelations', { strict: true }, (err, coll) => {
|
||||||
|
expect(err).not.toEqual(null);
|
||||||
|
done();
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can delete string fields and resave as number field', done => {
|
||||||
|
Parse.Object.disableSingleInstance();
|
||||||
|
var obj1 = hasAllPODobject();
|
||||||
|
var obj2 = hasAllPODobject();
|
||||||
|
var p = Parse.Object.saveAll([obj1, obj2])
|
||||||
|
.then(() => config.database.loadSchema())
|
||||||
|
.then(schema => schema.deleteField('aString', 'HasAllPOD', config.database.db, 'test_'))
|
||||||
|
.then(() => new Parse.Query('HasAllPOD').get(obj1.id))
|
||||||
|
.then(obj1Reloaded => {
|
||||||
|
expect(obj1Reloaded.get('aString')).toEqual(undefined);
|
||||||
|
obj1Reloaded.set('aString', ['not a string', 'this time']);
|
||||||
|
obj1Reloaded.save()
|
||||||
|
.then(obj1reloadedAgain => {
|
||||||
|
expect(obj1reloadedAgain.get('aString')).toEqual(['not a string', 'this time']);
|
||||||
|
return new Parse.Query('HasAllPOD').get(obj2.id);
|
||||||
|
})
|
||||||
|
.then(obj2reloaded => {
|
||||||
|
expect(obj2reloaded.get('aString')).toEqual(undefined);
|
||||||
|
done();
|
||||||
|
Parse.Object.enableSingleInstance();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can delete pointer fields and resave as string', done => {
|
||||||
|
Parse.Object.disableSingleInstance();
|
||||||
|
var obj1 = new Parse.Object('NewClass');
|
||||||
|
obj1.save()
|
||||||
|
.then(() => {
|
||||||
|
obj1.set('aPointer', obj1);
|
||||||
|
return obj1.save();
|
||||||
|
})
|
||||||
|
.then(obj1 => {
|
||||||
|
expect(obj1.get('aPointer').id).toEqual(obj1.id);
|
||||||
|
})
|
||||||
|
.then(() => config.database.loadSchema())
|
||||||
|
.then(schema => schema.deleteField('aPointer', 'NewClass', config.database.db, 'test_'))
|
||||||
|
.then(() => new Parse.Query('NewClass').get(obj1.id))
|
||||||
|
.then(obj1 => {
|
||||||
|
expect(obj1.get('aPointer')).toEqual(undefined);
|
||||||
|
obj1.set('aPointer', 'Now a string');
|
||||||
|
return obj1.save();
|
||||||
|
})
|
||||||
|
.then(obj1 => {
|
||||||
|
expect(obj1.get('aPointer')).toEqual('Now a string');
|
||||||
|
done();
|
||||||
|
Parse.Object.enableSingleInstance();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ ExportAdapter.prototype.connect = function() {
|
|||||||
|
|
||||||
// Returns a promise for a Mongo collection.
|
// Returns a promise for a Mongo collection.
|
||||||
// Generally just for internal use.
|
// Generally just for internal use.
|
||||||
var joinRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
|
|
||||||
var otherRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
||||||
ExportAdapter.prototype.collection = function(className) {
|
ExportAdapter.prototype.collection = function(className) {
|
||||||
if (!Schema.classNameIsValid(className)) {
|
if (!Schema.classNameIsValid(className)) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME,
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME,
|
||||||
|
|||||||
@@ -409,6 +409,88 @@ Schema.prototype.validateField = function(className, key, type, freeze) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Delete a field, and remove that data from all objects. This is intended
|
||||||
|
// to remove unused fields, if other writers are writing objects that include
|
||||||
|
// this field, the field may reappear. Returns a Promise that resolves with
|
||||||
|
// no object on success, or rejects with { code, error } on failure.
|
||||||
|
|
||||||
|
// Passing the database and prefix is necessary in order to drop relation collections
|
||||||
|
// and remove fields from objects. Ideally the database would belong to
|
||||||
|
// a database adapter and this fuction would close over it or access it via member.
|
||||||
|
Schema.prototype.deleteField = function(fieldName, className, database, prefix) {
|
||||||
|
if (!classNameIsValid(className)) {
|
||||||
|
return Promise.reject({
|
||||||
|
code: Parse.Error.INVALID_CLASS_NAME,
|
||||||
|
error: invalidClassNameMessage(className),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fieldNameIsValid(fieldName)) {
|
||||||
|
return Promise.reject({
|
||||||
|
code: Parse.Error.INVALID_KEY_NAME,
|
||||||
|
error: 'invalid field name: ' + fieldName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Don't allow deleting the default fields.
|
||||||
|
if (!fieldNameIsValidForClass(fieldName, className)) {
|
||||||
|
return Promise.reject({
|
||||||
|
code: 136,
|
||||||
|
error: 'field ' + fieldName + ' cannot be changed',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.reload()
|
||||||
|
.then(schema => {
|
||||||
|
return schema.hasClass(className)
|
||||||
|
.then(hasClass => {
|
||||||
|
if (!hasClass) {
|
||||||
|
return Promise.reject({
|
||||||
|
code: Parse.Error.INVALID_CLASS_NAME,
|
||||||
|
error: 'class ' + className + ' does not exist',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schema.data[className][fieldName]) {
|
||||||
|
return Promise.reject({
|
||||||
|
code: 255,
|
||||||
|
error: 'field ' + fieldName + ' does not exist, cannot delete',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.data[className][fieldName].startsWith('relation')) {
|
||||||
|
//For relations, drop the _Join table
|
||||||
|
return database.dropCollection(prefix + '_Join:' + fieldName + ':' + className)
|
||||||
|
//Save the _SCHEMA object
|
||||||
|
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }}));
|
||||||
|
} else {
|
||||||
|
//for non-relations, remove all the data. This is necessary to ensure that the data is still gone
|
||||||
|
//if they add the same field.
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
database.collection(prefix + className, (err, coll) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
var mongoFieldName = schema.data[className][fieldName].startsWith('*') ?
|
||||||
|
'_p_' + fieldName :
|
||||||
|
fieldName;
|
||||||
|
return coll.update({}, {
|
||||||
|
"$unset": { [mongoFieldName] : null },
|
||||||
|
}, {
|
||||||
|
multi: true,
|
||||||
|
})
|
||||||
|
//Save the _SCHEMA object
|
||||||
|
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }}))
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Given a schema promise, construct another schema promise that
|
// Given a schema promise, construct another schema promise that
|
||||||
// validates this field once the schema loads.
|
// validates this field once the schema loads.
|
||||||
function thenValidateField(schemaPromise, className, key, type) {
|
function thenValidateField(schemaPromise, className, key, type) {
|
||||||
@@ -477,6 +559,13 @@ Schema.prototype.getExpectedType = function(className, key) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Checks if a given class is in the schema. Needs to load the
|
||||||
|
// schema first, which is kinda janky. Hopefully we can refactor
|
||||||
|
// and make this be a regular value.
|
||||||
|
Schema.prototype.hasClass = function(className) {
|
||||||
|
return this.reload().then(newSchema => !!newSchema.data[className]);
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to check if a field is a pointer, returns true or false.
|
// Helper function to check if a field is a pointer, returns true or false.
|
||||||
Schema.prototype.isPointer = function(className, key) {
|
Schema.prototype.isPointer = function(className, key) {
|
||||||
var expected = this.getExpectedType(className, key);
|
var expected = this.getExpectedType(className, key);
|
||||||
|
|||||||
Reference in New Issue
Block a user