Skip to content

Conversation

Moumouls
Copy link
Member

@Moumouls Moumouls commented Oct 16, 2025

Pull Request

Issue

After save on Parse.Role don't work anymore, Parse SDK throw "A role's name can only be set before it has been saved." which is incorrect. It's related to how buildParseObject system work after the "afterSave" trigger.

Closes: FILL_THIS_OUT

Approach

Add "name" to readOnly attribute to avoid the error during buildObject

Tasks

  • Add tests
  • Add changes to documentation (guides, repository pages, code comments)
  • Add security check
  • Add new Parse Error codes to Parse JS SDK

Summary by CodeRabbit

  • Bug Fixes

    • Role names are now enforced as read-only after their initial save to prevent later updates and related errors.
  • Tests

    • Added comprehensive tests for role lifecycle hooks (beforeSave and afterSave), including cases that modify role data within hooks, verify name changes are persisted when intended, and validate hook invocation across role references.
    • Improved test reliability around hook execution and persistence checks.

@parse-github-assistant
Copy link

I will reformat the title to use the proper commit message syntax.

@parse-github-assistant parse-github-assistant bot changed the title fix: after save on Parse.Role fix: After save on Parse.Role Oct 16, 2025
@parse-github-assistant
Copy link

parse-github-assistant bot commented Oct 16, 2025

🚀 Thanks for opening this pull request!

❌ Please fill out all fields with a placeholder FILL_THIS_OUT. If a field does not apply to the pull request, fill in n/a or delete the line.

@parseplatformorg
Copy link
Contributor

parseplatformorg commented Oct 16, 2025

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@coderabbitai
Copy link

coderabbitai bot commented Oct 16, 2025

📝 Walkthrough

Walkthrough

Adds tests covering Parse.Role lifecycle hooks (beforeSave/afterSave) and a RestWrite guard that marks _Role.name as read-only after a save response with an objectId; also applies minor formatting/synchronization tweaks to CloudCodeLogger tests.

Changes

Cohort / File(s) Summary
Role hook tests
spec/ParseRole.spec.js
Adds tests for Parse.Role lifecycle hooks: afterSave invocation, beforeSave that adds a custom field, beforeSave invoked with Parse.Role class reference, and beforeSave that modifies the role name and verifies persistence using masterKey.
_Role name read-only guard
src/RestWrite.js
Adds a guard: when a save response for class _Role contains an objectId, append "name" to readOnlyAttributes if missing so subsequent updates exclude name (treat it as read-only after initial save).
Test formatting / sync
spec/CloudCodeLogger.spec.js
Minor formatting adjustments and promise/catch spacing changes; test synchronization tweaks to ensure afterSave hooks complete before assertions.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant RestWrite
    participant Hooks
    participant Database

    Client->>RestWrite: Save Role { "name": "Admins" } (masterKey)
    RestWrite->>Hooks: beforeSave(Parse.Role)
    Hooks-->>RestWrite: (may modify object: add fields or change name)
    RestWrite->>Database: Persist role (create/update)
    Database-->>RestWrite: Respond with { objectId, ... }
    Note right of RestWrite #DDEBF7: If class == _Role and objectId present\nappend "name" to readOnlyAttributes
    RestWrite->>Hooks: afterSave(Parse.Role)
    Hooks-->>RestWrite: Complete

    Client->>RestWrite: Later update attempting to change name
    RestWrite-->>Client: Excludes `name` from update (treated read-only)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • mtrezza

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description follows the required template structure with Issue, Approach, and Tasks sections present. The Approach section adequately describes the fix (adding "name" to readOnly attributes), and the Tasks section includes all required checkboxes. However, the Issue section contains a critical incomplete element: the "Closes:" field still shows the template placeholder "FILL_THIS_OUT" rather than an actual issue link, which is a required part of the template. While the author provided problem context in the Issue section, the explicit requirement to link to an issue remains unfulfilled.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Title Check ✅ Passed The pull request title "fix: Error in afterSave trigger for Parse.Role due to name field" is directly related to the main change in the changeset. The primary modification is in src/RestWrite.js, which adds a guard to treat the "name" attribute as read-only after a Parse.Role object has been saved, thereby preventing the error that occurs when role names are modified post-save. The supporting test additions in spec/ParseRole.spec.js validate this fix for lifecycle hooks, and the minor formatting adjustments in spec/CloudCodeLogger.spec.js are unrelated changes the author indicated they would remove. The title clearly and specifically identifies the core problem being solved without vague language or unnecessary details.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/RestWrite.js (1)

1746-1751: Clarify the comment to better explain the root cause.

