import type { Selectable } from 'kysely'
import type * as Webhooks from '../../internal/Webhooks.js'
import type * as Db from '../Db.js'
import type * as db_Schema from '../Schema.js'
/** Columns of the `webhook_deliveries` table, derived from `Schema.WebhookDelivery`. */
export type Table = db_Schema.WebhookDelivery
/** A stored webhook-delivery row. */
export type Record = Selectable
/** A failed delivery joined to its organization-owned subscription. */
export type OrganizationFailure = Pick<
Record,
'attempt' | 'createdAt' | 'error' | 'id' | 'responseStatus' | 'subscriptionId'
>
/** Maps a stored row to the domain delivery (null columns → absent fields). */
export function toDelivery(row: Record): Webhooks.Delivery {
return {
attempt: row.attempt,
createdAt: row.createdAt,
envelope: row.envelope,
...(row.error === null ? {} : { error: row.error }),
eventId: row.eventId,
id: row.id,
requestUrl: row.requestUrl,
...(row.responseMs === null ? {} : { responseMs: row.responseMs }),
...(row.responseStatus === null ? {} : { responseStatus: row.responseStatus }),
status: row.status,
subscriptionId: row.subscriptionId,
}
}
/**
* Inserts a delivery row from its domain value.
*
* @param db - The database.
* @param delivery - The delivery to insert.
* @param expiresAt - Retention deadline (ISO); the row is invisible past it.
*/
export async function insert(
db: Db.Db,
delivery: Webhooks.Delivery,
expiresAt: string,
): Promise {
await db.kysely
.insertInto('webhook_deliveries')
.values({
attempt: delivery.attempt,
createdAt: delivery.createdAt,
envelope: delivery.envelope,
error: delivery.error ?? null,
eventId: delivery.eventId,
expiresAt,
id: delivery.id,
requestUrl: delivery.requestUrl,
responseMs: delivery.responseMs ?? null,
responseStatus: delivery.responseStatus ?? null,
status: delivery.status,
subscriptionId: delivery.subscriptionId,
})
.execute()
}
/**
* Lists a subscription's unexpired deliveries, newest first, with keyset
* paging (`id < cursor` — ids embed a zero-padded timestamp, so lexical order
* is chronological).
*
* @param db - The database.
* @param subscriptionId - The owning subscription id (`wh_…`).
* @param options - Paging options plus the retention timestamp.
* @returns The deliveries.
*/
export async function list(
db: Db.Db,
subscriptionId: string,
options: list.Options,
): Promise {
let query = db.kysely
.selectFrom('webhook_deliveries')
.selectAll()
.where('subscriptionId', '=', subscriptionId)
.where('expiresAt', '>', options.now)
.orderBy('id', 'desc')
if (options.cursor) query = query.where('id', '<', options.cursor)
if (options.offset) query = query.offset(options.offset)
if (options.limit !== undefined) query = query.limit(options.limit)
return (await query.execute()).map(toDelivery)
}
/** Lists recent failed webhook deliveries across an organization. */
export function listFailuresByOrg(
db: Db.Db,
orgId: string,
options: listFailuresByOrg.Options,
): Promise {
return db.kysely
.selectFrom('webhook_deliveries')
.innerJoin(
'webhook_subscriptions',
'webhook_subscriptions.id',
'webhook_deliveries.subscriptionId',
)
.select([
'webhook_deliveries.attempt',
'webhook_deliveries.createdAt',
'webhook_deliveries.error',
'webhook_deliveries.id',
'webhook_deliveries.responseStatus',
'webhook_deliveries.subscriptionId',
])
.where('webhook_subscriptions.ownerId', '=', orgId)
.where('webhook_subscriptions.ownerType', '=', 'api_key')
.where('webhook_deliveries.createdAt', '>=', options.from)
.where('webhook_deliveries.expiresAt', '>', options.now)
.where('webhook_deliveries.status', '=', 'failed')
.orderBy('webhook_deliveries.createdAt', 'desc')
.orderBy('webhook_deliveries.id', 'desc')
.limit(options.limit)
.execute()
}
export declare namespace listFailuresByOrg {
/** Bounds for the organization failure scan. */
type Options = {
/** Window start (ISO 8601), inclusive. */
from: string
/** Maximum failures to return. */
limit: number
/** ISO timestamp retention is evaluated against. */
now: string
}
}
/** Counts failed webhook deliveries across an organization in a time window. */
export async function countFailuresByOrg(
db: Db.Db,
orgId: string,
options: countFailuresByOrg.Options,
): Promise {
const row = await db.kysely
.selectFrom('webhook_deliveries')
.innerJoin(
'webhook_subscriptions',
'webhook_subscriptions.id',
'webhook_deliveries.subscriptionId',
)
.select((eb) => eb.fn.countAll().as('count'))
.where('webhook_subscriptions.ownerId', '=', orgId)
.where('webhook_subscriptions.ownerType', '=', 'api_key')
.where('webhook_deliveries.createdAt', '>=', options.from)
.where('webhook_deliveries.expiresAt', '>', options.now)
.where('webhook_deliveries.status', '=', 'failed')
.executeTakeFirstOrThrow()
return Number(row.count)
}
export declare namespace countFailuresByOrg {
/** Bounds for the organization failure count. */
type Options = {
/** Window start (ISO 8601), inclusive. */
from: string
/** ISO timestamp retention is evaluated against. */
now: string
}
}
export declare namespace list {
/** Options for {@link list}. */
type Options = {
/** Return deliveries whose id sorts before this one (older entries). */
cursor?: string | undefined
/** Maximum deliveries to return. */
limit?: number | undefined
/** ISO timestamp retention is evaluated against. */
now: string
/** Rows to skip from the head (positional pagination; exclusive with `cursor`). */
offset?: number | undefined
}
}
/**
* Counts a subscription's unexpired deliveries.
*
* @param db - The database.
* @param subscriptionId - The owning subscription id (`wh_…`).
* @param now - ISO timestamp retention is evaluated against.
* @returns The count.
*/
export async function count(db: Db.Db, subscriptionId: string, now: string): Promise {
const row = await db.kysely
.selectFrom('webhook_deliveries')
.select((eb) => eb.fn.countAll().as('count'))
.where('subscriptionId', '=', subscriptionId)
.where('expiresAt', '>', now)
.executeTakeFirstOrThrow()
return Number(row.count)
}
/**
* Reads a single unexpired delivery scoped to its subscription.
*
* @param db - The database.
* @param subscriptionId - The owning subscription id (`wh_…`).
* @param deliveryId - The delivery id (`whd_…`).
* @param now - ISO timestamp retention is evaluated against.
* @returns The delivery, or `null` when absent.
*/
export async function get(
db: Db.Db,
subscriptionId: string,
deliveryId: string,
now: string,
): Promise {
const row = await db.kysely
.selectFrom('webhook_deliveries')
.selectAll()
.where('subscriptionId', '=', subscriptionId)
.where('id', '=', deliveryId)
.where('expiresAt', '>', now)
.executeTakeFirst()
return row ? toDelivery(row) : null
}
// Per-statement delete bound: prune runs every minute, so a backlog drains in
// a few ticks instead of one unbounded DELETE churning the origin.
const pruneBatchLimit = 50_000
/**
* Deletes deliveries past their retention deadline. Called best-effort from
* the poller tick.
*
* @param db - The database.
* @param now - ISO timestamp retention is evaluated against.
* @returns The number of rows deleted.
*/
export async function pruneExpired(db: Db.Db, now: string): Promise {
const result = await db.kysely
.deleteFrom('webhook_deliveries')
// Repeated on the outer delete: the row recheck under concurrency re-runs
// only this predicate, so a row renewed mid-statement survives.
.where('expiresAt', '<=', now)
.where('id', 'in', (eb) =>
eb
.selectFrom('webhook_deliveries')
.select('id')
.where('expiresAt', '<=', now)
.limit(pruneBatchLimit),
)
.executeTakeFirst()
return Number(result.numDeletedRows)
}