import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { CategoryNotFoundError, CategoryNotOptOutAbleError, ChannelNotFoundError, ForbiddenError, MissingRequiredFieldError, } from "../lib/errors.generated"; export interface BulkPreferenceEntry { categoryId: string; channelId: string; allowed: boolean; } export interface BulkUpdateNotificationPreferencesInput { userId: string; entries: BulkPreferenceEntry[]; } /** * Atomically upserts a caller's NotificationPreference matrix in one transaction, validating each entry's category, channel, and opt-out eligibility before any write. */ export async function run( db: Transaction, input: BulkUpdateNotificationPreferencesInput, ctx: CommandContext, ) { const { userId, entries } = input; if (ctx.actorId !== userId) { return err(new ForbiddenError(userId)); } if (!entries || entries.length === 0) { return err(new MissingRequiredFieldError("entries")); } for (const [index, entry] of entries.entries()) { if (!entry.categoryId || !entry.channelId || typeof entry.allowed !== "boolean") { return err(new MissingRequiredFieldError(`entries[${index}]`)); } } // Pre-flight: batch-load all unique categories and channels, then validate. const uniqueCategoryIds = new Set(entries.map((e) => e.categoryId)); const uniqueChannelIds = new Set(entries.map((e) => e.channelId)); const [categories, channels] = await Promise.all([ db .selectFrom("NotificationCategory") .selectAll() .where("id", "in", Array.from(uniqueCategoryIds)) .execute(), db .selectFrom("NotificationChannel") .selectAll() .where("id", "in", Array.from(uniqueChannelIds)) .execute(), ]); const categoryById = new Map(categories.map((c) => [c.id, c])); const channelById = new Map(channels.map((ch) => [ch.id, ch])); for (const entry of entries) { const category = categoryById.get(entry.categoryId); if (!category) { return err(new CategoryNotFoundError(entry.categoryId)); } const channel = channelById.get(entry.channelId); if (!channel) { return err(new ChannelNotFoundError(entry.channelId)); } const wantsOptOut = !entry.allowed; if (!category.optOutAllowed && wantsOptOut) { return err(new CategoryNotOptOutAbleError(entry.categoryId)); } } // All entries valid: upsert each row in the same transaction. const now = new Date(); const preferences: unknown[] = []; for (const entry of entries) { const existing = await db .selectFrom("NotificationPreference") .selectAll() .where("userId", "=", userId) .where("categoryId", "=", entry.categoryId) .where("channelId", "=", entry.channelId) .forUpdate() .executeTakeFirst(); if (existing) { const updated = await db .updateTable("NotificationPreference") .set({ allowed: entry.allowed, updatedAt: now, }) .where("id", "=", existing.id) .returningAll() .executeTakeFirstOrThrow(); preferences.push(updated); } else { const inserted = await db .insertInto("NotificationPreference") .values({ id: crypto.randomUUID(), userId, categoryId: entry.categoryId, channelId: entry.channelId, allowed: entry.allowed, }) .returningAll() .executeTakeFirstOrThrow(); preferences.push(inserted); } } return ok({ preferences }); }