mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Hide archived and unreviewed catalogue content from existing clients through RLS and guarded recommendation RPCs. Reject stale writes, preserve historical rows, and seed reviewed defaults when the cutover removes a couple's full active selection.
158 lines
4.8 KiB
TypeScript
158 lines
4.8 KiB
TypeScript
|
|
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
|
import { assert } from "std/testing/asserts.ts";
|
|
|
|
export const SUPABASE_URL = Deno.env.get("SUPABASE_URL") || "http://127.0.0.1:54321";
|
|
export const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
|
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
|
|
|
|
if (!SUPABASE_SERVICE_ROLE_KEY) {
|
|
throw new Error("SUPABASE_SERVICE_ROLE_KEY is required");
|
|
}
|
|
if (!SUPABASE_ANON_KEY) {
|
|
throw new Error("SUPABASE_ANON_KEY is required");
|
|
}
|
|
|
|
export const adminClient = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
|
|
auth: {
|
|
autoRefreshToken: false,
|
|
persistSession: false,
|
|
},
|
|
});
|
|
|
|
export async function createTestUser(email: string) {
|
|
const { data, error } = await adminClient.auth.admin.createUser({
|
|
email,
|
|
password: "password123",
|
|
email_confirm: true,
|
|
});
|
|
|
|
if (error) throw error;
|
|
if (!data.user) throw new Error("User not created");
|
|
|
|
return data.user;
|
|
}
|
|
|
|
export async function signInUser(email: string) {
|
|
const userClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY!, {
|
|
auth: { autoRefreshToken: false, persistSession: false },
|
|
});
|
|
const { data, error } = await userClient.auth.signInWithPassword({
|
|
email,
|
|
password: "password123",
|
|
});
|
|
|
|
if (error) throw error;
|
|
return data.session;
|
|
}
|
|
|
|
export async function createTestQuestion(packId?: string) {
|
|
let finalPackId = packId;
|
|
let createdPack = false;
|
|
if (!finalPackId) {
|
|
const { data: pack } = await adminClient.from("question_packs").insert({
|
|
name: "Test Pack " + Math.random(),
|
|
is_premium: false,
|
|
sort_order: 999
|
|
}).select().single();
|
|
finalPackId = pack!.id;
|
|
createdPack = true;
|
|
}
|
|
|
|
const { data: question } = await adminClient.from("questions").insert({
|
|
pack_id: finalPackId,
|
|
text: "Test Question?",
|
|
intensity: 1
|
|
}).select().single();
|
|
|
|
const { data: archivedQuestion } = await adminClient.from("questions").insert({
|
|
pack_id: finalPackId,
|
|
text: "Archived Test Question?",
|
|
intensity: 1
|
|
}).select().single();
|
|
|
|
if (createdPack) {
|
|
const { error: packReviewError } = await adminClient
|
|
.from("question_packs")
|
|
.update({
|
|
content_status: "allowed",
|
|
content_review_reason: "Approved local integration-test fixture",
|
|
})
|
|
.eq("id", finalPackId);
|
|
if (packReviewError) throw packReviewError;
|
|
}
|
|
|
|
const { error: questionReviewError } = await adminClient
|
|
.from("questions")
|
|
.update({
|
|
content_status: "allowed",
|
|
content_review_reason: "Approved local integration-test fixture",
|
|
})
|
|
.eq("id", question!.id);
|
|
if (questionReviewError) throw questionReviewError;
|
|
|
|
const { error: archivedQuestionReviewError } = await adminClient
|
|
.from("questions")
|
|
.update({
|
|
content_status: "archived",
|
|
content_review_reason: "Archived local integration-test fixture",
|
|
})
|
|
.eq("id", archivedQuestion!.id);
|
|
if (archivedQuestionReviewError) throw archivedQuestionReviewError;
|
|
|
|
return { question, archivedQuestion, packId: finalPackId };
|
|
}
|
|
|
|
export async function createRedemptionCode() {
|
|
const code = "TEST-" + Math.random().toString(36).substring(7).toUpperCase();
|
|
const { data, error } = await adminClient.from("redemption_codes").insert({
|
|
code,
|
|
max_uses: 1,
|
|
is_active: true,
|
|
expires_at: new Date(Date.now() + 3600000).toISOString() // 1 hour
|
|
}).select().single();
|
|
|
|
if (error) throw error;
|
|
return data;
|
|
}
|
|
|
|
export async function createCouple(user1Id: string, user2Id: string) {
|
|
// 1. Create a couple
|
|
const { data: couple, error: coupleError } = await adminClient
|
|
.from("couples")
|
|
.insert({
|
|
// invite_code is unique, so generate a random one
|
|
invite_code: Math.random().toString(36).substring(7),
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (coupleError) throw coupleError;
|
|
|
|
// 2. Link users to couple
|
|
const { error: updateError } = await adminClient
|
|
.from("profiles")
|
|
.update({ couple_id: couple.id })
|
|
.in("id", [user1Id, user2Id]);
|
|
|
|
if (updateError) throw updateError;
|
|
|
|
return couple;
|
|
}
|
|
|
|
export async function cleanup(userIds: string[], coupleId?: string) {
|
|
// Delete couple (should cascade to matches, responses, etc. if configured, but let's be safe)
|
|
// Based on schema analysis, explicit cleanup is safer.
|
|
|
|
if (coupleId) {
|
|
await adminClient.from("matches").delete().eq("couple_id", coupleId);
|
|
await adminClient.from("responses").delete().eq("couple_id", coupleId);
|
|
await adminClient.from("couples").delete().eq("id", coupleId);
|
|
}
|
|
|
|
// Delete users (cascades to profiles)
|
|
for (const id of userIds) {
|
|
await adminClient.auth.admin.deleteUser(id);
|
|
}
|
|
}
|