import type { AdminEventContext, AfterLifecycleHookEvent, BeforeLifecycleHookEvent, LifecycleHookAbort, LifecycleHookContext } from './types' import { getRegisteredAdminHooks, getRegisteredHooks } from './registry' function isLifecycleHookAbort(value: unknown): value is LifecycleHookAbort { return ( typeof value === 'object' && value !== null && 'abort' in value && (value as { abort: unknown }).abort === true ) } function createAbortError(event: BeforeLifecycleHookEvent, context: LifecycleHookContext): Error { return new Error(`Lifecycle hook ${event} aborted ${context.entityType}`) } export async function runBeforeHooks( event: BeforeLifecycleHookEvent, context: LifecycleHookContext ): Promise { const handlers = getRegisteredHooks(context.entityType, event) for (const handler of handlers) { const result = await handler(context) if (isLifecycleHookAbort(result)) { throw result.reason ? new Error(result.reason) : createAbortError(event, context) } } } function getAdminActionFromLifecycleEvent( event: AfterLifecycleHookEvent ): AdminEventContext['action'] | null { if (event === 'afterCreate') return 'create' if (event === 'afterUpdate') return 'update' if (event === 'afterDelete') return 'delete' if (event === 'afterPublish') return 'publish' if (event === 'afterUnpublish') return 'unpublish' if (event === 'afterRestore') return 'restore' if (event === 'afterReorder') return 'reorder' return null } function getTargetLabel(data: Record | undefined): string | null { if (!data) return null for (const fieldName of ['title', 'name', 'label', 'slug', 'email', 'filename']) { const value = data[fieldName] if (typeof value === 'string' && value.trim()) return value } return null } function getChangedFieldsFromLifecycleContext( event: AfterLifecycleHookEvent, context: LifecycleHookContext ): string[] { if (context.changedFields) return context.changedFields if (event === 'afterDelete') return [] return Object.keys(context.input ?? {}) .filter((fieldName) => fieldName !== 'id') .sort((a, b) => a.localeCompare(b)) } function toAdminEventContext( event: AfterLifecycleHookEvent, context: LifecycleHookContext ): AdminEventContext | null { const action = getAdminActionFromLifecycleEvent(event) if (!action) return null return { category: context.auditCategory ?? (context.schemaType === 'form' ? 'form' : 'entity'), action, targetType: context.entityType, targetId: context.entityId ?? null, targetLabel: context.targetLabel ?? getTargetLabel(context.data), user: context.user, timestamp: context.timestamp, sourceType: context.sourceType ?? 'admin_ui', ipAddress: context.ipAddress ?? null, userAgent: context.userAgent ?? null, changedFields: getChangedFieldsFromLifecycleContext(event, context), metadata: { entityType: context.entityType, schemaType: context.schemaType ?? 'entity', ...context.metadata } } } function runSettledHook( handler: (context: Context) => void | Promise, context: Context ): Promise { try { return Promise.resolve(handler(context)) } catch (error) { return Promise.reject(error) } } export function runAdminEventHooks(context: AdminEventContext): void { const handlers = getRegisteredAdminHooks({ category: context.category, action: context.action }) if (handlers.length === 0) return void Promise.allSettled(handlers.map((handler) => runSettledHook(handler, context))).then( (results) => { for (const result of results) { if (result.status === 'rejected') { console.error('Admin event hook failed:', { category: context.category, action: context.action, targetType: context.targetType, targetId: context.targetId, error: result.reason }) } } } ) } export function runAfterHooks(event: AfterLifecycleHookEvent, context: LifecycleHookContext): void { const handlers = getRegisteredHooks(context.entityType, event) if (handlers.length > 0) { void Promise.allSettled(handlers.map((handler) => runSettledHook(handler, context))).then( (results) => { for (const result of results) { if (result.status === 'rejected') { console.error('Lifecycle hook failed:', { entityType: context.entityType, event, error: result.reason }) } } } ) } const adminEvent = toAdminEventContext(event, context) if (adminEvent) { runAdminEventHooks(adminEvent) } }