From 04c3a32d63b374afaea91a4fddb674557669750d Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 16 Oct 2025 17:18:52 +0200 Subject: [PATCH 1/8] fix: after save on Parse.Role --- spec/ParseRole.spec.js | 30 ++++++++++++++++++++++++++++++ src/RestWrite.js | 6 ++++++ 2 files changed, 36 insertions(+) diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 35a91c6c15..cd98e2ca37 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -601,4 +601,34 @@ describe('Parse Role testing', () => { }); }); }); + + it('should trigger afterSave hook when using Parse.Role class reference', done => { + let afterSaveCalled = false; + + Parse.Cloud.afterSave(Parse.Role, req => { + afterSaveCalled = true; + expect(req.object).toBeDefined(); + expect(req.object.get('name')).toBe('AnotherTestRole'); + }); + + const acl = new Parse.ACL(); + acl.setPublicReadAccess(true); + const role = new Parse.Role('AnotherTestRole', acl); + + role + .save({}, { useMasterKey: true }) + .then(savedRole => { + expect(savedRole.id).toBeDefined(); + // Give the afterSave hook some time to execute + return new Promise(resolve => setTimeout(resolve, 100)); + }) + .then(() => { + expect(afterSaveCalled).toBe(true); + done(); + }) + .catch(err => { + fail(`Should not have failed: ${err.message}`); + done(); + }); + }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index 78dd8c8878..c13ab435f8 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1743,6 +1743,12 @@ RestWrite.prototype.buildParseObjects = function () { const readOnlyAttributes = className.constructor.readOnlyAttributes ? className.constructor.readOnlyAttributes() : []; + // For _Role class, the 'name' field is read-only after the object has been saved + // Since _handleSaveResponse is called after buildParseObjects and sets the objectId, + // we need to exclude 'name' from being set to avoid "A role's name can only be set before it has been saved" error + if (this.className === '_Role' && !readOnlyAttributes.includes('name')) { + readOnlyAttributes.push('name'); + } if (!this.originalData) { for (const attribute of readOnlyAttributes) { extraData[attribute] = this.data[attribute]; From 3bc5adf7b254bf35bc643f3258013a327e234d76 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 16 Oct 2025 17:54:04 +0200 Subject: [PATCH 2/8] fix: support before and after save --- spec/ParseRole.spec.js | 82 +++++++++++++++++++++++++++++++++++++++++- src/RestWrite.js | 10 +++--- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index cd98e2ca37..f825929f2d 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -602,7 +602,7 @@ describe('Parse Role testing', () => { }); }); - it('should trigger afterSave hook when using Parse.Role class reference', done => { + it('should trigger afterSave hook when using Parse.Role', done => { let afterSaveCalled = false; Parse.Cloud.afterSave(Parse.Role, req => { @@ -631,4 +631,84 @@ describe('Parse Role testing', () => { done(); }); }); + + it('should trigger beforeSave hook and allow modifying role in beforeSave', done => { + Parse.Cloud.beforeSave(Parse.Role, req => { + // Add a custom field in beforeSave + req.object.set('customField', 'addedInBeforeSave'); + }); + + const acl = new Parse.ACL(); + acl.setPublicReadAccess(true); + const role = new Parse.Role('ModifiedRole', acl); + + role + .save({}, { useMasterKey: true }) + .then(savedRole => { + expect(savedRole.id).toBeDefined(); + expect(savedRole.get('customField')).toBe('addedInBeforeSave'); + done(); + }) + .catch(err => { + fail(`Should not have failed: ${err.message}`); + done(); + }); + }); + + it('should trigger beforeSave hook using Parse.Role', done => { + let beforeSaveCalled = false; + + Parse.Cloud.beforeSave(Parse.Role, req => { + beforeSaveCalled = true; + expect(req.object).toBeDefined(); + expect(req.object.get('name')).toBe('BeforeSaveWithClassRef'); + }); + + const acl = new Parse.ACL(); + acl.setPublicReadAccess(true); + const role = new Parse.Role('BeforeSaveWithClassRef', acl); + + role + .save({}, { useMasterKey: true }) + .then(savedRole => { + expect(savedRole.id).toBeDefined(); + expect(beforeSaveCalled).toBe(true); + done(); + }) + .catch(err => { + fail(`Should not have failed: ${err.message}`); + done(); + }); + }); + + it('should allow modifying role name in beforeSave hook', done => { + Parse.Cloud.beforeSave(Parse.Role, req => { + // Modify the role name in beforeSave + if (req.object.get('name') === 'OriginalName') { + req.object.set('name', 'ModifiedName'); + } + }); + + const acl = new Parse.ACL(); + acl.setPublicReadAccess(true); + const role = new Parse.Role('OriginalName', acl); + + role + .save({}, { useMasterKey: true }) + .then(savedRole => { + expect(savedRole.id).toBeDefined(); + expect(savedRole.get('name')).toBe('ModifiedName'); + // Verify the name was actually saved to the database + const query = new Parse.Query(Parse.Role); + return query.get(savedRole.id, { useMasterKey: true }); + }) + .then(fetchedRole => { + expect(fetchedRole.get('name')).toBe('ModifiedName'); + done(); + }) + .catch(err => { + fail(`Should not have failed: ${err.message}`); + done(); + }); + }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index c13ab435f8..41b6c23468 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1743,10 +1743,12 @@ RestWrite.prototype.buildParseObjects = function () { const readOnlyAttributes = className.constructor.readOnlyAttributes ? className.constructor.readOnlyAttributes() : []; - // For _Role class, the 'name' field is read-only after the object has been saved - // Since _handleSaveResponse is called after buildParseObjects and sets the objectId, - // we need to exclude 'name' from being set to avoid "A role's name can only be set before it has been saved" error - if (this.className === '_Role' && !readOnlyAttributes.includes('name')) { + + // For _Role class, 'name' cannot be set after the role has an objectId. + // In afterSave context, _handleSaveResponse has already set the objectId, + // so we treat 'name' as read-only to avoid Parse SDK validation errors. + const isRoleAfterSave = this.className === '_Role' && this.response && !this.query; + if (isRoleAfterSave && this.data.name && !readOnlyAttributes.includes('name')) { readOnlyAttributes.push('name'); } if (!this.originalData) { From 1251918ed17d9e00caba8b0ac4e2eb4a856e5b26 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 16 Oct 2025 19:33:27 +0200 Subject: [PATCH 3/8] test: modernize --- spec/ParseRole.spec.js | 92 +++++++++++++----------------------------- 1 file changed, 28 insertions(+), 64 deletions(-) diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index f825929f2d..95e6189a6a 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -602,37 +602,26 @@ describe('Parse Role testing', () => { }); }); - it('should trigger afterSave hook when using Parse.Role', done => { - let afterSaveCalled = false; - - Parse.Cloud.afterSave(Parse.Role, req => { - afterSaveCalled = true; - expect(req.object).toBeDefined(); - expect(req.object.get('name')).toBe('AnotherTestRole'); + it('should trigger afterSave hook when using Parse.Role', async () => { + const afterSavePromise = new Promise(resolve => { + Parse.Cloud.afterSave(Parse.Role, req => { + expect(req.object).toBeDefined(); + expect(req.object.get('name')).toBe('AnotherTestRole'); + resolve(); + }); }); const acl = new Parse.ACL(); acl.setPublicReadAccess(true); const role = new Parse.Role('AnotherTestRole', acl); - role - .save({}, { useMasterKey: true }) - .then(savedRole => { - expect(savedRole.id).toBeDefined(); - // Give the afterSave hook some time to execute - return new Promise(resolve => setTimeout(resolve, 100)); - }) - .then(() => { - expect(afterSaveCalled).toBe(true); - done(); - }) - .catch(err => { - fail(`Should not have failed: ${err.message}`); - done(); - }); + const savedRole = await role.save({}, { useMasterKey: true }); + expect(savedRole.id).toBeDefined(); + + await afterSavePromise; }); - it('should trigger beforeSave hook and allow modifying role in beforeSave', done => { + it('should trigger beforeSave hook and allow modifying role in beforeSave', async () => { Parse.Cloud.beforeSave(Parse.Role, req => { // Add a custom field in beforeSave req.object.set('customField', 'addedInBeforeSave'); @@ -642,20 +631,12 @@ describe('Parse Role testing', () => { acl.setPublicReadAccess(true); const role = new Parse.Role('ModifiedRole', acl); - role - .save({}, { useMasterKey: true }) - .then(savedRole => { - expect(savedRole.id).toBeDefined(); - expect(savedRole.get('customField')).toBe('addedInBeforeSave'); - done(); - }) - .catch(err => { - fail(`Should not have failed: ${err.message}`); - done(); - }); + const savedRole = await role.save({}, { useMasterKey: true }); + expect(savedRole.id).toBeDefined(); + expect(savedRole.get('customField')).toBe('addedInBeforeSave'); }); - it('should trigger beforeSave hook using Parse.Role', done => { + it('should trigger beforeSave hook using Parse.Role', async () => { let beforeSaveCalled = false; Parse.Cloud.beforeSave(Parse.Role, req => { @@ -668,20 +649,12 @@ describe('Parse Role testing', () => { acl.setPublicReadAccess(true); const role = new Parse.Role('BeforeSaveWithClassRef', acl); - role - .save({}, { useMasterKey: true }) - .then(savedRole => { - expect(savedRole.id).toBeDefined(); - expect(beforeSaveCalled).toBe(true); - done(); - }) - .catch(err => { - fail(`Should not have failed: ${err.message}`); - done(); - }); + const savedRole = await role.save({}, { useMasterKey: true }); + expect(savedRole.id).toBeDefined(); + expect(beforeSaveCalled).toBe(true); }); - it('should allow modifying role name in beforeSave hook', done => { + it('should allow modifying role name in beforeSave hook', async () => { Parse.Cloud.beforeSave(Parse.Role, req => { // Modify the role name in beforeSave if (req.object.get('name') === 'OriginalName') { @@ -693,22 +666,13 @@ describe('Parse Role testing', () => { acl.setPublicReadAccess(true); const role = new Parse.Role('OriginalName', acl); - role - .save({}, { useMasterKey: true }) - .then(savedRole => { - expect(savedRole.id).toBeDefined(); - expect(savedRole.get('name')).toBe('ModifiedName'); - // Verify the name was actually saved to the database - const query = new Parse.Query(Parse.Role); - return query.get(savedRole.id, { useMasterKey: true }); - }) - .then(fetchedRole => { - expect(fetchedRole.get('name')).toBe('ModifiedName'); - done(); - }) - .catch(err => { - fail(`Should not have failed: ${err.message}`); - done(); - }); + const savedRole = await role.save({}, { useMasterKey: true }); + expect(savedRole.id).toBeDefined(); + expect(savedRole.get('name')).toBe('ModifiedName'); + + // Verify the name was actually saved to the database + const query = new Parse.Query(Parse.Role); + const fetchedRole = await query.get(savedRole.id, { useMasterKey: true }); + expect(fetchedRole.get('name')).toBe('ModifiedName'); }); }); From 54b9fefa2ed7379e33cea15c8087ea86ed7fd935 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 16 Oct 2025 20:37:01 +0200 Subject: [PATCH 4/8] test: try to fix flaky cloud error --- spec/CloudCodeLogger.spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/CloudCodeLogger.spec.js b/spec/CloudCodeLogger.spec.js index a405b6fc48..73277234e0 100644 --- a/spec/CloudCodeLogger.spec.js +++ b/spec/CloudCodeLogger.spec.js @@ -247,6 +247,8 @@ describe('Cloud Code Logger', () => { spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough(); const obj = new Parse.Object('TestClass'); await obj.save(); + // Wait for afterSave to complete + await new Promise(resolve => setTimeout(resolve, 100)); return { beforeSave: spy.calls @@ -391,6 +393,8 @@ describe('Cloud Code Logger', () => { const obj = new Parse.Object('TestClass'); await obj.save(); + // Wait for afterSave to complete + await new Promise(resolve => setTimeout(resolve, 100)); expect(spy).toHaveBeenCalledTimes(0); const objError = new Parse.Object('TestClassError'); From b908f95e7c135e4d13c255c8cc1b878d831b0603 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 16 Oct 2025 20:56:52 +0200 Subject: [PATCH 5/8] test: try to stab --- spec/CloudCodeLogger.spec.js | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/spec/CloudCodeLogger.spec.js b/spec/CloudCodeLogger.spec.js index 73277234e0..5cdaeca4dc 100644 --- a/spec/CloudCodeLogger.spec.js +++ b/spec/CloudCodeLogger.spec.js @@ -25,7 +25,7 @@ describe('Cloud Code Logger', () => { }) .then(() => { return Parse.User.signUp('tester', 'abc') - .catch(() => {}) + .catch(() => { }) .then(loggedInUser => (user = loggedInUser)) .then(() => Parse.User.logIn(user.get('username'), 'abc')); }) @@ -139,7 +139,7 @@ describe('Cloud Code Logger', () => { }); it_id('9857e15d-bb18-478d-8a67-fdaad3e89565')(it)('should log an afterSave', done => { - Parse.Cloud.afterSave('MyObject', () => {}); + Parse.Cloud.afterSave('MyObject', () => { }); new Parse.Object('MyObject') .save() .then(() => { @@ -383,22 +383,39 @@ describe('Cloud Code Logger', () => { triggerBeforeError: 'silent', }, }); + + let afterSaveCompleted; + const afterSavePromise = new Promise(resolve => { + afterSaveCompleted = resolve; + }); + Parse.Cloud.beforeSave('TestClassError', () => { throw new Error('Failed'); }); - Parse.Cloud.beforeSave('TestClass', () => {}); - Parse.Cloud.afterSave('TestClass', () => {}); + Parse.Cloud.beforeSave('TestClass', () => { }); + Parse.Cloud.afterSave('TestClass', async () => { + try { + // Ensure afterSave completes without errors + afterSaveCompleted(); + } catch (e) { + // Prevent unhandled rejections + afterSaveCompleted(); + } + }); spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough(); const obj = new Parse.Object('TestClass'); await obj.save(); - // Wait for afterSave to complete + // Wait for afterSave to actually complete + await afterSavePromise; await new Promise(resolve => setTimeout(resolve, 100)); expect(spy).toHaveBeenCalledTimes(0); const objError = new Parse.Object('TestClassError'); await expectAsync(objError.save()).toBeRejected(); + // Wait for any async error handling to complete + await new Promise(resolve => setTimeout(resolve, 100)); // Not "beforeSave failed for TestClassError for user ..." expect(spy).toHaveBeenCalledTimes(1); }); From 0ad0cac008e72a7b68225eaaf74f01fe62c39f8c Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Tue, 21 Oct 2025 09:23:50 +0200 Subject: [PATCH 6/8] test: cloud logger try catch remove --- spec/CloudCodeLogger.spec.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/spec/CloudCodeLogger.spec.js b/spec/CloudCodeLogger.spec.js index 5cdaeca4dc..f9e5a2b2e9 100644 --- a/spec/CloudCodeLogger.spec.js +++ b/spec/CloudCodeLogger.spec.js @@ -394,13 +394,7 @@ describe('Cloud Code Logger', () => { }); Parse.Cloud.beforeSave('TestClass', () => { }); Parse.Cloud.afterSave('TestClass', async () => { - try { - // Ensure afterSave completes without errors - afterSaveCompleted(); - } catch (e) { - // Prevent unhandled rejections - afterSaveCompleted(); - } + afterSaveCompleted(); }); spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough(); From b6415542fc9d8d04fa8e1f946908949f62bd5fd6 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Tue, 21 Oct 2025 10:23:38 +0200 Subject: [PATCH 7/8] test: revert cloud logger flaky fix attempt --- spec/CloudCodeLogger.spec.js | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/spec/CloudCodeLogger.spec.js b/spec/CloudCodeLogger.spec.js index f9e5a2b2e9..9faeeba9d5 100644 --- a/spec/CloudCodeLogger.spec.js +++ b/spec/CloudCodeLogger.spec.js @@ -247,8 +247,6 @@ describe('Cloud Code Logger', () => { spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough(); const obj = new Parse.Object('TestClass'); await obj.save(); - // Wait for afterSave to complete - await new Promise(resolve => setTimeout(resolve, 100)); return { beforeSave: spy.calls @@ -273,7 +271,7 @@ describe('Cloud Code Logger', () => { }); Parse.Cloud.run('aFunction', { foo: 'bar' }) - .catch(() => {}) + .catch(() => { }) .then(() => { const logs = spy.calls.all().reverse(); expect(logs[0].args[1]).toBe('Parse error: '); @@ -383,34 +381,21 @@ describe('Cloud Code Logger', () => { triggerBeforeError: 'silent', }, }); - - let afterSaveCompleted; - const afterSavePromise = new Promise(resolve => { - afterSaveCompleted = resolve; - }); - Parse.Cloud.beforeSave('TestClassError', () => { throw new Error('Failed'); }); Parse.Cloud.beforeSave('TestClass', () => { }); - Parse.Cloud.afterSave('TestClass', async () => { - afterSaveCompleted(); - }); + Parse.Cloud.afterSave('TestClass', () => { }); spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough(); const obj = new Parse.Object('TestClass'); await obj.save(); - // Wait for afterSave to actually complete - await afterSavePromise; - await new Promise(resolve => setTimeout(resolve, 100)); expect(spy).toHaveBeenCalledTimes(0); const objError = new Parse.Object('TestClassError'); await expectAsync(objError.save()).toBeRejected(); - // Wait for any async error handling to complete - await new Promise(resolve => setTimeout(resolve, 100)); // Not "beforeSave failed for TestClassError for user ..." expect(spy).toHaveBeenCalledTimes(1); }); -}); +}); \ No newline at end of file From 516ac38166552586661abf857f1459436a6a1925 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Tue, 21 Oct 2025 10:25:55 +0200 Subject: [PATCH 8/8] fix: lint --- spec/CloudCodeLogger.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/CloudCodeLogger.spec.js b/spec/CloudCodeLogger.spec.js index 9faeeba9d5..bcbc4f91cf 100644 --- a/spec/CloudCodeLogger.spec.js +++ b/spec/CloudCodeLogger.spec.js @@ -398,4 +398,4 @@ describe('Cloud Code Logger', () => { // Not "beforeSave failed for TestClassError for user ..." expect(spy).toHaveBeenCalledTimes(1); }); -}); \ No newline at end of file +});