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

    • Added support for transactional database sessions during write operations to improve consistency and isolation.
    • Transactional controls exposed within trigger contexts for finer control during trigger execution.
    • Automated Gitpod 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

Introduces a Gitpod config and transaction-session plumbing: DatabaseController gains setTransactionalSession and createTransactionalSession returns the session; RestWrite.execute sets/clears transactional session when context.transaction exists; trigger request contexts are extended with bound transactional session methods.

Changes

Cohort / File(s) Summary
Gitpod config
\.gitpod\.yml
New file: adds Gitpod init task (npm install, npm run build) and start command (npm run start).
Database controller
src/Controllers/DatabaseController.js
Added setTransactionalSession(transactionalSession); createTransactionalSession() now assigns and returns the transactional session instance.
Rest write flow
src/RestWrite.js
RestWrite.prototype.execute() now calls database.setTransactionalSession(context.transaction) when present and clears it in a finally block.
Trigger request context
src/triggers.js
Builds trigger request.context by merging bound methods from config.database: createTransactionalSession, commitTransactionalSession, abortTransactionalSession.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant RestWrite
    participant DatabaseController
    Client->>RestWrite: execute(request, context)
    alt context.transaction exists
        RestWrite->>DatabaseController: setTransactionalSession(context.transaction)
    end
    RestWrite->>DatabaseController: perform write operations
    RestWrite-->>Client: respond
    alt context.transaction exists
        RestWrite->>DatabaseController: setTransactionalSession(null) (finally)
    end
Loading
sequenceDiagram
    participant Trigger
    participant RequestBuilder
    participant DatabaseController
    Trigger->>RequestBuilder: build request for trigger
    RequestBuilder->>DatabaseController: bind createTransactionalSession()
    RequestBuilder->>DatabaseController: bind commitTransactionalSession(), abortTransactionalSession()
    RequestBuilder->>Trigger: return request with extended request.context
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • Moumouls
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is comprehensive and well-structured with features, rationale, implementation details, and usage examples. However, the PR template requires filling out the Issue and Tasks sections, which are not completed. Complete the required PR template sections: add the issue reference in 'Closes: FILL_THIS_OUT' and check off or remove applicable tasks from the Tasks checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature: enabling cross-trigger transactions via a shared context mechanism, which aligns perfectly with all core changes across the modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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.

@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

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

🤖 Fix all issues with AI agents
In `@src/RestWrite.js`:
- Around line 174-179: Remove the stray empty statement by deleting the extra
semicolon after the finally block; locate the finally() callback that checks
this.context.transaction and calls
this.config.database.setTransactionalSession(null) (appears as "}).;;" in
RestWrite.js) and change it to end with a single semicolon/closing brace only so
there is no double semicolon/empty statement.
🧹 Nitpick comments (1)
src/RestWrite.js (1)

100-101: Missing semicolon on Line 101.

Minor style nit: the statement on Line 101 is missing a trailing semicolon, which is inconsistent with the rest of the codebase.

-    this.config.database.setTransactionalSession(this.context.transaction)
+    this.config.database.setTransactionalSession(this.context.transaction);

Comment on lines +174 to +179
}).finally(() => {
if (this.context.transaction) {
// Ensure isolation even on uncaught errors
this.config.database.setTransactionalSession(null);
}
});;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Double semicolon produces an empty statement; remove the extra ;.

Line 179 has });; — the trailing extra semicolon is flagged by static analysis (Biome: noUnreachable). It's a no-op empty statement that should be removed.

Proposed fix
-    }).finally(() => {
-      if (this.context.transaction) {
-        // Ensure isolation even on uncaught errors
-        this.config.database.setTransactionalSession(null);
-      }
-    });;
+    }).finally(() => {
+      if (this.context.transaction) {
+        // Ensure isolation even on uncaught errors
+        this.config.database.setTransactionalSession(null);
+      }
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}).finally(() => {
if (this.context.transaction) {
// Ensure isolation even on uncaught errors
this.config.database.setTransactionalSession(null);
}
});;
}).finally(() => {
if (this.context.transaction) {
// Ensure isolation even on uncaught errors
this.config.database.setTransactionalSession(null);
}
});
🧰 Tools
🪛 Biome (2.3.13)

[error] 179-179: This code will never be reached ...

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

(lint/correctness/noUnreachable)

🤖 Prompt for AI Agents
In `@src/RestWrite.js` around lines 174 - 179, Remove the stray empty statement by
deleting the extra semicolon after the finally block; locate the finally()
callback that checks this.context.transaction and calls
this.config.database.setTransactionalSession(null) (appears as "}).;;" in
RestWrite.js) and change it to end with a single semicolon/closing brace only so
there is no double semicolon/empty statement.

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