UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

255 lines (254 loc) • 9.23 kB
import { getCurrentAdapter, getCurrentAuthContext, queueAfterTransactionHook } from "@better-auth/core/context"; import { ATTR_CONTEXT, ATTR_DB_COLLECTION_NAME, ATTR_HOOK_TYPE, withSpan } from "@better-auth/core/instrumentation"; //#region src/db/with-hooks.ts function getWithHooks(adapter, ctx) { const hooksEntries = ctx.hooks; async function createWithHooks(data, model, customCreateFn) { const context = await getCurrentAuthContext().catch(() => null); let actualData = data; for (const { source, hooks } of hooksEntries) { const toRun = hooks[model]?.create?.before; if (toRun) { const result = await withSpan(`db create.before ${model}`, { [ATTR_HOOK_TYPE]: "create.before", [ATTR_DB_COLLECTION_NAME]: model, [ATTR_CONTEXT]: source }, () => toRun(actualData, context)); if (result === false) return null; if (typeof result === "object" && "data" in result) actualData = { ...actualData, ...result.data }; } } let created = null; if (!customCreateFn || customCreateFn.executeMainFn) created = await (await getCurrentAdapter(adapter)).create({ model, data: actualData, forceAllowId: true }); if (customCreateFn?.fn) created = await customCreateFn.fn(created ?? actualData); for (const { source, hooks } of hooksEntries) { const toRun = hooks[model]?.create?.after; if (toRun) await queueAfterTransactionHook(async () => { await withSpan(`db create.after ${model}`, { [ATTR_HOOK_TYPE]: "create.after", [ATTR_DB_COLLECTION_NAME]: model, [ATTR_CONTEXT]: source }, () => toRun(created, context)); }); } return created; } async function updateWithHooks(data, where, model, customUpdateFn) { const context = await getCurrentAuthContext().catch(() => null); let actualData = data; for (const { source, hooks } of hooksEntries) { const toRun = hooks[model]?.update?.before; if (toRun) { const result = await withSpan(`db update.before ${model}`, { [ATTR_HOOK_TYPE]: "update.before", [ATTR_DB_COLLECTION_NAME]: model, [ATTR_CONTEXT]: source }, () => toRun(data, context)); if (result === false) return null; if (typeof result === "object" && "data" in result) actualData = { ...actualData, ...result.data }; } } const customUpdated = customUpdateFn ? await customUpdateFn.fn(actualData) : null; const updated = !customUpdateFn || customUpdateFn.executeMainFn ? await (await getCurrentAdapter(adapter)).update({ model, update: actualData, where }) : customUpdated; for (const { source, hooks } of hooksEntries) { const toRun = hooks[model]?.update?.after; if (toRun) await queueAfterTransactionHook(async () => { await withSpan(`db update.after ${model}`, { [ATTR_HOOK_TYPE]: "update.after", [ATTR_DB_COLLECTION_NAME]: model, [ATTR_CONTEXT]: source }, () => toRun(updated, context)); }); } return updated; } async function updateManyWithHooks(data, where, model, customUpdateFn) { const context = await getCurrentAuthContext().catch(() => null); let actualData = data; for (const { source, hooks } of hooksEntries) { const toRun = hooks[model]?.update?.before; if (toRun) { const result = await withSpan(`db updateMany.before ${model}`, { [ATTR_HOOK_TYPE]: "updateMany.before", [ATTR_DB_COLLECTION_NAME]: model, [ATTR_CONTEXT]: source }, () => toRun(data, context)); if (result === false) return null; if (typeof result === "object" && "data" in result) actualData = { ...actualData, ...result.data }; } } const customUpdated = customUpdateFn ? await customUpdateFn.fn(actualData) : null; const updated = !customUpdateFn || customUpdateFn.executeMainFn ? await (await getCurrentAdapter(adapter)).updateMany({ model, update: actualData, where }) : customUpdated; for (const { source, hooks } of hooksEntries) { const toRun = hooks[model]?.update?.after; if (toRun) await queueAfterTransactionHook(async () => { await withSpan(`db updateMany.after ${model}`, { [ATTR_HOOK_TYPE]: "updateMany.after", [ATTR_DB_COLLECTION_NAME]: model, [ATTR_CONTEXT]: source }, () => toRun(updated, context)); }); } return updated; } async function deleteWithHooks(where, model, customDeleteFn) { const context = await getCurrentAuthContext().catch(() => null); let entityToDelete = null; try { entityToDelete = (await (await getCurrentAdapter(adapter)).findMany({ model, where, limit: 1 }))[0] || null; } catch {} if (entityToDelete) for (const { source, hooks } of hooksEntries) { const toRun = hooks[model]?.delete?.before; if (toRun) { if (await withSpan(`db delete.before ${model}`, { [ATTR_HOOK_TYPE]: "delete.before", [ATTR_DB_COLLECTION_NAME]: model, [ATTR_CONTEXT]: source }, () => toRun(entityToDelete, context)) === false) return null; } } const customDeleted = customDeleteFn ? await customDeleteFn.fn(where) : null; const deleted = (!customDeleteFn || customDeleteFn.executeMainFn) && entityToDelete ? await (await getCurrentAdapter(adapter)).delete({ model, where }) : customDeleted; if (entityToDelete) for (const { source, hooks } of hooksEntries) { const toRun = hooks[model]?.delete?.after; if (toRun) await queueAfterTransactionHook(async () => { await withSpan(`db delete.after ${model}`, { [ATTR_HOOK_TYPE]: "delete.after", [ATTR_DB_COLLECTION_NAME]: model, [ATTR_CONTEXT]: source }, () => toRun(entityToDelete, context)); }); } return deleted; } async function deleteManyWithHooks(where, model, customDeleteFn) { const context = await getCurrentAuthContext().catch(() => null); let entitiesToDelete = []; try { entitiesToDelete = await (await getCurrentAdapter(adapter)).findMany({ model, where }); } catch {} for (const entity of entitiesToDelete) for (const { source, hooks } of hooksEntries) { const toRun = hooks[model]?.delete?.before; if (toRun) { if (await withSpan(`db delete.before ${model}`, { [ATTR_HOOK_TYPE]: "delete.before", [ATTR_DB_COLLECTION_NAME]: model, [ATTR_CONTEXT]: source }, () => toRun(entity, context)) === false) return null; } } const customDeleted = customDeleteFn ? await customDeleteFn.fn(where) : null; const deleted = !customDeleteFn || customDeleteFn.executeMainFn ? await (await getCurrentAdapter(adapter)).deleteMany({ model, where }) : customDeleted; for (const entity of entitiesToDelete) for (const { source, hooks } of hooksEntries) { const toRun = hooks[model]?.delete?.after; if (toRun) await queueAfterTransactionHook(async () => { await withSpan(`db delete.after ${model}`, { [ATTR_HOOK_TYPE]: "delete.after", [ATTR_DB_COLLECTION_NAME]: model, [ATTR_CONTEXT]: source }, () => toRun(entity, context)); }); } return deleted; } /** * Wraps an atomic consume operation in the plugin `delete.before` and * `delete.after` hook lifecycle. The caller supplies a `consumeFn` that * performs the actual single-row delete-and-return (typically the * adapter's `consumeOne`). The first concurrent caller wins, subsequent * racers resolve to `null` without firing `delete.after` hooks. * * `preSnapshot` lets the caller hand in a row it already fetched so * `delete.before` hooks don't trigger a second read. Without it, the * helper falls back to a best-effort `findMany` against `hookWhere`. * The snapshot only feeds `delete.before`; the `consumeFn` return value * is the race gate. * * Returning `false` from a `delete.before` hook aborts the consume and * the helper resolves to `null` (no `consumeFn` call, no after hooks). */ async function consumeOneWithHooks(model, hookWhere, consumeFn, preSnapshot) { const context = await getCurrentAuthContext().catch(() => null); const beforeHooks = hooksEntries.flatMap(({ source, hooks }) => { const fn = hooks[model]?.delete?.before; return fn ? [{ source, fn }] : []; }); let snapshot = preSnapshot ?? null; if (beforeHooks.length) { if (!snapshot) try { snapshot = (await (await getCurrentAdapter(adapter)).findMany({ model, where: hookWhere, limit: 1 }))[0] || null; } catch {} if (snapshot) { for (const { source, fn } of beforeHooks) if (await withSpan(`db delete.before ${model}`, { [ATTR_HOOK_TYPE]: "delete.before", [ATTR_DB_COLLECTION_NAME]: model, [ATTR_CONTEXT]: source }, () => fn(snapshot, context)) === false) return null; } } const consumed = await consumeFn(); if (!consumed) return null; for (const { source, hooks } of hooksEntries) { const toRun = hooks[model]?.delete?.after; if (toRun) await queueAfterTransactionHook(async () => { await withSpan(`db delete.after ${model}`, { [ATTR_HOOK_TYPE]: "delete.after", [ATTR_DB_COLLECTION_NAME]: model, [ATTR_CONTEXT]: source }, () => toRun(consumed, context)); }); } return consumed; } return { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks, consumeOneWithHooks }; } //#endregion export { getWithHooks };