diff --git a/ExportAdapter.js b/ExportAdapter.js index 4a626fa2..df417ac8 100644 --- a/ExportAdapter.js +++ b/ExportAdapter.js @@ -494,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(); diff --git a/Schema.js b/Schema.js index 5514dca2..b0ea4d81 100644 --- a/Schema.js +++ b/Schema.js @@ -137,7 +137,7 @@ function schemaAPITypeToMongoFieldType(type) { return { error: "invalid JSON", code: Parse.Error.INVALID_JSON }; } switch (type.type) { - default : return { error: 'invalid field type: ' + type.type }; + default: return { error: 'invalid field type: ' + type.type }; case 'Number': return { result: 'number' }; case 'String': return { result: 'string' }; case 'Boolean': return { result: 'boolean' }; @@ -241,14 +241,38 @@ Schema.prototype.addClassIfNotExists = function(className, fields) { } } - - - return this.collection.insertOne({ + 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 diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index c142f628..ce6f5968 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -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'); @@ -142,7 +143,8 @@ describe('Schema', () => { _id: 'NewClass', objectId: 'string', updatedAt: 'string', - createdAt: 'string' + createdAt: 'string', + foo: 'string', }) done(); }); @@ -183,7 +185,8 @@ describe('Schema', () => { _id: 'NewClass', objectId: 'string', updatedAt: 'string', - createdAt: 'string' + createdAt: 'string', + foo: 'string', }); }); Promise.all([p1,p2]) @@ -229,4 +232,136 @@ describe('Schema', () => { 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('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', + }); + 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(); + }); + }); });