diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 92e3073277..1a7eadddf8 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -224,6 +224,19 @@ describe('miscellaneous', function() { }); }); + it('test beforeSave returns value on create and update', (done) => { + var obj = new Parse.Object('BeforeSaveChanged'); + obj.set('foo', 'bing'); + obj.save().then(() => { + expect(obj.get('foo')).toEqual('baz'); + obj.set('foo', 'bar'); + return obj.save().then(() => { + expect(obj.get('foo')).toEqual('baz'); + done(); + }) + }) + }); + it('test afterSave ran and created an object', function(done) { var obj = new Parse.Object('AfterSaveTest'); obj.save(); @@ -383,6 +396,13 @@ describe('miscellaneous', function() { }); }); + it('should properly create an object in before save', (done) => { + Parse.Cloud.run('createBeforeSaveChangedObject').then((res) => { + expect(res.get('foo')).toEqual('baz'); + done(); + }); + }) + it('test rest_create_app', function(done) { var appId; Parse._request('POST', 'rest_create_app').then((res) => { @@ -868,6 +888,50 @@ describe('miscellaneous', function() { }); }); + it('should return the updated fields on PUT', (done) => { + let obj = new Parse.Object('GameScore'); + obj.save({a:'hello', c: 1, d: ['1'], e:['1'], f:['1','2']}).then(( ) => { + var headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Installation-Id': 'yolo' + }; + request.put({ + headers: headers, + url: 'http://localhost:8378/1/classes/GameScore/'+obj.id, + body: JSON.stringify({ + a: 'b', + c: {"__op":"Increment","amount":2}, + d: {"__op":"Add", objects: ['2']}, + e: {"__op":"AddUnique", objects: ['1', '2']}, + f: {"__op":"Remove", objects: ['2']}, + selfThing: {"__type":"Pointer","className":"GameScore","objectId":obj.id}, + }) + }, (error, response, body) => { + body = JSON.parse(body); + expect(body.a).toBeUndefined(); + expect(body.c).toEqual(3); // 2+1 + expect(body.d.length).toBe(2); + expect(body.d.indexOf('1') > -1).toBe(true); + expect(body.d.indexOf('2') > -1).toBe(true); + expect(body.e.length).toBe(2); + expect(body.e.indexOf('1') > -1).toBe(true); + expect(body.e.indexOf('2') > -1).toBe(true); + expect(body.f.length).toBe(1); + expect(body.f.indexOf('1') > -1).toBe(true); + // return nothing on other self + expect(body.selfThing).toBeUndefined(); + // updatedAt is always set + expect(body.updatedAt).not.toBeUndefined(); + done(); + }); + }).fail((err) => { + fail('Should not fail'); + done(); + }) + }) + it('test cloud function error handling', (done) => { // Register a function which will fail Parse.Cloud.define('willFail', (req, res) => { diff --git a/spec/cloud/main.js b/spec/cloud/main.js index 396fa86281..0785c0a624 100644 --- a/spec/cloud/main.js +++ b/spec/cloud/main.js @@ -108,3 +108,10 @@ Parse.Cloud.define('echoKeys', function(req, res){ javascriptKey: Parse.javascriptKey }) }); + +Parse.Cloud.define('createBeforeSaveChangedObject', function(req, res){ + var obj = new Parse.Object('BeforeSaveChanged'); + obj.save().then(() => { + res.success(obj); + }) +}) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 3e85eca0e6..7494cc88ff 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -139,6 +139,8 @@ DatabaseController.prototype.untransformObject = function( // one of the provided strings must provide the caller with // write permissions. DatabaseController.prototype.update = function(className, query, update, options) { + + const originalUpdate = update; // Make a copy of the object, so we don't mutate the incoming data. update = deepcopy(update); @@ -177,18 +179,27 @@ DatabaseController.prototype.update = function(className, query, update, options return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')); } - - let response = {}; - let inc = mongoUpdate['$inc']; - if (inc) { - Object.keys(inc).forEach(key => { - response[key] = result[key]; - }); - } - return response; + return sanitizeDatabaseResult(originalUpdate, result); }); }; +function sanitizeDatabaseResult(originalObject, result) { + let response = {}; + if (!result) { + return Promise.resolve(response); + } + Object.keys(originalObject).forEach(key => { + let keyUpdate = originalObject[key]; + // determine if that was an op + if (keyUpdate && typeof keyUpdate === 'object' && keyUpdate.__op + && ['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1) { + // only valid ops that produce an actionable result + response[key] = result[key]; + } + }); + return Promise.resolve(response); +} + // Processes relation-updating operations from a REST-format update. // Returns a promise that resolves successfully when these are // processed. @@ -313,6 +324,7 @@ DatabaseController.prototype.destroy = function(className, query, options = {}) // Returns a promise that resolves successfully iff the object saved. DatabaseController.prototype.create = function(className, object, options) { // Make a copy of the object, so we don't mutate the incoming data. + let originalObject = object; object = deepcopy(object); var schema; @@ -333,6 +345,9 @@ DatabaseController.prototype.create = function(className, object, options) { .then(coll => { var mongoObject = transform.transformCreate(schema, className, object); return coll.insertOne(mongoObject); + }) + .then(result => { + return sanitizeDatabaseResult(originalObject, result.ops[0]); }); }; @@ -474,7 +489,7 @@ DatabaseController.prototype.reduceRelationKeys = function(className, query) { DatabaseController.prototype.addInObjectIdsIds = function(ids, query) { if (typeof query.objectId == 'string') { - // Add equality op as we are sure + // Add equality op as we are sure // we had a constraint on that one query.objectId = {'$eq': query.objectId}; } diff --git a/src/RestWrite.js b/src/RestWrite.js index e241a98854..e0f1f5d5ec 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -711,6 +711,12 @@ RestWrite.prototype.runDatabaseOperation = function() { return this.config.database.update( this.className, this.query, this.data, this.runOptions).then((resp) => { resp.updatedAt = this.updatedAt; + if (this.storage['changedByTrigger']) { + resp = Object.keys(this.data).reduce((memo, key) => { + memo[key] = resp[key] || this.data[key]; + return memo; + }, resp); + } this.response = { response: resp }; @@ -725,13 +731,16 @@ RestWrite.prototype.runDatabaseOperation = function() { } // Run a create return this.config.database.create(this.className, this.data, this.runOptions) - .then(() => { - var resp = { + .then((resp) => { + Object.assign(resp, { objectId: this.data.objectId, createdAt: this.data.createdAt - }; + }); if (this.storage['changedByTrigger']) { - Object.assign(resp, this.data); + resp = Object.keys(this.data).reduce((memo, key) => { + memo[key] = resp[key] || this.data[key]; + return memo; + }, resp); } if (this.storage['token']) { resp.sessionToken = this.storage['token'];