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 joinRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
|
||||||
var otherRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
|
var otherRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
|
||||||
ExportAdapter.prototype.collection = function(className) {
|
ExportAdapter.prototype.collection = function(className) {
|
||||||
if (className !== '_User' &&
|
if (!Schema.classNameIsValid(className)) {
|
||||||
className !== '_Installation' &&
|
|
||||||
className !== '_Session' &&
|
|
||||||
className !== '_SCHEMA' &&
|
|
||||||
className !== '_Role' &&
|
|
||||||
!joinRegex.test(className) &&
|
|
||||||
!otherRegex.test(className)) {
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME,
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME,
|
||||||
'invalid className: ' + className);
|
'invalid className: ' + className);
|
||||||
}
|
}
|
||||||
@@ -500,6 +494,7 @@ ExportAdapter.prototype.smartFind = function(coll, where, options) {
|
|||||||
|
|
||||||
var index = {};
|
var index = {};
|
||||||
index[key] = '2d';
|
index[key] = '2d';
|
||||||
|
//TODO: condiser moving index creation logic into Schema.js
|
||||||
return coll.createIndex(index).then(() => {
|
return coll.createIndex(index).then(() => {
|
||||||
// Retry, but just once.
|
// Retry, but just once.
|
||||||
return coll.find(where, options).toArray();
|
return coll.find(where, options).toArray();
|
||||||
|
|||||||
218
Schema.js
218
Schema.js
@@ -17,6 +17,135 @@
|
|||||||
var Parse = require('parse/node').Parse;
|
var Parse = require('parse/node').Parse;
|
||||||
var transform = require('./transform');
|
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.
|
// Create a schema from a Mongo collection and the exported schema format.
|
||||||
// mongoSchema should be a list of objects, each with:
|
// mongoSchema should be a list of objects, each with:
|
||||||
@@ -71,9 +200,93 @@ Schema.prototype.reload = function() {
|
|||||||
return load(this.collection);
|
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
|
// 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.
|
// 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) {
|
Schema.prototype.validateClassName = function(className, freeze) {
|
||||||
if (this.data[className]) {
|
if (this.data[className]) {
|
||||||
return Promise.resolve(this);
|
return Promise.resolve(this);
|
||||||
@@ -348,5 +561,6 @@ function getObjectType(obj) {
|
|||||||
|
|
||||||
|
|
||||||
module.exports = {
|
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();
|
var router = new PromiseRouter();
|
||||||
|
|
||||||
function mongoFieldTypeToApiResponseType(type) {
|
function mongoFieldTypeToSchemaAPIType(type) {
|
||||||
if (type[0] === '*') {
|
if (type[0] === '*') {
|
||||||
return {
|
return {
|
||||||
type: 'Pointer',
|
type: 'Pointer',
|
||||||
@@ -32,10 +32,10 @@ function mongoFieldTypeToApiResponseType(type) {
|
|||||||
|
|
||||||
function mongoSchemaAPIResponseFields(schema) {
|
function mongoSchemaAPIResponseFields(schema) {
|
||||||
fieldNames = Object.keys(schema).filter(key => key !== '_id');
|
fieldNames = Object.keys(schema).filter(key => key !== '_id');
|
||||||
response = {};
|
response = fieldNames.reduce((obj, fieldName) => {
|
||||||
fieldNames.forEach(fieldName => {
|
obj[fieldName] = mongoFieldTypeToSchemaAPIType(schema[fieldName])
|
||||||
response[fieldName] = mongoFieldTypeToApiResponseType(schema[fieldName]);
|
return obj;
|
||||||
});
|
}, {});
|
||||||
response.ACL = {type: 'ACL'};
|
response.ACL = {type: 'ACL'};
|
||||||
response.createdAt = {type: 'Date'};
|
response.createdAt = {type: 'Date'};
|
||||||
response.updatedAt = {type: 'Date'};
|
response.updatedAt = {type: 'Date'};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// These tests check that the Schema operates correctly.
|
// These tests check that the Schema operates correctly.
|
||||||
var Config = require('../Config');
|
var Config = require('../Config');
|
||||||
var Schema = require('../Schema');
|
var Schema = require('../Schema');
|
||||||
|
var dd = require('deep-diff');
|
||||||
|
|
||||||
var config = new Config('test');
|
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