teqo-backup/src/utilities/postgresTransactionLocks.ts
Francisco Ignácio Fontoura Solla 4806c417c8 Engineering Pass 3: consolidate patterns and lock them with guards.
Fold duplicated campanha access, loaders, list/filter/form shells, and scripts into shared modules; reorganize utilities by domain; stabilize e2e; and document prevention-first audit workflow so the same smells cannot creep back in.
2026-07-29 06:11:29 -03:00

61 lines
2.3 KiB
TypeScript

import 'server-only'
import { sql } from '@payloadcms/db-postgres'
import type { Payload, PayloadRequest } from 'payload'
export type PostgresTransactionDatabase = {
execute: (query: ReturnType<typeof sql>) => Promise<unknown>
}
/** Thrown where the phone-dedup lock needs the Postgres adapter — one spelling for every action that guards on it. */
export const POSTGRES_DEDUP_LOCK_MESSAGE =
'O bloqueio de deduplicação exige o adaptador PostgreSQL.'
type TransactionPayload = Pick<Payload, 'db'>
type TransactionRequest = {
transactionID?: PayloadRequest['transactionID']
}
export const getPostgresTransactionDatabase = async (
payload: TransactionPayload,
req: TransactionRequest,
): Promise<PostgresTransactionDatabase> => {
if (payload.db.name !== 'postgres') {
throw new Error('O bloqueio transacional exige o adaptador PostgreSQL.')
}
const transactionID = await req.transactionID
if (transactionID === undefined || transactionID === null) {
throw new Error('A transação PostgreSQL não está disponível.')
}
const database = payload.db.sessions?.[String(transactionID)]?.db as
| PostgresTransactionDatabase
| undefined
if (!database || typeof database.execute !== 'function') {
throw new Error('A sessão PostgreSQL da transação não está disponível.')
}
return database
}
export const acquireTextAdvisoryLocks = async (
payload: TransactionPayload,
req: TransactionRequest,
keys: string[],
): Promise<void> => {
if (keys.some((key) => typeof key !== 'string' || key.trim().length === 0)) {
throw new Error('A chave do bloqueio PostgreSQL deve ser um texto não vazio.')
}
const sortedKeys = [...new Set(keys)].sort()
if (sortedKeys.length === 0) return
const database = await getPostgresTransactionDatabase(payload, req)
// Batch every key into a single round trip: unnest() preserves array order for a
// plain function scan, and pg_advisory_xact_lock is volatile (never parallelized),
// so locks are still acquired sequentially in sorted order — same deadlock-avoidance
// guarantee as the previous per-key loop, at 1 round trip instead of N.
const keyFragments = sortedKeys.map((key) => sql`${key}`)
await database.execute(
sql`SELECT pg_advisory_xact_lock(hashtextextended(k, 0)) FROM unnest(ARRAY[${sql.join(keyFragments, sql`, `)}]::text[]) AS k`,
)
}