The comment mentions that _handleSaveResponse sets the objectId, but the deeper issue is that Parse SDK's internal validation prevents setting the 'name' field on a Parse.Role object that has an objectId. The current approach correctly adds 'name' to readOnlyAttributes to prevent it from being set during object construction in buildParseObjects, while still making it available for reading (via extraData for creates on lines 1752-1756, or via originalData for updates).

Consider revising the comment to:

-  // 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
+  // For _Role class, the 'name' field is read-only after the object has an objectId.
+  // Parse SDK throws "A role's name can only be set before it has been saved" when
+  // attempting to set 'name' on a role with an objectId. Adding it to readOnlyAttributes
+  // prevents the field from being set during object construction while keeping it readable.
spec/ParseRole.spec.js (1)

605-633: Refactor to use async/await instead of promise chains with done().

Based on learnings, tests should use async/await patterns rather than .then() chains with done() callbacks. Additionally, the hardcoded 100ms timeout is fragile and could lead to flaky tests.

Based on learnings.

Apply this diff to modernize the test:

-  it('should trigger afterSave hook when using Parse.Role class reference', done => {
+  it('should trigger afterSave hook when using Parse.Role class reference', async () => {
     let afterSaveCalled = false;
+    const hookPromise = new Promise(resolve => {
+      Parse.Cloud.afterSave(Parse.Role, req => {
+        afterSaveCalled = true;
+        expect(req.object).toBeDefined();
+        expect(req.object.get('name')).toBe('AnotherTestRole');
+        resolve();
+      });
+    });
 
-    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();
-      });
+    const savedRole = await role.save({}, { useMasterKey: true });
+    expect(savedRole.id).toBeDefined();
+    
+    // Wait for the afterSave hook to complete
+    await hookPromise;
+    expect(afterSaveCalled).toBe(true);
   });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 115e76e and 04c3a32.

📒 Files selected for processing (2)
  • spec/ParseRole.spec.js (1 hunks)
  • src/RestWrite.js (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-09T09:59:06.289Z
Learnt from: mtrezza
PR: parse-community/parse-server#9445
File: spec/ParseLiveQuery.spec.js:1340-1375
Timestamp: 2025-05-09T09:59:06.289Z
Learning: Tests in the parse-server repository should use promise-based approaches rather than callback patterns with `done()`. Use a pattern where a Promise is created that resolves when the event occurs, then await that promise.

Applied to files:

  • spec/ParseRole.spec.js
📚 Learning: 2025-05-09T09:59:06.289Z
Learnt from: mtrezza
PR: parse-community/parse-server#9445
File: spec/ParseLiveQuery.spec.js:1340-1375
Timestamp: 2025-05-09T09:59:06.289Z
Learning: New tests in the parse-server repository should use async/await with promise-based patterns rather than callback patterns with `done()`. The preferred pattern is to create a Promise that resolves when an expected event occurs, then await that Promise.

Applied to files:

  • spec/ParseRole.spec.js
🧬 Code graph analysis (1)
spec/ParseRole.spec.js (3)
spec/helper.js (1)
  • Parse (4-4)
src/Auth.js (4)
  • Parse (1-1)
  • req (538-538)
  • req (551-551)
  • role (353-353)
spec/ParseLiveQuery.spec.js (1)
  • role (811-811)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Node 18
  • GitHub Check: MongoDB 7, ReplicaSet
  • GitHub Check: Node 20
  • GitHub Check: MongoDB 6, ReplicaSet
  • GitHub Check: Docker Build
  • GitHub Check: MongoDB 8, ReplicaSet
  • GitHub Check: Redis Cache
  • GitHub Check: PostgreSQL 15, PostGIS 3.4
  • GitHub Check: PostgreSQL 15, PostGIS 3.3
  • GitHub Check: PostgreSQL 16, PostGIS 3.5
  • GitHub Check: PostgreSQL 18, PostGIS 3.6
  • GitHub Check: PostgreSQL 15, PostGIS 3.5
  • GitHub Check: Code Analysis (javascript)
  • GitHub Check: PostgreSQL 17, PostGIS 3.5
🔇 Additional comments (1)
src/RestWrite.js (1)

1746-1751: LGTM! The fix correctly addresses the Parse.Role afterSave issue.

The implementation properly prevents the 'name' field from being set during object construction in afterSave triggers, while ensuring it remains readable through extraData (for creates) or originalData (for updates).

coderabbitai[bot]
coderabbitai bot previously approved these changes Oct 16, 2025
@codecov
Copy link

codecov bot commented Oct 16, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.03%. Comparing base (178bd5c) to head (516ac38).
⚠️ Report is 3 commits behind head on alpha.

Additional details and impacted files
@@           Coverage Diff           @@
##            alpha    #9883   +/-   ##
=======================================
  Coverage   93.02%   93.03%           
=======================================
  Files         187      187           
  Lines       15158    15161    +3     
  Branches      176      176           
=======================================
+ Hits        14101    14105    +4     
+ Misses       1045     1044    -1     
  Partials       12       12           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
spec/ParseRole.spec.js (2)

608-612: Consider verifying role persistence after afterSave completes.

To ensure the afterSave hook doesn't corrupt the role object (which was the original issue described in the PR), consider adding a query to fetch the role from the database after the hook completes.

Add this verification after the hook executes:

    Parse.Cloud.afterSave(Parse.Role, req => {
      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));
+       return afterSavePromise.then(() => savedRole.id);
      })
-     .then(() => {
+     .then(roleId => {
+       // Verify the role can still be fetched after afterSave
+       const query = new Parse.Query(Parse.Role);
+       return query.get(roleId, { useMasterKey: true });
+     })
+     .then(fetchedRole => {
+       expect(fetchedRole.get('name')).toBe('AnotherTestRole');
        done();
      })

684-713: Clarify that name modification in beforeSave is expected behavior.

This test modifies the role name in the beforeSave hook, which is valid since the role hasn't been saved yet. This is different from the afterSave scenario described in the PR where the role already has an objectId and name modification would be restricted.

Consider adding a comment to clarify this distinction.

  it('should allow modifying role name in beforeSave hook', done => {
+   // Note: Modifying the name in beforeSave is allowed because the role hasn't been saved yet.
+   // This is different from afterSave where the role already has an objectId.
    Parse.Cloud.beforeSave(Parse.Role, req => {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04c3a32 and 3bc5adf.

📒 Files selected for processing (2)
  • spec/ParseRole.spec.js (1 hunks)
  • src/RestWrite.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/RestWrite.js
🧰 Additional context used
🧬 Code graph analysis (1)
spec/ParseRole.spec.js (1)
spec/helper.js (1)
  • Parse (4-4)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: MongoDB 7, ReplicaSet
  • GitHub Check: PostgreSQL 18, PostGIS 3.6
  • GitHub Check: MongoDB 8, ReplicaSet
  • GitHub Check: Node 20
  • GitHub Check: Node 18
  • GitHub Check: PostgreSQL 16, PostGIS 3.5
  • GitHub Check: MongoDB 6, ReplicaSet
  • GitHub Check: Redis Cache
  • GitHub Check: PostgreSQL 15, PostGIS 3.5
  • GitHub Check: PostgreSQL 17, PostGIS 3.5
  • GitHub Check: PostgreSQL 15, PostGIS 3.4
  • GitHub Check: PostgreSQL 15, PostGIS 3.3
  • GitHub Check: Docker Build
🔇 Additional comments (1)
spec/ParseRole.spec.js (1)

605-713: Add cleanup for registered Cloud Function hooks.

The tests register Parse.Cloud.beforeSave and Parse.Cloud.afterSave hooks but never clean them up. Hooks are stored in _triggerStore[applicationId] and persist across tests, potentially causing test pollution.

Add an afterEach block to unregister hooks:

afterEach(() => {
  Parse.Cloud._triggers = undefined;
});

Or use the existing remove() function from triggers module if exposed, or check if Parse.Cloud provides an unregister method for cleanup.

coderabbitai[bot]
coderabbitai bot previously approved these changes Oct 16, 2025
coderabbitai[bot]
coderabbitai bot previously approved these changes Oct 16, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
spec/CloudCodeLogger.spec.js (2)

250-251: Recommend consistent promise-based synchronization.

The arbitrary 100ms delay may be insufficient if the afterSave hook takes longer, or wasteful if it completes faster. Consider adopting the promise-based pattern introduced later in this file (lines 386-404) for more precise synchronization.

Apply this pattern for consistency:

+    let afterSaveCompleted;
+    const afterSavePromise = new Promise(resolve => {
+      afterSaveCompleted = resolve;
+    });
+
-    Parse.Cloud.beforeSave('TestClass', () => { });
-    Parse.Cloud.afterSave('TestClass', () => { });
+    Parse.Cloud.beforeSave('TestClass', () => {});
+    Parse.Cloud.afterSave('TestClass', async () => {
+      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
-    await new Promise(resolve => setTimeout(resolve, 100));
+    await afterSavePromise;

417-418: Drop the 100 ms delay; error logging is synchronous in the beforeSave trigger, so you can assert spy immediately after await expectAsync(...).toBeRejected().

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54b9fef and b908f95.

📒 Files selected for processing (1)
  • spec/CloudCodeLogger.spec.js (4 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-05-09T09:59:06.289Z
Learnt from: mtrezza
PR: parse-community/parse-server#9445
File: spec/ParseLiveQuery.spec.js:1340-1375
Timestamp: 2025-05-09T09:59:06.289Z
Learning: New tests in the parse-server repository should use async/await with promise-based patterns rather than callback patterns with `done()`. The preferred pattern is to create a Promise that resolves when an expected event occurs, then await that Promise.

Applied to files:

  • spec/CloudCodeLogger.spec.js
📚 Learning: 2025-05-09T09:59:06.289Z
Learnt from: mtrezza
PR: parse-community/parse-server#9445
File: spec/ParseLiveQuery.spec.js:1340-1375
Timestamp: 2025-05-09T09:59:06.289Z
Learning: Tests in the parse-server repository should use promise-based approaches rather than callback patterns with `done()`. Use a pattern where a Promise is created that resolves when the event occurs, then await that promise.

Applied to files:

  • spec/CloudCodeLogger.spec.js
📚 Learning: 2025-05-04T20:41:05.147Z
Learnt from: mtrezza
PR: parse-community/parse-server#9445
File: spec/ParseLiveQuery.spec.js:1312-1338
Timestamp: 2025-05-04T20:41:05.147Z
Learning: New tests in the parse-server repository should use async/await with promise-based patterns rather than callback patterns with `done()`.

Applied to files:

  • spec/CloudCodeLogger.spec.js
🧬 Code graph analysis (1)
spec/CloudCodeLogger.spec.js (1)
src/Config.js (1)
  • Config (35-768)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: PostgreSQL 18, PostGIS 3.6
  • GitHub Check: PostgreSQL 15, PostGIS 3.4
  • GitHub Check: PostgreSQL 17, PostGIS 3.5
  • GitHub Check: Node 18
  • GitHub Check: PostgreSQL 15, PostGIS 3.3
  • GitHub Check: PostgreSQL 16, PostGIS 3.5
  • GitHub Check: Redis Cache
  • GitHub Check: Node 20
  • GitHub Check: PostgreSQL 15, PostGIS 3.5
  • GitHub Check: MongoDB 8, ReplicaSet
  • GitHub Check: MongoDB 6, ReplicaSet
  • GitHub Check: MongoDB 7, ReplicaSet
  • GitHub Check: Docker Build

coderabbitai[bot]
coderabbitai bot previously approved these changes Oct 16, 2025
const obj = new Parse.Object('TestClass');
await obj.save();
// Wait for afterSave to complete
await new Promise(resolve => setTimeout(resolve, 100));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't await obj.save(); finish only after afterSave completes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should but it seems that cloud logger is performing background actions and the timeout helps

@mtrezza
Copy link
Member

mtrezza commented Oct 20, 2025

I'm a bit confused by the scope of changes. Some seem to be just for test flakyness, while others seem to be for the actual bug of Parse.Role. I would't recommend to pack this all into a single PR, as we want to observe the effects of the role fix separately, especially if they have nothing to do with each other (which I'm just guessing).

@Moumouls
Copy link
Member Author

I know @mtrezza , sorry about that! I was just trying to fix the flaky test because it’s really annoying to get notifications and a failing CI status while working on a feature. It makes it hard to tell whether the failure is from the new feature or something else. I tried a small patch on the fly.

I suggest keeping it, since it should improve the contributor experience (DX). If the Cloud Logger keeps failing in future contributions, it might need an additional refactor and fully dedicated PR.

Just trying to help stabilize things :)

By the way, the Parse.Role bug is now fully covered and fixed.

coderabbitai[bot]
coderabbitai bot previously approved these changes Oct 21, 2025
@Moumouls
Copy link
Member Author

i'll remove it, it doesn't properly fix the cloud logger flaky

Copy link
Member

@mtrezza mtrezza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. @coderabbitai could this be a breaking change? and suggest an angular commit message.

@mtrezza
Copy link
Member

mtrezza commented Oct 22, 2025

@coderabbitai suggest an angular commit message; look at the code and suggest what exactly this fixes.

@coderabbitai
Copy link

coderabbitai bot commented Oct 22, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the diff for the PR to see exact changes
git fetch origin pull/9883/head:pr-9883
git diff origin/alpha...pr-9883 -- src/RestWrite.js spec/ParseRole.spec.js

Length of output: 3887


@mtrezza Based on the code changes, here's my suggested Angular commit message:

fix(role): prevent name validation error in afterSave trigger

Detailed explanation:

The fix addresses a bug where Parse.Role's name field was being included in the response during afterSave triggers, causing the Parse SDK to throw the error: "A role's name can only be set before it has been saved."

What the code does:

In src/RestWrite.js, the fix adds logic to treat the name attribute as read-only for the _Role class when:

  • The response has already been set (after _handleSaveResponse assigned the objectId)
  • It's not a query operation
  • The role has a name field

This prevents name from being included in the buildParseObjects response after the role has been saved, avoiding the SDK validation error when the afterSave hook runs.

Alternative commit message options:

If you prefer more context:

fix(role): prevent SDK error when name field is returned in afterSave

Or more technical:

fix(role): treat name as read-only after save to prevent SDK validation error

@mtrezza
Copy link
Member

mtrezza commented Oct 22, 2025

@coderabbitai Is this a breaking change, if the response is altered? For example for someone using the Parse Server REST API directly without using the Parse JS SDK.

@coderabbitai
Copy link

coderabbitai bot commented Oct 22, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the exact changes in RestWrite.js to understand the scope
git diff origin/alpha...HEAD -- src/RestWrite.js

# Also look at the context around where readOnlyAttributes is used
rg -n -C10 "readOnlyAttributes" src/RestWrite.js

Length of output: 3512


🏁 Script executed:

#!/bin/bash
# Look for how buildParseObjects is called and where responses are generated
rg -n -C5 "buildParseObjects" src/RestWrite.js

# Check if readOnlyAttributes affects the primary REST API response or just afterSave
ast-grep --pattern 'buildParseObjects($$$)'

Length of output: 2371


🏁 Script executed:

#!/bin/bash
# Look at the _handleSaveResponse method to understand the response flow
rg -n -C15 "_handleSaveResponse" src/RestWrite.js | head -100

Length of output: 2657


🏁 Script executed:

#!/bin/bash
# Find where this.response is set in RestWrite.js to understand the flow
rg -n "this\.response\s*=" src/RestWrite.js | head -20

# Also look at the execute method to understand the full flow
rg -n -C10 "RestWrite.prototype.execute" src/RestWrite.js | head -80

Length of output: 767


🏁 Script executed:

#!/bin/bash
# Check the flow in runAfterSaveTrigger to see when buildParseObjects is called relative to response being set
rg -n -C25 "runAfterSaveTrigger" src/RestWrite.js | head -100

Length of output: 3487


🏁 Script executed:

#!/bin/bash
# Look at how the response is returned to the client to see if buildParseObjects affects it
rg -n -C10 "response\.response" src/RestWrite.js | head -100

Length of output: 3852


🏁 Script executed:

#!/bin/bash
# Quick check - does anything modify this.response.response AFTER runAfterSaveTrigger?
rg -n -A5 "runAfterSaveTrigger" src/RestWrite.js | grep -A10 "return this.runAfterSaveTrigger"

# Also verify that buildParseObjects returns objects for triggers, not for response
rg -n "updatedObject\." src/RestWrite.js | head -30

Length of output: 813


#!/bin/bash
# Check if afterSave triggers can modify the response that's sent to the client
rg -n -C20 "triggers\.maybeRunTrigger" src/RestWrite.js | grep -A20 -B20 "afterSave"

# Also look at the triggers module to see if afterSave can modify responses
rg -n "afterSave" src/triggers.js | head -20

@mtrezza
Copy link
Member

mtrezza commented Oct 22, 2025

@coderabbitai Is this PR a breaking change, since it seems to change the response? For example for someone using the Parse Server REST API directly without using the Parse JS SDK.

@coderabbitai
Copy link

coderabbitai bot commented Oct 22, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the exact flow: when is the client response set vs when buildParseObjects is called
echo "=== Checking response.response assignment in RestWrite.js ==="
rg -n "this\.response\.response\s*=" src/RestWrite.js

echo -e "\n=== Checking if buildParseObjects result affects client response ==="
# Look for any code that modifies this.response.response after runAfterSaveTrigger
rg -n -C5 "runAfterSaveTrigger" src/RestWrite.js | grep -A10 "return this.runAfterSaveTrigger"

echo -e "\n=== Check the execute method flow ==="
ast-grep --pattern 'execute() {
  $$$
}'

echo -e "\n=== Verify buildParseObjects is only used for triggers ==="
rg -n "buildParseObjects" src/RestWrite.js

Length of output: 903


🏁 Script executed:

#!/bin/bash
# Check if readOnlyAttributes affects anything beyond buildParseObjects
echo "=== All uses of readOnlyAttributes in RestWrite.js ==="
rg -n "readOnlyAttributes" src/RestWrite.js

echo -e "\n=== Check buildParseObjects implementation ==="
ast-grep --pattern 'buildParseObjects($$$) {
  $$$
}'

Length of output: 523


🏁 Script executed:

#!/bin/bash
# Look at the actual test to see what the expected behavior is
echo "=== Check the new tests added in ParseRole.spec.js ==="
rg -n -C10 "afterSave" spec/ParseRole.spec.js | head -60

Length of output: 1330


🏁 Script executed:

#!/bin/bash
# Check the context of each buildParseObjects call to understand when they're executed
echo "=== buildParseObjects at line 238 (context) ==="
rg -n -C15 "^" src/RestWrite.js | grep -A15 -B5 "238:"

echo -e "\n=== buildParseObjects at line 817 (context) ==="
rg -n -C15 "^" src/RestWrite.js | grep -A15 -B5 "817:"

echo -e "\n=== buildParseObjects at line 952 (context) ==="
rg -n -C15 "^" src/RestWrite.js | grep -A15 -B5 "952:"

Length of output: 4542


🏁 Script executed:

#!/bin/bash
# Check the isRoleAfterSave condition more closely
echo "=== Check the isRoleAfterSave condition and where it's used ==="
rg -n -C10 "isRoleAfterSave" src/RestWrite.js

echo -e "\n=== Verify when this.response is set (before what operations) ==="
rg -n -C8 "this\.response\s*=" src/RestWrite.js | head -80

Length of output: 3874


🏁 Script executed:

#!/bin/bash
# Check the execute method to understand the full flow
echo "=== Full execute method to see the order of operations ==="
sed -n '135,165p' src/RestWrite.js

Length of output: 970


🏁 Script executed:

#!/bin/bash
# Check the full runAfterSaveTrigger implementation to confirm it doesn't modify client response
echo "=== Full runAfterSaveTrigger method ==="
sed -n '1655,1680p' src/RestWrite.js

Length of output: 891


🏁 Script executed:

#!/bin/bash
# Verify that after runAfterSaveTrigger, nothing modifies this.response
echo "=== Check what happens after runAfterSaveTrigger in execute ==="
sed -n '148,170p' src/RestWrite.js

echo -e "\n=== Check if triggers.maybeRunTrigger returns anything that modifies response ==="
rg -n "return triggers\.maybeRunTrigger" src/RestWrite.js

Length of output: 854


@mtrezza No, this is not a breaking change for direct REST API users.

Here's why:

Response Flow:

  1. The client response (this.response.response) is set in _handleSaveResponse during handleFollowup() (lines 1696-1698)
  2. runAfterSaveTrigger() is called after the response is already finalized
  3. The execute() method simply returns this.response at the end (line 166)

What the Fix Actually Does:
The isRoleAfterSave condition only activates when this.response is already set, which means:

  • buildParseObjects() at line 1664 (in runAfterSaveTrigger) creates Parse Objects solely for trigger invocation and LiveQuery notifications
  • These objects are passed to afterSave hooks and LiveQuery subscribers
  • They are never used to modify the client response

Impact:

  • Direct REST API users: No change - they receive the exact same response as before
  • Database: No change - the saved data is identical
  • SDK users via REST: No change - the HTTP response body is unchanged

What IS Changed:

  • The Parse.Role object passed to afterSave triggers will have the name field filtered out (treated as read-only)
  • This prevents the Parse SDK from throwing "A role's name can only be set before it has been saved" when constructing the trigger object

The fix is internal to trigger/LiveQuery processing and doesn't alter any client-facing API responses.

@coderabbitai
Copy link

coderabbitai bot commented Oct 22, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the complete flow of how responses are returned to clients
# Look at the execute method and see what's returned
rg -n -C30 "RestWrite.prototype.execute\s*=" src/RestWrite.js | head -100

Length of output: 2028


🏁 Script executed:

#!/bin/bash
# Check if buildParseObjects is ever used to modify this.response.response
rg -n -B5 -A5 "this\.response\.response.*buildParseObjects|buildParseObjects.*this\.response\.response" src/RestWrite.js

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the buildParseObjects function definition to understand what it returns
ast-grep --pattern 'function buildParseObjects($$$) {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if readOnlyAttributes affects anything other than buildParseObjects
rg -n "readOnlyAttributes" src/RestWrite.js

Length of output: 420


🏁 Script executed:

#!/bin/bash
# Look at the actual return statement in execute() to see what gets returned to client
rg -n -A2 "return this\.response" src/RestWrite.js

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Look at the complete buildParseObjects function and where it's called
rg -n -B10 -A40 "^function buildParseObjects\(" src/RestWrite.js

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check how this.response is set in _handleSaveResponse and if it's ever modified after
rg -n -C20 "_handleSaveResponse.*=" src/RestWrite.js | head -80

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check all the places where buildParseObjects is called
rg -n "buildParseObjects\(" src/RestWrite.js

Length of output: 304


🏁 Script executed:

#!/bin/bash
# Look at what runAfterSaveTrigger does with buildParseObjects result
rg -n -C15 "runAfterSaveTrigger\(\)" src/RestWrite.js

Length of output: 1046


🏁 Script executed:

#!/bin/bash
# Look at the full runAfterSaveTrigger method implementation
rg -n -A50 "RestWrite.prototype.runAfterSaveTrigger\s*=\s*function" src/RestWrite.js | head -100

Length of output: 1869


🏁 Script executed:

#!/bin/bash
# Look at the buildParseObjects method to see what it does
rg -n -A50 "RestWrite.prototype.buildParseObjects\s*=\s*function" src/RestWrite.js

Length of output: 2229


🏁 Script executed:

#!/bin/bash
# Check if the trigger can modify this.response
rg -n -C10 "triggers\.maybeRunTrigger.*afterSave" src/RestWrite.js

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at _handleSaveResponse to see when this.response.response is set
rg -n -A30 "RestWrite.prototype._handleSaveResponse\s*=\s*function" src/RestWrite.js | head -80

Length of output: 0


🏁 Script executed:

#!/bin/bash
# See the rest of buildParseObjects to confirm sanitized data is only used for the Parse object
rg -n -A20 "const sanitized = this\.sanitizedData\(\);" src/RestWrite.js

Length of output: 771


🏁 Script executed:

#!/bin/bash
# Check where this.response.response is actually set (in the database operation)
rg -n "this\.response\s*=\s*\{" src/RestWrite.js

Length of output: 134


🏁 Script executed:

#!/bin/bash
# Verify that the REST API response is never modified by readOnlyAttributes
rg -n -B5 -A5 "this\.response\.response\s*=" src/RestWrite.js | head -60

Length of output: 428


@mtrezza No, this is not a breaking change for users of the REST API.

Here's why:

The key flow:

  1. Database operation returns the response with { objectId, name, ... } and sets this.response.response to this data
  2. runAfterSaveTrigger is called (line 152) - at this point the REST API response is already set
  3. buildParseObjects creates Parse.Object instances (line 1664) solely for passing to triggers and LiveQuery
  4. The role's name field remains in the REST API response - it's only excluded from being re-set on the Parse.Object via the set() method

What readOnlyAttributes actually does:

Looking at lines 1783-1786, readOnlyAttributes removes fields from the sanitized data before calling updatedObject.set(sanitized). This prevents the Parse SDK from trying to set the name field on a Role that already has an objectId (which would trigger the validation error).

However, the name field is still present in:

  • The original this.response.response that gets returned to REST API clients
  • The updatedObject Parse.Object (it gets it via _handleSaveResponse on line 1665)
  • The updatedObject.toJSON() output if the trigger modifies and returns the object (line 1698-1699)

Conclusion: The REST API response still contains the name field exactly as before. This change only prevents the internal Parse.Object from triggering SDK validation errors during trigger execution. REST API users will see no difference in the response structure.

@coderabbitai
Copy link

coderabbitai bot commented Oct 22, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look at how this.response.response is constructed and returned to understand if it's affected by readOnlyAttributes
rg -n -C10 "this\.response\.response\s*=" src/RestWrite.js

# Also check the buildParseObjects function to see what readOnlyAttributes does
rg -n -C20 "function buildParseObjects" src/RestWrite.js

Length of output: 669


🏁 Script executed:

#!/bin/bash
# Check the exact location where readOnlyAttributes is added and what it affects
rg -n -C30 "readOnlyAttributes\.push\('name'\)" src/RestWrite.js

Length of output: 2655


🏁 Script executed:

#!/bin/bash
# Look at the execute method to see the complete flow of how responses are handled
ast-grep --pattern 'execute() {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find where the final response is returned to the REST API client
rg -n "return this\.response" src/RestWrite.js

Length of output: 31


🏁 Script executed:

#!/bin/bash
# Look at _handleSaveResponse to see what's in the initial response before afterSave
rg -n -A30 "_handleSaveResponse\s*=\s*function\|RestWrite\.prototype\._handleSaveResponse" src/RestWrite.js | head -80

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check where buildParseObjects is called and how its result is used
rg -n -B5 -A10 "buildParseObjects\(\)" src/RestWrite.js

Length of output: 2835


🏁 Script executed:

#!/bin/bash
# Look at the execute method flow to understand the complete sequence
rg -n -A100 "RestWrite\.prototype\.execute\s*=" src/RestWrite.js | head -120

Length of output: 3125


🏁 Script executed:

#!/bin/bash
# Check if afterSave triggers can modify the response that goes to the client
rg -n -C15 "afterSave.*response" src/RestWrite.js

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find where this.response is initially set (before afterSave)
rg -n "this\.response\s*=\s*\{" src/RestWrite.js

Length of output: 134


🏁 Script executed:

#!/bin/bash
# Look at _handleSaveResponse method on the updatedObject to understand what it does
rg -n "_handleSaveResponse" src/ --type js | head -20

Length of output: 210


🏁 Script executed:

#!/bin/bash
# Check the runDatabaseOperation to see how the response is initially constructed
rg -n -A30 "RestWrite\.prototype\.runDatabaseOperation" src/RestWrite.js | head -50

Length of output: 1084


🏁 Script executed:

#!/bin/bash
# Look at the createObject and updateObject methods to see what response they return
rg -n -A20 "createObject.*response\|updateObject.*response" src/RestWrite.js | head -60

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at what _handleSaveResponse does - it's a Parse SDK method
rg -n "_handleSaveResponse" node_modules/parse/lib/node/Parse.js 2>/dev/null || echo "Parse SDK not in node_modules, checking if there's info in comments"

# Check the comment about _handleSaveResponse in RestWrite.js
rg -n -B2 -A2 "_handleSaveResponse" src/RestWrite.js

Length of output: 618


🏁 Script executed:

#!/bin/bash
# Look at how this.response is initially constructed in runDatabaseOperation
rg -n -A80 "RestWrite\.prototype\.runDatabaseOperation" src/RestWrite.js | grep -A80 "runDatabaseOperation"

Length of output: 2947


🏁 Script executed:

#!/bin/bash
# Check _updateResponseWithData to understand how response is modified
rg -n -A20 "_updateResponseWithData" src/RestWrite.js

Length of output: 3361


🏁 Script executed:

#!/bin/bash
# Look at the context around line 1698 more carefully to understand the afterSave response flow
rg -n -B20 -A10 "this\.response\.response = this\._updateResponseWithData" src/RestWrite.js

Length of output: 873


@mtrezza No, this is not a breaking change.

Here's why:

What the change does:

The PR adds 'name' to readOnlyAttributes for _Role class during buildParseObjects() when constructing objects for the afterSave trigger.

Why there's no breaking change:

  1. The condition is specific: The change only applies when !this.query (CREATE operations), not UPDATE operations.

  2. The 'name' field is still included in the response: Looking at lines 1754-1757 in buildParseObjects():

    if (!this.originalData) {
      for (const attribute of readOnlyAttributes) {
        extraData[attribute] = this.data[attribute];
      }
    }

    For CREATE operations (no originalData), read-only attributes are explicitly copied to extraData, which is used to construct the updatedObject. So the name field is still present on the object.

  3. What changes internally: The readOnlyAttributes check only prevents setting the field from this.data at line 1764 (updatedObject.set(key, data[key])). Since the object already has name from extraData, and already has objectId from _handleSaveResponse() at line 1665, trying to set name again would trigger the Parse SDK error: "A role's name can only be set before it has been saved."

  4. Response flow: When this.response.response is updated at line 1698 with updatedObject.toJSON(), the name field is still present in the JSON—it's just that it was obtained from extraData rather than being set from this.data.

Conclusion: This is an internal implementation detail that prevents a Parse SDK validation error without changing the actual response content. REST API clients will still receive the name field in the response, whether they use the Parse JS SDK or call the REST API directly.

@mtrezza mtrezza changed the title fix: After save on Parse.Role fix: Error in afterSave trigger for Parse.Role due to name field Oct 22, 2025
@mtrezza mtrezza merged commit eb052d8 into parse-community:alpha Oct 22, 2025
42 of 46 checks passed
parseplatformorg pushed a commit that referenced this pull request Oct 22, 2025
# [8.3.0-alpha.10](8.3.0-alpha.9...8.3.0-alpha.10) (2025-10-22)

### Bug Fixes

* Error in `afterSave` trigger for `Parse.Role` due to `name` field ([#9883](#9883)) ([eb052d8](eb052d8))
@parseplatformorg
Copy link
Contributor

🎉 This change has been released in version 8.3.0-alpha.10

@parseplatformorg parseplatformorg added the state:released-alpha Released as alpha version label Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state:released-alpha Released as alpha version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants