@@ -346,22 +346,166 @@ const WAITPOINT_DEDICATED: DedicatedRelationSpec = {
346346 completedExecutionSnapshots : hydrateCompletedExecutionSnapshots ,
347347} ;
348348
349+ // Cross-generation Prisma error normalization.
350+ //
351+ // The store can be backed by the control-plane `@trigger.dev/database` client OR the
352+ // run-ops `@internal/run-ops-database` client. Each is a SEPARATELY generated client with
353+ // its own copy of the Prisma runtime, so each has its OWN `PrismaClientKnownRequestError`
354+ // class object (identical code, distinct module identity). A P2002 from the run-ops client
355+ // is therefore NOT `instanceof` the control-plane class — so the webapp's uniform
356+ // `error instanceof Prisma.PrismaClientKnownRequestError` P2002→422 conversion is skipped and
357+ // a raw 500 escapes. The store normalizes at its write boundary: any foreign
358+ // known-request-error is re-thrown as the control-plane class so every routed-write caller's
359+ // `instanceof` works regardless of which client raised it.
360+
361+ // `instanceof` can't detect a foreign generation's class, so key on the runtime `name` the
362+ // Prisma runtime stamps on every generation plus a string `code` (the P-code).
363+ function isForeignPrismaKnownRequestError (
364+ error : unknown
365+ ) : error is {
366+ name : string ;
367+ message : string ;
368+ code : string ;
369+ meta ?: unknown ;
370+ clientVersion ?: string ;
371+ } {
372+ return (
373+ typeof error === "object" &&
374+ error !== null &&
375+ ( error as { name ?: unknown } ) . name === "PrismaClientKnownRequestError" &&
376+ typeof ( error as { code ?: unknown } ) . code === "string" &&
377+ ! ( error instanceof Prisma . PrismaClientKnownRequestError )
378+ ) ;
379+ }
380+
381+ // Native + non-known-request errors are returned unchanged (caller re-throws the result).
382+ function normalizeRunOpsError ( error : unknown ) : unknown {
383+ if ( ! isForeignPrismaKnownRequestError ( error ) ) {
384+ return error ;
385+ }
386+ return new Prisma . PrismaClientKnownRequestError ( error . message , {
387+ code : error . code ,
388+ clientVersion : error . clientVersion ?? "unknown" ,
389+ meta : error . meta as Record < string , unknown > | undefined ,
390+ } ) ;
391+ }
392+
393+ // Only these Prisma-model delegates carry the create/update/upsert writes that raise P2002;
394+ // `$queryRaw`/`$executeRaw`/`$transaction` are left untouched (raw queries here never raise a
395+ // duplicate-key, and wrapping their tagged-template/callback contract would break it).
396+ const RUN_OPS_DELEGATE_KEYS : ReadonlySet < string > = new Set ( [
397+ "taskRun" ,
398+ "taskRunAttempt" ,
399+ "taskRunExecutionSnapshot" ,
400+ "taskRunWaitpoint" ,
401+ "taskRunCheckpoint" ,
402+ "checkpoint" ,
403+ "checkpointRestoreEvent" ,
404+ "taskRunDependency" ,
405+ "waitpoint" ,
406+ "completedWaitpoint" ,
407+ "waitpointRunConnection" ,
408+ "batchTaskRun" ,
409+ "batchTaskRunItem" ,
410+ ] ) ;
411+
412+ // Every method call on a delegate rewrites ONLY its rejection reason; success is untouched.
413+ function wrapDelegateForErrorNormalization < D extends object > ( delegate : D ) : D {
414+ return new Proxy ( delegate , {
415+ get ( target , prop , receiver ) {
416+ const value = Reflect . get ( target , prop , receiver ) ;
417+ if ( typeof value !== "function" ) {
418+ return value ;
419+ }
420+ return ( ...args : unknown [ ] ) => {
421+ let result : unknown ;
422+ try {
423+ result = ( value as ( ...a : unknown [ ] ) => unknown ) . apply ( target , args ) ;
424+ } catch ( error ) {
425+ throw normalizeRunOpsError ( error ) ;
426+ }
427+ // Delegate methods return a thenable PrismaPromise; rewrite its rejection only.
428+ if ( result != null && typeof ( result as { then ?: unknown } ) . then === "function" ) {
429+ return ( result as Promise < unknown > ) . then ( undefined , ( error ) => {
430+ throw normalizeRunOpsError ( error ) ;
431+ } ) ;
432+ }
433+ return result ;
434+ } ;
435+ } ,
436+ } ) ;
437+ }
438+
439+ // Model delegates are wrapped; `$transaction` wraps its tx client so inner writes normalize
440+ // too; every other property (incl. `$queryRaw`/`$executeRaw`) passes through unchanged.
441+ export function wrapRunOpsClientForErrorNormalization < C extends RunOpsCapableClient > ( client : C ) : C {
442+ // Some tests inject a non-object fake (or nothing) as the client; only a real client can be
443+ // proxied, and only a real client raises the foreign known-request-errors we normalize.
444+ if ( client == null || ( typeof client !== "object" && typeof client !== "function" ) ) {
445+ return client ;
446+ }
447+ const delegateCache = new Map < string , unknown > ( ) ;
448+ return new Proxy ( client , {
449+ get ( target , prop , receiver ) {
450+ if ( typeof prop === "string" && RUN_OPS_DELEGATE_KEYS . has ( prop ) ) {
451+ const cached = delegateCache . get ( prop ) ;
452+ if ( cached ) {
453+ return cached ;
454+ }
455+ const delegate = Reflect . get ( target , prop , receiver ) ;
456+ if ( delegate == null || typeof delegate !== "object" ) {
457+ return delegate ;
458+ }
459+ const wrapped = wrapDelegateForErrorNormalization ( delegate as object ) ;
460+ delegateCache . set ( prop , wrapped ) ;
461+ return wrapped ;
462+ }
463+
464+ if ( prop === "$transaction" ) {
465+ const original = Reflect . get ( target , prop , receiver ) ;
466+ if ( typeof original !== "function" ) {
467+ return original ;
468+ }
469+ return ( fnOrArray : unknown , ...rest : unknown [ ] ) => {
470+ // Interactive (callback) form: wrap the tx client so inner writes normalize too.
471+ if ( typeof fnOrArray === "function" ) {
472+ const wrappedFn = ( tx : RunOpsCapableClient ) =>
473+ ( fnOrArray as ( t : RunOpsCapableClient ) => unknown ) (
474+ wrapRunOpsClientForErrorNormalization ( tx )
475+ ) ;
476+ return ( original as ( ...a : unknown [ ] ) => unknown ) . call ( target , wrappedFn , ...rest ) ;
477+ }
478+ return ( original as ( ...a : unknown [ ] ) => unknown ) . call ( target , fnOrArray , ...rest ) ;
479+ } ;
480+ }
481+
482+ return Reflect . get ( target , prop , receiver ) ;
483+ } ,
484+ } ) as C ;
485+ }
486+
349487/**
350488 * Typed write layer for the task-run row, backed by the `taskRun` Prisma model.
351489 *
352490 * Each method is a verbatim relocation of the Prisma statement that lives at a
353491 * specific call site today. Methods write through `(tx ?? this.prisma).taskRun`
354- * so callers can opt into an existing transaction. Errors (including unique
355- * constraint violations) propagate to the caller unchanged.
492+ * so callers can opt into an existing transaction. Errors surface with unique
493+ * constraint violations (P2002 etc.) normalized to the control-plane
494+ * `Prisma.PrismaClientKnownRequestError` class (see `wrapRunOpsClientForErrorNormalization`),
495+ * so `instanceof Prisma.PrismaClientKnownRequestError` works regardless of which
496+ * generated client backs the store.
356497 */
357498export class PostgresRunStore implements RunStore {
358499 private readonly prisma : RunOpsCapableClient ;
359500 private readonly readOnlyPrisma : RunOpsCapableClient ;
360501 private readonly schemaVariant : RunStoreSchemaVariant ;
361502
362503 constructor ( options : PostgresRunStoreOptions ) {
363- this . prisma = options . prisma ;
364- this . readOnlyPrisma = options . readOnlyPrisma ;
504+ // Normalize foreign (run-ops-generation) Prisma known-request-errors to the control-plane
505+ // class at the write boundary so callers' `instanceof Prisma.PrismaClientKnownRequestError`
506+ // (P2002→422) works regardless of which generated client backs the store.
507+ this . prisma = wrapRunOpsClientForErrorNormalization ( options . prisma ) ;
508+ this . readOnlyPrisma = wrapRunOpsClientForErrorNormalization ( options . readOnlyPrisma ) ;
365509 this . schemaVariant = options . schemaVariant ?? "legacy" ;
366510 }
367511
0 commit comments