import { type EmptyFields } from "@tailor-platform/erp-kit/core"; import type { TailorAnyDBField } from "@tailor-platform/sdk"; import type { UserManagementModule } from "../user-management"; import { activateNotificationChannel } from "./command/activateNotificationChannel.generated"; import { anonymizeNotificationsForUser } from "./command/anonymizeNotificationsForUser.generated"; import { archiveNotification } from "./command/archiveNotification.generated"; // Slack destination (optional; gated by the `slack` defineModule option) import { beginSlackWorkspaceInstall } from "./command/beginSlackWorkspaceInstall.generated"; import { bulkUpdateNotificationPreferences } from "./command/bulkUpdateNotificationPreferences.generated"; import { completeSlackWorkspaceInstall } from "./command/completeSlackWorkspaceInstall.generated"; import { createNotificationChannel } from "./command/createNotificationChannel.generated"; import { createNotificationSubscription } from "./command/createNotificationSubscription.generated"; import { createNotificationTemplate } from "./command/createNotificationTemplate.generated"; import { deactivateNotificationChannel } from "./command/deactivateNotificationChannel.generated"; import { deleteNotificationSubscription } from "./command/deleteNotificationSubscription.generated"; import { deleteNotificationTemplate } from "./command/deleteNotificationTemplate.generated"; import { handleSlackAppUninstalled } from "./command/handleSlackAppUninstalled.generated"; import { logNotificationEvent } from "./command/logNotificationEvent.generated"; import { markAllNotificationsAsRead } from "./command/markAllNotificationsAsRead.generated"; import { markNotificationAsRead } from "./command/markNotificationAsRead.generated"; import { markNotificationsAsSeen } from "./command/markNotificationsAsSeen.generated"; import { recordDeliveryEvent } from "./command/recordDeliveryEvent.generated"; import { resetNotificationPreferences } from "./command/resetNotificationPreferences.generated"; import { runNotificationAuditRetentionSweep } from "./command/runNotificationAuditRetentionSweep.generated"; import { updateNotificationChannel } from "./command/updateNotificationChannel.generated"; import { updateNotificationPreference } from "./command/updateNotificationPreference.generated"; import { updateNotificationTemplate } from "./command/updateNotificationTemplate.generated"; import { createChannelRoutingBindingType, type CreateChannelRoutingBindingTypeParams, } from "./db/channelRoutingBinding"; import { createDestinationDeliveryLogType } from "./db/destinationDeliveryLog"; import { eventCategoryBinding } from "./db/eventCategoryBinding"; import { createNotificationType, type CreateNotificationTypeParams } from "./db/notification"; import { notificationCategory } from "./db/notificationCategory"; import { notificationChannel } from "./db/notificationChannel"; import { createNotificationDeliveryAuditType, type CreateNotificationDeliveryAuditTypeParams, } from "./db/notificationDeliveryAudit"; import { createNotificationEventType, type CreateNotificationEventTypeParams, } from "./db/notificationEvent"; import { createNotificationPreferenceType, type CreateNotificationPreferenceTypeParams, } from "./db/notificationPreference"; import { createNotificationSubscriptionType, type CreateNotificationSubscriptionTypeParams, } from "./db/notificationSubscription"; import { notificationTemplate } from "./db/notificationTemplate"; import { createSlackWorkspaceIntegrationType, type CreateSlackWorkspaceIntegrationTypeParams, } from "./db/slackWorkspaceIntegration"; import { dispatchNotificationEvents, type DispatchNotificationEventsParams, } from "./executor/dispatchNotificationEvents"; import { redrainNotificationEvents, type RedrainNotificationEventsParams, } from "./executor/redrainNotificationEvents"; import type { DB } from "./generated/kysely-tailordb"; import type { DispatchAdapters } from "./lib/dispatchAdapters"; import type { SlackChatPostMessageResult } from "./lib/slackClient"; import { createSlackDestinationAdapter } from "./lib/slackDestinationAdapter"; import { getEffectiveNotificationPreference } from "./query/getEffectiveNotificationPreference.generated"; import { getEventCategoryBinding } from "./query/getEventCategoryBinding.generated"; import { getNotification } from "./query/getNotification.generated"; import { getNotificationChannel } from "./query/getNotificationChannel.generated"; import { getNotificationPreference } from "./query/getNotificationPreference.generated"; import { getNotificationTemplate } from "./query/getNotificationTemplate.generated"; import { getSlackWorkspaceIntegration } from "./query/getSlackWorkspaceIntegration.generated"; import { listAuditByNotification } from "./query/listAuditByNotification.generated"; import { listAuditBySource } from "./query/listAuditBySource.generated"; import { listInboxNotifications } from "./query/listInboxNotifications.generated"; import { listNotificationCategories } from "./query/listNotificationCategories.generated"; import { listNotificationChannels } from "./query/listNotificationChannels.generated"; import { listNotificationPreferences } from "./query/listNotificationPreferences.generated"; import { listNotificationSubscriptions } from "./query/listNotificationSubscriptions.generated"; import { listNotificationTemplates } from "./query/listNotificationTemplates.generated"; import { renderNotificationTemplate } from "./query/renderNotificationTemplate.generated"; import { searchNotificationDeliveryAudit } from "./query/searchNotificationDeliveryAudit.generated"; type UserManagementDB = UserManagementModule["db"]; /** * Opt-in Slack destination configuration. When present, {@link defineModule} * adds the `SlackWorkspaceIntegration` model, the install/uninstall commands, and * the redacted connection query, and auto-wires the Slack `DestinationAdapter` * into `adapters.destination` (so the host app does not pass `destination` * itself). When omitted, none of the Slack schema or commands are created. */ export interface SlackOptions = EmptyFields> { secretManager: { getSecret(vault: string, key: string): Promise; }; slackWorkspaceIntegration?: CreateSlackWorkspaceIntegrationTypeParams; postChatMessage?: ( botToken: string, channel: string, text: string, ) => Promise; } /** Configuration for {@link defineModule}: optional model extensions, the user-management linkage, channel adapters, and executor wiring. */ export interface DefineModuleParams< NF extends Record = EmptyFields, PF extends Record = EmptyFields, SF extends Record = EmptyFields, AF extends Record = EmptyFields, CF extends Record = EmptyFields, EvF extends Record = EmptyFields, RedrainCron extends string = string, WF extends Record = EmptyFields, S extends SlackOptions | undefined = undefined, > { notification?: CreateNotificationTypeParams; notificationPreference?: Omit, "userType">; notificationSubscription?: Omit, "userType">; notificationDeliveryAudit?: CreateNotificationDeliveryAuditTypeParams; channelRoutingBinding?: CreateChannelRoutingBindingTypeParams; notificationEvent?: CreateNotificationEventTypeParams; userManagement: { db: { user: UserManagementDB["user"] } }; /** * Channel-delivery ports. When the `slack` option is enabled the module * supplies `destination` itself, so the host app must not pass it — the type * omits `destination` in that case. */ adapters: S extends SlackOptions ? Omit : DispatchAdapters; /** Opt-in Slack destination; when present the module creates the Slack schema/commands and auto-wires the Slack adapter. */ slack?: S; executors: { /** Returns the app-side Kysely instance for the shared DB (shared by all executors). */ getDB: DispatchNotificationEventsParams["getDB"]; /** CDC drain of newly created NotificationEvents (primary delivery path). */ dispatchNotificationEvents?: Omit< DispatchNotificationEventsParams, "notificationEventType" | "adapters" | "getDB" >; /** Insurance cron re-draining NotificationEvents stuck in PENDING. */ redrainNotificationEvents: Omit< RedrainNotificationEventsParams, "adapters" | "getDB" >; }; } /** * Build the optional Slack destination parts: the `SlackWorkspaceIntegration` * model, the install/uninstall commands, the redacted connection query, and the * concrete Slack {@link createSlackDestinationAdapter} wired against the * module's own connection row (read/written through the executor db handle). */ function buildSlackParts>( slack: SlackOptions, getDB: () => DB, now: () => Date, ) { const slackWorkspaceIntegration = createSlackWorkspaceIntegrationType( slack.slackWorkspaceIntegration ?? {}, ); const loadConnection = async (): Promise<{ status: string; teamId: string } | null> => { const conn = await getDB() .selectFrom("SlackWorkspaceIntegration") .select(["status", "teamId"]) .executeTakeFirst(); return conn ? { status: conn.status, teamId: conn.teamId } : null; }; const destination = createSlackDestinationAdapter({ loadConnection, secretManager: slack.secretManager, markConnectionRevoked: async (_reason: string) => { await getDB() .updateTable("SlackWorkspaceIntegration") .set({ status: "REVOKED", revokedAt: now() }) .where("status", "=", "ACTIVE") .execute(); }, ...(slack.postChatMessage ? { postChatMessage: slack.postChatMessage } : {}), }); return { db: { slackWorkspaceIntegration }, commands: { beginSlackWorkspaceInstall: beginSlackWorkspaceInstall(), completeSlackWorkspaceInstall: completeSlackWorkspaceInstall(), handleSlackAppUninstalled: handleSlackAppUninstalled(), }, queries: { getSlackWorkspaceIntegration }, destination, }; } /** Slack parts merged in only when the `slack` option is enabled (type-level gate keyed on `S`). */ type WithSlack, Part> = S extends SlackOptions ? Part : Record; /** Final {@link defineModule} return shape: core parts always, Slack parts only when enabled. */ type AssembledModule< Core extends { db: unknown; commands: unknown; queries: unknown; executors: unknown }, Slack extends { db: unknown; commands: unknown; queries: unknown }, S, WF extends Record, > = { db: Core["db"] & WithSlack; commands: Core["commands"] & WithSlack; queries: Core["queries"] & WithSlack; executors: Core["executors"]; }; /** Assemble the notification module's DB models, commands, queries, and executors from the host app's configuration. */ /* @__NO_SIDE_EFFECTS__ */ export const defineModule = < const NF extends Record = EmptyFields, const PF extends Record = EmptyFields, const SF extends Record = EmptyFields, const AF extends Record = EmptyFields, const CF extends Record = EmptyFields, const EvF extends Record = EmptyFields, const RedrainCron extends string = string, const WF extends Record = EmptyFields, const S extends SlackOptions | undefined = undefined, >( params: DefineModuleParams, ) => { const now = params.adapters.now ?? (() => new Date()); const slackParts = params.slack ? buildSlackParts(params.slack, params.executors.getDB, now) : null; // When Slack is enabled the module owns `destination`; otherwise the host's // adapters pass through unchanged. const adapters = ( slackParts ? { ...params.adapters, destination: slackParts.destination } : params.adapters ) as DispatchAdapters; const notification = createNotificationType(params.notification ?? {}); const notificationPreference = createNotificationPreferenceType({ ...params.notificationPreference, userType: params.userManagement.db.user, }); const notificationSubscription = createNotificationSubscriptionType({ ...params.notificationSubscription, userType: params.userManagement.db.user, }); const notificationDeliveryAudit = createNotificationDeliveryAuditType( params.notificationDeliveryAudit ?? {}, ); const channelRoutingBinding = createChannelRoutingBindingType(params.channelRoutingBinding ?? {}); const notificationEvent = createNotificationEventType(params.notificationEvent ?? {}); const destinationDeliveryLog = createDestinationDeliveryLogType({}); const executorDeps = { getDB: params.executors.getDB, adapters, }; const dispatchNotificationEventsExecutor = dispatchNotificationEvents({ ...params.executors.dispatchNotificationEvents, ...executorDeps, notificationEventType: notificationEvent, }); const redrainNotificationEventsExecutor = redrainNotificationEvents({ ...params.executors.redrainNotificationEvents, ...executorDeps, }); const core = { db: { channelRoutingBinding, destinationDeliveryLog, eventCategoryBinding, notification, notificationEvent, notificationCategory, notificationChannel, notificationDeliveryAudit, notificationPreference, notificationSubscription, notificationTemplate, }, commands: { activateNotificationChannel: activateNotificationChannel(), anonymizeNotificationsForUser: anonymizeNotificationsForUser(), archiveNotification: archiveNotification(), bulkUpdateNotificationPreferences: bulkUpdateNotificationPreferences(), createNotificationChannel: createNotificationChannel(), createNotificationSubscription: createNotificationSubscription(), createNotificationTemplate: createNotificationTemplate(), deactivateNotificationChannel: deactivateNotificationChannel(), deleteNotificationSubscription: deleteNotificationSubscription(), deleteNotificationTemplate: deleteNotificationTemplate(), logNotificationEvent: logNotificationEvent(), markAllNotificationsAsRead: markAllNotificationsAsRead(), markNotificationAsRead: markNotificationAsRead(), markNotificationsAsSeen: markNotificationsAsSeen(), recordDeliveryEvent: recordDeliveryEvent(), resetNotificationPreferences: resetNotificationPreferences(), runNotificationAuditRetentionSweep: runNotificationAuditRetentionSweep(), updateNotificationChannel: updateNotificationChannel(), updateNotificationPreference: updateNotificationPreference(), updateNotificationTemplate: updateNotificationTemplate(), }, queries: { getEffectiveNotificationPreference, getEventCategoryBinding, getNotification, getNotificationChannel, getNotificationPreference, getNotificationTemplate, listAuditByNotification, listAuditBySource, listInboxNotifications, listNotificationCategories, listNotificationChannels, listNotificationPreferences, listNotificationSubscriptions, listNotificationTemplates, renderNotificationTemplate, searchNotificationDeliveryAudit, }, executors: { dispatchNotificationEvents: dispatchNotificationEventsExecutor, redrainNotificationEvents: redrainNotificationEventsExecutor, }, }; return { db: { ...core.db, ...slackParts?.db }, commands: { ...core.commands, ...slackParts?.commands }, queries: { ...core.queries, ...slackParts?.queries }, executors: core.executors, } as AssembledModule, S, WF>; }; /** EmptyFields-instantiated alias with Slack disabled — use instead of `ReturnType`, which widens field generics to their constraint. */ export type NotificationModule = ReturnType< typeof defineModule< EmptyFields, EmptyFields, EmptyFields, EmptyFields, EmptyFields, EmptyFields, string, EmptyFields, undefined > >;