diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index faaa6b826a..4df3c90275 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1598,6 +1598,32 @@ describe('Cloud Code', () => { expect(obj.get('count')).toBe(0); }); + it('pointer should not be cleared by triggers', async () => { + Parse.Cloud.afterSave('MyObject', () => {}); + const foo = await new Parse.Object('Test', { foo: 'bar' }).save(); + const obj = await new Parse.Object('MyObject', { foo }).save(); + const foo2 = obj.get('foo'); + expect(foo2.get('foo')).toBe('bar'); + }); + + it('can set a pointer in triggers', async () => { + Parse.Cloud.beforeSave('MyObject', () => {}); + Parse.Cloud.afterSave( + 'MyObject', + async ({ object }) => { + const foo = await new Parse.Object('Test', { foo: 'bar' }).save(); + object.set({ foo }); + await object.save(null, { useMasterKey: true }); + }, + { + skipWithMasterKey: true, + } + ); + const obj = await new Parse.Object('MyObject').save(); + const foo2 = obj.get('foo'); + expect(foo2.get('foo')).toBe('bar'); + }); + it('beforeSave should not sanitize database', async done => { const { adapter } = Config.get(Parse.applicationId).database; const spy = spyOn(adapter, 'findOneAndUpdate').and.callThrough(); diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index c8e3adb49d..deac233915 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -429,4 +429,26 @@ describe('RestQuery.each', () => { done(); }); }); + + it('test afterSave should not affect save response', async () => { + Parse.Cloud.beforeSave('TestObject2', ({ object }) => { + object.set('addedBeforeSave', true); + }); + Parse.Cloud.afterSave('TestObject2', ({ object }) => { + object.set('addedAfterSave', true); + object.unset('initialToRemove'); + }); + const { response } = await rest.create(config, nobody, 'TestObject2', { + initialSave: true, + initialToRemove: true, + }); + expect(Object.keys(response).sort()).toEqual([ + 'addedAfterSave', + 'addedBeforeSave', + 'createdAt', + 'initialToRemove', + 'objectId', + 'updatedAt', + ]); + }); }); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 9ae6628088..1d857b47fd 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -150,9 +150,15 @@ const defaultColumns: { [string]: SchemaFields } = Object.freeze({ }, }); +// fields required for read or write operations on their respective classes. const requiredColumns = Object.freeze({ - _Product: ['productIdentifier', 'icon', 'order', 'title', 'subtitle'], - _Role: ['name', 'ACL'], + read: { + _User: ['username'], + }, + write: { + _Product: ['productIdentifier', 'icon', 'order', 'title', 'subtitle'], + _Role: ['name', 'ACL'], + } }); const invalidColumns = ['length']; @@ -1269,7 +1275,7 @@ export default class SchemaController { // Validates that all the properties are set for the object validateRequiredColumns(className: string, object: any, query: any) { - const columns = requiredColumns[className]; + const columns = requiredColumns.write[className]; if (!columns || columns.length == 0) { return Promise.resolve(this); } @@ -1600,4 +1606,5 @@ export { convertSchemaToAdapterSchema, VolatileClassesSchemas, SchemaController, + requiredColumns, }; diff --git a/src/RestWrite.js b/src/RestWrite.js index 3e20328a9a..0b9bf2a8d4 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -15,6 +15,7 @@ var ClientSDK = require('./ClientSDK'); import RestQuery from './RestQuery'; import _ from 'lodash'; import logger from './logger'; +import { requiredColumns } from './Controllers/SchemaController'; // query and data are both provided in REST API format. So data // types are encoded by plain old objects. @@ -1556,7 +1557,7 @@ RestWrite.prototype.runAfterSaveTrigger = function () { this.response.response = result; } else { this.response.response = this._updateResponseWithData( - (result || updatedObject)._toFullJSON(), + (result || updatedObject).toJSON(), this.data ); } @@ -1665,6 +1666,21 @@ RestWrite.prototype._updateResponseWithData = function (response, data) { this.storage.fieldsChangedByTrigger.push(key); } } + const skipKeys = [ + 'objectId', + 'createdAt', + 'updatedAt', + ...(requiredColumns.read[this.className] || []), + ]; + for (const key in response) { + if (skipKeys.includes(key)) { + continue; + } + const value = response[key]; + if (value == null || (value.__type && value.__type === 'Pointer') || data[key] === value) { + delete response[key]; + } + } if (_.isEmpty(this.storage.fieldsChangedByTrigger)) { return response; }