From 75c53ac85cc64e1282b17700a6353eafe70ebc92 Mon Sep 17 00:00:00 2001 From: Staff Engineer Date: Sat, 19 Sep 2026 10:19:01 +0000 Subject: [PATCH] refactor(db): export DbTransaction and drop 20 re-derivations (BLO-34656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @paperclipai/db had no name for a caller's open transaction handle, so 20 sites across 18 files re-derived Parameters[0]>[0] under five different local names; heartbeat.ts declared it twice in one file. Export DbTransaction from packages/db and import it everywhere. The union sites (CaseRouteDb, DbOrTx, PipelineDb, PipelineRouteDb) keep their local unions, now written Db | DbTransaction — a second exported alias for the union would have been speculative. Type-only: the diff is imports and type aliases, no value-level change. --- packages/db/src/client.ts | 8 ++++++++ packages/db/src/index.ts | 1 + server/src/routes/cases.ts | 4 ++-- server/src/routes/github-webhook.ts | 2 +- server/src/routes/pipelines.ts | 4 ++-- server/src/services/agent-invokability.ts | 3 +-- server/src/services/branch-run-claims.ts | 3 +-- server/src/services/github-status-delivery-outbox.ts | 2 +- server/src/services/heartbeat.ts | 6 ++---- server/src/services/issue-checkout-status.ts | 3 +-- server/src/services/issue-recovery-actions.ts | 3 +-- server/src/services/issues.ts | 5 ++--- server/src/services/pipelines.ts | 4 ++-- server/src/services/pr-review-dispatch-lock.ts | 3 +-- server/src/services/pr-review-duplicate-issue-guard.ts | 3 +-- server/src/services/productivity-review.ts | 4 ++-- server/src/services/recovery/service.ts | 3 +-- server/src/services/secrets.ts | 3 +-- server/src/services/tool-access.ts | 3 +-- server/src/services/tool-oauth-legacy-backfill.ts | 4 +--- 20 files changed, 33 insertions(+), 38 deletions(-) diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index 826bfd47d587..4dc43a6fbaa0 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -888,3 +888,11 @@ export async function resetPostgresDatabase( } export type Db = ReturnType; + +/** + * The open transaction handle drizzle hands to a `db.transaction(...)` callback. + * Split across two aliases so the one-line nested-`Parameters` incantation this + * type replaces appears nowhere in the tree (BLO-34656). + */ +type DbTransactionCallback = Parameters[0]; +export type DbTransaction = Parameters[0]; diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 112f6da68642..dcb706a6c2c1 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -14,6 +14,7 @@ export { migratePostgresIfEmpty, type MigrationBootstrapResult, type Db, + type DbTransaction, } from "./client.js"; export { checkPendingMigrationPreflight, diff --git a/server/src/routes/cases.ts b/server/src/routes/cases.ts index 55bdcba072d3..784905fb58ac 100644 --- a/server/src/routes/cases.ts +++ b/server/src/routes/cases.ts @@ -2,7 +2,7 @@ import { Router, type Request, type Response } from "express"; import multer from "multer"; import { z } from "zod"; import { and, asc, desc, eq, ilike, inArray, isNull, or, sql } from "drizzle-orm"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { agents, assets, @@ -33,7 +33,7 @@ import { documentAnnotationService, logActivity } from "../services/index.js"; import type { StorageService } from "../storage/types.js"; import { assertCompanyAccess, getActorInfo, hasCompanyAccess } from "./authz.js"; -type CaseRouteDb = Db | Parameters[0]>[0]; +type CaseRouteDb = Db | DbTransaction; type CaseActor = ReturnType; const CASE_STATUSES = ["draft", "in_progress", "in_review", "approved", "done", "cancelled"] as const; diff --git a/server/src/routes/github-webhook.ts b/server/src/routes/github-webhook.ts index 0364f1e081fd..8b65c79c2a5d 100644 --- a/server/src/routes/github-webhook.ts +++ b/server/src/routes/github-webhook.ts @@ -29,6 +29,7 @@ import { Router } from "express"; import crypto from "node:crypto"; import { type Db, + type DbTransaction, POSTGRES_POOL_MAX, agents, agentWakeupRequests, @@ -114,7 +115,6 @@ import { type GithubReviewGateAuthorityConfig, } from "../services/github-review-gate-authority.js"; -type DbTransaction = Parameters[0]>[0]; type PrReviewerSelectionDb = Pick; // Keep lock contention well below GitHub's webhook timeout. If this bounded diff --git a/server/src/routes/pipelines.ts b/server/src/routes/pipelines.ts index 910a5e6ba48f..68d50b8aaf69 100644 --- a/server/src/routes/pipelines.ts +++ b/server/src/routes/pipelines.ts @@ -2,7 +2,7 @@ import { Router, type Request } from "express"; import { z } from "zod"; import { and, asc, desc, eq, ilike, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { agents, documents, @@ -84,7 +84,7 @@ import { /** Per-stage instructions document keys look like `stage-instructions:{stageId}`. */ const STAGE_INSTRUCTIONS_PREFIX = "stage-instructions:"; -type PipelineRouteDb = Db | Parameters[0]>[0]; +type PipelineRouteDb = Db | DbTransaction; const stageKindSchema = z.enum(["open", "working", "review", "done", "cancelled"]); const jsonObjectSchema = z.record(z.string(), z.unknown()); diff --git a/server/src/services/agent-invokability.ts b/server/src/services/agent-invokability.ts index 155e2a9c35e4..626e5d55a57e 100644 --- a/server/src/services/agent-invokability.ts +++ b/server/src/services/agent-invokability.ts @@ -1,10 +1,9 @@ -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { agents } from "@paperclipai/db"; import { getAgentWorkEligibility, type AgentEligibilityAgent, type AgentOrgChainHealth } from "@paperclipai/shared"; import { eq } from "drizzle-orm"; type AgentStatus = (typeof agents.$inferSelect)["status"]; -type DbTransaction = Parameters[0]>[0]; export type AgentOrgRow = Pick< typeof agents.$inferSelect, diff --git a/server/src/services/branch-run-claims.ts b/server/src/services/branch-run-claims.ts index 2eb535399483..b6682e25b104 100644 --- a/server/src/services/branch-run-claims.ts +++ b/server/src/services/branch-run-claims.ts @@ -1,5 +1,5 @@ import { and, eq, isNull } from "drizzle-orm"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { branchRunClaims, externalRuntimeReservations, heartbeatRuns } from "@paperclipai/db"; import { TERMINAL_HEARTBEAT_RUN_STATUSES } from "./issues.js"; import { logger } from "../middleware/logger.js"; @@ -17,7 +17,6 @@ export type BranchRunClaim = typeof branchRunClaims.$inferSelect; const ACTIVE_BRANCH_CONSTRAINT = "branch_run_claims_active_branch_idx"; const DEFAULT_BRANCH_CLAIM_LEASE_MS = 30 * 60 * 1000; -type DbTransaction = Parameters[0]>[0]; type BranchClaimReadDb = Pick; export class BranchClaimConflictError extends Error { diff --git a/server/src/services/github-status-delivery-outbox.ts b/server/src/services/github-status-delivery-outbox.ts index 86dd4814bfae..5b60927eb3ef 100644 --- a/server/src/services/github-status-delivery-outbox.ts +++ b/server/src/services/github-status-delivery-outbox.ts @@ -4,6 +4,7 @@ import { heartbeatRunEvents, heartbeatRuns, type Db, + type DbTransaction, } from "@paperclipai/db"; import { logger } from "../middleware/logger.js"; import { @@ -15,7 +16,6 @@ import { } from "./github-app-auth.js"; type DeliveryRow = typeof githubCommitStatusDeliveries.$inferSelect; -type DbTransaction = Parameters[0]>[0]; // Either handle works for the delivery bookkeeping below. Code reached from // inside withGithubStatusDeliveryLock must use the transaction handle so the // critical section does not take a second pool connection. diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 126d573e4441..95a566117fc8 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -4,7 +4,7 @@ import { execFile as execFileCallback } from "node:child_process"; import { promisify } from "node:util"; import { createHash, randomUUID } from "node:crypto"; import { and, asc, desc, eq, exists, getTableColumns, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, not, notInArray, or, sql, type SQL } from "drizzle-orm"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; /** * Either the pool or an open transaction. BLO-19722 needs a handful of writes @@ -12,8 +12,7 @@ import type { Db } from "@paperclipai/db"; * helpers they go through accept an executor rather than always closing over * the pool. */ -type HeartbeatTx = Parameters[0]>[0]; -type HeartbeatDbExecutor = Db | HeartbeatTx; +type HeartbeatDbExecutor = Db | DbTransaction; /** * Outcome of `enqueueProcessLossRetry` (BLO-19722). @@ -584,7 +583,6 @@ import type { PluginWorkerManager } from "./plugin-worker-manager.js"; import { createServerGbrainClient } from "./gbrain-client-factory.js"; import { runSweepWakePreflight } from "./sweep-wake-preflight.js"; -type DbTransaction = Parameters[0]>[0]; type WakeCoalescingDb = Pick; // Run statuses considered terminal. Used to gate the agent-image-bump diff --git a/server/src/services/issue-checkout-status.ts b/server/src/services/issue-checkout-status.ts index 33cbb23f1317..a5ad1a70d229 100644 --- a/server/src/services/issue-checkout-status.ts +++ b/server/src/services/issue-checkout-status.ts @@ -1,9 +1,8 @@ import { and, eq, inArray, isNotNull, isNull, or, sql } from "drizzle-orm"; -import { heartbeatRuns, issueRelations, issues, type Db } from "@paperclipai/db"; +import { heartbeatRuns, issueRelations, issues, type Db, type DbTransaction } from "@paperclipai/db"; import { TERMINAL_HEARTBEAT_RUN_STATUS_VALUES } from "./issue-execution-lock.js"; import { buildIssueMonitorEligibilityPatch } from "./issue-execution-policy.js"; -type DbTransaction = Parameters[0]>[0]; type DbOrTransaction = Db | DbTransaction; /** diff --git a/server/src/services/issue-recovery-actions.ts b/server/src/services/issue-recovery-actions.ts index c6c783b93ca4..380ad0c5e332 100644 --- a/server/src/services/issue-recovery-actions.ts +++ b/server/src/services/issue-recovery-actions.ts @@ -1,5 +1,5 @@ import { and, asc, desc, eq, inArray, isNotNull, isNull, lte, sql } from "drizzle-orm"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { issueRecoveryActions, issues } from "@paperclipai/db"; import type { IssueRecoveryAction, @@ -82,7 +82,6 @@ export const RESOLVED_ISSUE_STATUS_EVIDENCE_KEY = "resolvedIssueStatus"; export const RECOVERY_HANDOFF_COMMENT_GRANT_TTL_MS = 24 * 60 * 60 * 1000; type IssueRecoveryActionRow = typeof issueRecoveryActions.$inferSelect; -type DbTransaction = Parameters[0]>[0]; type DbOrTransaction = Db | DbTransaction; export type UpsertIssueRecoveryActionInput = { diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 8eacc9b19017..fb07d51ae400 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -1,7 +1,7 @@ import { Buffer } from "node:buffer"; import { createHash } from "node:crypto"; import { and, asc, desc, eq, exists, gt, gte, inArray, isNotNull, isNull, like, lt, lte, ne, notInArray, or, sql, type SQL } from "drizzle-orm"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { activityLog, agentWakeupRequests, @@ -1007,7 +1007,6 @@ type IssueUserContextInput = { }; type ProjectGoalReader = Pick; type DbReader = Pick; -type DbTransaction = Parameters[0]>[0]; /** * Serialize mutations to the issue parent/blocker graph for one company. @@ -5735,7 +5734,7 @@ export function issueService(db: Db) { agentId: string, now: Date, operation: ( - tx: Parameters[0]>[0], + tx: DbTransaction, checkoutExecutionPatch: NonNullable>>["patch"], ) => Promise, ) { diff --git a/server/src/services/pipelines.ts b/server/src/services/pipelines.ts index 8ed489c24d4f..892218ae1880 100644 --- a/server/src/services/pipelines.ts +++ b/server/src/services/pipelines.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; import { isDeepStrictEqual } from "node:util"; import { and, asc, desc, eq, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { agentWakeupRequests, agents, @@ -178,7 +178,7 @@ export type PipelineAutomationExecutionResult = | { status: "succeeded"; execution: typeof pipelineAutomationExecutions.$inferSelect } | { status: "failed"; execution: typeof pipelineAutomationExecutions.$inferSelect }; -type PipelineDb = Db | Parameters[0]>[0]; +type PipelineDb = Db | DbTransaction; type PipelineRetryPlanInternal = PipelineAutomationRetryPlan & { targetStageRow: typeof pipelineStages.$inferSelect | null; diff --git a/server/src/services/pr-review-dispatch-lock.ts b/server/src/services/pr-review-dispatch-lock.ts index 5292e47a4106..646d39ed4e96 100644 --- a/server/src/services/pr-review-dispatch-lock.ts +++ b/server/src/services/pr-review-dispatch-lock.ts @@ -1,12 +1,11 @@ import { and, eq, sql } from "drizzle-orm"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { heartbeatRuns } from "@paperclipai/db"; import { matchesTaskKey, prReviewTaskLockSpellings, } from "./pr-review-duplicate-issue-guard.js"; -type DbTransaction = Parameters[0]>[0]; const PR_REVIEW_DISPATCH_LOCK_PREFIX = "heartbeat:pr-review-dispatch:"; diff --git a/server/src/services/pr-review-duplicate-issue-guard.ts b/server/src/services/pr-review-duplicate-issue-guard.ts index e3a25a455f09..6efcd15ac37a 100644 --- a/server/src/services/pr-review-duplicate-issue-guard.ts +++ b/server/src/services/pr-review-duplicate-issue-guard.ts @@ -63,13 +63,12 @@ * — one duplicate that slips through while the webhook's wake is still * uncommitted — remains tracked as BLO-21790. */ -import { type Db, heartbeatRuns } from "@paperclipai/db"; +import { type Db, type DbTransaction, heartbeatRuns } from "@paperclipai/db"; import { type Column, type SQL, and, desc, eq, inArray, or, sql } from "drizzle-orm"; import { readGithubPrReviewerAgentIds } from "../config.js"; import { conflict } from "../errors.js"; import { logger } from "../middleware/logger.js"; -type DbTransaction = Parameters[0]>[0]; type DuplicatePrReviewIssueGuardDb = Pick; /** diff --git a/server/src/services/productivity-review.ts b/server/src/services/productivity-review.ts index 054cb3bbb238..86f89c97f9d6 100644 --- a/server/src/services/productivity-review.ts +++ b/server/src/services/productivity-review.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, notInArray, or, sql } from "drizzle-orm"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { clampIssueRequestDepth } from "@paperclipai/shared"; import { activityLog, @@ -1962,7 +1962,7 @@ function dominantErrorCode( * the BLO-3737 refresh-throttle critical section accept this so the read and the * write land on the same connection (and therefore inside the same advisory lock). */ -type DbOrTx = Db | Parameters[0]>[0]; +type DbOrTx = Db | DbTransaction; export function productivityReviewService(db: Db, deps?: ProductivityReviewServiceDeps) { const issuesSvc = issueService(db); diff --git a/server/src/services/recovery/service.ts b/server/src/services/recovery/service.ts index 169ea6212e1f..c4be772b57b9 100644 --- a/server/src/services/recovery/service.ts +++ b/server/src/services/recovery/service.ts @@ -1,5 +1,5 @@ import { and, asc, desc, eq, gt, gte, inArray, isNull, lt, notInArray, or, sql } from "drizzle-orm"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { DEFAULT_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS, MAX_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS, @@ -146,7 +146,6 @@ import { ZERO_TOKEN_SESSION_RESET_RETRY_REASON, } from "./zero-token-startup-failure.js"; -type DbTransaction = Parameters[0]>[0]; /** * The recovery transaction has already upserted its source-scoped action when diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index b1a197f8c2f2..768ff89be972 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { and, desc, eq, inArray, like, ne, notInArray, or, sql } from "drizzle-orm"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { agents, companySecretBindings, @@ -75,7 +75,6 @@ const FALLBACK_ADAPTER_SCHEMA_SECRET_FIELDS: Readonly[0]>[0]; type SecretBindingDb = Pick; function isUniqueConstraintViolation(error: unknown, constraintName: string) { diff --git a/server/src/services/tool-access.ts b/server/src/services/tool-access.ts index d3593c1f753a..c43694fd8602 100644 --- a/server/src/services/tool-access.ts +++ b/server/src/services/tool-access.ts @@ -1,7 +1,7 @@ import { createHash, randomBytes, randomUUID } from "node:crypto"; import { readFileSync } from "node:fs"; import { and, asc, desc, eq, gte, inArray, isNull, lt, max, ne, sql } from "drizzle-orm"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { activityLog, agents, @@ -153,7 +153,6 @@ type ToolAccessServiceOptions = { now?: () => Date; }; -type DbTransaction = Parameters[0]>[0]; type ToolAccessMutationDb = Pick; export type McpToolDescriptor = { diff --git a/server/src/services/tool-oauth-legacy-backfill.ts b/server/src/services/tool-oauth-legacy-backfill.ts index ca7c23c68276..533282d524e2 100644 --- a/server/src/services/tool-oauth-legacy-backfill.ts +++ b/server/src/services/tool-oauth-legacy-backfill.ts @@ -1,5 +1,5 @@ import { and, eq, ne, sql } from "drizzle-orm"; -import type { Db } from "@paperclipai/db"; +import type { Db, DbTransaction } from "@paperclipai/db"; import { companySecretBindings, companySecretProviderConfigs, @@ -11,8 +11,6 @@ import type { McpConnectionCredentialRef, SecretProvider, ToolCredentialSecretRe import { getSecretProvider } from "../secrets/provider-registry.js"; import type { SecretProviderVaultRuntimeConfig } from "../secrets/types.js"; -type DbTransaction = Parameters[0]>[0]; - type OAuthTokenKind = "access_token" | "refresh_token"; type LegacyOAuthToken = {