-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: sequential transcation support (#30) #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ import { getNodeModulesFolder, getPrismaVersion, getZenStackVersion } from './ut | |||||||||||||||||||||
| import { blue, grey, red } from 'colors' | ||||||||||||||||||||||
| import semver from 'semver' | ||||||||||||||||||||||
| import { CliError } from './cli-error' | ||||||||||||||||||||||
| import SuperJSON from 'superjson' | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export interface ServerOptions { | ||||||||||||||||||||||
| zenstackPath: string | undefined | ||||||||||||||||||||||
|
|
@@ -20,6 +21,25 @@ type EnhancementKind = 'password' | 'omit' | 'policy' | 'validation' | 'delegate | |||||||||||||||||||||
| // enable all enhancements except policy | ||||||||||||||||||||||
| const Enhancements: EnhancementKind[] = ['password', 'omit', 'validation', 'delegate', 'encryption'] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const VALID_OPS = new Set([ | ||||||||||||||||||||||
| 'findMany', | ||||||||||||||||||||||
| 'findUnique', | ||||||||||||||||||||||
| 'findFirst', | ||||||||||||||||||||||
| 'create', | ||||||||||||||||||||||
| 'createMany', | ||||||||||||||||||||||
| 'createManyAndReturn', | ||||||||||||||||||||||
| 'update', | ||||||||||||||||||||||
| 'updateMany', | ||||||||||||||||||||||
| 'updateManyAndReturn', | ||||||||||||||||||||||
| 'upsert', | ||||||||||||||||||||||
| 'delete', | ||||||||||||||||||||||
| 'deleteMany', | ||||||||||||||||||||||
| 'count', | ||||||||||||||||||||||
| 'aggregate', | ||||||||||||||||||||||
| 'groupBy', | ||||||||||||||||||||||
| 'exists', | ||||||||||||||||||||||
| ]) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Resolve the absolute path to the Prisma schema directory | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
|
|
@@ -203,6 +223,96 @@ async function loadZenStackModules( | |||||||||||||||||||||
| return { PrismaClient, modelMeta, enums, zenstackVersion, enhanceFunc } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function makeError(message: string, status = 400) { | ||||||||||||||||||||||
| return { status, body: { error: message } } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function lowerCaseFirst(input: string) { | ||||||||||||||||||||||
| return input.charAt(0).toLowerCase() + input.slice(1) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function isValidModel(modelMeta: any, modelName: string): boolean { | ||||||||||||||||||||||
| return lowerCaseFirst(modelName) in modelMeta.models | ||||||||||||||||||||||
|
||||||||||||||||||||||
| return lowerCaseFirst(modelName) in modelMeta.models | |
| const name = lowerCaseFirst(modelName) | |
| return Object.prototype.hasOwnProperty.call(modelMeta.models, name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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:
- 1: https://expressjs.com/en/guide/error-handling
- 2: https://expressjs.com/en/guide/error-handling.html
- 3: https://stackoverflow.com/questions/55504066/how-to-gracefully-handle-promise-rejection-in-express
- 4: https://oneuptime.com/blog/post/2026-01-25-handle-async-await-errors-properly/view
- 5: https://oneuptime.com/blog/post/2026-01-26-express-error-handling/view
- 6: Support catching rejected promises in middleware functions expressjs/express#3604
- 7: https://stackoverflow.com/questions/49417580/express-middleware-cannot-trap-errors-thrown-by-async-await-but-why
- 8: https://stackoverflow.com/questions/77596099/how-to-stop-express-server-from-exiting-after-error-in-synchronous-code
- 9: https://expressjs.com/en/4x/api.html
🏁 Script executed:
# First, find the server.ts file
fd -n "server.ts" -t f
# Then read the relevant sectionRepository: 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 -80Repository: 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 sectionRepository: 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 -100Repository: 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.
Copilot
AI
Mar 29, 2026
There was a problem hiding this comment.
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.
| 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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Copilot
AI
Mar 29, 2026
There was a problem hiding this comment.
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.
| app.post('/api/model/\\$transaction/sequential', async (_req, res) => { | |
| app.post('/api/model/$transaction/sequential', async (_req, res) => { |
Copilot
AI
Mar 29, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Does Prisma expose the same model delegate methods across all supported providers and versions, or are operations such ascreateManyAndReturn/updateManyAndReturnprovider- 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.
createManyAndReturn()/updateManyAndReturn()are only available starting in Prisma ORM 5.14.0 (and later). [1]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
createManyAndReturnandupdateManyAndReturnare 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 inVALID_OPSbut unavailable on the current client instance, the code at Line 293 will callundefined(...), resulting in a500error instead of validation failure.The suggested fix prevents this by checking the delegate at request time before execution.
Suggested change
Also applies to: Lines 268–269, 293
🤖 Prompt for AI Agents