migrate: replace Supabase REST with Drizzle/pg in communications + marketing (wave 2 redo)

This commit is contained in:
2026-06-07 03:58:26 +00:00
parent 99a3d66636
commit 3ad2a48fc3
19 changed files with 1738 additions and 1089 deletions
+101 -37
View File
@@ -1,8 +1,19 @@
"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { svcHeaders } from "@/lib/svc-headers";
import { withTenant } from "@/db/client";
import { brandSettings } from "@/db/schema";
import { eq } from "drizzle-orm";
/**
* The new schema does not have a `communication_settings` table. The
* fields below are now read from `brand_settings.feature_flags` JSONB
* (the same field that stores add-on toggles). The well-known keys
* are: `comm_default_sender_email`, `comm_default_sender_name`,
* `comm_reply_to_email`, `comm_email_provider`,
* `comm_email_footer_html`. Missing keys fall back to sensible
* defaults derived from the brand row.
*/
export type CommunicationSettings = {
id: string;
brand_id: string;
@@ -23,22 +34,50 @@ export type UpsertSettingsResult = {
error: string;
};
function readFlag(
flags: Record<string, unknown> | null,
key: string
): string | null {
if (!flags) return null;
const v = flags[key];
if (typeof v === "string") return v;
if (v === null || v === undefined) return null;
return String(v);
}
export async function getCommunicationSettings(brandId: string): Promise<CommunicationSettings | null> {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
try {
const rows = await withTenant(brandId, (db) =>
db
.select({
tenantId: brandSettings.tenantId,
featureFlags: brandSettings.featureFlags,
updatedAt: brandSettings.updatedAt,
})
.from(brandSettings)
.where(eq(brandSettings.tenantId, brandId))
.limit(1),
);
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/get_communication_settings`,
{
method: "POST",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
body: JSON.stringify({ p_brand_id: brandId }),
}
);
const row = rows[0];
if (!row) return null;
if (!response.ok) return null;
const data = await response.json();
return data?.settings ?? null;
const flags = (row.featureFlags ?? {}) as Record<string, unknown>;
return {
id: row.tenantId,
brand_id: row.tenantId,
default_sender_email: readFlag(flags, "comm_default_sender_email"),
default_sender_name: readFlag(flags, "comm_default_sender_name"),
reply_to_email: readFlag(flags, "comm_reply_to_email"),
email_provider: readFlag(flags, "comm_email_provider") ?? "resend",
email_footer_html: readFlag(flags, "comm_email_footer_html"),
created_at: row.updatedAt.toISOString(),
updated_at: row.updatedAt.toISOString(),
};
} catch {
return null;
}
}
export async function upsertCommunicationSettings(params: {
@@ -52,34 +91,59 @@ export async function upsertCommunicationSettings(params: {
const adminUser = await getAdminUser();
if (!adminUser) return { success: false, error: "Not authenticated" };
// Brand scoping: brand_admin can only modify their own brand's settings
if (adminUser.role === "brand_admin" && adminUser.brand_id !== params.brand_id) {
return { success: false, error: "Not authorized to modify this brand's communication settings" };
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
try {
const updated = await withTenant(params.brand_id, async (db) => {
const existing = await db
.select({ flags: brandSettings.featureFlags })
.from(brandSettings)
.where(eq(brandSettings.tenantId, params.brand_id))
.limit(1);
const baseFlags = (existing[0]?.flags ?? {}) as Record<string, unknown>;
const nextFlags: Record<string, unknown> = {
...baseFlags,
comm_default_sender_email: params.sender_email ?? null,
comm_default_sender_name: params.sender_name ?? null,
comm_reply_to_email: params.reply_to_email ?? null,
comm_email_provider: params.provider ?? "resend",
comm_email_footer_html: params.footer_html ?? null,
};
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/upsert_communication_settings`,
{
method: "POST",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
body: JSON.stringify({
p_brand_id: params.brand_id,
p_sender_email: params.sender_email ?? null,
p_sender_name: params.sender_name ?? null,
p_reply_to_email: params.reply_to_email ?? null,
p_provider: params.provider ?? "resend",
p_footer_html: params.footer_html ?? null,
}),
const result = await db
.update(brandSettings)
.set({ featureFlags: nextFlags, updatedAt: new Date() })
.where(eq(brandSettings.tenantId, params.brand_id))
.returning({
tenantId: brandSettings.tenantId,
updatedAt: brandSettings.updatedAt,
});
return result[0] ?? null;
});
if (!updated) {
return { success: false, error: "Brand settings not found" };
}
);
const data = await response.json();
if (!response.ok || !data?.id) {
return { success: false, error: data?.message ?? "Failed to save settings" };
const settings: CommunicationSettings = {
id: updated.tenantId,
brand_id: updated.tenantId,
default_sender_email: params.sender_email ?? null,
default_sender_name: params.sender_name ?? null,
reply_to_email: params.reply_to_email ?? null,
email_provider: params.provider ?? "resend",
email_footer_html: params.footer_html ?? null,
created_at: updated.updatedAt.toISOString(),
updated_at: updated.updatedAt.toISOString(),
};
return { success: true, settings };
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : "Failed to save settings",
};
}
return { success: true, settings: data };
}
}