@@ -448,6 +448,228 @@ export function createEventsStorage(drizzle: Drizzle): Storage['events'] {
448448 const resolveData = params ?. resolveData ?? 'all' ;
449449 return { event : filterEventData ( parsed , resolveData ) , run, step, hook } ;
450450 } ,
451+ async createBatch ( runId , data , params ) : Promise < EventResult [ ] > {
452+ if ( data . length === 0 ) {
453+ return [ ] ;
454+ }
455+
456+ const resolveData = params ?. resolveData ?? 'all' ;
457+ const now = new Date ( ) ;
458+
459+ // For run_created events, generate runId server-side if not provided
460+ const hasRunCreated = data . some ( ( e ) => e . eventType === 'run_created' ) ;
461+ let effectiveRunId = runId ;
462+ if ( hasRunCreated && ( ! runId || runId === '' ) ) {
463+ effectiveRunId = `wrun_${ ulid ( ) } ` ;
464+ }
465+
466+ // Separate events by type for entity creation
467+ const runCreatedEvents = data . filter (
468+ ( e ) => e . eventType === 'run_created'
469+ ) ;
470+ const runStartedEvents = data . filter (
471+ ( e ) => e . eventType === 'run_started'
472+ ) ;
473+ const runCompletedEvents = data . filter (
474+ ( e ) => e . eventType === 'run_completed'
475+ ) ;
476+ const runFailedEvents = data . filter ( ( e ) => e . eventType === 'run_failed' ) ;
477+ const runCancelledEvents = data . filter (
478+ ( e ) => e . eventType === 'run_cancelled'
479+ ) ;
480+ const runPausedEvents = data . filter ( ( e ) => e . eventType === 'run_paused' ) ;
481+ const runResumedEvents = data . filter (
482+ ( e ) => e . eventType === 'run_resumed'
483+ ) ;
484+ const stepCreatedEvents = data . filter (
485+ ( e ) => e . eventType === 'step_created'
486+ ) ;
487+ const hookCreatedEvents = data . filter (
488+ ( e ) => e . eventType === 'hook_created'
489+ ) ;
490+
491+ // Create run entities atomically with events
492+ if ( runCreatedEvents . length > 0 ) {
493+ const runsToInsert = runCreatedEvents . map ( ( eventData ) => {
494+ const runData = ( eventData as any ) . eventData as {
495+ deploymentId : string ;
496+ workflowName : string ;
497+ input : any [ ] ;
498+ executionContext ?: Record < string , any > ;
499+ } ;
500+ return {
501+ runId : effectiveRunId ,
502+ deploymentId : runData . deploymentId ,
503+ workflowName : runData . workflowName ,
504+ input : runData . input as SerializedContent ,
505+ executionContext : runData . executionContext as
506+ | SerializedContent
507+ | undefined ,
508+ status : 'pending' as const ,
509+ } ;
510+ } ) ;
511+ await drizzle
512+ . insert ( Schema . runs )
513+ . values ( runsToInsert )
514+ . onConflictDoNothing ( ) ;
515+ }
516+
517+ // Update run status for run_started events
518+ if ( runStartedEvents . length > 0 ) {
519+ await drizzle
520+ . update ( Schema . runs )
521+ . set ( {
522+ status : 'running' ,
523+ startedAt : now ,
524+ updatedAt : now ,
525+ } )
526+ . where ( eq ( Schema . runs . runId , effectiveRunId ) ) ;
527+ }
528+
529+ // Update run status for run_completed events
530+ if ( runCompletedEvents . length > 0 ) {
531+ const completedData = ( runCompletedEvents [ 0 ] as any ) . eventData as {
532+ output ?: any ;
533+ } ;
534+ await drizzle
535+ . update ( Schema . runs )
536+ . set ( {
537+ status : 'completed' ,
538+ output : completedData . output as SerializedContent | undefined ,
539+ completedAt : now ,
540+ updatedAt : now ,
541+ } )
542+ . where ( eq ( Schema . runs . runId , effectiveRunId ) ) ;
543+ }
544+
545+ // Update run status for run_failed events
546+ if ( runFailedEvents . length > 0 ) {
547+ const failedData = ( runFailedEvents [ 0 ] as any ) . eventData as {
548+ error : any ;
549+ errorCode ?: string ;
550+ } ;
551+ const errorMessage =
552+ typeof failedData . error === 'string'
553+ ? failedData . error
554+ : ( failedData . error ?. message ?? 'Unknown error' ) ;
555+ // Store structured error as JSON for deserializeRunError to parse
556+ const errorJson = JSON . stringify ( {
557+ message : errorMessage ,
558+ stack : failedData . error ?. stack ,
559+ code : failedData . errorCode ,
560+ } ) ;
561+ await drizzle
562+ . update ( Schema . runs )
563+ . set ( {
564+ status : 'failed' ,
565+ error : errorJson ,
566+ completedAt : now ,
567+ updatedAt : now ,
568+ } )
569+ . where ( eq ( Schema . runs . runId , effectiveRunId ) ) ;
570+ }
571+
572+ // Update run status for run_cancelled events
573+ if ( runCancelledEvents . length > 0 ) {
574+ await drizzle
575+ . update ( Schema . runs )
576+ . set ( {
577+ status : 'cancelled' ,
578+ completedAt : now ,
579+ updatedAt : now ,
580+ } )
581+ . where ( eq ( Schema . runs . runId , effectiveRunId ) ) ;
582+ }
583+
584+ // Update run status for run_paused events
585+ if ( runPausedEvents . length > 0 ) {
586+ await drizzle
587+ . update ( Schema . runs )
588+ . set ( {
589+ status : 'paused' ,
590+ updatedAt : now ,
591+ } )
592+ . where ( eq ( Schema . runs . runId , effectiveRunId ) ) ;
593+ }
594+
595+ // Update run status for run_resumed events
596+ if ( runResumedEvents . length > 0 ) {
597+ await drizzle
598+ . update ( Schema . runs )
599+ . set ( {
600+ status : 'running' ,
601+ updatedAt : now ,
602+ } )
603+ . where ( eq ( Schema . runs . runId , effectiveRunId ) ) ;
604+ }
605+
606+ // Create step entities atomically with events
607+ if ( stepCreatedEvents . length > 0 ) {
608+ const stepsToInsert = stepCreatedEvents . map ( ( eventData ) => {
609+ const stepData = ( eventData as any ) . eventData as {
610+ stepName : string ;
611+ input : any ;
612+ } ;
613+ return {
614+ runId : effectiveRunId ,
615+ stepId : eventData . correlationId ! ,
616+ stepName : stepData . stepName ,
617+ input : stepData . input as SerializedContent ,
618+ status : 'pending' as const ,
619+ attempt : 0 ,
620+ } ;
621+ } ) ;
622+ await drizzle
623+ . insert ( Schema . steps )
624+ . values ( stepsToInsert )
625+ . onConflictDoNothing ( ) ;
626+ }
627+
628+ // Create hook entities atomically with events
629+ if ( hookCreatedEvents . length > 0 ) {
630+ const hooksToInsert = hookCreatedEvents . map ( ( eventData ) => {
631+ const hookData = ( eventData as any ) . eventData as {
632+ token : string ;
633+ metadata ?: any ;
634+ } ;
635+ return {
636+ runId : effectiveRunId ,
637+ hookId : eventData . correlationId ! ,
638+ token : hookData . token ,
639+ metadata : hookData . metadata as SerializedContent ,
640+ ownerId : '' , // TODO: get from context
641+ projectId : '' , // TODO: get from context
642+ environment : '' , // TODO: get from context
643+ } ;
644+ } ) ;
645+ await drizzle
646+ . insert ( Schema . hooks )
647+ . values ( hooksToInsert )
648+ . onConflictDoNothing ( ) ;
649+ }
650+
651+ // Insert all events in a single batch query
652+ const eventsToInsert = data . map ( ( eventData ) => ( {
653+ runId : effectiveRunId ,
654+ eventId : `wevt_${ ulid ( ) } ` ,
655+ correlationId : eventData . correlationId ,
656+ eventType : eventData . eventType ,
657+ eventData : 'eventData' in eventData ? eventData . eventData : undefined ,
658+ } ) ) ;
659+
660+ const values = await drizzle
661+ . insert ( events )
662+ . values ( eventsToInsert )
663+ . returning ( { eventId : events . eventId , createdAt : events . createdAt } ) ;
664+
665+ // Combine input data with returned values
666+ // TODO: Return actual entity data from the database after entity creation is moved here
667+ return data . map ( ( eventData , i ) => {
668+ const result = { ...eventData , ...values [ i ] , runId : effectiveRunId } ;
669+ const parsed = EventSchema . parse ( result ) ;
670+ return { event : filterEventData ( parsed , resolveData ) } ;
671+ } ) ;
672+ } ,
451673 async list ( params : ListEventsParams ) : Promise < PaginatedResponse < Event > > {
452674 const limit = params ?. pagination ?. limit ?? 100 ;
453675 const sortOrder = params . pagination ?. sortOrder || 'asc' ;
0 commit comments