Merge pull request #276 from drew-gross/schema-creation-logic
Schema creation logic
This commit is contained in:
@@ -60,13 +60,7 @@ ExportAdapter.prototype.connect = function() {
|
||||
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) {
|
||||
if (className !== '_User' &&
|
||||
className !== '_Installation' &&
|
||||
className !== '_Session' &&
|
||||
className !== '_SCHEMA' &&
|
||||
className !== '_Role' &&
|
||||
!joinRegex.test(className) &&
|
||||
!otherRegex.test(className)) {
|
||||
if (!Schema.classNameIsValid(className)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME,
|
||||
'invalid className: ' + className);
|
||||
}
|
||||
@@ -500,6 +494,7 @@ ExportAdapter.prototype.smartFind = function(coll, where, options) {
|
||||
|
||||
var index = {};
|
||||
index[key] = '2d';
|
||||
//TODO: condiser moving index creation logic into Schema.js
|
||||
return coll.createIndex(index).then(() => {
|
||||
// Retry, but just once.
|
||||
return coll.find(where, options).toArray();
|
||||
|
||||
218
Schema.js
218
Schema.js
@@ -17,6 +17,135 @@
|
||||
var Parse = require('parse/node').Parse;
|
||||
var transform = require('./transform');
|
||||
|
||||
defaultColumns = {
|
||||
// Contain the default columns for every parse object type (except _Join collection)
|
||||
_Default: {
|
||||
"objectId": {type:'String'},
|
||||
"createdAt": {type:'Date'},
|
||||
"updatedAt": {type:'Date'},
|
||||
"ACL": {type:'ACL'},
|
||||
},
|
||||
// The additional default columns for the _User collection (in addition to DefaultCols)
|
||||
_User: {
|
||||
"username": {type:'String'},
|
||||
"password": {type:'String'},
|
||||
"authData": {type:'Object'},
|
||||
"email": {type:'String'},
|
||||
"emailVerified": {type:'Boolean'},
|
||||
},
|
||||
// The additional default columns for the _User collection (in addition to DefaultCols)
|
||||
_Installation: {
|
||||
"installationId": {type:'String'},
|
||||
"deviceToken": {type:'String'},
|
||||
"channels": {type:'Array'},
|
||||
"deviceType": {type:'String'},
|
||||
"pushType": {type:'String'},
|
||||
"GCMSenderId": {type:'String'},
|
||||
"timeZone": {type:'String'},
|
||||
"localeIdentifier": {type:'String'},
|
||||
"badge": {type:'Number'},
|
||||
},
|
||||
// The additional default columns for the _User collection (in addition to DefaultCols)
|
||||
_Role: {
|
||||
"name": {type:'String'},
|
||||
"users": {type:'Relation',className:'_User'},
|
||||
"roles": {type:'Relation',className:'_Role'},
|
||||
},
|
||||
// The additional default columns for the _User collection (in addition to DefaultCols)
|
||||
_Session: {
|
||||
"restricted": {type:'Boolean'},
|
||||
"user": {type:'Pointer', className:'_User'},
|
||||
"installationId": {type:'String'},
|
||||
"sessionToken": {type:'String'},
|
||||
"expiresAt": {type:'Date'},
|
||||
"createdWith": {type:'Object'},
|
||||
},
|
||||
}
|
||||
|
||||
// Valid classes must:
|
||||
// Be one of _User, _Installation, _Role, _Session OR
|
||||
// Be a join table OR
|
||||
// Include only alpha-numeric and underscores, and not start with an underscore or number
|
||||
var joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
|
||||
var classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
|
||||
function classNameIsValid(className) {
|
||||
return (
|
||||
className === '_User' ||
|
||||
className === '_Installation' ||
|
||||
className === '_Session' ||
|
||||
className === '_SCHEMA' || //TODO: remove this, as _SCHEMA is not a valid class name for storing Parse Objects.
|
||||
className === '_Role' ||
|
||||
joinClassRegex.test(className) ||
|
||||
//Class names have the same constraints as field names, but also allow the previous additional names.
|
||||
fieldNameIsValid(className)
|
||||
);
|
||||
}
|
||||
|
||||
// Valid fields must be alpha-numeric, and not start with an underscore or number
|
||||
function fieldNameIsValid(fieldName) {
|
||||
return classAndFieldRegex.test(fieldName);
|
||||
}
|
||||
|
||||
// Checks that it's not trying to clobber one of the default fields of the class.
|
||||
function fieldNameIsValidForClass(fieldName, className) {
|
||||
if (!fieldNameIsValid(fieldName)) {
|
||||
return false;
|
||||
}
|
||||
if (defaultColumns._Default[fieldName]) {
|
||||
return false;
|
||||
}
|
||||
if (defaultColumns[className] && defaultColumns[className][fieldName]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function invalidClassNameMessage(className) {
|
||||
return 'Invalid classname: ' + className + ', classnames can only have alphanumeric characters and _, and must start with an alpha character ';
|
||||
}
|
||||
|
||||
// Returns { error: "message", code: ### } if the type could not be
|
||||
// converted, otherwise returns a returns { result: "mongotype" }
|
||||
// where mongotype is suitable for inserting into mongo _SCHEMA collection
|
||||
function schemaAPITypeToMongoFieldType(type) {
|
||||
var invalidJsonError = { error: "invalid JSON", code: Parse.Error.INVALID_JSON };
|
||||
if (type.type == 'Pointer') {
|
||||
if (!type.targetClass) {
|
||||
return { error: 'type Pointer needs a class name', code: 135 };
|
||||
} else if (typeof type.targetClass !== 'string') {
|
||||
return invalidJsonError;
|
||||
} else if (!classNameIsValid(type.targetClass)) {
|
||||
return { error: invalidClassNameMessage(type.targetClass), code: Parse.Error.INVALID_CLASS_NAME };
|
||||
} else {
|
||||
return { result: '*' + type.targetClass };
|
||||
}
|
||||
}
|
||||
if (type.type == 'Relation') {
|
||||
if (!type.targetClass) {
|
||||
return { error: 'type Relation needs a class name', code: 135 };
|
||||
} else if (typeof type.targetClass !== 'string') {
|
||||
return invalidJsonError;
|
||||
} else if (!classNameIsValid(type.targetClass)) {
|
||||
return { error: invalidClassNameMessage(type.targetClass), code: Parse.Error.INVALID_CLASS_NAME };
|
||||
} else {
|
||||
return { result: 'relation<' + type.targetClass + '>' };
|
||||
}
|
||||
}
|
||||
if (typeof type.type !== 'string') {
|
||||
return { error: "invalid JSON", code: Parse.Error.INVALID_JSON };
|
||||
}
|
||||
switch (type.type) {
|
||||
default: return { error: 'invalid field type: ' + type.type, code: Parse.Error.INCORRECT_TYPE };
|
||||
case 'Number': return { result: 'number' };
|
||||
case 'String': return { result: 'string' };
|
||||
case 'Boolean': return { result: 'boolean' };
|
||||
case 'Date': return { result: 'date' };
|
||||
case 'Object': return { result: 'object' };
|
||||
case 'Array': return { result: 'array' };
|
||||
case 'GeoPoint': return { result: 'geopoint' };
|
||||
case 'File': return { result: 'file' };
|
||||
}
|
||||
}
|
||||
|
||||
// Create a schema from a Mongo collection and the exported schema format.
|
||||
// mongoSchema should be a list of objects, each with:
|
||||
@@ -71,9 +200,93 @@ Schema.prototype.reload = function() {
|
||||
return load(this.collection);
|
||||
};
|
||||
|
||||
// Create a new class that includes the three default fields.
|
||||
// ACL is an implicit column that does not get an entry in the
|
||||
// _SCHEMAS database. Returns a promise that resolves with the
|
||||
// created schema, in mongo format.
|
||||
// on success, and rejects with an error on fail. Ensure you
|
||||
// have authorization (master key, or client class creation
|
||||
// enabled) before calling this function.
|
||||
Schema.prototype.addClassIfNotExists = function(className, fields) {
|
||||
if (this.data[className]) {
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class ' + className + ' already exists',
|
||||
});
|
||||
}
|
||||
|
||||
if (!classNameIsValid(className)) {
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: invalidClassNameMessage(className),
|
||||
});
|
||||
}
|
||||
for (fieldName in fields) {
|
||||
if (!fieldNameIsValid(fieldName)) {
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
error: 'invalid field name: ' + fieldName,
|
||||
});
|
||||
}
|
||||
if (!fieldNameIsValidForClass(fieldName, className)) {
|
||||
return Promise.reject({
|
||||
code: 136,
|
||||
error: 'field ' + fieldName + ' cannot be added',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var mongoObject = {
|
||||
_id: className,
|
||||
objectId: 'string',
|
||||
updatedAt: 'string',
|
||||
createdAt: 'string',
|
||||
};
|
||||
for (fieldName in defaultColumns[className]) {
|
||||
validatedField = schemaAPITypeToMongoFieldType(defaultColumns[className][fieldName]);
|
||||
if (validatedField.code) {
|
||||
return Promise.reject(validatedField);
|
||||
}
|
||||
mongoObject[fieldName] = validatedField.result;
|
||||
}
|
||||
|
||||
for (fieldName in fields) {
|
||||
validatedField = schemaAPITypeToMongoFieldType(fields[fieldName]);
|
||||
if (validatedField.code) {
|
||||
return Promise.reject(validatedField);
|
||||
}
|
||||
mongoObject[fieldName] = validatedField.result;
|
||||
}
|
||||
|
||||
var geoPoints = Object.keys(mongoObject).filter(key => mongoObject[key] === 'geopoint');
|
||||
|
||||
if (geoPoints.length > 1) {
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INCORRECT_TYPE,
|
||||
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
|
||||
});
|
||||
}
|
||||
|
||||
return this.collection.insertOne(mongoObject)
|
||||
.then(result => result.ops[0])
|
||||
.catch(error => {
|
||||
if (error.code === 11000) { //Mongo's duplicate key error
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class ' + className + ' already exists',
|
||||
});
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that resolves successfully to the new schema
|
||||
// object.
|
||||
// object or fails with a reason.
|
||||
// If 'freeze' is true, refuse to update the schema.
|
||||
// WARNING: this function has side-effects, and doesn't actually
|
||||
// do any validation of the format of the className. You probably
|
||||
// should use classNameIsValid or addClassIfNotExists or something
|
||||
// like that instead. TODO: rename or remove this function.
|
||||
Schema.prototype.validateClassName = function(className, freeze) {
|
||||
if (this.data[className]) {
|
||||
return Promise.resolve(this);
|
||||
@@ -348,5 +561,6 @@ function getObjectType(obj) {
|
||||
|
||||
|
||||
module.exports = {
|
||||
load: load
|
||||
load: load,
|
||||
classNameIsValid: classNameIsValid,
|
||||
};
|
||||
|
||||
10
schemas.js
10
schemas.js
@@ -5,7 +5,7 @@ var express = require('express'),
|
||||
|
||||
var router = new PromiseRouter();
|
||||
|
||||
function mongoFieldTypeToApiResponseType(type) {
|
||||
function mongoFieldTypeToSchemaAPIType(type) {
|
||||
if (type[0] === '*') {
|
||||
return {
|
||||
type: 'Pointer',
|
||||
@@ -32,10 +32,10 @@ function mongoFieldTypeToApiResponseType(type) {
|
||||
|
||||
function mongoSchemaAPIResponseFields(schema) {
|
||||
fieldNames = Object.keys(schema).filter(key => key !== '_id');
|
||||
response = {};
|
||||
fieldNames.forEach(fieldName => {
|
||||
response[fieldName] = mongoFieldTypeToApiResponseType(schema[fieldName]);
|
||||
});
|
||||
response = fieldNames.reduce((obj, fieldName) => {
|
||||
obj[fieldName] = mongoFieldTypeToSchemaAPIType(schema[fieldName])
|
||||
return obj;
|
||||
}, {});
|
||||
response.ACL = {type: 'ACL'};
|
||||
response.createdAt = {type: 'Date'};
|
||||
response.updatedAt = {type: 'Date'};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// These tests check that the Schema operates correctly.
|
||||
var Config = require('../Config');
|
||||
var Schema = require('../Schema');
|
||||
var dd = require('deep-diff');
|
||||
|
||||
var config = new Config('test');
|
||||
|
||||
@@ -131,4 +132,278 @@ describe('Schema', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can add classes without needing an object', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
foo: {type: 'String'}
|
||||
}))
|
||||
.then(result => {
|
||||
expect(result).toEqual({
|
||||
_id: 'NewClass',
|
||||
objectId: 'string',
|
||||
updatedAt: 'string',
|
||||
createdAt: 'string',
|
||||
foo: 'string',
|
||||
})
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('will fail to create a class if that class was already created by an object', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => {
|
||||
schema.validateObject('NewClass', {foo: 7})
|
||||
.then(() => {
|
||||
schema.reload()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
foo: {type: 'String'}
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME)
|
||||
expect(error.error).toEqual('class NewClass already exists');
|
||||
done();
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('will resolve class creation races appropriately', done => {
|
||||
// If two callers race to create the same schema, the response to the
|
||||
// race loser should be the same as if they hadn't been racing.
|
||||
config.database.loadSchema()
|
||||
.then(schema => {
|
||||
var p1 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}});
|
||||
var p2 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}});
|
||||
Promise.race([p1, p2]) //Use race because we expect the first completed promise to be the successful one
|
||||
.then(response => {
|
||||
expect(response).toEqual({
|
||||
_id: 'NewClass',
|
||||
objectId: 'string',
|
||||
updatedAt: 'string',
|
||||
createdAt: 'string',
|
||||
foo: 'string',
|
||||
});
|
||||
});
|
||||
Promise.all([p1,p2])
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(error.error).toEqual('class NewClass already exists');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to create classes with invalid names', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => {
|
||||
schema.addClassIfNotExists('_InvalidName', {foo: {type: 'String'}})
|
||||
.catch(error => {
|
||||
expect(error.error).toEqual(
|
||||
'Invalid classname: _InvalidName, classnames can only have alphanumeric characters and _, and must start with an alpha character '
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to add fields with invalid names', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {'0InvalidName': {type: 'String'}}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_KEY_NAME);
|
||||
expect(error.error).toEqual('invalid field name: 0InvalidName');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to explicitly create the default fields for custom classes', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {objectId: {type: 'String'}}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(136);
|
||||
expect(error.error).toEqual('field objectId cannot be added');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to explicitly create the default fields for non-custom classes', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('_Installation', {localeIdentifier: {type: 'String'}}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(136);
|
||||
expect(error.error).toEqual('field localeIdentifier cannot be added');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to add fields with invalid types', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
foo: {type: 7}
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_JSON);
|
||||
expect(error.error).toEqual('invalid JSON');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to add fields with invalid pointer types', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
foo: {type: 'Pointer'},
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(135);
|
||||
expect(error.error).toEqual('type Pointer needs a class name');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to add fields with invalid pointer target', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
foo: {type: 'Pointer', targetClass: 7},
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_JSON);
|
||||
expect(error.error).toEqual('invalid JSON');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to add fields with invalid Relation type', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
foo: {type: 'Relation', uselessKey: 7},
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(135);
|
||||
expect(error.error).toEqual('type Relation needs a class name');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to add fields with invalid relation target', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
foo: {type: 'Relation', targetClass: 7},
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_JSON);
|
||||
expect(error.error).toEqual('invalid JSON');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to add fields with uncreatable pointer target class', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
foo: {type: 'Pointer', targetClass: 'not a valid class name'},
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(error.error).toEqual('Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character ');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to add fields with uncreatable relation target class', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
foo: {type: 'Relation', targetClass: 'not a valid class name'},
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
|
||||
expect(error.error).toEqual('Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character ');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to add fields with unknown types', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
foo: {type: 'Unknown'},
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
||||
expect(error.error).toEqual('invalid field type: Unknown');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('will create classes', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
aNumber: {type: 'Number'},
|
||||
aString: {type: 'String'},
|
||||
aBool: {type: 'Boolean'},
|
||||
aDate: {type: 'Date'},
|
||||
aObject: {type: 'Object'},
|
||||
aArray: {type: 'Array'},
|
||||
aGeoPoint: {type: 'GeoPoint'},
|
||||
aFile: {type: 'File'},
|
||||
aPointer: {type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet'},
|
||||
aRelation: {type: 'Relation', targetClass: 'NewClass'},
|
||||
}))
|
||||
.then(mongoObj => {
|
||||
expect(mongoObj).toEqual({
|
||||
_id: 'NewClass',
|
||||
objectId: 'string',
|
||||
createdAt: 'string',
|
||||
updatedAt: 'string',
|
||||
aNumber: 'number',
|
||||
aString: 'string',
|
||||
aBool: 'boolean',
|
||||
aDate: 'date',
|
||||
aObject: 'object',
|
||||
aArray: 'array',
|
||||
aGeoPoint: 'geopoint',
|
||||
aFile: 'file',
|
||||
aPointer: '*ThisClassDoesNotExistYet',
|
||||
aRelation: 'relation<NewClass>',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('creates the default fields for non-custom classes', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('_Installation', {
|
||||
foo: {type: 'Number'},
|
||||
}))
|
||||
.then(mongoObj => {
|
||||
expect(mongoObj).toEqual({
|
||||
_id: '_Installation',
|
||||
createdAt: 'string',
|
||||
updatedAt: 'string',
|
||||
objectId: 'string',
|
||||
foo: 'number',
|
||||
installationId: 'string',
|
||||
deviceToken: 'string',
|
||||
channels: 'array',
|
||||
deviceType: 'string',
|
||||
pushType: 'string',
|
||||
GCMSenderId: 'string',
|
||||
timeZone: 'string',
|
||||
localeIdentifier: 'string',
|
||||
badge: 'number',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses to create two geopoints', done => {
|
||||
config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists('NewClass', {
|
||||
geo1: {type: 'GeoPoint'},
|
||||
geo2: {type: 'GeoPoint'},
|
||||
}))
|
||||
.catch(error => {
|
||||
expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);
|
||||
expect(error.error).toEqual('currently, only one GeoPoint field may exist in an object. Adding geo2 when geo1 already exists.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user