From 0136d3995361082c1208030a6ea2d3a1e0fa98e4 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Fri, 21 Aug 2026 14:08:38 +0100 Subject: [PATCH] refactor(webapp): route the global flags write through the transaction helper Use the $transaction helper from ~/db.server instead of calling client.$transaction directly, so the write gets tracing and infra-error boundary logging. The helper is callback-only, so the batched upserts become sequential statements inside one interactive transaction, and an undefined result is treated as a failure rather than a silent no-op. --- apps/webapp/app/v3/featureFlags.server.ts | 40 ++++++++++++++--------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/apps/webapp/app/v3/featureFlags.server.ts b/apps/webapp/app/v3/featureFlags.server.ts index bc1e85cc0..d4a8dea6c 100644 --- a/apps/webapp/app/v3/featureFlags.server.ts +++ b/apps/webapp/app/v3/featureFlags.server.ts @@ -1,6 +1,6 @@ import { type z } from "zod"; import type { PrismaClient } from "@trigger.dev/database"; -import { boundedIn, prisma, type PrismaClientOrTransaction } from "~/db.server"; +import { $transaction, boundedIn, prisma, type PrismaClientOrTransaction } from "~/db.server"; import { FEATURE_FLAG, type FeatureFlagCatalogSchema, @@ -240,28 +240,36 @@ export async function replaceGlobalFeatureFlags( } ): Promise { const canDeleteLocked = params.unlockLockedFlags && !params.isManagedCloud; - const upsertOps: ReturnType[] = []; + const toUpsert: { key: FeatureFlagKey; value: unknown }[] = []; const keysToDelete: string[] = []; for (const key of params.catalogKeys) { if (key in params.requestedFlags) { - const value = params.requestedFlags[key]; - upsertOps.push( - client.featureFlag.upsert({ - where: { key }, - create: { key, value: value as any }, - update: { value: value as any }, - }) - ); + toUpsert.push({ key, value: params.requestedFlags[key] }); } else if (canDeleteLocked || !GLOBAL_LOCKED_FLAGS.includes(key)) { keysToDelete.push(key); } } - await client.$transaction([ - ...upsertOps, - ...(keysToDelete.length > 0 - ? [client.featureFlag.deleteMany({ where: { key: { in: boundedIn(keysToDelete) } } })] - : []), - ]); + const applied = await $transaction(client, "replaceGlobalFeatureFlags", async (tx) => { + for (const { key, value } of toUpsert) { + await tx.featureFlag.upsert({ + where: { key }, + create: { key, value: value as any }, + update: { value: value as any }, + }); + } + + if (keysToDelete.length > 0) { + await tx.featureFlag.deleteMany({ where: { key: { in: boundedIn(keysToDelete) } } }); + } + + return true; + }); + + // The helper resolves undefined instead of throwing when Prisma errors are swallowed. This + // write deletes flags, so treat a transaction that did not run as a failure the caller sees. + if (!applied) { + throw new Error("replaceGlobalFeatureFlags: transaction did not complete"); + } }