Skip to content

Conversation

@Yumcoder-dev
Copy link

@Yumcoder-dev Yumcoder-dev commented Jun 10, 2025

Transaction Support Across Parse Server Triggers

This modification extends Parse Server to support multi-operation transactions across beforeSave, afterSave, and other Cloud Code triggers using a shared transactional context.


✨ Features

  • ✅ Create a transaction in beforeSave
  • ✅ Reuse the same MongoDB/PostgreSQL session in nested .save() calls
  • ✅ Preserve context across triggers (beforeSave, afterSave, etc.)
  • ✅ Explicit control over commit and abort timing
  • ✅ Integration with Parse Server’s internal RestWrite logic

🧠 Why

Out-of-the-box, Parse Server creates a new DatabaseController per internal operation, which leads to:

  • Loss of transactional session context across chained .save()s
  • Inability to group multiple object writes into a true transaction (with in trigger event)

This patch ensures the transaction session is persisted across triggers and reused consistently, enabling ACID-safe operations.


🛠 How It Works

1. Modify getRequestObject in triggers.js

Inject transactional helpers into the request.context:

request.context = Object.assign({
  createTransactionalSession: config.database.createTransactionalSession.bind(config.database),
  commitTransactionalSession: config.database.commitTransactionalSession.bind(config.database),
  abortTransactionalSession: config.database.abortTransactionalSession.bind(config.database),
}, context);

2. Extend DatabaseController.js

Add support for:

// new method
setTransactionalSession(session) {
  this._transactionalSession = session;
}

createTransactionalSession() {
  return this.adapter.createTransactionalSession().then(session => {
    this._transactionalSession = session;
    return this._transactionalSession ; // add this line
  });
}

commitTransactionalSession() {
  // currently impl.
}

abortTransactionalSession() {
  // currently impl.
}

3. Patch RestWrite.execute() in RestWrite.js

Apply the shared transaction session before executing write logic:

if (this.context.transaction) {
  this.config.database.setTransactionalSession(this.context.transaction);
}

✅ Usage Example in Cloud Code

Parse.Cloud.beforeSave('TestObject', async (request) => {
  const session = await request.context.createTransactionalSession();
  const context = Object.assign(request.context, { transaction: session });
 
  try {
    const obj1 = new Parse.Object('Dependent_TestObject_1');
    obj1.set('name', request.object.get('name'));
    await obj1.save(null, { context });
  } catch (err) {
    await request.context.abortTransactionalSession();
    throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Transaction failed');
  }
});

Parse.Cloud.afterSave('TestObject', async (request) => {
  const { transaction, commitTransactionalSession, abortTransactionalSession } = request.context;
  const context = { transaction };

  try {
    const obj2 = new Parse.Object('Dependent_TestObject_2');
    obj2.set('name', request.object.get('name'));
    await obj2.save(null, { context });

    await commitTransactionalSession();
  } catch (err) {
    await abortTransactionalSession();
    throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Transaction failed');
  }
});

🧪 Behavior

  • context.transaction is injected into every .save() call
  • Nested triggers can access and reuse the transaction session
  • RestWrite ensures internal DB calls are linked to the correct transaction
  • Final commit/abort logic is handled manually in the final trigger (usually afterSave)

Summary by CodeRabbit

  • New Features

    • Support for transactional database sessions during write operations to improve data consistency and isolation.
    • Transactional session controls exposed within trigger contexts for finer control during trigger execution.
    • Automated Gitpod development environment setup to simplify onboarding and local development.
  • Bug Fixes

    • Transactional sessions are reliably cleared after operations to prevent session leakage.

@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 feat: enable cross-trigger transactions via shared context feat: Enable cross-trigger transactions via shared context Jun 10, 2025
@parse-github-assistant
Copy link

parse-github-assistant bot commented Jun 10, 2025

🚀 Thanks for opening this pull request!

@coderabbitai
Copy link

coderabbitai bot commented Jun 10, 2025

📝 Walkthrough

Walkthrough

Adds a Gitpod config. DatabaseController gains setTransactionalSession and its createTransactionalSession now returns the session. RestWrite.execute sets/clears a transactional session when context.transaction exists. Trigger request contexts are extended with bound transactional session methods.

Changes

Cohort / File(s) Change Summary
Gitpod config
.gitpod.yml
New file: configures Gitpod init task (npm install, npm run build) and start command (npm run start).
Database controller
src/Controllers/DatabaseController.js
Added setTransactionalSession(transactionalSession) method; createTransactionalSession() now assigns and returns the transactional session instance.
Rest write flow
src/RestWrite.js
RestWrite.prototype.execute() updated to call database.setTransactionalSession(context.transaction) when present and to clear it in a finally block.
Trigger request context
src/triggers.js
Trigger request objects now merge bound methods from config.database: createTransactionalSession, commitTransactionalSession, abortTransactionalSession into request.context.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant RestWrite
    participant DatabaseController

    Client->>RestWrite: execute(context)
    alt context has transaction
        RestWrite->>DatabaseController: setTransactionalSession(context.transaction)
    end
    RestWrite->>DatabaseController: perform write operations
    alt context has transaction
        RestWrite->>DatabaseController: setTransactionalSession(null) (finally)
    end
