@@ -7,6 +7,7 @@ import { getNodeModulesFolder, getPrismaVersion, getZenStackVersion } from './ut
77import { blue , grey , red } from 'colors'
88import semver from 'semver'
99import { CliError } from './cli-error'
10+ import SuperJSON from 'superjson'
1011
1112export interface ServerOptions {
1213 zenstackPath : string | undefined
@@ -20,6 +21,25 @@ type EnhancementKind = 'password' | 'omit' | 'policy' | 'validation' | 'delegate
2021// enable all enhancements except policy
2122const Enhancements : EnhancementKind [ ] = [ 'password' , 'omit' , 'validation' , 'delegate' , 'encryption' ]
2223
24+ const VALID_OPS = new Set ( [
25+ 'findMany' ,
26+ 'findUnique' ,
27+ 'findFirst' ,
28+ 'create' ,
29+ 'createMany' ,
30+ 'createManyAndReturn' ,
31+ 'update' ,
32+ 'updateMany' ,
33+ 'updateManyAndReturn' ,
34+ 'upsert' ,
35+ 'delete' ,
36+ 'deleteMany' ,
37+ 'count' ,
38+ 'aggregate' ,
39+ 'groupBy' ,
40+ 'exists' ,
41+ ] )
42+
2343/**
2444 * Resolve the absolute path to the Prisma schema directory
2545 */
@@ -203,6 +223,96 @@ async function loadZenStackModules(
203223 return { PrismaClient, modelMeta, enums, zenstackVersion, enhanceFunc }
204224}
205225
226+ function makeError ( message : string , status = 400 ) {
227+ return { status, body : { error : message } }
228+ }
229+
230+ function lowerCaseFirst ( input : string ) {
231+ return input . charAt ( 0 ) . toLowerCase ( ) + input . slice ( 1 )
232+ }
233+
234+ function isValidModel ( modelMeta : any , modelName : string ) : boolean {
235+ return lowerCaseFirst ( modelName ) in modelMeta . models
236+ }
237+
238+ function processRequestPayload ( args : any ) {
239+ if ( args === null || args === undefined ) {
240+ return args
241+ }
242+ const { meta, ...rest } = args
243+ if ( meta ?. serialization ) {
244+ // superjson deserialization
245+ return SuperJSON . deserialize ( { json : rest , meta : meta . serialization } )
246+ } else {
247+ return args
248+ }
249+ }
250+
251+ async function handleTransaction ( modelMeta : any , client : any , requestBody : unknown ) {
252+ const processedOps : Array < { model : string ; op : string ; args : unknown } > = [ ]
253+ if ( ! requestBody || ! Array . isArray ( requestBody ) || requestBody . length === 0 ) {
254+ return makeError ( 'request body must be a non-empty array of operations' )
255+ }
256+ for ( let i = 0 ; i < requestBody . length ; i ++ ) {
257+ const item = requestBody [ i ]
258+ if ( ! item || typeof item !== 'object' ) {
259+ return makeError ( `operation at index ${ i } must be an object` )
260+ }
261+ const { model : itemModel , op : itemOp , args : itemArgs } = item as any
262+ if ( ! itemModel || typeof itemModel !== 'string' ) {
263+ return makeError ( `operation at index ${ i } is missing a valid "model" field` )
264+ }
265+ if ( ! itemOp || typeof itemOp !== 'string' ) {
266+ return makeError ( `operation at index ${ i } is missing a valid "op" field` )
267+ }
268+ if ( ! VALID_OPS . has ( itemOp ) ) {
269+ return makeError ( `operation at index ${ i } has invalid op: ${ itemOp } ` )
270+ }
271+ if ( ! isValidModel ( modelMeta , itemModel ) ) {
272+ return makeError ( `operation at index ${ i } has unknown model: ${ itemModel } ` )
273+ }
274+ if (
275+ itemArgs !== undefined &&
276+ itemArgs !== null &&
277+ ( typeof itemArgs !== 'object' || Array . isArray ( itemArgs ) )
278+ ) {
279+ return makeError ( `operation at index ${ i } has invalid "args" field` )
280+ }
281+
282+ processedOps . push ( {
283+ model : lowerCaseFirst ( itemModel ) ,
284+ op : itemOp ,
285+ args : processRequestPayload ( itemArgs ) ,
286+ } )
287+ }
288+
289+ try {
290+ const clientResult = await client . $transaction ( async ( tx : any ) => {
291+ const result : any [ ] = [ ]
292+ for ( const { model, op, args } of processedOps ) {
293+ result . push ( await ( tx as any ) [ model ] [ op ] ( args ) )
294+ }
295+ return result
296+ } )
297+
298+ const { json, meta } = SuperJSON . serialize ( clientResult )
299+ const responseBody : any = { data : json }
300+ if ( meta ) {
301+ responseBody . meta = { serialization : meta }
302+ }
303+
304+ const response = { status : 200 , body : responseBody }
305+
306+ return response
307+ } catch ( err ) {
308+ console . error ( 'error occurred when handling "$transaction" request:' , err )
309+ return makeError (
310+ 'Transaction failed: ' + ( err instanceof Error ? err . message : String ( err ) ) ,
311+ 500
312+ )
313+ }
314+ }
315+
206316/**
207317 * Start the Express server with ZenStack proxy
208318 */
@@ -237,6 +347,22 @@ export async function startServer(options: ServerOptions) {
237347 app . use ( express . urlencoded ( { extended : true , limit : '5mb' } ) )
238348
239349 // ZenStack API endpoint
350+
351+ app . post ( '/api/model/\\$transaction/sequential' , async ( _req , res ) => {
352+ const response = await handleTransaction (
353+ modelMeta ,
354+ enhanceFunc (
355+ prisma ,
356+ { } ,
357+ {
358+ kinds : Enhancements ,
359+ }
360+ ) ,
361+ _req . body
362+ )
363+ res . status ( response . status ) . json ( response . body )
364+ } )
365+
240366 app . use (
241367 '/api/model' ,
242368 ZenStackMiddleware ( {
0 commit comments