From cef5a5fabfeed1dc3356e736fd1869c174600b0d Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 9 Feb 2016 11:26:46 -0800 Subject: [PATCH 1/2] First part of schemas PUT --- spec/schemas.spec.js | 99 +++++++++++++++++++++++++++++++++++++++++++- src/schemas.js | 69 +++++++++++++++++++++--------- 2 files changed, 148 insertions(+), 20 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 68ac31c9..907c5f65 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -94,7 +94,7 @@ describe('schemas', () => { headers: restKeyHeaders, }, (error, response, body) => { expect(response.statusCode).toEqual(401); - expect(body.error).toEqual('unauthorized'); + expect(body.error).toEqual('master key not specified'); done(); }); }); @@ -318,4 +318,101 @@ describe('schemas', () => { done(); }); }); + + it('requires the master key to modify schemas', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, (error, response, body) => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: noAuthHeaders, + json: true, + body: {}, + }, (error, response, body) => { + expect(response.statusCode).toEqual(403); + expect(body.error).toEqual('unauthorized'); + done(); + }); + }); + }); + + it('rejects class name mis-matches', done => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {className: 'WrongClassName'} + }, (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); + expect(body.error).toEqual('class name mismatch between WrongClassName and NewClass'); + }); + }); + + it('refuses to add fields to non-existent classes', done => { + request.put({ + url: 'http://localhost:8378/1/schemas/NoClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + newField: {type: 'String'} + } + } + }, (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); + expect(body.error).toEqual('class NoClass does not exist'); + done(); + }); + }); + + it('put with no modifications returns all fields', done => { + var obj = hasAllPODobject(); + obj.save() + .then(() => { + request.put({ + url: 'http://localhost:8378/1/schemas/HasAllPOD' + headers: masterKeyHeaders, + json: true, + body: {}, + }, (error, response, body) => { + expect(body).toEqual(plainOldDataSchema); + done(); + }); + }); + }); + + it('lets you add fields', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, (error, response, body) => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + newField: {type: 'String'} + } + } + }, (error, response, body) => { + expect(body).toEqual('blah'); + request.get({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(body).toEqual('blah'); + done(); + }); + }); + }) + }); }); diff --git a/src/schemas.js b/src/schemas.js index 837224ab..c17e9407 100644 --- a/src/schemas.js +++ b/src/schemas.js @@ -7,6 +7,23 @@ var express = require('express'), var router = new PromiseRouter(); +function masterKeyRequiredResponse() { + return Promise.resolve({ + status: 401, + response: {error: 'master key not specified'}, + }) +} + +function classNameMismatchResponse(bodyClass, pathClass) { + return Promise.resolve({ + status: 400, + response: { + code: Parse.Error.INVALID_CLASS_NAME, + error: 'class name mismatch between ' + bodyClass + ' and ' + pathClass, + } + }); +} + function mongoFieldTypeToSchemaAPIType(type) { if (type[0] === '*') { return { @@ -55,10 +72,7 @@ function mongoSchemaToSchemaAPIResponse(schema) { function getAllSchemas(req) { if (!req.auth.isMaster) { - return Promise.resolve({ - status: 401, - response: {error: 'master key not specified'}, - }); + return masterKeyRequiredResponse(); } return req.config.database.collection('_SCHEMA') .then(coll => coll.find({}).toArray()) @@ -69,10 +83,7 @@ function getAllSchemas(req) { function getOneSchema(req) { if (!req.auth.isMaster) { - return Promise.resolve({ - status: 401, - response: {error: 'unauthorized'}, - }); + return masterKeyRequiredResponse(); } return req.config.database.collection('_SCHEMA') .then(coll => coll.findOne({'_id': req.params.className})) @@ -88,20 +99,11 @@ function getOneSchema(req) { function createSchema(req) { if (!req.auth.isMaster) { - return Promise.resolve({ - status: 401, - response: {error: 'master key not specified'}, - }); + return masterKeyRequiredResponse(); } if (req.params.className && req.body.className) { if (req.params.className != req.body.className) { - return Promise.resolve({ - status: 400, - response: { - code: Parse.Error.INVALID_CLASS_NAME, - error: 'class name mismatch between ' + req.body.className + ' and ' + req.params.className, - }, - }); + return classNameMismatchResponse(req.body.className, req.params.className); } } var className = req.params.className || req.body.className; @@ -123,9 +125,38 @@ function createSchema(req) { })); } +function modifySchema(req) { + if (!req.auth.isMaster) { + return masterKeyRequiredResponse(); + } + + if (req.body.className && req.body.className != req.params.className) { + return classNameMismatchResponse(req.body.className, req.path.className); + } + + if (!req.body.fields) { + req.body.fields = {}; + } + + return req.config.database.loadSchema() + .then(schema => schema.hasClass(req.params.className)) + .then(hasClass => { + if (!hasClass) { + return Promise.resolve({ + status: 400, + response: { + code: Parse.Error.INVALID_CLASS_NAME, + error: 'class ' + req.params.className + ' does not exist', + } + }); + } + }); +} + router.route('GET', '/schemas', getAllSchemas); router.route('GET', '/schemas/:className', getOneSchema); router.route('POST', '/schemas', createSchema); router.route('POST', '/schemas/:className', createSchema); +router.route('PUT', '/schemas/:className', modifySchema); module.exports = router; From a455e1b23fb219ae5410390ef5c82c0b65fe4bc3 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 16 Feb 2016 12:30:30 -0800 Subject: [PATCH 2/2] Finish implementation of PUT /schemas/:className --- spec/Schema.spec.js | 31 ++++++ spec/schemas.spec.js | 228 ++++++++++++++++++++++++++++++++++++++++++- src/Schema.js | 169 ++++++++++++++++++++++---------- src/schemas.js | 98 ++++++++++++------- 4 files changed, 436 insertions(+), 90 deletions(-) diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 7f59fec2..a5c28c5b 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -162,6 +162,9 @@ describe('Schema', () => { foo: 'string', }) done(); + }) + .catch(error => { + fail('Error creating class: ' + JSON.stringify(error)); }); }); @@ -570,4 +573,32 @@ describe('Schema', () => { Parse.Object.enableSingleInstance(); }); }); + + it('can merge schemas', done => { + expect(Schema.buildMergedSchemaObject({ + _id: 'SomeClass', + someType: 'number' + }, { + newType: {type: 'Number'} + })).toEqual({ + someType: {type: 'Number'}, + newType: {type: 'Number'}, + }); + done(); + }); + + it('can merge deletions', done => { + expect(Schema.buildMergedSchemaObject({ + _id: 'SomeClass', + someType: 'number', + outDatedType: 'string', + },{ + newType: {type: 'GeoPoint'}, + outDatedType: {__op: 'Delete'}, + })).toEqual({ + someType: {type: 'Number'}, + newType: {type: 'GeoPoint'}, + }); + done(); + }); }); diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 907c5f65..fd136df4 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -339,7 +339,7 @@ describe('schemas', () => { }); }); - it('rejects class name mis-matches', done => { + it('rejects class name mis-matches in put', done => { request.put({ url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, @@ -349,6 +349,7 @@ describe('schemas', () => { expect(response.statusCode).toEqual(400); expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); expect(body.error).toEqual('class name mismatch between WrongClassName and NewClass'); + done(); }); }); @@ -370,12 +371,133 @@ describe('schemas', () => { }); }); + it('refuses to put to existing fields, even if it would not be a change', done => { + var obj = hasAllPODobject(); + obj.save() + .then(() => { + request.put({ + url: 'http://localhost:8378/1/schemas/HasAllPOD', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aString: {type: 'String'} + } + } + }, (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(255); + expect(body.error).toEqual('field aString exists, cannot update'); + done(); + }); + }) + }); + + it('refuses to delete non-existant fields', done => { + var obj = hasAllPODobject(); + obj.save() + .then(() => { + request.put({ + url: 'http://localhost:8378/1/schemas/HasAllPOD', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + nonExistantKey: {__op: "Delete"}, + } + } + }, (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(255); + expect(body.error).toEqual('field nonExistantKey does not exist, cannot delete'); + done(); + }); + }); + }); + + it('refuses to add a geopoint to a class that already has one', done => { + var obj = hasAllPODobject(); + obj.save() + .then(() => { + request.put({ + url: 'http://localhost:8378/1/schemas/HasAllPOD', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + newGeo: {type: 'GeoPoint'} + } + } + }, (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(Parse.Error.INCORRECT_TYPE); + expect(body.error).toEqual('currently, only one GeoPoint field may exist in an object. Adding newGeo when aGeoPoint already exists.'); + done(); + }); + }); + }); + + it('refuses to add two geopoints', done => { + var obj = new Parse.Object('NewClass'); + obj.set('aString', 'aString'); + obj.save() + .then(() => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + newGeo1: {type: 'GeoPoint'}, + newGeo2: {type: 'GeoPoint'}, + } + } + }, (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(Parse.Error.INCORRECT_TYPE); + expect(body.error).toEqual('currently, only one GeoPoint field may exist in an object. Adding newGeo2 when newGeo1 already exists.'); + done(); + }); + }); + }); + + it('allows you to delete and add a geopoint in the same request', done => { + var obj = new Parse.Object('NewClass'); + obj.set('geo1', new Parse.GeoPoint({latitude: 0, longitude: 0})); + obj.save() + .then(() => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + geo2: {type: 'GeoPoint'}, + geo1: {__op: 'Delete'} + } + } + }, (error, response, body) => { + expect(dd(body, { + "className": "NewClass", + "fields": { + "ACL": {"type": "ACL"}, + "createdAt": {"type": "Date"}, + "objectId": {"type": "String"}, + "updatedAt": {"type": "Date"}, + "geo2": {"type": "GeoPoint"}, + } + })).toEqual(undefined); + done(); + }); + }) + }); + it('put with no modifications returns all fields', done => { var obj = hasAllPODobject(); obj.save() .then(() => { request.put({ - url: 'http://localhost:8378/1/schemas/HasAllPOD' + url: 'http://localhost:8378/1/schemas/HasAllPOD', headers: masterKeyHeaders, json: true, body: {}, @@ -383,7 +505,7 @@ describe('schemas', () => { expect(body).toEqual(plainOldDataSchema); done(); }); - }); + }) }); it('lets you add fields', done => { @@ -403,16 +525,112 @@ describe('schemas', () => { } } }, (error, response, body) => { - expect(body).toEqual('blah'); + expect(dd(body, { + className: 'NewClass', + fields: { + "ACL": {"type": "ACL"}, + "createdAt": {"type": "Date"}, + "objectId": {"type": "String"}, + "updatedAt": {"type": "Date"}, + "newField": {"type": "String"}, + }, + })).toEqual(undefined); request.get({ url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, }, (error, response, body) => { - expect(body).toEqual('blah'); + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + newField: {type: 'String'}, + } + }); done(); }); }); }) }); + + it('lets you delete multiple fields and add fields', done => { + var obj1 = hasAllPODobject(); + obj1.save() + .then(() => { + request.put({ + url: 'http://localhost:8378/1/schemas/HasAllPOD', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aString: {__op: 'Delete'}, + aNumber: {__op: 'Delete'}, + aNewString: {type: 'String'}, + aNewNumber: {type: 'Number'}, + aNewRelation: {type: 'Relation', targetClass: 'HasAllPOD'}, + aNewPointer: {type: 'Pointer', targetClass: 'HasAllPOD'}, + } + } + }, (error, response, body) => { + expect(body).toEqual({ + className: 'HasAllPOD', + fields: { + //Default fields + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + //Custom fields + aBool: {type: 'Boolean'}, + aDate: {type: 'Date'}, + aObject: {type: 'Object'}, + aArray: {type: 'Array'}, + aGeoPoint: {type: 'GeoPoint'}, + aFile: {type: 'File'}, + aNewNumber: {type: 'Number'}, + aNewString: {type: 'String'}, + aNewPointer: {type: 'Pointer', targetClass: 'HasAllPOD'}, + aNewRelation: {type: 'Relation', targetClass: 'HasAllPOD'}, + } + }); + var obj2 = new Parse.Object('HasAllPOD'); + obj2.set('aNewPointer', obj1); + var relation = obj2.relation('aNewRelation'); + relation.add(obj1); + obj2.save().then(done); //Just need to make sure saving works on the new object. + }); + }); + }); + + it('will not delete any fields if the additions are invalid', done => { + var obj = hasAllPODobject(); + obj.save() + .then(() => { + request.put({ + url: 'http://localhost:8378/1/schemas/HasAllPOD', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + fakeNewField: {type: 'fake type'}, + aString: {__op: 'Delete'} + } + } + }, (error, response, body) => { + expect(body.code).toEqual(Parse.Error.INCORRECT_TYPE); + expect(body.error).toEqual('invalid field type: fake type'); + request.get({ + url: 'http://localhost:8378/1/schemas/HasAllPOD', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(response.body).toEqual(plainOldDataSchema); + done(); + }); + }); + }); + }); }); diff --git a/src/Schema.js b/src/Schema.js index d8f38499..a07018bf 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -116,7 +116,7 @@ function schemaAPITypeToMongoFieldType(type) { return invalidJsonError; } else if (!classNameIsValid(type.targetClass)) { return { error: invalidClassNameMessage(type.targetClass), code: Parse.Error.INVALID_CLASS_NAME }; - } else { + } else { return { result: '*' + type.targetClass }; } } @@ -200,6 +200,114 @@ Schema.prototype.reload = function() { return load(this.collection); }; +// Returns { code, error } if invalid, or { result }, an object +// suitable for inserting into _SCHEMA collection, otherwise +function mongoSchemaFromFieldsAndClassName(fields, className) { + if (!classNameIsValid(className)) { + return { + code: Parse.Error.INVALID_CLASS_NAME, + error: invalidClassNameMessage(className), + }; + } + + for (var fieldName in fields) { + if (!fieldNameIsValid(fieldName)) { + return { + code: Parse.Error.INVALID_KEY_NAME, + error: 'invalid field name: ' + fieldName, + }; + } + if (!fieldNameIsValidForClass(fieldName, className)) { + return { + code: 136, + error: 'field ' + fieldName + ' cannot be added', + }; + } + } + + var mongoObject = { + _id: className, + objectId: 'string', + updatedAt: 'string', + createdAt: 'string' + }; + + for (var fieldName in defaultColumns[className]) { + var validatedField = schemaAPITypeToMongoFieldType(defaultColumns[className][fieldName]); + if (!validatedField.result) { + return validatedField; + } + mongoObject[fieldName] = validatedField.result; + } + + for (var fieldName in fields) { + var validatedField = schemaAPITypeToMongoFieldType(fields[fieldName]); + if (!validatedField.result) { + return validatedField; + } + mongoObject[fieldName] = validatedField.result; + } + + var geoPoints = Object.keys(mongoObject).filter(key => mongoObject[key] === 'geopoint'); + if (geoPoints.length > 1) { + return { + 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 { result: mongoObject }; +} + +function mongoFieldTypeToSchemaAPIType(type) { + if (type[0] === '*') { + return { + type: 'Pointer', + targetClass: type.slice(1), + }; + } + if (type.startsWith('relation<')) { + return { + type: 'Relation', + targetClass: type.slice('relation<'.length, type.length - 1), + }; + } + switch (type) { + case 'number': return {type: 'Number'}; + case 'string': return {type: 'String'}; + case 'boolean': return {type: 'Boolean'}; + case 'date': return {type: 'Date'}; + case 'map': + case 'object': return {type: 'Object'}; + case 'array': return {type: 'Array'}; + case 'geopoint': return {type: 'GeoPoint'}; + case 'file': return {type: 'File'}; + } +} + +// Builds a new schema (in schema API response format) out of an +// existing mongo schema + a schemas API put request. This response +// does not include the default fields, as it is intended to be passed +// to mongoSchemaFromFieldsAndClassName. No validation is done here, it +// is done in mongoSchemaFromFieldsAndClassName. +function buildMergedSchemaObject(mongoObject, putRequest) { + var newSchema = {}; + for (var oldField in mongoObject) { + if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') { + var fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete' + if (!fieldIsDeleted) { + newSchema[oldField] = mongoFieldTypeToSchemaAPIType(mongoObject[oldField]); + } + } + } + for (var newField in putRequest) { + if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') { + newSchema[newField] = putRequest[newField]; + } + } + return newSchema; +} + // 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 @@ -215,58 +323,13 @@ Schema.prototype.addClassIfNotExists = function(className, fields) { }); } - if (!classNameIsValid(className)) { - return Promise.reject({ - code: Parse.Error.INVALID_CLASS_NAME, - error: invalidClassNameMessage(className), - }); - } - for (var 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 = mongoSchemaFromFieldsAndClassName(fields, className); + + if (!mongoObject.result) { + return Promise.reject(mongoObject); } - var mongoObject = { - _id: className, - objectId: 'string', - updatedAt: 'string', - createdAt: 'string' - }; - for (var fieldName in defaultColumns[className]) { - var validatedField = schemaAPITypeToMongoFieldType(defaultColumns[className][fieldName]); - if (validatedField.code) { - return Promise.reject(validatedField); - } - mongoObject[fieldName] = validatedField.result; - } - - for (var fieldName in fields) { - var 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) + return this.collection.insertOne(mongoObject.result) .then(result => result.ops[0]) .catch(error => { if (error.code === 11000) { //Mongo's duplicate key error @@ -651,4 +714,8 @@ function getObjectType(obj) { module.exports = { load: load, classNameIsValid: classNameIsValid, + mongoSchemaFromFieldsAndClassName: mongoSchemaFromFieldsAndClassName, + schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType, + buildMergedSchemaObject: buildMergedSchemaObject, + mongoFieldTypeToSchemaAPIType: mongoFieldTypeToSchemaAPIType, }; diff --git a/src/schemas.js b/src/schemas.js index c17e9407..cd8b92ec 100644 --- a/src/schemas.js +++ b/src/schemas.js @@ -24,36 +24,10 @@ function classNameMismatchResponse(bodyClass, pathClass) { }); } -function mongoFieldTypeToSchemaAPIType(type) { - if (type[0] === '*') { - return { - type: 'Pointer', - targetClass: type.slice(1), - }; - } - if (type.startsWith('relation<')) { - return { - type: 'Relation', - targetClass: type.slice('relation<'.length, type.length - 1), - }; - } - switch (type) { - case 'number': return {type: 'Number'}; - case 'string': return {type: 'String'}; - case 'boolean': return {type: 'Boolean'}; - case 'date': return {type: 'Date'}; - case 'map': - case 'object': return {type: 'Object'}; - case 'array': return {type: 'Array'}; - case 'geopoint': return {type: 'GeoPoint'}; - case 'file': return {type: 'File'}; - } -} - function mongoSchemaAPIResponseFields(schema) { var fieldNames = Object.keys(schema).filter(key => key !== '_id' && key !== '_metadata'); var response = fieldNames.reduce((obj, fieldName) => { - obj[fieldName] = mongoFieldTypeToSchemaAPIType(schema[fieldName]) + obj[fieldName] = Schema.mongoFieldTypeToSchemaAPIType(schema[fieldName]) return obj; }, {}); response.ACL = {type: 'ACL'}; @@ -131,17 +105,15 @@ function modifySchema(req) { } if (req.body.className && req.body.className != req.params.className) { - return classNameMismatchResponse(req.body.className, req.path.className); + return classNameMismatchResponse(req.body.className, req.params.className); } - if (!req.body.fields) { - req.body.fields = {}; - } + var submittedFields = req.body.fields || {}; + var className = req.params.className; return req.config.database.loadSchema() - .then(schema => schema.hasClass(req.params.className)) - .then(hasClass => { - if (!hasClass) { + .then(schema => { + if (!schema.data[className]) { return Promise.resolve({ status: 400, response: { @@ -150,6 +122,64 @@ function modifySchema(req) { } }); } + var existingFields = schema.data[className]; + + for (var submittedFieldName in submittedFields) { + if (existingFields[submittedFieldName] && submittedFields[submittedFieldName].__op !== 'Delete') { + return Promise.resolve({ + status: 400, + response: { + code: 255, + error: 'field ' + submittedFieldName + ' exists, cannot update', + } + }); + } + + if (!existingFields[submittedFieldName] && submittedFields[submittedFieldName].__op === 'Delete') { + return Promise.resolve({ + status: 400, + response: { + code: 255, + error: 'field ' + submittedFieldName + ' does not exist, cannot delete', + } + }); + } + } + + var newSchema = Schema.buildMergedSchemaObject(existingFields, submittedFields); + var mongoObject = Schema.mongoSchemaFromFieldsAndClassName(newSchema, className); + if (!mongoObject.result) { + return Promise.resolve({ + status: 400, + response: mongoObject, + }); + } + + // Finally we have checked to make sure the request is valid and we can start deleting fields. + // Do all deletions first, then a single save to _SCHEMA collection to handle all additions. + var deletionPromises = [] + Object.keys(submittedFields).forEach(submittedFieldName => { + if (submittedFields[submittedFieldName].__op === 'Delete') { + var promise = req.config.database.connect() + .then(() => schema.deleteField( + submittedFieldName, + className, + req.config.database.db, + req.config.database.collectionPrefix + )); + deletionPromises.push(promise); + } + }); + + return Promise.all(deletionPromises) + .then(() => new Promise((resolve, reject) => { + schema.collection.update({_id: className}, mongoObject.result, {w: 1}, (err, docs) => { + if (err) { + reject(err); + } + resolve({ response: mongoSchemaToSchemaAPIResponse(mongoObject.result)}); + }) + })); }); }