Skip to content

feat: sequential transcation support (#30)#31

Merged
jiashengguo merged 1 commit intomainfrom
dev
Mar 29, 2026
Merged

feat: sequential transcation support (#30)#31
jiashengguo merged 1 commit intomainfrom
dev

Conversation

@jiashengguo
Copy link
Copy Markdown
Member

@jiashengguo jiashengguo commented Mar 29, 2026

  • feat: sequential transcation support

  • fix comments

Summary by CodeRabbit

  • New Features
    • Added a new sequential transaction endpoint enabling multiple model operations to be executed in sequence within a single atomic transaction, supporting complex multi-step workflows.

* feat: sequential transcation support

* fix comments
Copilot AI review requested due to automatic review settings March 29, 2026 04:14
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 2026

📝 Walkthrough

Walkthrough

The PR increments the package version to 0.4.0, adds the superjson dependency for payload serialization, and introduces a new /api/model/$transaction/sequential endpoint that validates and executes sequential database operations with encoding/decoding support.

Changes

Cohort / File(s) Summary
Dependency Management
package.json
Version bump from 0.3.0 to 0.4.0; added superjson ^2.2.6 dependency.
Sequential Transaction Endpoint
src/server.ts
Integrated superjson for request/response payload serialization; added POST /api/model/$transaction/sequential endpoint with validation for operation arrays (model, op, args), checks against VALID_OPS set and modelMeta, validates args as objects, executes operations sequentially in a transaction, and returns structured error or result responses.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 A transaction so fine, now runs in a line,
With superjson magic, each payload will shine,
Sequential hops through the database we go,
Validation checks bloom wherever we flow! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title describes sequential transaction support, which aligns with the main change adding a new sequential transaction endpoint in src/server.ts.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new sequential transaction endpoint to the proxy server so multiple Prisma/ZendStack model operations can be executed in order within a single $transaction, with SuperJSON (de)serialization support.

Changes:

  • Introduce a /api/model/$transaction/sequential POST endpoint that validates and executes a list of model operations inside an interactive Prisma transaction.
  • Add SuperJSON support to deserialize request args and serialize transaction results.
  • Bump package version and add superjson dependency (plus lockfile updates).

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/server.ts Adds sequential transaction handler, request validation, and SuperJSON (de)serialization; registers a new Express route.
package.json Bumps version to 0.4.0 and adds superjson dependency.
pnpm-lock.yaml Locks new dependency tree for superjson and transitive packages.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +282 to +285
processedOps.push({
model: lowerCaseFirst(itemModel),
op: itemOp,
args: processRequestPayload(itemArgs),
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

args is allowed to be null and is passed through to Prisma delegate calls. Prisma client methods generally expect args to be an object or undefined; passing null will typically throw at runtime. Consider rejecting null in validation or normalizing null to undefined before invoking the operation.

Suggested change
processedOps.push({
model: lowerCaseFirst(itemModel),
op: itemOp,
args: processRequestPayload(itemArgs),
const normalizedArgs = itemArgs === null ? undefined : processRequestPayload(itemArgs)
processedOps.push({
model: lowerCaseFirst(itemModel),
op: itemOp,
args: normalizedArgs,

Copilot uses AI. Check for mistakes.
Comment on lines +351 to +362
app.post('/api/model/\\$transaction/sequential', async (_req, res) => {
const response = await handleTransaction(
modelMeta,
enhanceFunc(
prisma,
{},
{
kinds: Enhancements,
}
),
_req.body
)
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

The request object is named _req but is actively used (_req.body). Since the underscore prefix typically indicates an intentionally unused parameter, rename it to req to avoid confusion.

Copilot uses AI. Check for mistakes.
}

function isValidModel(modelMeta: any, modelName: string): boolean {
return lowerCaseFirst(modelName) in modelMeta.models
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

isValidModel uses the in operator with untrusted input. This can return true for prototype properties like __proto__/constructor, potentially allowing unexpected property access when later indexing tx[model]. Use an own-property check (e.g., Object.hasOwn(...) / hasOwnProperty.call) against modelMeta.models instead of in.

Suggested change
return lowerCaseFirst(modelName) in modelMeta.models
const name = lowerCaseFirst(modelName)
return Object.prototype.hasOwnProperty.call(modelMeta.models, name)

Copilot uses AI. Check for mistakes.

// ZenStack API endpoint

app.post('/api/model/\\$transaction/sequential', async (_req, res) => {
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

The route is registered as '/api/model/\\$transaction/sequential', which evaluates to a path containing a literal backslash (/api/model/\$transaction/sequential). Requests to /api/model/$transaction/sequential won’t match. Use a literal $ in the route string instead.

Suggested change
app.post('/api/model/\\$transaction/sequential', async (_req, res) => {
app.post('/api/model/$transaction/sequential', async (_req, res) => {

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/server.ts`:
- Around line 307-312: In the catch block that handles the "$transaction"
request, stop returning the raw backend error message and avoid treating all
failures as 500; instead log the full error via console.error (or processLogger)
including err and stack, map expected client-side errors to appropriate 4xx
responses (e.g., detect types like ValidationError, BadRequestError or an
err.status/statusCode property) and return makeError with a generic "Transaction
failed" message and a 500 status for true server errors; keep references to the
existing symbols (the catch(err) block around the "$transaction" handler and the
makeError call) and implement detection of known client error classes or status
codes to return proper 4xx codes while always logging detailed err internally.
- Around line 243-245: processRequestPayload is invoked before the surrounding
try block and can throw, which causes unhandled rejections when
handleTransaction is called from the async route handler; wrap the call to
processRequestPayload(itemArgs) in a try-catch (or move it inside the existing
try) and on error forward the exception to Express (e.g., call next(err) or
rethrow so the outer try/catch handles it) so that malformed meta.serialization
doesn't produce an unhandled rejection; update the code around
processRequestPayload, handleTransaction, and the route handler to ensure thrown
errors are caught and propagated to Express's error handling.
- Around line 24-41: The current static VALID_OPS whitelist can include
operations not available on the runtime Prisma delegate; update the request
validation to ensure the requested op is both in VALID_OPS and actually exists
as a function on the resolved delegate before invoking it. In the handler that
resolves the delegate (the code that gets the model delegate, e.g.
prisma[modelName] or the variable named delegate) add a runtime check like
typeof delegate[op] === 'function' and if false return a 4xx validation error
(bad request) instead of proceeding; only call delegate[op](...) when that check
passes. This ensures ops like createManyAndReturn/updateManyAndReturn are
rejected when not present on the loaded delegate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fbf6ae78-82d2-4e81-82a1-d02b7856007b

📥 Commits

Reviewing files that changed from the base of the PR and between 37f9bb7 and d4841a3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • package.json
  • src/server.ts

Comment on lines +24 to +41
const VALID_OPS = new Set([
'findMany',
'findUnique',
'findFirst',
'create',
'createMany',
'createManyAndReturn',
'update',
'updateMany',
'updateManyAndReturn',
'upsert',
'delete',
'deleteMany',
'count',
'aggregate',
'groupBy',
'exists',
])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does Prisma expose the same model delegate methods across all supported providers and versions, or are operations such as createManyAndReturn/updateManyAndReturn provider- or version-dependent?

💡 Result:

Prisma does not expose an identical set of model-delegate CRUD methods/behaviors across all providers and versions—some operations are provider- and/or version-dependent.

  • Version-dependent: createManyAndReturn() / updateManyAndReturn() are only available starting in Prisma ORM 5.14.0 (and later). [1]
  • Provider-dependent: those “AndReturn” bulk operations are supported only on PostgreSQL, CockroachDB, and SQLite (not on MySQL, SQL Server, MongoDB, etc.). [1][2]
  • Even when a method exists, capabilities can differ by provider (e.g., createMany({ skipDuplicates: true }) is not supported on some connectors like SQLite). [1]

So if you rely on createManyAndReturn / updateManyAndReturn, you must gate it by Prisma version and datasource provider (and regenerate Prisma Client after upgrades/provider changes). [1][2]

Sources:
[1] Prisma Docs – CRUD (v6) (notes provider + “available in 5.14.0 and later”) (docs.prisma.io)
[2] Prisma Docs – Prisma Client API reference (method-level support list) (docs.prisma.io)

Citations:


Validate operations against the actual loaded delegate, not only VALID_OPS.

The whitelist is static, but the Prisma delegate surface is provider- and version-dependent. Operations like createManyAndReturn and updateManyAndReturn are only available in Prisma 5.14.0+, and only on PostgreSQL, CockroachDB, and SQLite—not on MySQL, SQL Server, or MongoDB. If an op is in VALID_OPS but unavailable on the current client instance, the code at Line 293 will call undefined(...), resulting in a 500 error instead of validation failure.

The suggested fix prevents this by checking the delegate at request time before execution.

Suggested change
-    if (!VALID_OPS.has(itemOp)) {
+    const model = lowerCaseFirst(itemModel)
+    const delegate = (client as any)[model]
+    if (!VALID_OPS.has(itemOp) || typeof delegate?.[itemOp] !== 'function') {
       return makeError(`operation at index ${i} has invalid op: ${itemOp}`)
     }
@@
     processedOps.push({
-      model: lowerCaseFirst(itemModel),
+      model,
       op: itemOp,
       args: processRequestPayload(itemArgs),
     })

Also applies to: Lines 268–269, 293

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server.ts` around lines 24 - 41, The current static VALID_OPS whitelist
can include operations not available on the runtime Prisma delegate; update the
request validation to ensure the requested op is both in VALID_OPS and actually
exists as a function on the resolved delegate before invoking it. In the handler
that resolves the delegate (the code that gets the model delegate, e.g.
prisma[modelName] or the variable named delegate) add a runtime check like
typeof delegate[op] === 'function' and if false return a 4xx validation error
(bad request) instead of proceeding; only call delegate[op](...) when that check
passes. This ensures ops like createManyAndReturn/updateManyAndReturn are
rejected when not present on the loaded delegate.

Comment on lines +243 to +245
if (meta?.serialization) {
// superjson deserialization
return SuperJSON.deserialize({ json: rest, meta: meta.serialization })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does Express 4 automatically convert rejected promises from async route handlers into error responses, or do they need to be caught and passed to next(err) manually?

💡 Result:

No, Express 4 does not automatically convert rejected promises from async route handlers into error responses. They need to be caught manually and passed to next(err). In Express 4, only synchronous errors in route handlers are automatically caught by Express. For asynchronous code, including rejected promises from async/await handlers, developers must explicitly handle errors using try-catch blocks or .catch(next) and call next(err) to propagate them to Express error handling middleware. Unhandled rejections will not trigger Express error handlers and may cause the process to emit unhandledRejection events or crash. This behavior changes in Express 5, where rejected promises from async route handlers automatically call next with the error. However, since the query specifies Express 4 and both 4.x and 5.x are actively maintained as of 2026, Express 4 requires manual error handling for async routes.

Citations:


🏁 Script executed:

# First, find the server.ts file
fd -n "server.ts" -t f

# Then read the relevant section

Repository: zenstackhq/proxy

Length of output: 290


🏁 Script executed:

# Find server.ts file
fd "server\.ts" src/

# Then read the relevant section with line numbers
cat -n src/server.ts | sed -n '240,295p'

Repository: zenstackhq/proxy

Length of output: 2431


🏁 Script executed:

# Check what processRequestPayload returns and if it can throw
cat -n src/server.ts | sed -n '235,249p'

# Search for other calls to processRequestPayload
rg "processRequestPayload" src/

Repository: zenstackhq/proxy

Length of output: 651


🏁 Script executed:

# Search for where handleTransaction is called
rg "handleTransaction" src/

# Also check if there's a route handler wrapping it
rg -A 5 -B 5 "handleTransaction" src/ | head -80

Repository: zenstackhq/proxy

Length of output: 1227


🏁 Script executed:

# Find the exact line where handleTransaction is called
rg -n "handleTransaction" src/server.ts

# Then read that section

Repository: zenstackhq/proxy

Length of output: 199


🏁 Script executed:

# Read the route handler containing the handleTransaction call
cat -n src/server.ts | sed -n '345,380p'

Repository: zenstackhq/proxy

Length of output: 1015


🏁 Script executed:

# Check SuperJSON imports and usage
rg "SuperJSON" src/

# Also check if there's error handling elsewhere in the codebase for async route handlers
rg -B 3 -A 3 "async.*req.*res.*=>" src/server.ts | head -100

Repository: zenstackhq/proxy

Length of output: 456


Wrap processRequestPayload in try-catch to prevent unhandled rejection in async route handler.

processRequestPayload(itemArgs) can throw on malformed meta.serialization at line 285, before the try block at line 289. Since handleTransaction is async and called without error handling in the route handler (line 351), the unhandled rejection will not be automatically converted to the JSON error response by Express 4.

Suggested change
-    processedOps.push({
-      model: lowerCaseFirst(itemModel),
-      op: itemOp,
-      args: processRequestPayload(itemArgs),
-    })
+    let processedArgs: unknown
+    try {
+      processedArgs = processRequestPayload(itemArgs)
+    } catch {
+      return makeError(`operation at index ${i} has invalid serialization metadata`)
+    }
+
+    processedOps.push({
+      model: lowerCaseFirst(itemModel),
+      op: itemOp,
+      args: processedArgs,
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server.ts` around lines 243 - 245, processRequestPayload is invoked
before the surrounding try block and can throw, which causes unhandled
rejections when handleTransaction is called from the async route handler; wrap
the call to processRequestPayload(itemArgs) in a try-catch (or move it inside
the existing try) and on error forward the exception to Express (e.g., call
next(err) or rethrow so the outer try/catch handles it) so that malformed
meta.serialization doesn't produce an unhandled rejection; update the code
around processRequestPayload, handleTransaction, and the route handler to ensure
thrown errors are caught and propagated to Express's error handling.

Comment on lines +307 to +312
} catch (err) {
console.error('error occurred when handling "$transaction" request:', err)
return makeError(
'Transaction failed: ' + (err instanceof Error ? err.message : String(err)),
500
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't return raw backend exception text in every 500 response.

This branch turns all execution failures into 500 and echoes err.message back to the caller. That leaks internal query/schema details and misclassifies request errors as server faults. Keep the detailed error in logs, but return a generic 500 body after mapping expected client-side failures to 4xx.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server.ts` around lines 307 - 312, In the catch block that handles the
"$transaction" request, stop returning the raw backend error message and avoid
treating all failures as 500; instead log the full error via console.error (or
processLogger) including err and stack, map expected client-side errors to
appropriate 4xx responses (e.g., detect types like ValidationError,
BadRequestError or an err.status/statusCode property) and return makeError with
a generic "Transaction failed" message and a 500 status for true server errors;
keep references to the existing symbols (the catch(err) block around the
"$transaction" handler and the makeError call) and implement detection of known
client error classes or status codes to return proper 4xx codes while always
logging detailed err internally.

@jiashengguo jiashengguo merged commit b25acc2 into main Mar 29, 2026
7 checks passed
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.

2 participants