Loading
sequenceDiagram
    participant Trigger
    participant getRequestObject
    participant DatabaseController

    Trigger->>getRequestObject: build request for trigger
    getRequestObject->>DatabaseController: bind createTransactionalSession/commit/abort
    getRequestObject->>Trigger: return request with extended request.context
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning While the PR description is substantive and detailed, providing comprehensive explanations of features, implementation approach, and usage examples, it does not follow the required template structure specified in the repository. The description is missing critical template sections: the "Issue" section with a "Closes:" link to reference the related GitHub issue, and the "Tasks" checklist that documents which validation activities (tests, documentation, security checks, etc.) have been completed. Although the content covers the "Approach" implicitly through the "How It Works" section, it lacks the formal structure and sections required by the template. Reorganize the PR description to follow the repository template: add an "Issue" section at the top with "Closes: #" to link this PR to the related GitHub issue, include an "Approach" section with the high-level description of changes, and add the "Tasks" checklist indicating which activities have been completed (tests, documentation, security checks, etc.). The current detailed content can be preserved as supplementary explanation but should be structured within the template's required sections.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "feat: Enable cross-trigger transactions via shared context" is concise, specific, and directly reflects the main objective of the changeset. It accurately summarizes the primary change from the developer's perspective—enabling multi-operation transactions that work across multiple Parse Server triggers by sharing transactional context. The title clearly distinguishes the main feature being introduced without being vague or off-topic.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c23c364 and f6ada16.

📒 Files selected for processing (2)
  • src/Controllers/DatabaseController.js (1 hunks)
  • src/triggers.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/triggers.js
🔇 Additional comments (2)
src/Controllers/DatabaseController.js (2)

1689-1691: LGTM - Setter implementation is correct.

The setTransactionalSession method provides the necessary external control for transaction lifecycle management as outlined in the PR objectives. The race condition concern raised in previous reviews has been properly addressed and withdrawn after confirming that DatabaseController is instantiated per-request.


1696-1696: LGTM - Return statement enables direct session access.

Returning the created transactional session allows callers to immediately use the session without an additional getter call, which aligns with the PR's goal to expose the session for cross-trigger transaction support.


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.

@parseplatformorg
Copy link
Contributor

parseplatformorg commented Jun 10, 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.

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: 3

🧹 Nitpick comments (3)
.gitpod.yml (1)

7-10: Gitpod task runs both npm run build and npm run start in the same task – can cause blocking

npm run build generally exits, but if the script starts a watcher or leaves a process running (common with tsc -w, webpack-watch, etc.), the subsequent npm run start command will never be executed.
Consider splitting the init and workspace tasks or adding a second task entry:

tasks:
  - init: npm install && npm run build
  - command: npm run start

This keeps the build phase isolated and avoids accidental hangs.

src/Controllers/DatabaseController.js (1)

1694-1697: createTransactionalSession now returns the session – good, but missing defensive check

Returning the session is useful. Add a guard to avoid silently re-creating when one is already active:

+  if (this._transactionalSession) {
+    return Promise.resolve(this._transactionalSession);
+  }
   return this.adapter.createTransactionalSession().then(transactionalSession => {
     this._transactionalSession = transactionalSession;
     return this._transactionalSession;
   });

Prevents nested calls from starting multiple DB transactions.

src/triggers.js (1)

284-288: Minor: avoid per-call re-binding to cut allocation noise

The three bind() calls run on every trigger invocation. Consider hoisting the bound helpers once (e.g. in module scope or caching them on the database instance) and reusing the same function references to reduce needless allocations in hot paths.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ae3176 and 83ab9f7.

📒 Files selected for processing (4)
  • .gitpod.yml (1 hunks)
  • src/Controllers/DatabaseController.js (1 hunks)
  • src/RestWrite.js (1 hunks)
  • src/triggers.js (1 hunks)

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

♻️ Duplicate comments (1)
src/RestWrite.js (1)

98-100: 🛠️ Refactor suggestion

Guard session-clear with an explicit sessionSet flag

The assignment logic is correct, yet we still mutate the shared DatabaseController.
Introduce a local sessionSet boolean so the subsequent cleanup only runs when this call actually set the session, avoiding accidental removal of a session established by an outer/sibling write.

-if (this.context.transaction) {
-  this.config.database.setTransactionalSession(this.context.transaction)
-}
+let sessionSet = false;
+if (this.context.transaction) {
+  this.config.database.setTransactionalSession(this.context.transaction);
+  sessionSet = true;
+}
🧹 Nitpick comments (1)
src/RestWrite.js (1)

172-177: Nit: use sessionSet & drop stray semicolon

  1. Re-use the sessionSet flag instead of repeating the this.context.transaction check—clearer intent and safer.
  2. The });; leaves an extra semicolon that trips linters (noUnreachable warning in Biome).
-    }).finally(() => {
-      if (this.context.transaction) {
+    })
+    .finally(() => {
+      if (sessionSet) {
         this.config.database.setTransactionalSession(null);
       }
-    });;
+    });
🧰 Tools
🪛 Biome (1.9.4)

[error] 177-177: This code is unreachable

... because this statement will return from the function beforehand

(lint/correctness/noUnreachable)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83ab9f7 and c23c364.

📒 Files selected for processing (2)
  • src/RestWrite.js (2 hunks)
  • src/triggers.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/triggers.js
🧰 Additional context used
🪛 Biome (1.9.4)
src/RestWrite.js

[error] 177-177: This code is unreachable

... because this statement will return from the function beforehand

(lint/correctness/noUnreachable)

@Yumcoder-dev
Copy link
Author

#up

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants