Skip to content

Commit f63fb2b

Browse files
authored
fix: return correct response when revert is used in beforeSave (#7839)
1 parent 9ef1968 commit f63fb2b

File tree

2 files changed

+165
-28
lines changed

2 files changed

+165
-28
lines changed

Diff for: spec/CloudCode.spec.js

+134
Original file line numberDiff line numberDiff line change
@@ -1494,6 +1494,110 @@ describe('Cloud Code', () => {
14941494
});
14951495
});
14961496

1497+
it('before save can revert fields', async () => {
1498+
Parse.Cloud.beforeSave('TestObject', ({ object }) => {
1499+
object.revert('foo');
1500+
return object;
1501+
});
1502+
1503+
Parse.Cloud.afterSave('TestObject', ({ object }) => {
1504+
expect(object.get('foo')).toBeUndefined();
1505+
return object;
1506+
});
1507+
1508+
const obj = new TestObject();
1509+
obj.set('foo', 'bar');
1510+
await obj.save();
1511+
1512+
expect(obj.get('foo')).toBeUndefined();
1513+
await obj.fetch();
1514+
1515+
expect(obj.get('foo')).toBeUndefined();
1516+
});
1517+
1518+
it('before save can revert fields with existing object', async () => {
1519+
Parse.Cloud.beforeSave(
1520+
'TestObject',
1521+
({ object }) => {
1522+
object.revert('foo');
1523+
return object;
1524+
},
1525+
{
1526+
skipWithMasterKey: true,
1527+
}
1528+
);
1529+
1530+
Parse.Cloud.afterSave(
1531+
'TestObject',
1532+
({ object }) => {
1533+
expect(object.get('foo')).toBe('bar');
1534+
return object;
1535+
},
1536+
{
1537+
skipWithMasterKey: true,
1538+
}
1539+
);
1540+
1541+
const obj = new TestObject();
1542+
obj.set('foo', 'bar');
1543+
await obj.save(null, { useMasterKey: true });
1544+
1545+
expect(obj.get('foo')).toBe('bar');
1546+
obj.set('foo', 'yolo');
1547+
await obj.save();
1548+
expect(obj.get('foo')).toBe('bar');
1549+
});
1550+
1551+
it('can unset in afterSave', async () => {
1552+
Parse.Cloud.beforeSave('TestObject', ({ object }) => {
1553+
if (!object.existed()) {
1554+
object.set('secret', true);
1555+
return object;
1556+
}
1557+
object.revert('secret');
1558+
});
1559+
1560+
Parse.Cloud.afterSave('TestObject', ({ object }) => {
1561+
object.unset('secret');
1562+
});
1563+
1564+
Parse.Cloud.beforeFind(
1565+
'TestObject',
1566+
({ query }) => {
1567+
query.exclude('secret');
1568+
},
1569+
{
1570+
skipWithMasterKey: true,
1571+
}
1572+
);
1573+
1574+
const obj = new TestObject();
1575+
await obj.save();
1576+
expect(obj.get('secret')).toBeUndefined();
1577+
await obj.fetch();
1578+
expect(obj.get('secret')).toBeUndefined();
1579+
await obj.fetch({ useMasterKey: true });
1580+
expect(obj.get('secret')).toBe(true);
1581+
});
1582+
1583+
it('should revert in beforeSave', async () => {
1584+
Parse.Cloud.beforeSave('MyObject', ({ object }) => {
1585+
if (!object.existed()) {
1586+
object.set('count', 0);
1587+
return object;
1588+
}
1589+
object.revert('count');
1590+
return object;
1591+
});
1592+
const obj = await new Parse.Object('MyObject').save();
1593+
expect(obj.get('count')).toBe(0);
1594+
obj.set('count', 10);
1595+
await obj.save();
1596+
expect(obj.get('count')).toBe(0);
1597+
await obj.fetch();
1598+
expect(obj.get('count')).toBe(0);
1599+
});
1600+
14971601
it('beforeSave should not sanitize database', async done => {
14981602
const { adapter } = Config.get(Parse.applicationId).database;
14991603
const spy = spyOn(adapter, 'findOneAndUpdate').and.callThrough();
@@ -1860,6 +1964,36 @@ describe('afterSave hooks', () => {
18601964
const myObject = new MyObject();
18611965
myObject.save().then(() => done());
18621966
});
1967+
1968+
it('should unset in afterSave', async () => {
1969+
Parse.Cloud.afterSave(
1970+
'MyObject',
1971+
({ object }) => {
1972+
object.unset('secret');
1973+
},
1974+
{
1975+
skipWithMasterKey: true,
1976+
}
1977+
);
1978+
const obj = new Parse.Object('MyObject');
1979+
obj.set('secret', 'bar');
1980+
await obj.save();
1981+
expect(obj.get('secret')).toBeUndefined();
1982+
await obj.fetch();
1983+
expect(obj.get('secret')).toBe('bar');
1984+
});
1985+
1986+
it('should unset', async () => {
1987+
Parse.Cloud.beforeSave('MyObject', ({ object }) => {
1988+
object.set('secret', 'hidden');
1989+
});
1990+
1991+
Parse.Cloud.afterSave('MyObject', ({ object }) => {
1992+
object.unset('secret');
1993+
});
1994+
const obj = await new Parse.Object('MyObject').save();
1995+
expect(obj.get('secret')).toBeUndefined();
1996+
});
18631997
});
18641998

18651999
describe('beforeDelete hooks', () => {

Diff for: src/RestWrite.js

+31-28
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
9595
// Shared SchemaController to be reused to reduce the number of loadSchema() calls per request
9696
// Once set the schemaData should be immutable
9797
this.validSchemaController = null;
98+
this.pendingOps = {};
9899
}
99100

100101
// A convenient method to perform all the steps of processing the
@@ -225,18 +226,11 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
225226
return Promise.resolve();
226227
}
227228

228-
// Cloud code gets a bit of extra data for its objects
229-
var extraData = { className: this.className };
230-
if (this.query && this.query.objectId) {
231-
extraData.objectId = this.query.objectId;
232-
}
229+
const { originalObject, updatedObject } = this.buildParseObjects();
233230

234-
let originalObject = null;
235-
const updatedObject = this.buildUpdatedObject(extraData);
236-
if (this.query && this.query.objectId) {
237-
// This is an update for existing object.
238-
originalObject = triggers.inflate(extraData, this.originalData);
239-
}
231+
const stateController = Parse.CoreManager.getObjectStateController();
232+
const [pending] = stateController.getPendingOps(updatedObject._getStateIdentifier());
233+
this.pendingOps = { ...pending };
240234

241235
return Promise.resolve()
242236
.then(() => {
@@ -1531,20 +1525,7 @@ RestWrite.prototype.runAfterSaveTrigger = function () {
15311525
return Promise.resolve();
15321526
}
15331527

1534-
var extraData = { className: this.className };
1535-
if (this.query && this.query.objectId) {
1536-
extraData.objectId = this.query.objectId;
1537-
}
1538-
1539-
// Build the original object, we only do this for a update write.
1540-
let originalObject;
1541-
if (this.query && this.query.objectId) {
1542-
originalObject = triggers.inflate(extraData, this.originalData);
1543-
}
1544-
1545-
// Build the inflated object, different from beforeSave, originalData is not empty
1546-
// since developers can change data in the beforeSave.
1547-
const updatedObject = this.buildUpdatedObject(extraData);
1528+
const { originalObject, updatedObject } = this.buildParseObjects();
15481529
updatedObject._handleSaveResponse(this.response.response, this.response.status || 200);
15491530

15501531
this.config.database.loadSchema().then(schemaController => {
@@ -1569,8 +1550,15 @@ RestWrite.prototype.runAfterSaveTrigger = function () {
15691550
this.context
15701551
)
15711552
.then(result => {
1572-
if (result && typeof result === 'object') {
1553+
const jsonReturned = result && !result._toFullJSON;
1554+
if (jsonReturned) {
1555+
this.pendingOps = {};
15731556
this.response.response = result;
1557+
} else {
1558+
this.response.response = this._updateResponseWithData(
1559+
(result || updatedObject)._toFullJSON(),
1560+
this.data
1561+
);
15741562
}
15751563
})
15761564
.catch(function (err) {
@@ -1604,7 +1592,13 @@ RestWrite.prototype.sanitizedData = function () {
16041592
};
16051593

16061594
// Returns an updated copy of the object
1607-
RestWrite.prototype.buildUpdatedObject = function (extraData) {
1595+
RestWrite.prototype.buildParseObjects = function () {
1596+
const extraData = { className: this.className, objectId: this.query?.objectId };
1597+
let originalObject;
1598+
if (this.query && this.query.objectId) {
1599+
originalObject = triggers.inflate(extraData, this.originalData);
1600+
}
1601+
16081602
const className = Parse.Object.fromJSON(extraData);
16091603
const readOnlyAttributes = className.constructor.readOnlyAttributes
16101604
? className.constructor.readOnlyAttributes()
@@ -1642,7 +1636,7 @@ RestWrite.prototype.buildUpdatedObject = function (extraData) {
16421636
delete sanitized[attribute];
16431637
}
16441638
updatedObject.set(sanitized);
1645-
return updatedObject;
1639+
return { updatedObject, originalObject };
16461640
};
16471641

16481642
RestWrite.prototype.cleanUserAuthData = function () {
@@ -1662,6 +1656,15 @@ RestWrite.prototype.cleanUserAuthData = function () {
16621656
};
16631657

16641658
RestWrite.prototype._updateResponseWithData = function (response, data) {
1659+
const { updatedObject } = this.buildParseObjects();
1660+
const stateController = Parse.CoreManager.getObjectStateController();
1661+
const [pending] = stateController.getPendingOps(updatedObject._getStateIdentifier());
1662+
for (const key in this.pendingOps) {
1663+
if (!pending[key]) {
1664+
data[key] = this.originalData ? this.originalData[key] : { __op: 'Delete' };
1665+
this.storage.fieldsChangedByTrigger.push(key);
1666+
}
1667+
}
16651668
if (_.isEmpty(this.storage.fieldsChangedByTrigger)) {
16661669
return response;
16671670
}

0 commit comments

Comments
 (0)