first commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
.env
|
||||
@@ -0,0 +1,30 @@
|
||||
# For detailed configuration reference documentation, visit:
|
||||
# https://supabase.com/docs/guides/local-development/cli/config
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "cadam"
|
||||
|
||||
[auth.email]
|
||||
enable_signup = true
|
||||
|
||||
[auth.external.google]
|
||||
enabled = true
|
||||
client_id = "env(GOOGLE_CLIENT_ID)"
|
||||
secret = "env(GOOGLE_SECRET)"
|
||||
|
||||
[storage.buckets.meshes]
|
||||
public = false
|
||||
|
||||
[storage.buckets.images]
|
||||
public = false
|
||||
|
||||
[storage.buckets.previews]
|
||||
public = false
|
||||
|
||||
[db.migrations]
|
||||
schema_paths = [
|
||||
"./schemas/types.sql",
|
||||
"./schemas/conversations.sql",
|
||||
"./schemas/prompts.sql",
|
||||
"./schemas/*.sql",
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"lint": {
|
||||
"rules": {
|
||||
"exclude": ["no-import-prefix", "no-unversioned-import"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
|
||||
create table "public"."conversations" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"created_at" timestamp with time zone default now(),
|
||||
"updated_at" timestamp with time zone default now(),
|
||||
"user_id" uuid not null,
|
||||
"title" text not null,
|
||||
"current_message_leaf_id" uuid
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."conversations" enable row level security;
|
||||
|
||||
|
||||
create table "public"."messages" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"created_at" timestamp with time zone not null default now(),
|
||||
"conversation_id" uuid not null,
|
||||
"role" text not null,
|
||||
"content" jsonb not null,
|
||||
"parent_message_id" uuid
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."messages" enable row level security;
|
||||
|
||||
CREATE INDEX conversations_created_at_idx ON public.conversations USING btree (created_at);
|
||||
|
||||
CREATE UNIQUE INDEX conversations_pkey ON public.conversations USING btree (id);
|
||||
|
||||
CREATE INDEX conversations_updated_at_idx ON public.conversations USING btree (updated_at);
|
||||
|
||||
CREATE INDEX conversations_user_id_idx ON public.conversations USING btree (user_id);
|
||||
|
||||
CREATE INDEX messages_conversation_id_idx ON public.messages USING btree (conversation_id);
|
||||
|
||||
CREATE UNIQUE INDEX messages_pkey ON public.messages USING btree (id);
|
||||
|
||||
alter table "public"."conversations" add constraint "conversations_pkey" PRIMARY KEY using index "conversations_pkey";
|
||||
|
||||
alter table "public"."messages" add constraint "messages_pkey" PRIMARY KEY using index "messages_pkey";
|
||||
|
||||
alter table "public"."conversations" add constraint "conversations_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."conversations" validate constraint "conversations_user_id_fkey";
|
||||
|
||||
alter table "public"."messages" add constraint "messages_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."messages" validate constraint "messages_conversation_id_fkey";
|
||||
|
||||
alter table "public"."messages" add constraint "messages_role_check" CHECK ((role = ANY (ARRAY['user'::text, 'assistant'::text]))) not valid;
|
||||
|
||||
alter table "public"."messages" validate constraint "messages_role_check";
|
||||
|
||||
set check_function_bodies = off;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.update_conversation_leaf()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
begin
|
||||
update conversations set
|
||||
current_message_leaf_id = new.id,
|
||||
updated_at = now()
|
||||
where id = new.conversation_id;
|
||||
return new;
|
||||
end;
|
||||
$function$
|
||||
;
|
||||
|
||||
grant delete on table "public"."conversations" to "anon";
|
||||
|
||||
grant insert on table "public"."conversations" to "anon";
|
||||
|
||||
grant references on table "public"."conversations" to "anon";
|
||||
|
||||
grant select on table "public"."conversations" to "anon";
|
||||
|
||||
grant trigger on table "public"."conversations" to "anon";
|
||||
|
||||
grant truncate on table "public"."conversations" to "anon";
|
||||
|
||||
grant update on table "public"."conversations" to "anon";
|
||||
|
||||
grant delete on table "public"."conversations" to "authenticated";
|
||||
|
||||
grant insert on table "public"."conversations" to "authenticated";
|
||||
|
||||
grant references on table "public"."conversations" to "authenticated";
|
||||
|
||||
grant select on table "public"."conversations" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."conversations" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."conversations" to "authenticated";
|
||||
|
||||
grant update on table "public"."conversations" to "authenticated";
|
||||
|
||||
grant delete on table "public"."conversations" to "service_role";
|
||||
|
||||
grant insert on table "public"."conversations" to "service_role";
|
||||
|
||||
grant references on table "public"."conversations" to "service_role";
|
||||
|
||||
grant select on table "public"."conversations" to "service_role";
|
||||
|
||||
grant trigger on table "public"."conversations" to "service_role";
|
||||
|
||||
grant truncate on table "public"."conversations" to "service_role";
|
||||
|
||||
grant update on table "public"."conversations" to "service_role";
|
||||
|
||||
grant delete on table "public"."messages" to "anon";
|
||||
|
||||
grant insert on table "public"."messages" to "anon";
|
||||
|
||||
grant references on table "public"."messages" to "anon";
|
||||
|
||||
grant select on table "public"."messages" to "anon";
|
||||
|
||||
grant trigger on table "public"."messages" to "anon";
|
||||
|
||||
grant truncate on table "public"."messages" to "anon";
|
||||
|
||||
grant update on table "public"."messages" to "anon";
|
||||
|
||||
grant delete on table "public"."messages" to "authenticated";
|
||||
|
||||
grant insert on table "public"."messages" to "authenticated";
|
||||
|
||||
grant references on table "public"."messages" to "authenticated";
|
||||
|
||||
grant select on table "public"."messages" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."messages" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."messages" to "authenticated";
|
||||
|
||||
grant update on table "public"."messages" to "authenticated";
|
||||
|
||||
grant delete on table "public"."messages" to "service_role";
|
||||
|
||||
grant insert on table "public"."messages" to "service_role";
|
||||
|
||||
grant references on table "public"."messages" to "service_role";
|
||||
|
||||
grant select on table "public"."messages" to "service_role";
|
||||
|
||||
grant trigger on table "public"."messages" to "service_role";
|
||||
|
||||
grant truncate on table "public"."messages" to "service_role";
|
||||
|
||||
grant update on table "public"."messages" to "service_role";
|
||||
|
||||
|
||||
create policy "Users can manage their own conversations"
|
||||
on "public"."conversations"
|
||||
as permissive
|
||||
for all
|
||||
to public
|
||||
using ((( SELECT auth.uid() AS uid) = user_id));
|
||||
|
||||
|
||||
|
||||
create policy "Users can manage messages in their conversations"
|
||||
on "public"."messages"
|
||||
as permissive
|
||||
for all
|
||||
to public
|
||||
using ((( SELECT auth.uid() AS uid) IN ( SELECT conversations.user_id
|
||||
FROM conversations
|
||||
WHERE (conversations.id = messages.conversation_id))));
|
||||
|
||||
|
||||
CREATE TRIGGER update_leaf_trigger AFTER INSERT ON public.messages FOR EACH ROW EXECUTE FUNCTION update_conversation_leaf();
|
||||
|
||||
|
||||
create policy "Give users access to own folder images_delete"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for delete
|
||||
to public
|
||||
using (((bucket_id = 'images'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
|
||||
|
||||
|
||||
|
||||
create policy "Give users access to own folder images_insert"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for insert
|
||||
to public
|
||||
with check (((bucket_id = 'images'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
|
||||
|
||||
|
||||
|
||||
create policy "Give users access to own folder images_select"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for select
|
||||
to public
|
||||
using (((bucket_id = 'images'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
|
||||
|
||||
|
||||
|
||||
create policy "Give users access to own folder images_update"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for update
|
||||
to public
|
||||
using (((bucket_id = 'images'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,890 @@
|
||||
create type "public"."conversation-type" as enum ('parametric', 'creative');
|
||||
|
||||
create type "public"."generation-status" as enum ('pending', 'success', 'failure');
|
||||
|
||||
create type "public"."mesh_file_type" as enum ('glb', 'stl', 'obj', 'fbx');
|
||||
|
||||
create type "public"."mesh_model_type" as enum ('quality', 'fast');
|
||||
|
||||
create type "public"."privacy_type" as enum ('public', 'private');
|
||||
|
||||
create type "public"."prompt_type" as enum ('mesh', 'image', 'chat');
|
||||
|
||||
create type "public"."stripe-level" as enum ('pro', 'standard');
|
||||
|
||||
create type "public"."subscription_level" as enum ('pro', 'standard', 'free');
|
||||
|
||||
|
||||
create table "public"."images" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"created_at" timestamp with time zone not null default now(),
|
||||
"status" public."generation-status" not null default 'pending'::public."generation-status",
|
||||
"user_id" uuid not null,
|
||||
"conversation_id" uuid not null,
|
||||
"image_generation_call_id" text,
|
||||
"prompt" jsonb not null default '{}'::jsonb
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."images" enable row level security;
|
||||
|
||||
|
||||
create table "public"."meshes" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"created_at" timestamp with time zone not null default now(),
|
||||
"status" public."generation-status" not null default 'pending'::public."generation-status",
|
||||
"user_id" uuid not null,
|
||||
"images" uuid[],
|
||||
"conversation_id" uuid not null,
|
||||
"prompt" jsonb not null default '{}'::jsonb,
|
||||
"file_type" public.mesh_file_type not null default 'glb'::public.mesh_file_type
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."meshes" enable row level security;
|
||||
|
||||
|
||||
create table "public"."previews" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"created_at" timestamp with time zone not null default now(),
|
||||
"updated_at" timestamp with time zone not null default now(),
|
||||
"status" public."generation-status" not null default 'pending'::public."generation-status",
|
||||
"user_id" uuid not null,
|
||||
"conversation_id" uuid not null,
|
||||
"mesh_id" uuid not null
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."previews" enable row level security;
|
||||
|
||||
|
||||
create table "public"."profiles" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"created_at" timestamp with time zone not null default now(),
|
||||
"updated_at" timestamp with time zone not null default now(),
|
||||
"user_id" uuid not null,
|
||||
"full_name" text not null,
|
||||
"notifications_enabled" boolean not null default false,
|
||||
"avatar_path" text
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."profiles" enable row level security;
|
||||
|
||||
|
||||
create table "public"."prompts" (
|
||||
"id" bigint generated by default as identity not null,
|
||||
"created_at" timestamp with time zone not null default now(),
|
||||
"user_id" uuid not null,
|
||||
"type" public.prompt_type not null default 'chat'::public.prompt_type
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."prompts" enable row level security;
|
||||
|
||||
|
||||
create table "public"."subscriptions" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"user_id" uuid not null,
|
||||
"stripe_customer_id" text,
|
||||
"stripe_subscription_id" text,
|
||||
"status" text,
|
||||
"created_at" timestamp with time zone default now(),
|
||||
"level" public."stripe-level" not null default 'pro'::public."stripe-level"
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."subscriptions" enable row level security;
|
||||
|
||||
|
||||
create table "public"."trial_users" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"user_id" uuid not null
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."trial_users" enable row level security;
|
||||
|
||||
alter table "public"."conversations" add column "privacy" public.privacy_type not null default 'private'::public.privacy_type;
|
||||
|
||||
alter table "public"."conversations" add column "settings" jsonb not null default '{}'::jsonb;
|
||||
|
||||
alter table "public"."conversations" add column "type" public."conversation-type" not null default 'parametric'::public."conversation-type";
|
||||
|
||||
alter table "public"."messages" add column "rating" smallint not null default '0'::smallint;
|
||||
|
||||
CREATE INDEX idx_images_image_generation_call_id ON public.images USING btree (image_generation_call_id);
|
||||
|
||||
CREATE INDEX idx_subscriptions_stripe_customer_id ON public.subscriptions USING btree (stripe_customer_id);
|
||||
|
||||
CREATE INDEX idx_subscriptions_stripe_subscription_id ON public.subscriptions USING btree (stripe_subscription_id);
|
||||
|
||||
CREATE INDEX idx_subscriptions_user_id ON public.subscriptions USING btree (user_id);
|
||||
|
||||
CREATE UNIQUE INDEX images_pkey ON public.images USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX meshes_pkey ON public.meshes USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX previews_pkey ON public.previews USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX profiles_pkey ON public.profiles USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX prompts_pkey ON public.prompts USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX subscriptions_pkey ON public.subscriptions USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX trial_users_pkey ON public.trial_users USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX trial_users_user_id_key ON public.trial_users USING btree (user_id);
|
||||
|
||||
alter table "public"."images" add constraint "images_pkey" PRIMARY KEY using index "images_pkey";
|
||||
|
||||
alter table "public"."meshes" add constraint "meshes_pkey" PRIMARY KEY using index "meshes_pkey";
|
||||
|
||||
alter table "public"."previews" add constraint "previews_pkey" PRIMARY KEY using index "previews_pkey";
|
||||
|
||||
alter table "public"."profiles" add constraint "profiles_pkey" PRIMARY KEY using index "profiles_pkey";
|
||||
|
||||
alter table "public"."prompts" add constraint "prompts_pkey" PRIMARY KEY using index "prompts_pkey";
|
||||
|
||||
alter table "public"."subscriptions" add constraint "subscriptions_pkey" PRIMARY KEY using index "subscriptions_pkey";
|
||||
|
||||
alter table "public"."trial_users" add constraint "trial_users_pkey" PRIMARY KEY using index "trial_users_pkey";
|
||||
|
||||
alter table "public"."images" add constraint "images_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES public.conversations(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."images" validate constraint "images_conversation_id_fkey";
|
||||
|
||||
alter table "public"."images" add constraint "images_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."images" validate constraint "images_user_id_fkey";
|
||||
|
||||
alter table "public"."meshes" add constraint "meshes_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES public.conversations(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."meshes" validate constraint "meshes_conversation_id_fkey";
|
||||
|
||||
alter table "public"."meshes" add constraint "meshes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."meshes" validate constraint "meshes_user_id_fkey";
|
||||
|
||||
alter table "public"."previews" add constraint "previews_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES public.conversations(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."previews" validate constraint "previews_conversation_id_fkey";
|
||||
|
||||
alter table "public"."previews" add constraint "previews_mesh_id_fkey" FOREIGN KEY (mesh_id) REFERENCES public.meshes(id) ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."previews" validate constraint "previews_mesh_id_fkey";
|
||||
|
||||
alter table "public"."previews" add constraint "previews_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."previews" validate constraint "previews_user_id_fkey";
|
||||
|
||||
alter table "public"."profiles" add constraint "profiles_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."profiles" validate constraint "profiles_user_id_fkey";
|
||||
|
||||
alter table "public"."prompts" add constraint "prompts_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."prompts" validate constraint "prompts_user_id_fkey";
|
||||
|
||||
alter table "public"."subscriptions" add constraint "subscriptions_status_check" CHECK ((status = ANY (ARRAY['active'::text, 'canceled'::text, 'incomplete'::text, 'incomplete_expired'::text, 'past_due'::text, 'trialing'::text, 'unpaid'::text]))) not valid;
|
||||
|
||||
alter table "public"."subscriptions" validate constraint "subscriptions_status_check";
|
||||
|
||||
alter table "public"."subscriptions" add constraint "subscriptions_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) not valid;
|
||||
|
||||
alter table "public"."subscriptions" validate constraint "subscriptions_user_id_fkey";
|
||||
|
||||
alter table "public"."trial_users" add constraint "trial_users_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."trial_users" validate constraint "trial_users_user_id_fkey";
|
||||
|
||||
alter table "public"."trial_users" add constraint "trial_users_user_id_key" UNIQUE using index "trial_users_user_id_key";
|
||||
|
||||
set check_function_bodies = off;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.handle_mesh_insert()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
BEGIN
|
||||
-- Insert a prompt entry immediately when a mesh is created
|
||||
INSERT INTO public.prompts (user_id, type)
|
||||
VALUES (NEW.user_id, 'mesh');
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.handle_mesh_status_update()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
BEGIN
|
||||
-- If mesh status changed to 'failure', remove the most recent mesh prompt for this user
|
||||
IF OLD.status != 'failure' AND NEW.status = 'failure' THEN
|
||||
DELETE FROM public.prompts
|
||||
WHERE user_id = NEW.user_id
|
||||
AND type = 'mesh'
|
||||
AND id = (
|
||||
SELECT id FROM public.prompts
|
||||
WHERE user_id = NEW.user_id
|
||||
AND type = 'mesh'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path TO ''
|
||||
AS $function$
|
||||
BEGIN
|
||||
INSERT INTO public.profiles (user_id, full_name)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
COALESCE(
|
||||
NEW.raw_user_meta_data->>'full_name',
|
||||
split_part(NEW.email, '@', 1)
|
||||
)
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
create type "public"."user_data" as ("hasTrialed" boolean, "sublevel" public.subscription_level, "generationsRemaining" integer);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.user_extradata(user_id_input uuid)
|
||||
RETURNS public.user_data
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $function$
|
||||
DECLARE
|
||||
hasTrialed boolean;
|
||||
userlevel public.subscriptions.level%TYPE;
|
||||
userstatus public.subscriptions.status%TYPE;
|
||||
daily_generations_used integer;
|
||||
monthly_generations_used integer;
|
||||
ret user_data;
|
||||
BEGIN
|
||||
-- Get trial status
|
||||
SELECT (
|
||||
(SELECT count(*) FROM public.trial_users WHERE user_id = user_id_input) > 0
|
||||
) INTO hasTrialed;
|
||||
|
||||
-- Get subscription info
|
||||
SELECT STATUS, LEVEL INTO userstatus, userlevel
|
||||
FROM public.subscriptions
|
||||
WHERE user_id = user_id_input;
|
||||
|
||||
-- Get daily generation count (for free tier)
|
||||
SELECT COUNT(*) INTO daily_generations_used
|
||||
FROM public.prompts
|
||||
WHERE user_id = user_id_input
|
||||
AND type != 'image'
|
||||
AND type != 'chat' -- Exclude parametric generations
|
||||
AND created_at >= NOW() - INTERVAL '1 day';
|
||||
|
||||
-- Get monthly generation count (for standard tier)
|
||||
SELECT COUNT(*) INTO monthly_generations_used
|
||||
FROM public.prompts
|
||||
WHERE user_id = user_id_input
|
||||
AND type != 'image'
|
||||
AND type != 'chat' -- Exclude parametric generations
|
||||
AND created_at >= date_trunc('month', NOW());
|
||||
|
||||
-- Set return values
|
||||
ret."hasTrialed" = hasTrialed;
|
||||
|
||||
-- Set subscription level
|
||||
IF (userstatus = 'active') THEN
|
||||
ret."sublevel" = userlevel;
|
||||
ELSIF (userstatus = 'trialing') THEN
|
||||
ret."sublevel" = 'pro';
|
||||
ELSE
|
||||
ret."sublevel" = 'free';
|
||||
END IF;
|
||||
|
||||
-- Calculate remaining generations based on subscription level
|
||||
IF ret."sublevel" = 'free' THEN
|
||||
-- Free tier: 3 generations per day
|
||||
ret."generationsRemaining" = GREATEST(3 - daily_generations_used, 0);
|
||||
ELSIF ret."sublevel" = 'standard' THEN
|
||||
-- Standard tier: 100 generations per month
|
||||
ret."generationsRemaining" = GREATEST(100 - monthly_generations_used, 0);
|
||||
ELSE
|
||||
-- Pro or enterprise: unlimited
|
||||
ret."generationsRemaining" = 999999; -- effectively unlimited
|
||||
END IF;
|
||||
|
||||
RETURN ret;
|
||||
|
||||
EXCEPTION
|
||||
WHEN others THEN
|
||||
RAISE EXCEPTION 'An error occurred in function user_extradata(): %', SQLERRM;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
grant delete on table "public"."images" to "anon";
|
||||
|
||||
grant insert on table "public"."images" to "anon";
|
||||
|
||||
grant references on table "public"."images" to "anon";
|
||||
|
||||
grant select on table "public"."images" to "anon";
|
||||
|
||||
grant trigger on table "public"."images" to "anon";
|
||||
|
||||
grant truncate on table "public"."images" to "anon";
|
||||
|
||||
grant update on table "public"."images" to "anon";
|
||||
|
||||
grant delete on table "public"."images" to "authenticated";
|
||||
|
||||
grant insert on table "public"."images" to "authenticated";
|
||||
|
||||
grant references on table "public"."images" to "authenticated";
|
||||
|
||||
grant select on table "public"."images" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."images" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."images" to "authenticated";
|
||||
|
||||
grant update on table "public"."images" to "authenticated";
|
||||
|
||||
grant delete on table "public"."images" to "service_role";
|
||||
|
||||
grant insert on table "public"."images" to "service_role";
|
||||
|
||||
grant references on table "public"."images" to "service_role";
|
||||
|
||||
grant select on table "public"."images" to "service_role";
|
||||
|
||||
grant trigger on table "public"."images" to "service_role";
|
||||
|
||||
grant truncate on table "public"."images" to "service_role";
|
||||
|
||||
grant update on table "public"."images" to "service_role";
|
||||
|
||||
grant delete on table "public"."meshes" to "anon";
|
||||
|
||||
grant insert on table "public"."meshes" to "anon";
|
||||
|
||||
grant references on table "public"."meshes" to "anon";
|
||||
|
||||
grant select on table "public"."meshes" to "anon";
|
||||
|
||||
grant trigger on table "public"."meshes" to "anon";
|
||||
|
||||
grant truncate on table "public"."meshes" to "anon";
|
||||
|
||||
grant update on table "public"."meshes" to "anon";
|
||||
|
||||
grant delete on table "public"."meshes" to "authenticated";
|
||||
|
||||
grant insert on table "public"."meshes" to "authenticated";
|
||||
|
||||
grant references on table "public"."meshes" to "authenticated";
|
||||
|
||||
grant select on table "public"."meshes" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."meshes" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."meshes" to "authenticated";
|
||||
|
||||
grant update on table "public"."meshes" to "authenticated";
|
||||
|
||||
grant delete on table "public"."meshes" to "service_role";
|
||||
|
||||
grant insert on table "public"."meshes" to "service_role";
|
||||
|
||||
grant references on table "public"."meshes" to "service_role";
|
||||
|
||||
grant select on table "public"."meshes" to "service_role";
|
||||
|
||||
grant trigger on table "public"."meshes" to "service_role";
|
||||
|
||||
grant truncate on table "public"."meshes" to "service_role";
|
||||
|
||||
grant update on table "public"."meshes" to "service_role";
|
||||
|
||||
grant delete on table "public"."previews" to "anon";
|
||||
|
||||
grant insert on table "public"."previews" to "anon";
|
||||
|
||||
grant references on table "public"."previews" to "anon";
|
||||
|
||||
grant select on table "public"."previews" to "anon";
|
||||
|
||||
grant trigger on table "public"."previews" to "anon";
|
||||
|
||||
grant truncate on table "public"."previews" to "anon";
|
||||
|
||||
grant update on table "public"."previews" to "anon";
|
||||
|
||||
grant delete on table "public"."previews" to "authenticated";
|
||||
|
||||
grant insert on table "public"."previews" to "authenticated";
|
||||
|
||||
grant references on table "public"."previews" to "authenticated";
|
||||
|
||||
grant select on table "public"."previews" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."previews" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."previews" to "authenticated";
|
||||
|
||||
grant update on table "public"."previews" to "authenticated";
|
||||
|
||||
grant delete on table "public"."previews" to "service_role";
|
||||
|
||||
grant insert on table "public"."previews" to "service_role";
|
||||
|
||||
grant references on table "public"."previews" to "service_role";
|
||||
|
||||
grant select on table "public"."previews" to "service_role";
|
||||
|
||||
grant trigger on table "public"."previews" to "service_role";
|
||||
|
||||
grant truncate on table "public"."previews" to "service_role";
|
||||
|
||||
grant update on table "public"."previews" to "service_role";
|
||||
|
||||
grant delete on table "public"."profiles" to "anon";
|
||||
|
||||
grant insert on table "public"."profiles" to "anon";
|
||||
|
||||
grant references on table "public"."profiles" to "anon";
|
||||
|
||||
grant select on table "public"."profiles" to "anon";
|
||||
|
||||
grant trigger on table "public"."profiles" to "anon";
|
||||
|
||||
grant truncate on table "public"."profiles" to "anon";
|
||||
|
||||
grant update on table "public"."profiles" to "anon";
|
||||
|
||||
grant delete on table "public"."profiles" to "authenticated";
|
||||
|
||||
grant insert on table "public"."profiles" to "authenticated";
|
||||
|
||||
grant references on table "public"."profiles" to "authenticated";
|
||||
|
||||
grant select on table "public"."profiles" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."profiles" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."profiles" to "authenticated";
|
||||
|
||||
grant update on table "public"."profiles" to "authenticated";
|
||||
|
||||
grant delete on table "public"."profiles" to "service_role";
|
||||
|
||||
grant insert on table "public"."profiles" to "service_role";
|
||||
|
||||
grant references on table "public"."profiles" to "service_role";
|
||||
|
||||
grant select on table "public"."profiles" to "service_role";
|
||||
|
||||
grant trigger on table "public"."profiles" to "service_role";
|
||||
|
||||
grant truncate on table "public"."profiles" to "service_role";
|
||||
|
||||
grant update on table "public"."profiles" to "service_role";
|
||||
|
||||
grant delete on table "public"."prompts" to "anon";
|
||||
|
||||
grant insert on table "public"."prompts" to "anon";
|
||||
|
||||
grant references on table "public"."prompts" to "anon";
|
||||
|
||||
grant select on table "public"."prompts" to "anon";
|
||||
|
||||
grant trigger on table "public"."prompts" to "anon";
|
||||
|
||||
grant truncate on table "public"."prompts" to "anon";
|
||||
|
||||
grant update on table "public"."prompts" to "anon";
|
||||
|
||||
grant delete on table "public"."prompts" to "authenticated";
|
||||
|
||||
grant insert on table "public"."prompts" to "authenticated";
|
||||
|
||||
grant references on table "public"."prompts" to "authenticated";
|
||||
|
||||
grant select on table "public"."prompts" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."prompts" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."prompts" to "authenticated";
|
||||
|
||||
grant update on table "public"."prompts" to "authenticated";
|
||||
|
||||
grant delete on table "public"."prompts" to "service_role";
|
||||
|
||||
grant insert on table "public"."prompts" to "service_role";
|
||||
|
||||
grant references on table "public"."prompts" to "service_role";
|
||||
|
||||
grant select on table "public"."prompts" to "service_role";
|
||||
|
||||
grant trigger on table "public"."prompts" to "service_role";
|
||||
|
||||
grant truncate on table "public"."prompts" to "service_role";
|
||||
|
||||
grant update on table "public"."prompts" to "service_role";
|
||||
|
||||
grant delete on table "public"."subscriptions" to "anon";
|
||||
|
||||
grant insert on table "public"."subscriptions" to "anon";
|
||||
|
||||
grant references on table "public"."subscriptions" to "anon";
|
||||
|
||||
grant select on table "public"."subscriptions" to "anon";
|
||||
|
||||
grant trigger on table "public"."subscriptions" to "anon";
|
||||
|
||||
grant truncate on table "public"."subscriptions" to "anon";
|
||||
|
||||
grant update on table "public"."subscriptions" to "anon";
|
||||
|
||||
grant delete on table "public"."subscriptions" to "authenticated";
|
||||
|
||||
grant insert on table "public"."subscriptions" to "authenticated";
|
||||
|
||||
grant references on table "public"."subscriptions" to "authenticated";
|
||||
|
||||
grant select on table "public"."subscriptions" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."subscriptions" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."subscriptions" to "authenticated";
|
||||
|
||||
grant update on table "public"."subscriptions" to "authenticated";
|
||||
|
||||
grant delete on table "public"."subscriptions" to "service_role";
|
||||
|
||||
grant insert on table "public"."subscriptions" to "service_role";
|
||||
|
||||
grant references on table "public"."subscriptions" to "service_role";
|
||||
|
||||
grant select on table "public"."subscriptions" to "service_role";
|
||||
|
||||
grant trigger on table "public"."subscriptions" to "service_role";
|
||||
|
||||
grant truncate on table "public"."subscriptions" to "service_role";
|
||||
|
||||
grant update on table "public"."subscriptions" to "service_role";
|
||||
|
||||
grant delete on table "public"."trial_users" to "anon";
|
||||
|
||||
grant insert on table "public"."trial_users" to "anon";
|
||||
|
||||
grant references on table "public"."trial_users" to "anon";
|
||||
|
||||
grant select on table "public"."trial_users" to "anon";
|
||||
|
||||
grant trigger on table "public"."trial_users" to "anon";
|
||||
|
||||
grant truncate on table "public"."trial_users" to "anon";
|
||||
|
||||
grant update on table "public"."trial_users" to "anon";
|
||||
|
||||
grant delete on table "public"."trial_users" to "authenticated";
|
||||
|
||||
grant insert on table "public"."trial_users" to "authenticated";
|
||||
|
||||
grant references on table "public"."trial_users" to "authenticated";
|
||||
|
||||
grant select on table "public"."trial_users" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."trial_users" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."trial_users" to "authenticated";
|
||||
|
||||
grant update on table "public"."trial_users" to "authenticated";
|
||||
|
||||
grant delete on table "public"."trial_users" to "service_role";
|
||||
|
||||
grant insert on table "public"."trial_users" to "service_role";
|
||||
|
||||
grant references on table "public"."trial_users" to "service_role";
|
||||
|
||||
grant select on table "public"."trial_users" to "service_role";
|
||||
|
||||
grant trigger on table "public"."trial_users" to "service_role";
|
||||
|
||||
grant truncate on table "public"."trial_users" to "service_role";
|
||||
|
||||
grant update on table "public"."trial_users" to "service_role";
|
||||
|
||||
|
||||
create policy "Anyone can view a public conversation"
|
||||
on "public"."conversations"
|
||||
as permissive
|
||||
for select
|
||||
to authenticated, anon
|
||||
using ((privacy = 'public'::public.privacy_type));
|
||||
|
||||
|
||||
|
||||
create policy "Public conversations images"
|
||||
on "public"."images"
|
||||
as permissive
|
||||
for select
|
||||
to authenticated, anon
|
||||
using ((EXISTS ( SELECT 1
|
||||
FROM public.conversations
|
||||
WHERE ((conversations.id = images.conversation_id) AND (conversations.privacy = 'public'::public.privacy_type)))));
|
||||
|
||||
|
||||
|
||||
create policy "User can manage their data"
|
||||
on "public"."images"
|
||||
as permissive
|
||||
for all
|
||||
to authenticated
|
||||
using ((( SELECT auth.uid() AS uid) = user_id))
|
||||
with check ((( SELECT auth.uid() AS uid) = user_id));
|
||||
|
||||
|
||||
|
||||
create policy "Everyone can view meshes associated with public conversations"
|
||||
on "public"."meshes"
|
||||
as permissive
|
||||
for select
|
||||
to authenticated, anon
|
||||
using ((EXISTS ( SELECT 1
|
||||
FROM public.conversations
|
||||
WHERE ((conversations.id = meshes.conversation_id) AND (conversations.privacy = 'public'::public.privacy_type)))));
|
||||
|
||||
|
||||
|
||||
create policy "Users can manage their meshes"
|
||||
on "public"."meshes"
|
||||
as permissive
|
||||
for all
|
||||
to public
|
||||
using ((( SELECT auth.uid() AS uid) = user_id));
|
||||
|
||||
|
||||
|
||||
create policy "Public conversations messages"
|
||||
on "public"."messages"
|
||||
as permissive
|
||||
for select
|
||||
to authenticated, anon
|
||||
using ((EXISTS ( SELECT 1
|
||||
FROM public.conversations
|
||||
WHERE ((conversations.id = messages.conversation_id) AND (conversations.privacy = 'public'::public.privacy_type)))));
|
||||
|
||||
|
||||
|
||||
create policy "Users can manage their own previews"
|
||||
on "public"."previews"
|
||||
as permissive
|
||||
for all
|
||||
to public
|
||||
using ((( SELECT auth.uid() AS uid) = user_id));
|
||||
|
||||
|
||||
|
||||
create policy "Users can manage their own profile"
|
||||
on "public"."profiles"
|
||||
as permissive
|
||||
for all
|
||||
to public
|
||||
using ((( SELECT auth.uid() AS uid) = user_id));
|
||||
|
||||
|
||||
|
||||
create policy "Users can view their own prompts"
|
||||
on "public"."prompts"
|
||||
as permissive
|
||||
for select
|
||||
to public
|
||||
using ((( SELECT auth.uid() AS uid) = user_id));
|
||||
|
||||
|
||||
|
||||
create policy "Service role can manage all subscriptions"
|
||||
on "public"."subscriptions"
|
||||
as permissive
|
||||
for all
|
||||
to service_role
|
||||
using (true)
|
||||
with check (true);
|
||||
|
||||
|
||||
|
||||
create policy "Users can read their own subscriptions"
|
||||
on "public"."subscriptions"
|
||||
as permissive
|
||||
for select
|
||||
to authenticated
|
||||
using ((( SELECT auth.uid() AS uid) = user_id));
|
||||
|
||||
|
||||
|
||||
create policy "Enable users to view their own data only"
|
||||
on "public"."trial_users"
|
||||
as permissive
|
||||
for select
|
||||
to authenticated
|
||||
using ((( SELECT auth.uid() AS uid) = user_id));
|
||||
|
||||
|
||||
CREATE TRIGGER mesh_insert_prompt_trigger AFTER INSERT ON public.meshes FOR EACH ROW EXECUTE FUNCTION public.handle_mesh_insert();
|
||||
|
||||
CREATE TRIGGER mesh_status_update_trigger AFTER UPDATE ON public.meshes FOR EACH ROW WHEN ((old.status IS DISTINCT FROM new.status)) EXECUTE FUNCTION public.handle_mesh_status_update();
|
||||
|
||||
CREATE TRIGGER update_previews_updated_at BEFORE UPDATE ON public.previews FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
||||
|
||||
|
||||
create policy "Allow public read access to temp multiview images"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for select
|
||||
to public
|
||||
using ((bucket_id = 'temp-multiview'::text));
|
||||
|
||||
|
||||
|
||||
create policy "Allow service role to delete temp multiview images"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for delete
|
||||
to service_role
|
||||
using ((bucket_id = 'temp-multiview'::text));
|
||||
|
||||
|
||||
|
||||
create policy "Allow service role to upload temp multiview images"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for insert
|
||||
to service_role
|
||||
with check ((bucket_id = 'temp-multiview'::text));
|
||||
|
||||
|
||||
|
||||
create policy "Give users access to own folder meshes_delete"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for delete
|
||||
to public
|
||||
using (((bucket_id = 'meshes'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
|
||||
|
||||
|
||||
|
||||
create policy "Give users access to own folder meshes_insert"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for insert
|
||||
to public
|
||||
with check (((bucket_id = 'meshes'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
|
||||
|
||||
|
||||
|
||||
create policy "Give users access to own folder meshes_select"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for select
|
||||
to public
|
||||
using (((bucket_id = 'meshes'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
|
||||
|
||||
|
||||
|
||||
create policy "Give users access to own folder meshes_update"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for update
|
||||
to public
|
||||
using (((bucket_id = 'meshes'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
|
||||
|
||||
|
||||
|
||||
create policy "Give users access to own folder previews_delete"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for delete
|
||||
to public
|
||||
using (((bucket_id = 'previews'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
|
||||
|
||||
|
||||
|
||||
create policy "Give users access to own folder previews_insert"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for insert
|
||||
to public
|
||||
with check (((bucket_id = 'previews'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
|
||||
|
||||
|
||||
|
||||
create policy "Give users access to own folder previews_select"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for select
|
||||
to public
|
||||
using (((bucket_id = 'previews'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
|
||||
|
||||
|
||||
|
||||
create policy "Give users access to own folder previews_update"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for update
|
||||
to public
|
||||
using (((bucket_id = 'previews'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
|
||||
|
||||
|
||||
|
||||
create policy "Public conversations allow anyone to view images_select"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for select
|
||||
to anon, authenticated
|
||||
using (((bucket_id = 'images'::text) AND (EXISTS ( SELECT 1
|
||||
FROM public.conversations
|
||||
WHERE ((conversations.privacy = 'public'::public.privacy_type) AND ((conversations.id)::text = (storage.foldername(objects.name))[2]))))));
|
||||
|
||||
|
||||
|
||||
create policy "Public conversations allow anyone to view meshes_select"
|
||||
on "storage"."objects"
|
||||
as permissive
|
||||
for select
|
||||
to anon, authenticated
|
||||
using (((bucket_id = 'meshes'::text) AND (EXISTS ( SELECT 1
|
||||
FROM public.conversations
|
||||
WHERE ((conversations.privacy = 'public'::public.privacy_type) AND ((conversations.id)::text = (storage.foldername(objects.name))[2]))))));
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,723 @@
|
||||
create extension if not exists "pg_cron" with schema "pg_catalog";
|
||||
|
||||
create type "public"."token_operation_type" as enum ('mesh', 'parametric', 'chat', 'refund');
|
||||
|
||||
create type "public"."token_source_type" as enum ('subscription', 'purchased');
|
||||
|
||||
drop trigger if exists "mesh_insert_prompt_trigger" on "public"."meshes";
|
||||
|
||||
drop function if exists "public"."handle_mesh_insert"();
|
||||
|
||||
drop function if exists "public"."user_extradata"(uuid);
|
||||
|
||||
drop type "public"."user_data";
|
||||
|
||||
|
||||
create table "public"."token_balances" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"user_id" uuid not null,
|
||||
"source" public.token_source_type not null,
|
||||
"balance" integer not null default 0,
|
||||
"expires_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone not null default now(),
|
||||
"updated_at" timestamp with time zone not null default now()
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."token_balances" enable row level security;
|
||||
|
||||
|
||||
create table "public"."token_costs" (
|
||||
"operation" public.token_operation_type not null,
|
||||
"cost" integer not null,
|
||||
"created_at" timestamp with time zone not null default now(),
|
||||
"updated_at" timestamp with time zone not null default now()
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."token_costs" enable row level security;
|
||||
|
||||
|
||||
create table "public"."token_pack_products" (
|
||||
"id" uuid not null default gen_random_uuid(),
|
||||
"stripe_lookup_key" text not null,
|
||||
"token_amount" integer not null,
|
||||
"name" text not null,
|
||||
"price_cents" integer not null,
|
||||
"active" boolean not null default true,
|
||||
"created_at" timestamp with time zone not null default now()
|
||||
);
|
||||
|
||||
-- Default costs
|
||||
INSERT INTO "public"."token_costs" ("operation", "cost") VALUES
|
||||
('mesh', 30),
|
||||
('parametric', 5),
|
||||
('chat', 1);
|
||||
|
||||
alter table "public"."token_pack_products" enable row level security;
|
||||
|
||||
|
||||
create table "public"."token_transactions" (
|
||||
"id" bigint generated always as identity not null,
|
||||
"user_id" uuid not null,
|
||||
"operation" public.token_operation_type not null,
|
||||
"amount" integer not null,
|
||||
"source" public.token_source_type not null,
|
||||
"reference_id" text,
|
||||
"subscription_balance_after" integer not null,
|
||||
"purchased_balance_after" integer not null,
|
||||
"created_at" timestamp with time zone not null default now()
|
||||
);
|
||||
|
||||
|
||||
alter table "public"."token_transactions" enable row level security;
|
||||
|
||||
CREATE UNIQUE INDEX token_balances_pkey ON public.token_balances USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX token_balances_user_id_source_key ON public.token_balances USING btree (user_id, source);
|
||||
|
||||
CREATE UNIQUE INDEX token_costs_pkey ON public.token_costs USING btree (operation);
|
||||
|
||||
CREATE UNIQUE INDEX token_pack_products_pkey ON public.token_pack_products USING btree (id);
|
||||
|
||||
CREATE UNIQUE INDEX token_pack_products_stripe_lookup_key_key ON public.token_pack_products USING btree (stripe_lookup_key);
|
||||
|
||||
CREATE UNIQUE INDEX token_transactions_pkey ON public.token_transactions USING btree (id);
|
||||
|
||||
alter table "public"."token_balances" add constraint "token_balances_pkey" PRIMARY KEY using index "token_balances_pkey";
|
||||
|
||||
alter table "public"."token_costs" add constraint "token_costs_pkey" PRIMARY KEY using index "token_costs_pkey";
|
||||
|
||||
alter table "public"."token_pack_products" add constraint "token_pack_products_pkey" PRIMARY KEY using index "token_pack_products_pkey";
|
||||
|
||||
alter table "public"."token_transactions" add constraint "token_transactions_pkey" PRIMARY KEY using index "token_transactions_pkey";
|
||||
|
||||
alter table "public"."token_balances" add constraint "token_balances_balance_check" CHECK ((balance >= 0)) not valid;
|
||||
|
||||
alter table "public"."token_balances" validate constraint "token_balances_balance_check";
|
||||
|
||||
alter table "public"."token_balances" add constraint "token_balances_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."token_balances" validate constraint "token_balances_user_id_fkey";
|
||||
|
||||
alter table "public"."token_balances" add constraint "token_balances_user_id_source_key" UNIQUE using index "token_balances_user_id_source_key";
|
||||
|
||||
alter table "public"."token_pack_products" add constraint "token_pack_products_stripe_lookup_key_key" UNIQUE using index "token_pack_products_stripe_lookup_key_key";
|
||||
|
||||
alter table "public"."token_transactions" add constraint "token_transactions_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid;
|
||||
|
||||
alter table "public"."token_transactions" validate constraint "token_transactions_user_id_fkey";
|
||||
|
||||
set check_function_bodies = off;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.credit_purchased_tokens(p_user_id uuid, p_amount integer, p_reference_id text DEFAULT NULL::text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_new_balance integer;
|
||||
v_sub_balance integer;
|
||||
BEGIN
|
||||
INSERT INTO public.token_balances (user_id, source, balance)
|
||||
VALUES (p_user_id, 'purchased'::public.token_source_type, p_amount)
|
||||
ON CONFLICT (user_id, source) DO UPDATE
|
||||
SET balance = token_balances.balance + p_amount, updated_at = now()
|
||||
RETURNING balance INTO v_new_balance;
|
||||
|
||||
SELECT COALESCE(balance, 0) INTO v_sub_balance
|
||||
FROM public.token_balances
|
||||
WHERE user_id = p_user_id AND source = 'subscription'::public.token_source_type;
|
||||
|
||||
v_sub_balance := COALESCE(v_sub_balance, 0);
|
||||
|
||||
INSERT INTO public.token_transactions (
|
||||
user_id, operation, amount, source, reference_id,
|
||||
subscription_balance_after, purchased_balance_after
|
||||
) VALUES (
|
||||
p_user_id, 'chat'::public.token_operation_type, p_amount, 'purchased'::public.token_source_type, p_reference_id,
|
||||
v_sub_balance, v_new_balance
|
||||
);
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'success', true,
|
||||
'tokensAdded', p_amount,
|
||||
'purchasedBalance', v_new_balance
|
||||
);
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.deduct_tokens(p_user_id uuid, p_operation public.token_operation_type, p_reference_id text DEFAULT NULL::text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_cost integer;
|
||||
v_sub_balance integer;
|
||||
v_pur_balance integer;
|
||||
v_sub_expires timestamptz;
|
||||
v_remaining integer;
|
||||
v_sub_deduct integer;
|
||||
v_pur_deduct integer;
|
||||
BEGIN
|
||||
-- Get cost for operation
|
||||
SELECT cost INTO v_cost FROM public.token_costs WHERE operation = p_operation;
|
||||
IF v_cost IS NULL THEN
|
||||
RETURN jsonb_build_object('success', false, 'error', 'unknown_operation');
|
||||
END IF;
|
||||
|
||||
-- Lock and read subscription balance
|
||||
SELECT balance, expires_at INTO v_sub_balance, v_sub_expires
|
||||
FROM public.token_balances
|
||||
WHERE user_id = p_user_id AND source = 'subscription'
|
||||
FOR UPDATE;
|
||||
|
||||
-- If subscription tokens have expired, treat as 0
|
||||
IF v_sub_expires IS NOT NULL AND v_sub_expires < now() THEN
|
||||
v_sub_balance := 0;
|
||||
END IF;
|
||||
|
||||
-- Lock and read purchased balance
|
||||
SELECT balance INTO v_pur_balance
|
||||
FROM public.token_balances
|
||||
WHERE user_id = p_user_id AND source = 'purchased'
|
||||
FOR UPDATE;
|
||||
|
||||
-- Default to 0 if no rows exist
|
||||
v_sub_balance := COALESCE(v_sub_balance, 0);
|
||||
v_pur_balance := COALESCE(v_pur_balance, 0);
|
||||
|
||||
-- Check total available
|
||||
IF (v_sub_balance + v_pur_balance) < v_cost THEN
|
||||
RETURN jsonb_build_object(
|
||||
'success', false,
|
||||
'error', 'insufficient_tokens',
|
||||
'tokensRequired', v_cost,
|
||||
'tokensAvailable', v_sub_balance + v_pur_balance
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- Consume subscription tokens first (they expire anyway)
|
||||
v_sub_deduct := LEAST(v_cost, v_sub_balance);
|
||||
v_pur_deduct := v_cost - v_sub_deduct;
|
||||
|
||||
-- Update subscription balance
|
||||
IF v_sub_deduct > 0 THEN
|
||||
UPDATE public.token_balances
|
||||
SET balance = balance - v_sub_deduct, updated_at = now()
|
||||
WHERE user_id = p_user_id AND source = 'subscription';
|
||||
END IF;
|
||||
|
||||
-- Update purchased balance
|
||||
IF v_pur_deduct > 0 THEN
|
||||
UPDATE public.token_balances
|
||||
SET balance = balance - v_pur_deduct, updated_at = now()
|
||||
WHERE user_id = p_user_id AND source = 'purchased';
|
||||
END IF;
|
||||
|
||||
-- Record transaction
|
||||
INSERT INTO public.token_transactions (
|
||||
user_id, operation, amount, source, reference_id,
|
||||
subscription_balance_after, purchased_balance_after
|
||||
) VALUES (
|
||||
p_user_id, p_operation, -v_cost,
|
||||
CASE WHEN v_sub_deduct > 0 THEN 'subscription'::public.token_source_type ELSE 'purchased'::public.token_source_type END,
|
||||
p_reference_id,
|
||||
v_sub_balance - v_sub_deduct,
|
||||
v_pur_balance - v_pur_deduct
|
||||
);
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'success', true,
|
||||
'tokensDeducted', v_cost,
|
||||
'subscriptionBalance', v_sub_balance - v_sub_deduct,
|
||||
'purchasedBalance', v_pur_balance - v_pur_deduct,
|
||||
'totalBalance', (v_sub_balance - v_sub_deduct) + (v_pur_balance - v_pur_deduct)
|
||||
);
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_subscription_token_limit(p_user_id uuid)
|
||||
RETURNS integer
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $function$
|
||||
DECLARE
|
||||
userlevel public.subscriptions.level%TYPE;
|
||||
userstatus public.subscriptions.status%TYPE;
|
||||
BEGIN
|
||||
SELECT status, level INTO userstatus, userlevel
|
||||
FROM public.subscriptions
|
||||
WHERE user_id = p_user_id;
|
||||
|
||||
IF userstatus = 'active' OR userstatus = 'trialing' THEN
|
||||
IF userlevel = 'pro' THEN
|
||||
RETURN 5000;
|
||||
ELSIF userlevel = 'standard' THEN
|
||||
RETURN 1000;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Free tier
|
||||
RETURN 50;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.grant_subscription_tokens(p_user_id uuid, p_token_amount integer, p_expires_at timestamp with time zone)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_pur_balance integer;
|
||||
BEGIN
|
||||
INSERT INTO public.token_balances (user_id, source, balance, expires_at)
|
||||
VALUES (p_user_id, 'subscription'::public.token_source_type, p_token_amount, p_expires_at)
|
||||
ON CONFLICT (user_id, source) DO UPDATE
|
||||
SET balance = p_token_amount, expires_at = p_expires_at, updated_at = now();
|
||||
|
||||
SELECT COALESCE(balance, 0) INTO v_pur_balance
|
||||
FROM public.token_balances
|
||||
WHERE user_id = p_user_id AND source = 'purchased'::public.token_source_type;
|
||||
|
||||
v_pur_balance := COALESCE(v_pur_balance, 0);
|
||||
|
||||
INSERT INTO public.token_transactions (
|
||||
user_id, operation, amount, source, reference_id,
|
||||
subscription_balance_after, purchased_balance_after
|
||||
) VALUES (
|
||||
p_user_id, 'chat'::public.token_operation_type, p_token_amount, 'subscription'::public.token_source_type, 'subscription_grant',
|
||||
p_token_amount, v_pur_balance
|
||||
);
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'success', true,
|
||||
'tokensGranted', p_token_amount,
|
||||
'subscriptionBalance', p_token_amount,
|
||||
'expiresAt', p_expires_at
|
||||
);
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.refund_tokens(p_user_id uuid, p_operation public.token_operation_type, p_reference_id text DEFAULT NULL::text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_cost integer;
|
||||
v_sub_balance integer;
|
||||
v_pur_balance integer;
|
||||
v_sub_limit integer;
|
||||
v_sub_refund integer;
|
||||
v_pur_refund integer;
|
||||
BEGIN
|
||||
-- Get cost for operation
|
||||
SELECT cost INTO v_cost FROM public.token_costs WHERE operation = p_operation;
|
||||
IF v_cost IS NULL THEN
|
||||
RETURN jsonb_build_object('success', false, 'error', 'unknown_operation');
|
||||
END IF;
|
||||
|
||||
-- Get current balances with lock
|
||||
SELECT balance INTO v_sub_balance
|
||||
FROM public.token_balances
|
||||
WHERE user_id = p_user_id AND source = 'subscription'
|
||||
FOR UPDATE;
|
||||
|
||||
SELECT balance INTO v_pur_balance
|
||||
FROM public.token_balances
|
||||
WHERE user_id = p_user_id AND source = 'purchased'
|
||||
FOR UPDATE;
|
||||
|
||||
v_sub_balance := COALESCE(v_sub_balance, 0);
|
||||
v_pur_balance := COALESCE(v_pur_balance, 0);
|
||||
|
||||
-- Get subscription tier limit
|
||||
v_sub_limit := public.get_subscription_token_limit(p_user_id);
|
||||
|
||||
-- Refund to subscription first (up to tier limit), remainder to purchased
|
||||
v_sub_refund := LEAST(v_cost, v_sub_limit - v_sub_balance);
|
||||
v_sub_refund := GREATEST(v_sub_refund, 0);
|
||||
v_pur_refund := v_cost - v_sub_refund;
|
||||
|
||||
-- Update balances
|
||||
IF v_sub_refund > 0 THEN
|
||||
UPDATE public.token_balances
|
||||
SET balance = balance + v_sub_refund, updated_at = now()
|
||||
WHERE user_id = p_user_id AND source = 'subscription';
|
||||
END IF;
|
||||
|
||||
IF v_pur_refund > 0 THEN
|
||||
UPDATE public.token_balances
|
||||
SET balance = balance + v_pur_refund, updated_at = now()
|
||||
WHERE user_id = p_user_id AND source = 'purchased';
|
||||
END IF;
|
||||
|
||||
-- Record refund transaction
|
||||
INSERT INTO public.token_transactions (
|
||||
user_id, operation, amount, source, reference_id,
|
||||
subscription_balance_after, purchased_balance_after
|
||||
) VALUES (
|
||||
p_user_id, 'refund'::public.token_operation_type, v_cost,
|
||||
CASE WHEN v_sub_refund > 0 THEN 'subscription'::public.token_source_type ELSE 'purchased'::public.token_source_type END,
|
||||
p_reference_id,
|
||||
v_sub_balance + v_sub_refund,
|
||||
v_pur_balance + v_pur_refund
|
||||
);
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'success', true,
|
||||
'tokensRefunded', v_cost,
|
||||
'subscriptionBalance', v_sub_balance + v_sub_refund,
|
||||
'purchasedBalance', v_pur_balance + v_pur_refund
|
||||
);
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.reset_free_tier_tokens()
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
UPDATE public.token_balances tb
|
||||
SET balance = 50,
|
||||
expires_at = now() + interval '1 day',
|
||||
updated_at = now()
|
||||
WHERE tb.source = 'subscription'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.subscriptions s
|
||||
WHERE s.user_id = tb.user_id
|
||||
AND s.status IN ('active', 'trialing')
|
||||
)
|
||||
AND (tb.expires_at IS NULL OR tb.expires_at < now());
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.handle_mesh_status_update()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
BEGIN
|
||||
-- If mesh status changed to 'failure', refund the tokens
|
||||
IF OLD.status != 'failure' AND NEW.status = 'failure' THEN
|
||||
PERFORM public.refund_tokens(NEW.user_id, 'mesh'::public.token_operation_type, NEW.id::text);
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path TO ''
|
||||
AS $function$
|
||||
BEGIN
|
||||
INSERT INTO public.profiles (user_id, full_name)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
COALESCE(
|
||||
NEW.raw_user_meta_data->>'full_name',
|
||||
split_part(NEW.email, '@', 1)
|
||||
)
|
||||
);
|
||||
|
||||
-- Initialize subscription token balance (free tier: 50 tokens, 1-day expiry)
|
||||
INSERT INTO public.token_balances (user_id, source, balance, expires_at)
|
||||
VALUES (NEW.id, 'subscription'::public.token_source_type, 50, now() + interval '1 day');
|
||||
|
||||
-- Initialize purchased token balance (0)
|
||||
INSERT INTO public.token_balances (user_id, source, balance)
|
||||
VALUES (NEW.id, 'purchased'::public.token_source_type, 0);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
create type "public"."user_data" as ("hasTrialed" boolean, "sublevel" public.subscription_level, "subscriptionTokens" integer, "purchasedTokens" integer, "totalTokens" integer, "subscriptionTokenLimit" integer, "subscriptionExpiresAt" timestamp with time zone);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.user_extradata(user_id_input uuid)
|
||||
RETURNS public.user_data
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $function$
|
||||
DECLARE
|
||||
hasTrialed boolean;
|
||||
userlevel public.subscriptions.level%TYPE;
|
||||
userstatus public.subscriptions.status%TYPE;
|
||||
v_sub_balance integer;
|
||||
v_pur_balance integer;
|
||||
v_sub_expires timestamptz;
|
||||
v_sub_limit integer;
|
||||
ret user_data;
|
||||
BEGIN
|
||||
-- Get trial status
|
||||
SELECT (
|
||||
(SELECT count(*) FROM public.trial_users WHERE user_id = user_id_input) > 0
|
||||
) INTO hasTrialed;
|
||||
|
||||
-- Get subscription info
|
||||
SELECT STATUS, LEVEL INTO userstatus, userlevel
|
||||
FROM public.subscriptions
|
||||
WHERE user_id = user_id_input;
|
||||
|
||||
-- Get token balances
|
||||
SELECT balance, expires_at INTO v_sub_balance, v_sub_expires
|
||||
FROM public.token_balances
|
||||
WHERE user_id = user_id_input AND source = 'subscription';
|
||||
|
||||
SELECT balance INTO v_pur_balance
|
||||
FROM public.token_balances
|
||||
WHERE user_id = user_id_input AND source = 'purchased';
|
||||
|
||||
v_sub_balance := COALESCE(v_sub_balance, 0);
|
||||
v_pur_balance := COALESCE(v_pur_balance, 0);
|
||||
|
||||
-- If subscription tokens have expired, treat as 0
|
||||
IF v_sub_expires IS NOT NULL AND v_sub_expires < now() THEN
|
||||
v_sub_balance := 0;
|
||||
END IF;
|
||||
|
||||
-- Set return values
|
||||
ret."hasTrialed" = hasTrialed;
|
||||
|
||||
-- Set subscription level
|
||||
IF (userstatus = 'active') THEN
|
||||
ret."sublevel" = userlevel;
|
||||
ELSIF (userstatus = 'trialing') THEN
|
||||
ret."sublevel" = 'pro';
|
||||
ELSE
|
||||
ret."sublevel" = 'free';
|
||||
END IF;
|
||||
|
||||
-- Get token limit for tier
|
||||
v_sub_limit := public.get_subscription_token_limit(user_id_input);
|
||||
|
||||
-- Set token values
|
||||
ret."subscriptionTokens" = v_sub_balance;
|
||||
ret."purchasedTokens" = v_pur_balance;
|
||||
ret."totalTokens" = v_sub_balance + v_pur_balance;
|
||||
ret."subscriptionTokenLimit" = v_sub_limit;
|
||||
ret."subscriptionExpiresAt" = v_sub_expires;
|
||||
|
||||
RETURN ret;
|
||||
|
||||
EXCEPTION
|
||||
WHEN others THEN
|
||||
RAISE EXCEPTION 'An error occurred in function user_extradata(): %', SQLERRM;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
grant delete on table "public"."token_balances" to "anon";
|
||||
|
||||
grant insert on table "public"."token_balances" to "anon";
|
||||
|
||||
grant references on table "public"."token_balances" to "anon";
|
||||
|
||||
grant select on table "public"."token_balances" to "anon";
|
||||
|
||||
grant trigger on table "public"."token_balances" to "anon";
|
||||
|
||||
grant truncate on table "public"."token_balances" to "anon";
|
||||
|
||||
grant update on table "public"."token_balances" to "anon";
|
||||
|
||||
grant delete on table "public"."token_balances" to "authenticated";
|
||||
|
||||
grant insert on table "public"."token_balances" to "authenticated";
|
||||
|
||||
grant references on table "public"."token_balances" to "authenticated";
|
||||
|
||||
grant select on table "public"."token_balances" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."token_balances" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."token_balances" to "authenticated";
|
||||
|
||||
grant update on table "public"."token_balances" to "authenticated";
|
||||
|
||||
grant delete on table "public"."token_balances" to "service_role";
|
||||
|
||||
grant insert on table "public"."token_balances" to "service_role";
|
||||
|
||||
grant references on table "public"."token_balances" to "service_role";
|
||||
|
||||
grant select on table "public"."token_balances" to "service_role";
|
||||
|
||||
grant trigger on table "public"."token_balances" to "service_role";
|
||||
|
||||
grant truncate on table "public"."token_balances" to "service_role";
|
||||
|
||||
grant update on table "public"."token_balances" to "service_role";
|
||||
|
||||
grant delete on table "public"."token_costs" to "anon";
|
||||
|
||||
grant insert on table "public"."token_costs" to "anon";
|
||||
|
||||
grant references on table "public"."token_costs" to "anon";
|
||||
|
||||
grant select on table "public"."token_costs" to "anon";
|
||||
|
||||
grant trigger on table "public"."token_costs" to "anon";
|
||||
|
||||
grant truncate on table "public"."token_costs" to "anon";
|
||||
|
||||
grant update on table "public"."token_costs" to "anon";
|
||||
|
||||
grant delete on table "public"."token_costs" to "authenticated";
|
||||
|
||||
grant insert on table "public"."token_costs" to "authenticated";
|
||||
|
||||
grant references on table "public"."token_costs" to "authenticated";
|
||||
|
||||
grant select on table "public"."token_costs" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."token_costs" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."token_costs" to "authenticated";
|
||||
|
||||
grant update on table "public"."token_costs" to "authenticated";
|
||||
|
||||
grant delete on table "public"."token_costs" to "service_role";
|
||||
|
||||
grant insert on table "public"."token_costs" to "service_role";
|
||||
|
||||
grant references on table "public"."token_costs" to "service_role";
|
||||
|
||||
grant select on table "public"."token_costs" to "service_role";
|
||||
|
||||
grant trigger on table "public"."token_costs" to "service_role";
|
||||
|
||||
grant truncate on table "public"."token_costs" to "service_role";
|
||||
|
||||
grant update on table "public"."token_costs" to "service_role";
|
||||
|
||||
grant delete on table "public"."token_pack_products" to "anon";
|
||||
|
||||
grant insert on table "public"."token_pack_products" to "anon";
|
||||
|
||||
grant references on table "public"."token_pack_products" to "anon";
|
||||
|
||||
grant select on table "public"."token_pack_products" to "anon";
|
||||
|
||||
grant trigger on table "public"."token_pack_products" to "anon";
|
||||
|
||||
grant truncate on table "public"."token_pack_products" to "anon";
|
||||
|
||||
grant update on table "public"."token_pack_products" to "anon";
|
||||
|
||||
grant delete on table "public"."token_pack_products" to "authenticated";
|
||||
|
||||
grant insert on table "public"."token_pack_products" to "authenticated";
|
||||
|
||||
grant references on table "public"."token_pack_products" to "authenticated";
|
||||
|
||||
grant select on table "public"."token_pack_products" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."token_pack_products" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."token_pack_products" to "authenticated";
|
||||
|
||||
grant update on table "public"."token_pack_products" to "authenticated";
|
||||
|
||||
grant delete on table "public"."token_pack_products" to "service_role";
|
||||
|
||||
grant insert on table "public"."token_pack_products" to "service_role";
|
||||
|
||||
grant references on table "public"."token_pack_products" to "service_role";
|
||||
|
||||
grant select on table "public"."token_pack_products" to "service_role";
|
||||
|
||||
grant trigger on table "public"."token_pack_products" to "service_role";
|
||||
|
||||
grant truncate on table "public"."token_pack_products" to "service_role";
|
||||
|
||||
grant update on table "public"."token_pack_products" to "service_role";
|
||||
|
||||
grant delete on table "public"."token_transactions" to "anon";
|
||||
|
||||
grant insert on table "public"."token_transactions" to "anon";
|
||||
|
||||
grant references on table "public"."token_transactions" to "anon";
|
||||
|
||||
grant select on table "public"."token_transactions" to "anon";
|
||||
|
||||
grant trigger on table "public"."token_transactions" to "anon";
|
||||
|
||||
grant truncate on table "public"."token_transactions" to "anon";
|
||||
|
||||
grant update on table "public"."token_transactions" to "anon";
|
||||
|
||||
grant delete on table "public"."token_transactions" to "authenticated";
|
||||
|
||||
grant insert on table "public"."token_transactions" to "authenticated";
|
||||
|
||||
grant references on table "public"."token_transactions" to "authenticated";
|
||||
|
||||
grant select on table "public"."token_transactions" to "authenticated";
|
||||
|
||||
grant trigger on table "public"."token_transactions" to "authenticated";
|
||||
|
||||
grant truncate on table "public"."token_transactions" to "authenticated";
|
||||
|
||||
grant update on table "public"."token_transactions" to "authenticated";
|
||||
|
||||
grant delete on table "public"."token_transactions" to "service_role";
|
||||
|
||||
grant insert on table "public"."token_transactions" to "service_role";
|
||||
|
||||
grant references on table "public"."token_transactions" to "service_role";
|
||||
|
||||
grant select on table "public"."token_transactions" to "service_role";
|
||||
|
||||
grant trigger on table "public"."token_transactions" to "service_role";
|
||||
|
||||
grant truncate on table "public"."token_transactions" to "service_role";
|
||||
|
||||
grant update on table "public"."token_transactions" to "service_role";
|
||||
|
||||
|
||||
create policy "token_balances_read_own"
|
||||
on "public"."token_balances"
|
||||
as permissive
|
||||
for select
|
||||
to authenticated
|
||||
using ((auth.uid() = user_id));
|
||||
|
||||
|
||||
|
||||
create policy "token_costs_read"
|
||||
on "public"."token_costs"
|
||||
as permissive
|
||||
for select
|
||||
to authenticated
|
||||
using (true);
|
||||
|
||||
|
||||
|
||||
create policy "token_pack_products_read"
|
||||
on "public"."token_pack_products"
|
||||
as permissive
|
||||
for select
|
||||
to authenticated
|
||||
using ((active = true));
|
||||
|
||||
|
||||
|
||||
create policy "token_transactions_read_own"
|
||||
on "public"."token_transactions"
|
||||
as permissive
|
||||
for select
|
||||
to authenticated
|
||||
using ((auth.uid() = user_id));
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
-- Fix free-tier daily token reset.
|
||||
--
|
||||
-- Bug 1: reset_free_tier_tokens() set expires_at = now() + 1 day. Because pg_cron
|
||||
-- fires with small execution jitter, each day's stored expires_at crept
|
||||
-- slightly ahead of the next cron's now(), and the gate
|
||||
-- "expires_at < now()" made the cron skip its own prior resets.
|
||||
--
|
||||
-- Bug 2: user_extradata() and deduct_tokens() zero the displayed balance the
|
||||
-- instant expires_at < now(). So any window between expiry and the next
|
||||
-- (possibly skipped) cron caused users to see "You've used all your tokens"
|
||||
-- with their real balance still sitting unchanged in the DB.
|
||||
--
|
||||
-- Fix: add a just-in-time reset on every read/deduct. The cron stays as a
|
||||
-- backstop and is rewritten to pin expires_at to the next UTC day boundary so
|
||||
-- it no longer drifts or skips.
|
||||
|
||||
CREATE OR REPLACE FUNCTION "public"."ensure_free_tier_fresh"("p_user_id" uuid)
|
||||
RETURNS void
|
||||
LANGUAGE "plpgsql"
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_is_free boolean;
|
||||
v_caller uuid;
|
||||
BEGIN
|
||||
-- Only allow service role (server-side edge functions) or the user acting
|
||||
-- on themselves. auth.role() = 'anon' with uid = NULL would otherwise slip
|
||||
-- past a uid-only check. Silent return, not an exception, so that a
|
||||
-- legitimate caller's outer flow (user_extradata, deduct_tokens) continues
|
||||
-- on its own fallback path rather than erroring to the client.
|
||||
v_caller := auth.uid();
|
||||
IF auth.role() IS DISTINCT FROM 'service_role' AND v_caller IS DISTINCT FROM p_user_id THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT NOT EXISTS (
|
||||
SELECT 1 FROM public.subscriptions
|
||||
WHERE user_id = p_user_id
|
||||
AND status IN ('active', 'trialing')
|
||||
) INTO v_is_free;
|
||||
|
||||
IF NOT v_is_free THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
INSERT INTO public.token_balances (user_id, source, balance, expires_at, updated_at)
|
||||
VALUES (
|
||||
p_user_id,
|
||||
'subscription',
|
||||
50,
|
||||
date_trunc('day', now()) + interval '1 day',
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (user_id, source) DO UPDATE
|
||||
SET balance = 50,
|
||||
expires_at = date_trunc('day', now()) + interval '1 day',
|
||||
updated_at = now()
|
||||
WHERE token_balances.expires_at IS NULL
|
||||
OR token_balances.expires_at <= now();
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION "public"."deduct_tokens"(
|
||||
"p_user_id" uuid,
|
||||
"p_operation" "public"."token_operation_type",
|
||||
"p_reference_id" text DEFAULT NULL
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE "plpgsql"
|
||||
AS $$
|
||||
DECLARE
|
||||
v_cost integer;
|
||||
v_sub_balance integer;
|
||||
v_pur_balance integer;
|
||||
v_sub_expires timestamptz;
|
||||
v_remaining integer;
|
||||
v_sub_deduct integer;
|
||||
v_pur_deduct integer;
|
||||
BEGIN
|
||||
SELECT cost INTO v_cost FROM public.token_costs WHERE operation = p_operation;
|
||||
IF v_cost IS NULL THEN
|
||||
RETURN jsonb_build_object('success', false, 'error', 'unknown_operation');
|
||||
END IF;
|
||||
|
||||
PERFORM public.ensure_free_tier_fresh(p_user_id);
|
||||
|
||||
SELECT balance, expires_at INTO v_sub_balance, v_sub_expires
|
||||
FROM public.token_balances
|
||||
WHERE user_id = p_user_id AND source = 'subscription'
|
||||
FOR UPDATE;
|
||||
|
||||
-- Expired subscription tokens count as 0. For free-tier users this is
|
||||
-- unreachable after ensure_free_tier_fresh (expires_at is always future).
|
||||
-- It is load-bearing for paid users whose billing-cycle sub has lapsed,
|
||||
-- and for the edge case where ensure_free_tier_fresh silently bailed
|
||||
-- (auth-mismatch). Don't remove without preserving both cases.
|
||||
IF v_sub_expires IS NOT NULL AND v_sub_expires < now() THEN
|
||||
v_sub_balance := 0;
|
||||
END IF;
|
||||
|
||||
SELECT balance INTO v_pur_balance
|
||||
FROM public.token_balances
|
||||
WHERE user_id = p_user_id AND source = 'purchased'
|
||||
FOR UPDATE;
|
||||
|
||||
v_sub_balance := COALESCE(v_sub_balance, 0);
|
||||
v_pur_balance := COALESCE(v_pur_balance, 0);
|
||||
|
||||
IF (v_sub_balance + v_pur_balance) < v_cost THEN
|
||||
RETURN jsonb_build_object(
|
||||
'success', false,
|
||||
'error', 'insufficient_tokens',
|
||||
'tokensRequired', v_cost,
|
||||
'tokensAvailable', v_sub_balance + v_pur_balance
|
||||
);
|
||||
END IF;
|
||||
|
||||
v_sub_deduct := LEAST(v_cost, v_sub_balance);
|
||||
v_pur_deduct := v_cost - v_sub_deduct;
|
||||
|
||||
IF v_sub_deduct > 0 THEN
|
||||
UPDATE public.token_balances
|
||||
SET balance = balance - v_sub_deduct, updated_at = now()
|
||||
WHERE user_id = p_user_id AND source = 'subscription';
|
||||
END IF;
|
||||
|
||||
IF v_pur_deduct > 0 THEN
|
||||
UPDATE public.token_balances
|
||||
SET balance = balance - v_pur_deduct, updated_at = now()
|
||||
WHERE user_id = p_user_id AND source = 'purchased';
|
||||
END IF;
|
||||
|
||||
INSERT INTO public.token_transactions (
|
||||
user_id, operation, amount, source, reference_id,
|
||||
subscription_balance_after, purchased_balance_after
|
||||
) VALUES (
|
||||
p_user_id, p_operation, -v_cost,
|
||||
CASE WHEN v_sub_deduct > 0 THEN 'subscription'::public.token_source_type ELSE 'purchased'::public.token_source_type END,
|
||||
p_reference_id,
|
||||
v_sub_balance - v_sub_deduct,
|
||||
v_pur_balance - v_pur_deduct
|
||||
);
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'success', true,
|
||||
'tokensDeducted', v_cost,
|
||||
'subscriptionBalance', v_sub_balance - v_sub_deduct,
|
||||
'purchasedBalance', v_pur_balance - v_pur_deduct,
|
||||
'totalBalance', (v_sub_balance - v_sub_deduct) + (v_pur_balance - v_pur_deduct)
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION "public"."reset_free_tier_tokens"()
|
||||
RETURNS void
|
||||
LANGUAGE "plpgsql"
|
||||
AS $$
|
||||
BEGIN
|
||||
UPDATE public.token_balances tb
|
||||
SET balance = 50,
|
||||
expires_at = date_trunc('day', now()) + interval '1 day',
|
||||
updated_at = now()
|
||||
WHERE tb.source = 'subscription'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.subscriptions s
|
||||
WHERE s.user_id = tb.user_id
|
||||
AND s.status IN ('active', 'trialing')
|
||||
)
|
||||
-- Idempotency guard: don't re-credit users whose period is still live.
|
||||
-- With expires_at pinned to the day boundary, the scheduled midnight run
|
||||
-- satisfies `<=` exactly; manual or retried mid-day runs are safely no-ops.
|
||||
AND (tb.expires_at IS NULL OR tb.expires_at <= now());
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION "public"."user_extradata"("user_id_input" "uuid")
|
||||
RETURNS "public"."user_data"
|
||||
LANGUAGE "plpgsql"
|
||||
AS $$
|
||||
DECLARE
|
||||
hasTrialed boolean;
|
||||
userlevel public.subscriptions.level%TYPE;
|
||||
userstatus public.subscriptions.status%TYPE;
|
||||
v_sub_balance integer;
|
||||
v_pur_balance integer;
|
||||
v_sub_expires timestamptz;
|
||||
v_sub_limit integer;
|
||||
ret user_data;
|
||||
BEGIN
|
||||
PERFORM public.ensure_free_tier_fresh(user_id_input);
|
||||
|
||||
SELECT (
|
||||
(SELECT count(*) FROM public.trial_users WHERE user_id = user_id_input) > 0
|
||||
) INTO hasTrialed;
|
||||
|
||||
SELECT STATUS, LEVEL INTO userstatus, userlevel
|
||||
FROM public.subscriptions
|
||||
WHERE user_id = user_id_input;
|
||||
|
||||
SELECT balance, expires_at INTO v_sub_balance, v_sub_expires
|
||||
FROM public.token_balances
|
||||
WHERE user_id = user_id_input AND source = 'subscription';
|
||||
|
||||
SELECT balance INTO v_pur_balance
|
||||
FROM public.token_balances
|
||||
WHERE user_id = user_id_input AND source = 'purchased';
|
||||
|
||||
v_sub_balance := COALESCE(v_sub_balance, 0);
|
||||
v_pur_balance := COALESCE(v_pur_balance, 0);
|
||||
|
||||
-- See matching comment in deduct_tokens: this branch is load-bearing for
|
||||
-- paid users whose billing-cycle sub has lapsed and as a fallback if
|
||||
-- ensure_free_tier_fresh silently bailed. Do not remove.
|
||||
IF v_sub_expires IS NOT NULL AND v_sub_expires < now() THEN
|
||||
v_sub_balance := 0;
|
||||
END IF;
|
||||
|
||||
ret."hasTrialed" = hasTrialed;
|
||||
|
||||
IF (userstatus = 'active') THEN
|
||||
ret."sublevel" = userlevel;
|
||||
ELSIF (userstatus = 'trialing') THEN
|
||||
ret."sublevel" = 'pro';
|
||||
ELSE
|
||||
ret."sublevel" = 'free';
|
||||
END IF;
|
||||
|
||||
v_sub_limit := public.get_subscription_token_limit(user_id_input);
|
||||
|
||||
ret."subscriptionTokens" = v_sub_balance;
|
||||
ret."purchasedTokens" = v_pur_balance;
|
||||
ret."totalTokens" = v_sub_balance + v_pur_balance;
|
||||
ret."subscriptionTokenLimit" = v_sub_limit;
|
||||
ret."subscriptionExpiresAt" = v_sub_expires;
|
||||
|
||||
RETURN ret;
|
||||
|
||||
EXCEPTION
|
||||
WHEN others THEN
|
||||
RAISE EXCEPTION 'An error occurred in function user_extradata(): %', SQLERRM;
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Unschedule the free-tier cron. adam-billing now owns free-tier refreshes
|
||||
-- (it runs its own /v1/cron/refresh-free-tier against its own DB), so CADAM's
|
||||
-- reset_free_tier_tokens() no longer needs to fire. The function itself is
|
||||
-- preserved for now as part of the deferred-drop plan; a follow-up migration
|
||||
-- will drop the billing tables/functions after adam-billing is stable.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM cron.job WHERE jobname = 'reset-free-tier-tokens'
|
||||
) THEN
|
||||
PERFORM cron.unschedule('reset-free-tier-tokens');
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,385 @@
|
||||
drop trigger if exists "mesh_status_update_trigger" on "public"."meshes";
|
||||
|
||||
drop policy "Service role can manage all subscriptions" on "public"."subscriptions";
|
||||
|
||||
drop policy "Users can read their own subscriptions" on "public"."subscriptions";
|
||||
|
||||
drop policy "token_balances_read_own" on "public"."token_balances";
|
||||
|
||||
drop policy "token_costs_read" on "public"."token_costs";
|
||||
|
||||
drop policy "token_pack_products_read" on "public"."token_pack_products";
|
||||
|
||||
drop policy "token_transactions_read_own" on "public"."token_transactions";
|
||||
|
||||
drop policy "Enable users to view their own data only" on "public"."trial_users";
|
||||
|
||||
revoke delete on table "public"."subscriptions" from "anon";
|
||||
|
||||
revoke insert on table "public"."subscriptions" from "anon";
|
||||
|
||||
revoke references on table "public"."subscriptions" from "anon";
|
||||
|
||||
revoke select on table "public"."subscriptions" from "anon";
|
||||
|
||||
revoke trigger on table "public"."subscriptions" from "anon";
|
||||
|
||||
revoke truncate on table "public"."subscriptions" from "anon";
|
||||
|
||||
revoke update on table "public"."subscriptions" from "anon";
|
||||
|
||||
revoke delete on table "public"."subscriptions" from "authenticated";
|
||||
|
||||
revoke insert on table "public"."subscriptions" from "authenticated";
|
||||
|
||||
revoke references on table "public"."subscriptions" from "authenticated";
|
||||
|
||||
revoke select on table "public"."subscriptions" from "authenticated";
|
||||
|
||||
revoke trigger on table "public"."subscriptions" from "authenticated";
|
||||
|
||||
revoke truncate on table "public"."subscriptions" from "authenticated";
|
||||
|
||||
revoke update on table "public"."subscriptions" from "authenticated";
|
||||
|
||||
revoke delete on table "public"."subscriptions" from "service_role";
|
||||
|
||||
revoke insert on table "public"."subscriptions" from "service_role";
|
||||
|
||||
revoke references on table "public"."subscriptions" from "service_role";
|
||||
|
||||
revoke select on table "public"."subscriptions" from "service_role";
|
||||
|
||||
revoke trigger on table "public"."subscriptions" from "service_role";
|
||||
|
||||
revoke truncate on table "public"."subscriptions" from "service_role";
|
||||
|
||||
revoke update on table "public"."subscriptions" from "service_role";
|
||||
|
||||
revoke delete on table "public"."token_balances" from "anon";
|
||||
|
||||
revoke insert on table "public"."token_balances" from "anon";
|
||||
|
||||
revoke references on table "public"."token_balances" from "anon";
|
||||
|
||||
revoke select on table "public"."token_balances" from "anon";
|
||||
|
||||
revoke trigger on table "public"."token_balances" from "anon";
|
||||
|
||||
revoke truncate on table "public"."token_balances" from "anon";
|
||||
|
||||
revoke update on table "public"."token_balances" from "anon";
|
||||
|
||||
revoke delete on table "public"."token_balances" from "authenticated";
|
||||
|
||||
revoke insert on table "public"."token_balances" from "authenticated";
|
||||
|
||||
revoke references on table "public"."token_balances" from "authenticated";
|
||||
|
||||
revoke select on table "public"."token_balances" from "authenticated";
|
||||
|
||||
revoke trigger on table "public"."token_balances" from "authenticated";
|
||||
|
||||
revoke truncate on table "public"."token_balances" from "authenticated";
|
||||
|
||||
revoke update on table "public"."token_balances" from "authenticated";
|
||||
|
||||
revoke delete on table "public"."token_balances" from "service_role";
|
||||
|
||||
revoke insert on table "public"."token_balances" from "service_role";
|
||||
|
||||
revoke references on table "public"."token_balances" from "service_role";
|
||||
|
||||
revoke select on table "public"."token_balances" from "service_role";
|
||||
|
||||
revoke trigger on table "public"."token_balances" from "service_role";
|
||||
|
||||
revoke truncate on table "public"."token_balances" from "service_role";
|
||||
|
||||
revoke update on table "public"."token_balances" from "service_role";
|
||||
|
||||
revoke delete on table "public"."token_costs" from "anon";
|
||||
|
||||
revoke insert on table "public"."token_costs" from "anon";
|
||||
|
||||
revoke references on table "public"."token_costs" from "anon";
|
||||
|
||||
revoke select on table "public"."token_costs" from "anon";
|
||||
|
||||
revoke trigger on table "public"."token_costs" from "anon";
|
||||
|
||||
revoke truncate on table "public"."token_costs" from "anon";
|
||||
|
||||
revoke update on table "public"."token_costs" from "anon";
|
||||
|
||||
revoke delete on table "public"."token_costs" from "authenticated";
|
||||
|
||||
revoke insert on table "public"."token_costs" from "authenticated";
|
||||
|
||||
revoke references on table "public"."token_costs" from "authenticated";
|
||||
|
||||
revoke select on table "public"."token_costs" from "authenticated";
|
||||
|
||||
revoke trigger on table "public"."token_costs" from "authenticated";
|
||||
|
||||
revoke truncate on table "public"."token_costs" from "authenticated";
|
||||
|
||||
revoke update on table "public"."token_costs" from "authenticated";
|
||||
|
||||
revoke delete on table "public"."token_costs" from "service_role";
|
||||
|
||||
revoke insert on table "public"."token_costs" from "service_role";
|
||||
|
||||
revoke references on table "public"."token_costs" from "service_role";
|
||||
|
||||
revoke select on table "public"."token_costs" from "service_role";
|
||||
|
||||
revoke trigger on table "public"."token_costs" from "service_role";
|
||||
|
||||
revoke truncate on table "public"."token_costs" from "service_role";
|
||||
|
||||
revoke update on table "public"."token_costs" from "service_role";
|
||||
|
||||
revoke delete on table "public"."token_pack_products" from "anon";
|
||||
|
||||
revoke insert on table "public"."token_pack_products" from "anon";
|
||||
|
||||
revoke references on table "public"."token_pack_products" from "anon";
|
||||
|
||||
revoke select on table "public"."token_pack_products" from "anon";
|
||||
|
||||
revoke trigger on table "public"."token_pack_products" from "anon";
|
||||
|
||||
revoke truncate on table "public"."token_pack_products" from "anon";
|
||||
|
||||
revoke update on table "public"."token_pack_products" from "anon";
|
||||
|
||||
revoke delete on table "public"."token_pack_products" from "authenticated";
|
||||
|
||||
revoke insert on table "public"."token_pack_products" from "authenticated";
|
||||
|
||||
revoke references on table "public"."token_pack_products" from "authenticated";
|
||||
|
||||
revoke select on table "public"."token_pack_products" from "authenticated";
|
||||
|
||||
revoke trigger on table "public"."token_pack_products" from "authenticated";
|
||||
|
||||
revoke truncate on table "public"."token_pack_products" from "authenticated";
|
||||
|
||||
revoke update on table "public"."token_pack_products" from "authenticated";
|
||||
|
||||
revoke delete on table "public"."token_pack_products" from "service_role";
|
||||
|
||||
revoke insert on table "public"."token_pack_products" from "service_role";
|
||||
|
||||
revoke references on table "public"."token_pack_products" from "service_role";
|
||||
|
||||
revoke select on table "public"."token_pack_products" from "service_role";
|
||||
|
||||
revoke trigger on table "public"."token_pack_products" from "service_role";
|
||||
|
||||
revoke truncate on table "public"."token_pack_products" from "service_role";
|
||||
|
||||
revoke update on table "public"."token_pack_products" from "service_role";
|
||||
|
||||
revoke delete on table "public"."token_transactions" from "anon";
|
||||
|
||||
revoke insert on table "public"."token_transactions" from "anon";
|
||||
|
||||
revoke references on table "public"."token_transactions" from "anon";
|
||||
|
||||
revoke select on table "public"."token_transactions" from "anon";
|
||||
|
||||
revoke trigger on table "public"."token_transactions" from "anon";
|
||||
|
||||
revoke truncate on table "public"."token_transactions" from "anon";
|
||||
|
||||
revoke update on table "public"."token_transactions" from "anon";
|
||||
|
||||
revoke delete on table "public"."token_transactions" from "authenticated";
|
||||
|
||||
revoke insert on table "public"."token_transactions" from "authenticated";
|
||||
|
||||
revoke references on table "public"."token_transactions" from "authenticated";
|
||||
|
||||
revoke select on table "public"."token_transactions" from "authenticated";
|
||||
|
||||
revoke trigger on table "public"."token_transactions" from "authenticated";
|
||||
|
||||
revoke truncate on table "public"."token_transactions" from "authenticated";
|
||||
|
||||
revoke update on table "public"."token_transactions" from "authenticated";
|
||||
|
||||
revoke delete on table "public"."token_transactions" from "service_role";
|
||||
|
||||
revoke insert on table "public"."token_transactions" from "service_role";
|
||||
|
||||
revoke references on table "public"."token_transactions" from "service_role";
|
||||
|
||||
revoke select on table "public"."token_transactions" from "service_role";
|
||||
|
||||
revoke trigger on table "public"."token_transactions" from "service_role";
|
||||
|
||||
revoke truncate on table "public"."token_transactions" from "service_role";
|
||||
|
||||
revoke update on table "public"."token_transactions" from "service_role";
|
||||
|
||||
revoke delete on table "public"."trial_users" from "anon";
|
||||
|
||||
revoke insert on table "public"."trial_users" from "anon";
|
||||
|
||||
revoke references on table "public"."trial_users" from "anon";
|
||||
|
||||
revoke select on table "public"."trial_users" from "anon";
|
||||
|
||||
revoke trigger on table "public"."trial_users" from "anon";
|
||||
|
||||
revoke truncate on table "public"."trial_users" from "anon";
|
||||
|
||||
revoke update on table "public"."trial_users" from "anon";
|
||||
|
||||
revoke delete on table "public"."trial_users" from "authenticated";
|
||||
|
||||
revoke insert on table "public"."trial_users" from "authenticated";
|
||||
|
||||
revoke references on table "public"."trial_users" from "authenticated";
|
||||
|
||||
revoke select on table "public"."trial_users" from "authenticated";
|
||||
|
||||
revoke trigger on table "public"."trial_users" from "authenticated";
|
||||
|
||||
revoke truncate on table "public"."trial_users" from "authenticated";
|
||||
|
||||
revoke update on table "public"."trial_users" from "authenticated";
|
||||
|
||||
revoke delete on table "public"."trial_users" from "service_role";
|
||||
|
||||
revoke insert on table "public"."trial_users" from "service_role";
|
||||
|
||||
revoke references on table "public"."trial_users" from "service_role";
|
||||
|
||||
revoke select on table "public"."trial_users" from "service_role";
|
||||
|
||||
revoke trigger on table "public"."trial_users" from "service_role";
|
||||
|
||||
revoke truncate on table "public"."trial_users" from "service_role";
|
||||
|
||||
revoke update on table "public"."trial_users" from "service_role";
|
||||
|
||||
alter table "public"."subscriptions" drop constraint "subscriptions_status_check";
|
||||
|
||||
alter table "public"."subscriptions" drop constraint "subscriptions_user_id_fkey";
|
||||
|
||||
alter table "public"."token_balances" drop constraint "token_balances_balance_check";
|
||||
|
||||
alter table "public"."token_balances" drop constraint "token_balances_user_id_fkey";
|
||||
|
||||
alter table "public"."token_balances" drop constraint "token_balances_user_id_source_key";
|
||||
|
||||
alter table "public"."token_pack_products" drop constraint "token_pack_products_stripe_lookup_key_key";
|
||||
|
||||
alter table "public"."token_transactions" drop constraint "token_transactions_user_id_fkey";
|
||||
|
||||
alter table "public"."trial_users" drop constraint "trial_users_user_id_fkey";
|
||||
|
||||
alter table "public"."trial_users" drop constraint "trial_users_user_id_key";
|
||||
|
||||
drop function if exists "public"."credit_purchased_tokens"(p_user_id uuid, p_amount integer, p_reference_id text);
|
||||
|
||||
drop function if exists "public"."deduct_tokens"(p_user_id uuid, p_operation public.token_operation_type, p_reference_id text);
|
||||
|
||||
drop function if exists "public"."ensure_free_tier_fresh"(p_user_id uuid);
|
||||
|
||||
drop function if exists "public"."get_subscription_token_limit"(p_user_id uuid);
|
||||
|
||||
drop function if exists "public"."grant_subscription_tokens"(p_user_id uuid, p_token_amount integer, p_expires_at timestamp with time zone);
|
||||
|
||||
drop function if exists "public"."handle_mesh_status_update"();
|
||||
|
||||
drop function if exists "public"."refund_tokens"(p_user_id uuid, p_operation public.token_operation_type, p_reference_id text);
|
||||
|
||||
drop function if exists "public"."reset_free_tier_tokens"();
|
||||
|
||||
drop function if exists "public"."user_extradata"(user_id_input uuid);
|
||||
|
||||
drop type "public"."user_data";
|
||||
|
||||
alter table "public"."subscriptions" drop constraint "subscriptions_pkey";
|
||||
|
||||
alter table "public"."token_balances" drop constraint "token_balances_pkey";
|
||||
|
||||
alter table "public"."token_costs" drop constraint "token_costs_pkey";
|
||||
|
||||
alter table "public"."token_pack_products" drop constraint "token_pack_products_pkey";
|
||||
|
||||
alter table "public"."token_transactions" drop constraint "token_transactions_pkey";
|
||||
|
||||
alter table "public"."trial_users" drop constraint "trial_users_pkey";
|
||||
|
||||
drop index if exists "public"."idx_subscriptions_stripe_customer_id";
|
||||
|
||||
drop index if exists "public"."idx_subscriptions_stripe_subscription_id";
|
||||
|
||||
drop index if exists "public"."idx_subscriptions_user_id";
|
||||
|
||||
drop index if exists "public"."subscriptions_pkey";
|
||||
|
||||
drop index if exists "public"."token_balances_pkey";
|
||||
|
||||
drop index if exists "public"."token_balances_user_id_source_key";
|
||||
|
||||
drop index if exists "public"."token_costs_pkey";
|
||||
|
||||
drop index if exists "public"."token_pack_products_pkey";
|
||||
|
||||
drop index if exists "public"."token_pack_products_stripe_lookup_key_key";
|
||||
|
||||
drop index if exists "public"."token_transactions_pkey";
|
||||
|
||||
drop index if exists "public"."trial_users_pkey";
|
||||
|
||||
drop index if exists "public"."trial_users_user_id_key";
|
||||
|
||||
drop table "public"."subscriptions";
|
||||
|
||||
drop table "public"."token_balances";
|
||||
|
||||
drop table "public"."token_costs";
|
||||
|
||||
drop table "public"."token_pack_products";
|
||||
|
||||
drop table "public"."token_transactions";
|
||||
|
||||
drop table "public"."trial_users";
|
||||
|
||||
drop type "public"."stripe-level";
|
||||
|
||||
drop type "public"."subscription_level";
|
||||
|
||||
drop type "public"."token_operation_type";
|
||||
|
||||
drop type "public"."token_source_type";
|
||||
|
||||
set check_function_bodies = off;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path TO ''
|
||||
AS $function$
|
||||
BEGIN
|
||||
INSERT INTO public.profiles (user_id, full_name)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
COALESCE(
|
||||
NEW.raw_user_meta_data->>'full_name',
|
||||
split_part(NEW.email, '@', 1)
|
||||
)
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
-- Add the new AI-SDK-shaped columns to messages. `parts` is a discriminated
|
||||
-- union of AI SDK UI parts (`{type:'text',...}`, `{type:'tool-...'}`, etc.);
|
||||
-- `metadata` carries per-message bookkeeping like the `model` that generated
|
||||
-- the response.
|
||||
--
|
||||
-- The migration uses a CREATE-TABLE-and-swap pattern instead of an in-place
|
||||
-- UPDATE. Bench on 745k rows of prod data: the compute (CASE + jsonb_build)
|
||||
-- takes 1.3s but rewriting every row in place takes 80s+ because of MVCC,
|
||||
-- per-row WAL, and TOAST writes on the 968 MB of artifact code being copied
|
||||
-- into `parts.input`. A bulk INSERT into a fresh table writes page-level
|
||||
-- WAL, has no dead tuples to clean up, and builds the indexes once at the
|
||||
-- end — total runtime drops well under a minute.
|
||||
|
||||
-- Tune for bulk work. Plain SET (not SET LOCAL) so this file works whether
|
||||
-- run inside a transaction (supabase db push wraps it implicitly) or in
|
||||
-- autocommit mode (psql on the direct connection); both forms scope to the
|
||||
-- current session and don't leak.
|
||||
SET statement_timeout = 0;
|
||||
SET maintenance_work_mem = '512MB';
|
||||
SET max_parallel_maintenance_workers = 4;
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. Build a new messages table with the parts/metadata columns pre-populated.
|
||||
-- No indexes, constraints, or policies yet — those slow down bulk INSERT.
|
||||
-- ============================================================================
|
||||
CREATE TABLE public.messages_new AS
|
||||
SELECT
|
||||
m.id,
|
||||
m.created_at,
|
||||
m.conversation_id,
|
||||
m.role,
|
||||
m.content,
|
||||
m.parent_message_id,
|
||||
m.rating,
|
||||
CASE
|
||||
WHEN m.content IS NULL OR jsonb_typeof(m.content) <> 'object' THEN '[]'::jsonb
|
||||
WHEN m.role = 'user' THEN
|
||||
-- text part
|
||||
(CASE WHEN COALESCE(m.content->>'text','') <> ''
|
||||
THEN jsonb_build_array(jsonb_build_object('type','text','text', m.content->>'text'))
|
||||
ELSE '[]'::jsonb END)
|
||||
-- file parts from images. Storage path is `${user}/${conv}/${imageId}`
|
||||
-- with no extension; .png suffix is just a display label.
|
||||
|| (CASE WHEN jsonb_typeof(m.content->'images') = 'array'
|
||||
THEN COALESCE((
|
||||
SELECT jsonb_agg(jsonb_build_object(
|
||||
'type','file',
|
||||
'mediaType','image/png',
|
||||
'filename', img || '.png',
|
||||
'url', '/storage/v1/object/public/images/' || c.user_id::text || '/' || c.id::text || '/' || img
|
||||
))
|
||||
FROM jsonb_array_elements_text(m.content->'images') AS img
|
||||
), '[]'::jsonb)
|
||||
ELSE '[]'::jsonb END)
|
||||
-- data-mesh-context
|
||||
|| (CASE WHEN jsonb_typeof(m.content->'mesh') = 'object'
|
||||
AND (m.content->'mesh'->>'id') IS NOT NULL
|
||||
THEN jsonb_build_array(jsonb_build_object(
|
||||
'type','data-mesh-context',
|
||||
'data', jsonb_strip_nulls(jsonb_build_object(
|
||||
'meshId', m.content->'mesh'->>'id',
|
||||
'fileType', COALESCE(m.content->'mesh'->>'fileType','glb'),
|
||||
'filename', m.content->>'meshFilename',
|
||||
'boundingBox', m.content->'meshBoundingBox'
|
||||
))
|
||||
))
|
||||
ELSE '[]'::jsonb END)
|
||||
-- data-mesh-preferences
|
||||
|| (CASE WHEN (m.content ? 'meshTopology') OR (m.content ? 'polygonCount')
|
||||
THEN jsonb_build_array(jsonb_build_object(
|
||||
'type','data-mesh-preferences',
|
||||
'data', jsonb_build_object(
|
||||
'topology', COALESCE(m.content->>'meshTopology','polys'),
|
||||
'polygonCount', COALESCE(NULLIF(m.content->>'polygonCount','')::int, 100000)
|
||||
)
|
||||
))
|
||||
ELSE '[]'::jsonb END)
|
||||
WHEN m.role = 'assistant' THEN
|
||||
-- text part (assistant text lands with state:'done')
|
||||
(CASE WHEN COALESCE(m.content->>'text','') <> ''
|
||||
THEN jsonb_build_array(jsonb_build_object(
|
||||
'type','text','text', m.content->>'text','state','done'))
|
||||
ELSE '[]'::jsonb END)
|
||||
-- tool-build_parametric_model. The legacy toolCalls array was the
|
||||
-- source of the original tool call id, but ~98% of assistant rows have
|
||||
-- toolCalls=[], so we synthesize a deterministic id keyed on the
|
||||
-- message uuid instead. Re-runs are byte-identical for the same row.
|
||||
-- The artifact object is referenced directly rather than rebuilt
|
||||
-- field-by-field — same data, no re-serialization cost.
|
||||
|| (CASE WHEN jsonb_typeof(m.content->'artifact') = 'object'
|
||||
THEN jsonb_build_array(jsonb_build_object(
|
||||
'type','tool-build_parametric_model',
|
||||
'toolCallId', 'tool_legacy_bp_' || m.id::text,
|
||||
'state','output-available',
|
||||
'input', m.content->'artifact',
|
||||
'output', jsonb_build_object('status','success','message','')
|
||||
))
|
||||
ELSE '[]'::jsonb END)
|
||||
-- tool-create_mesh
|
||||
|| (CASE WHEN jsonb_typeof(m.content->'mesh') = 'object'
|
||||
AND (m.content->'mesh'->>'id') IS NOT NULL
|
||||
THEN jsonb_build_array(jsonb_build_object(
|
||||
'type','tool-create_mesh',
|
||||
'toolCallId', m.content->'mesh'->>'id',
|
||||
'state','output-available',
|
||||
'input', '{}'::jsonb,
|
||||
'output', jsonb_build_object(
|
||||
'id', m.content->'mesh'->>'id',
|
||||
'fileType', COALESCE(m.content->'mesh'->>'fileType','glb')
|
||||
)
|
||||
))
|
||||
ELSE '[]'::jsonb END)
|
||||
-- assistant-side file parts (legacy create_image tool outputs)
|
||||
|| (CASE WHEN jsonb_typeof(m.content->'images') = 'array'
|
||||
THEN COALESCE((
|
||||
SELECT jsonb_agg(jsonb_build_object(
|
||||
'type','file',
|
||||
'mediaType','image/png',
|
||||
'filename', img || '.png',
|
||||
'url', '/storage/v1/object/public/images/' || c.user_id::text || '/' || c.id::text || '/' || img
|
||||
))
|
||||
FROM jsonb_array_elements_text(m.content->'images') AS img
|
||||
), '[]'::jsonb)
|
||||
ELSE '[]'::jsonb END)
|
||||
ELSE '[]'::jsonb
|
||||
END AS parts,
|
||||
jsonb_strip_nulls(jsonb_build_object('model', m.content->>'model')) AS metadata
|
||||
FROM public.messages m
|
||||
LEFT JOIN public.conversations c ON c.id = m.conversation_id;
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. Restore the column constraints CTAS strips out. `content` becomes
|
||||
-- nullable (new code writes parts only); `parts` and `metadata` get the
|
||||
-- same defaults as the original ADD COLUMN form.
|
||||
-- ============================================================================
|
||||
ALTER TABLE public.messages_new
|
||||
ALTER COLUMN id SET DEFAULT gen_random_uuid(),
|
||||
ALTER COLUMN id SET NOT NULL,
|
||||
ALTER COLUMN created_at SET DEFAULT now(),
|
||||
ALTER COLUMN created_at SET NOT NULL,
|
||||
ALTER COLUMN conversation_id SET NOT NULL,
|
||||
ALTER COLUMN role SET NOT NULL,
|
||||
ALTER COLUMN rating SET DEFAULT 0::smallint,
|
||||
ALTER COLUMN rating SET NOT NULL,
|
||||
ALTER COLUMN parts SET DEFAULT '[]'::jsonb,
|
||||
ALTER COLUMN parts SET NOT NULL,
|
||||
ALTER COLUMN metadata SET DEFAULT '{}'::jsonb,
|
||||
ALTER COLUMN metadata SET NOT NULL;
|
||||
|
||||
-- ============================================================================
|
||||
-- 3. Indexes. Built once at the end of the bulk load — far faster than
|
||||
-- maintaining them per-row during INSERT.
|
||||
-- ============================================================================
|
||||
ALTER TABLE public.messages_new
|
||||
ADD CONSTRAINT messages_new_pkey PRIMARY KEY (id);
|
||||
|
||||
CREATE INDEX messages_new_conversation_id_idx
|
||||
ON public.messages_new USING btree (conversation_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- 4. Constraints. The role check matches the original schema. The
|
||||
-- payload-present check is new and added NOT VALID to skip the full
|
||||
-- table scan — every row trivially satisfies it (content was NOT NULL
|
||||
-- in the legacy schema and every row carries either parts or content).
|
||||
-- ============================================================================
|
||||
ALTER TABLE public.messages_new
|
||||
ADD CONSTRAINT messages_role_check
|
||||
CHECK (role = ANY (ARRAY['user'::text, 'assistant'::text])) NOT VALID;
|
||||
|
||||
ALTER TABLE public.messages_new
|
||||
ADD CONSTRAINT messages_payload_present
|
||||
CHECK (jsonb_array_length(parts) > 0 OR content IS NOT NULL) NOT VALID;
|
||||
|
||||
ALTER TABLE public.messages_new
|
||||
ADD CONSTRAINT messages_new_conversation_id_fkey
|
||||
FOREIGN KEY (conversation_id) REFERENCES public.conversations(id)
|
||||
ON DELETE CASCADE NOT VALID;
|
||||
|
||||
-- ============================================================================
|
||||
-- 5. RLS — same policies as the legacy table.
|
||||
-- ============================================================================
|
||||
ALTER TABLE public.messages_new ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Public conversations messages"
|
||||
ON public.messages_new
|
||||
FOR SELECT
|
||||
TO anon, authenticated
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM public.conversations
|
||||
WHERE conversations.id = messages_new.conversation_id
|
||||
AND conversations.privacy = 'public'::privacy_type
|
||||
));
|
||||
|
||||
CREATE POLICY "Users can manage messages in their conversations"
|
||||
ON public.messages_new
|
||||
USING ((SELECT auth.uid() AS uid) IN (
|
||||
SELECT conversations.user_id FROM public.conversations
|
||||
WHERE conversations.id = messages_new.conversation_id
|
||||
));
|
||||
|
||||
-- ============================================================================
|
||||
-- 6. Grants — mirror the legacy table's permissions for anon/authenticated/
|
||||
-- service_role/postgres.
|
||||
-- ============================================================================
|
||||
GRANT ALL ON TABLE public.messages_new TO anon;
|
||||
GRANT ALL ON TABLE public.messages_new TO authenticated;
|
||||
GRANT ALL ON TABLE public.messages_new TO service_role;
|
||||
GRANT ALL ON TABLE public.messages_new TO postgres;
|
||||
|
||||
-- ============================================================================
|
||||
-- 7. Trigger — keep conversations.current_message_leaf_id pointed at the
|
||||
-- latest inserted message (function is unchanged, defined in the initial
|
||||
-- schema migration).
|
||||
-- ============================================================================
|
||||
CREATE TRIGGER update_leaf_trigger
|
||||
AFTER INSERT ON public.messages_new
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_conversation_leaf();
|
||||
|
||||
-- ============================================================================
|
||||
-- 8. Atomic swap. Drop the old table, rename the new one into its place,
|
||||
-- rename the indexes/constraints so future migrations and ORMs see the
|
||||
-- canonical names. This whole step holds an ACCESS EXCLUSIVE lock on
|
||||
-- `messages` for milliseconds.
|
||||
-- ============================================================================
|
||||
DROP TABLE public.messages;
|
||||
|
||||
ALTER TABLE public.messages_new RENAME TO messages;
|
||||
ALTER INDEX public.messages_new_pkey RENAME TO messages_pkey;
|
||||
ALTER INDEX public.messages_new_conversation_id_idx RENAME TO messages_conversation_id_idx;
|
||||
ALTER TABLE public.messages RENAME CONSTRAINT messages_new_conversation_id_fkey TO messages_conversation_id_fkey;
|
||||
|
||||
-- ============================================================================
|
||||
-- 9. The legacy `conversations.legacy` column is no longer needed.
|
||||
-- ============================================================================
|
||||
ALTER TABLE public.conversations DROP COLUMN IF EXISTS legacy;
|
||||
|
||||
-- ============================================================================
|
||||
-- 10. Atomic write for `conversations.settings.suggestions`. The chat server
|
||||
-- previously read settings, merged the new suggestions array client-side,
|
||||
-- and wrote the whole object back — which under concurrent assistant
|
||||
-- turns could silently drop the other update. `jsonb_set` runs in one
|
||||
-- statement so only the `suggestions` key is touched.
|
||||
-- ============================================================================
|
||||
CREATE OR REPLACE FUNCTION public.set_conversation_suggestions(
|
||||
p_conversation_id uuid,
|
||||
p_suggestions jsonb
|
||||
) RETURNS void LANGUAGE sql VOLATILE SECURITY INVOKER AS $$
|
||||
UPDATE public.conversations
|
||||
SET settings = jsonb_set(
|
||||
COALESCE(settings, '{}'::jsonb),
|
||||
'{suggestions}',
|
||||
p_suggestions,
|
||||
true
|
||||
)
|
||||
WHERE id = p_conversation_id;
|
||||
$$;
|
||||
@@ -0,0 +1,34 @@
|
||||
CREATE TABLE IF NOT EXISTS "public"."conversations" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"(),
|
||||
"updated_at" timestamp with time zone DEFAULT "now"(),
|
||||
"user_id" "uuid" NOT NULL,
|
||||
"title" "text" NOT NULL,
|
||||
"type" "public"."conversation-type" DEFAULT 'parametric'::"public"."conversation-type" NOT NULL,
|
||||
"privacy" "public"."privacy_type" DEFAULT 'private'::"public"."privacy_type" NOT NULL,
|
||||
"current_message_leaf_id" "uuid",
|
||||
"settings" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL
|
||||
);
|
||||
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS conversations_pkey ON "public"."conversations" USING btree (id);
|
||||
|
||||
ALTER TABLE "public"."conversations" ADD CONSTRAINT "conversations_pkey" PRIMARY KEY USING INDEX "conversations_pkey";
|
||||
|
||||
ALTER TABLE "public"."conversations" ADD CONSTRAINT "conversations_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
ALTER TABLE "public"."conversations" VALIDATE CONSTRAINT "conversations_user_id_fkey";
|
||||
|
||||
|
||||
CREATE INDEX IF NOT EXISTS conversations_created_at_idx ON "public"."conversations" USING btree (created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS conversations_updated_at_idx ON "public"."conversations" USING btree (updated_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS conversations_user_id_idx ON "public"."conversations" USING btree (user_id);
|
||||
|
||||
|
||||
CREATE POLICY "Anyone can view a public conversation" ON "public"."conversations" FOR SELECT TO "authenticated", "anon" USING (("privacy" = 'public'::"public"."privacy_type"));
|
||||
|
||||
CREATE POLICY "Users can manage their own conversations" ON "public"."conversations" USING ( (SELECT "auth"."uid"()) = "user_id" );
|
||||
|
||||
ALTER TABLE "public"."conversations" ENABLE ROW LEVEL SECURITY;
|
||||
@@ -0,0 +1,34 @@
|
||||
CREATE TABLE IF NOT EXISTS "public"."images" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"status" "public"."generation-status" DEFAULT 'pending'::"public"."generation-status" NOT NULL,
|
||||
"user_id" "uuid" NOT NULL,
|
||||
"conversation_id" "uuid" NOT NULL,
|
||||
"image_generation_call_id" "text",
|
||||
"prompt" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL
|
||||
);
|
||||
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS images_pkey ON "public"."images" USING btree (id);
|
||||
|
||||
ALTER TABLE "public"."images" ADD CONSTRAINT "images_pkey" PRIMARY KEY USING INDEX "images_pkey";
|
||||
|
||||
ALTER TABLE "public"."images" ADD CONSTRAINT "images_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
ALTER TABLE "public"."images" VALIDATE CONSTRAINT "images_conversation_id_fkey";
|
||||
|
||||
ALTER TABLE "public"."images" ADD CONSTRAINT "images_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
ALTER TABLE "public"."images" VALIDATE CONSTRAINT "images_user_id_fkey";
|
||||
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_images_image_generation_call_id ON "public"."images" USING "btree" ("image_generation_call_id");
|
||||
|
||||
|
||||
CREATE POLICY "Public conversations images" ON "public"."images" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
|
||||
FROM "public"."conversations"
|
||||
WHERE (("conversations"."id" = "images"."conversation_id") AND ("conversations"."privacy" = 'public'::"public"."privacy_type")))));
|
||||
|
||||
CREATE POLICY "User can manage their data" ON "public"."images" TO "authenticated" USING ((( SELECT "auth"."uid"()) = "user_id")) WITH CHECK ((( SELECT "auth"."uid"()) = "user_id"));
|
||||
|
||||
ALTER TABLE "public"."images" ENABLE ROW LEVEL SECURITY;
|
||||
@@ -0,0 +1,32 @@
|
||||
CREATE TABLE IF NOT EXISTS "public"."meshes" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"status" "public"."generation-status" DEFAULT 'pending'::"public"."generation-status" NOT NULL,
|
||||
"user_id" "uuid" NOT NULL,
|
||||
"images" "uuid"[],
|
||||
"conversation_id" "uuid" NOT NULL,
|
||||
"prompt" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
|
||||
"file_type" "public"."mesh_file_type" DEFAULT 'glb'::"public"."mesh_file_type" NOT NULL
|
||||
);
|
||||
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS meshes_pkey ON "public"."meshes" USING btree (id);
|
||||
|
||||
ALTER TABLE "public"."meshes" ADD CONSTRAINT "meshes_pkey" PRIMARY KEY USING INDEX "meshes_pkey";
|
||||
|
||||
ALTER TABLE "public"."meshes" ADD CONSTRAINT "meshes_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
ALTER TABLE "public"."meshes" VALIDATE CONSTRAINT "meshes_conversation_id_fkey";
|
||||
|
||||
ALTER TABLE "public"."meshes" ADD CONSTRAINT "meshes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
ALTER TABLE "public"."meshes" VALIDATE CONSTRAINT "meshes_user_id_fkey";
|
||||
|
||||
|
||||
CREATE POLICY "Everyone can view meshes associated with public conversations" ON "public"."meshes" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
|
||||
FROM "public"."conversations"
|
||||
WHERE (("conversations"."id" = "meshes"."conversation_id") AND ("conversations"."privacy" = 'public'::"public"."privacy_type")))));
|
||||
|
||||
CREATE POLICY "Users can manage their meshes" ON "public"."meshes" USING ( (SELECT "auth"."uid"()) = "user_id" );
|
||||
|
||||
ALTER TABLE "public"."meshes" ENABLE ROW LEVEL SECURITY;
|
||||
@@ -0,0 +1,36 @@
|
||||
CREATE TABLE IF NOT EXISTS "public"."messages" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"conversation_id" "uuid" NOT NULL,
|
||||
"role" "text" NOT NULL,
|
||||
"parts" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL,
|
||||
"metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
|
||||
"content" "jsonb",
|
||||
"rating" smallint DEFAULT '0'::smallint NOT NULL,
|
||||
"parent_message_id" "uuid",
|
||||
CONSTRAINT "messages_role_check" CHECK (("role" = ANY (ARRAY['user'::"text", 'assistant'::"text"]))),
|
||||
CONSTRAINT "messages_payload_present" CHECK ((jsonb_array_length("parts") > 0) OR ("content" IS NOT NULL))
|
||||
);
|
||||
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS messages_pkey ON "public"."messages" USING btree (id);
|
||||
|
||||
ALTER TABLE "public"."messages" ADD CONSTRAINT "messages_pkey" PRIMARY KEY USING INDEX "messages_pkey";
|
||||
|
||||
ALTER TABLE "public"."messages" ADD CONSTRAINT "messages_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE not valid;
|
||||
|
||||
ALTER TABLE "public"."messages" VALIDATE CONSTRAINT "messages_conversation_id_fkey";
|
||||
|
||||
|
||||
CREATE INDEX IF NOT EXISTS messages_conversation_id_idx ON "public"."messages" USING btree (conversation_id);
|
||||
|
||||
|
||||
CREATE POLICY "Public conversations messages" ON "public"."messages" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
|
||||
FROM "public"."conversations"
|
||||
WHERE (("conversations"."id" = "messages"."conversation_id") AND ("conversations"."privacy" = 'public'::"public"."privacy_type")))));
|
||||
|
||||
CREATE POLICY "Users can manage messages in their conversations" ON "public"."messages" USING (((SELECT "auth"."uid"()) IN ( SELECT "conversations"."user_id"
|
||||
FROM "public"."conversations"
|
||||
WHERE ("conversations"."id" = "messages"."conversation_id"))));
|
||||
|
||||
ALTER TABLE "public"."messages" ENABLE ROW LEVEL SECURITY;
|
||||
@@ -0,0 +1,31 @@
|
||||
CREATE TABLE IF NOT EXISTS "public"."previews" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"status" "public"."generation-status" DEFAULT 'pending'::"public"."generation-status" NOT NULL,
|
||||
"user_id" "uuid" NOT NULL,
|
||||
"conversation_id" "uuid" NOT NULL,
|
||||
"mesh_id" "uuid" NOT NULL
|
||||
);
|
||||
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS previews_pkey ON "public"."previews" USING btree (id);
|
||||
|
||||
ALTER TABLE "public"."previews" ADD CONSTRAINT "previews_pkey" PRIMARY KEY USING INDEX "previews_pkey";
|
||||
|
||||
ALTER TABLE "public"."previews" ADD CONSTRAINT "previews_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
ALTER TABLE "public"."previews" VALIDATE CONSTRAINT "previews_user_id_fkey";
|
||||
|
||||
ALTER TABLE "public"."previews" ADD CONSTRAINT "previews_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
ALTER TABLE "public"."previews" VALIDATE CONSTRAINT "previews_conversation_id_fkey";
|
||||
|
||||
ALTER TABLE "public"."previews" ADD CONSTRAINT "previews_mesh_id_fkey" FOREIGN KEY (mesh_id) REFERENCES meshes(id) ON DELETE CASCADE not valid;
|
||||
|
||||
ALTER TABLE "public"."previews" VALIDATE CONSTRAINT "previews_mesh_id_fkey";
|
||||
|
||||
|
||||
ALTER TABLE "public"."previews" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Users can manage their own previews" ON "public"."previews" USING ( (SELECT "auth"."uid"()) = "user_id" );
|
||||
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE IF NOT EXISTS "public"."profiles" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"user_id" "uuid" NOT NULL,
|
||||
"full_name" "text" NOT NULL,
|
||||
"notifications_enabled" boolean DEFAULT false NOT NULL,
|
||||
"avatar_path" "text" DEFAULT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS profiles_pkey ON "public"."profiles" USING btree (id);
|
||||
|
||||
ALTER TABLE "public"."profiles" ADD CONSTRAINT "profiles_pkey" PRIMARY KEY USING INDEX "profiles_pkey";
|
||||
|
||||
ALTER TABLE "public"."profiles" ADD CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
ALTER TABLE "public"."profiles" VALIDATE CONSTRAINT "profiles_user_id_fkey";
|
||||
|
||||
CREATE POLICY "Users can manage their own profile" ON "public"."profiles" USING ( (SELECT "auth"."uid"()) = "user_id" );
|
||||
|
||||
ALTER TABLE "public"."profiles" ENABLE ROW LEVEL SECURITY;
|
||||
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE IF NOT EXISTS "public"."prompts" (
|
||||
"id" bigint generated by default as identity not null,
|
||||
"created_at" timestamp with time zone not null default now(),
|
||||
"user_id" "uuid" NOT NULL,
|
||||
"type" "public"."prompt_type" DEFAULT 'chat'::"public"."prompt_type" NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS prompts_pkey ON "public"."prompts" USING btree (id);
|
||||
|
||||
ALTER TABLE "public"."prompts" ADD CONSTRAINT "prompts_pkey" PRIMARY KEY USING INDEX "prompts_pkey";
|
||||
|
||||
ALTER TABLE "public"."prompts" ADD CONSTRAINT "prompts_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
|
||||
|
||||
ALTER TABLE "public"."prompts" VALIDATE CONSTRAINT "prompts_user_id_fkey";
|
||||
|
||||
CREATE POLICY "Users can view their own prompts" ON "public"."prompts" FOR SELECT USING ((SELECT "auth"."uid"()) = "user_id");
|
||||
|
||||
ALTER TABLE "public"."prompts" ENABLE ROW LEVEL SECURITY;
|
||||
@@ -0,0 +1,48 @@
|
||||
-- Images and Meshes
|
||||
CREATE POLICY "Give users access to own folder images_select" ON storage.objects FOR SELECT TO public USING (bucket_id = 'images' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
|
||||
|
||||
CREATE POLICY "Give users access to own folder images_insert" ON storage.objects FOR INSERT TO public WITH CHECK (bucket_id = 'images' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
|
||||
|
||||
CREATE POLICY "Give users access to own folder images_update" ON storage.objects FOR UPDATE TO public USING (bucket_id = 'images' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
|
||||
|
||||
CREATE POLICY "Give users access to own folder images_delete" ON storage.objects FOR DELETE TO public USING (bucket_id = 'images' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
|
||||
|
||||
CREATE POLICY "Give users access to own folder meshes_select" ON storage.objects FOR SELECT TO public USING (bucket_id = 'meshes' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
|
||||
|
||||
CREATE POLICY "Give users access to own folder meshes_insert" ON storage.objects FOR INSERT TO public WITH CHECK (bucket_id = 'meshes' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
|
||||
|
||||
CREATE POLICY "Give users access to own folder meshes_update" ON storage.objects FOR UPDATE TO public USING (bucket_id = 'meshes' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
|
||||
|
||||
CREATE POLICY "Give users access to own folder meshes_delete" ON storage.objects FOR DELETE TO public USING (bucket_id = 'meshes' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
|
||||
|
||||
-- Shared Conversations
|
||||
CREATE POLICY "Public conversations allow anyone to view images_select" ON storage.objects FOR SELECT TO anon, authenticated USING (((bucket_id = 'images'::text) AND (EXISTS ( SELECT 1 FROM conversations WHERE ((conversations.privacy = 'public') AND ((conversations.id)::text = (storage.foldername(objects.name))[2]))))));
|
||||
|
||||
CREATE POLICY "Public conversations allow anyone to view meshes_select" ON storage.objects FOR SELECT TO anon, authenticated USING (((bucket_id = 'meshes'::text) AND (EXISTS ( SELECT 1 FROM conversations WHERE ((conversations.privacy = 'public') AND ((conversations.id)::text = (storage.foldername(objects.name))[2]))))));
|
||||
|
||||
-- Previews
|
||||
CREATE POLICY "Give users access to own folder previews_select" ON storage.objects FOR SELECT TO public USING (bucket_id = 'previews' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
|
||||
|
||||
CREATE POLICY "Give users access to own folder previews_insert" ON storage.objects FOR INSERT TO public WITH CHECK (bucket_id = 'previews' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
|
||||
|
||||
CREATE POLICY "Give users access to own folder previews_update" ON storage.objects FOR UPDATE TO public USING (bucket_id = 'previews' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
|
||||
|
||||
CREATE POLICY "Give users access to own folder previews_delete" ON storage.objects FOR DELETE TO public USING (bucket_id = 'previews' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
|
||||
|
||||
-- Only allow service role to upload temp multiview images (from our functions)
|
||||
CREATE POLICY "Allow service role to upload temp multiview images"
|
||||
ON storage.objects FOR INSERT
|
||||
TO service_role
|
||||
WITH CHECK (bucket_id = 'temp-multiview');
|
||||
|
||||
-- Allow public read access (for Tripo to download)
|
||||
CREATE POLICY "Allow public read access to temp multiview images"
|
||||
ON storage.objects FOR SELECT
|
||||
TO public
|
||||
USING (bucket_id = 'temp-multiview');
|
||||
|
||||
-- Allow service role to delete expired files (for manual cleanup if needed)
|
||||
CREATE POLICY "Allow service role to delete temp multiview images"
|
||||
ON storage.objects FOR DELETE
|
||||
TO service_role
|
||||
USING (bucket_id = 'temp-multiview');
|
||||
@@ -0,0 +1,54 @@
|
||||
CREATE OR REPLACE FUNCTION "public"."update_conversation_leaf"() RETURNS "trigger"
|
||||
LANGUAGE "plpgsql"
|
||||
AS $$
|
||||
begin
|
||||
update conversations set
|
||||
current_message_leaf_id = new.id,
|
||||
updated_at = now()
|
||||
where id = new.conversation_id;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE TRIGGER "update_leaf_trigger" AFTER INSERT ON "public"."messages" FOR EACH ROW EXECUTE FUNCTION "public"."update_conversation_leaf"();
|
||||
|
||||
-- Previews updated_at trigger
|
||||
-- Function to update the updated_at column
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Trigger to automatically update updated_at for previews
|
||||
CREATE OR REPLACE TRIGGER update_previews_updated_at
|
||||
BEFORE UPDATE ON "public"."previews"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Profile creation trigger for new users
|
||||
-- Create function to handle new user sign ups
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO public.profiles (user_id, full_name)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
COALESCE(
|
||||
NEW.raw_user_meta_data->>'full_name',
|
||||
split_part(NEW.email, '@', 1)
|
||||
)
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Create trigger to automatically create profile on user creation
|
||||
CREATE OR REPLACE TRIGGER on_auth_user_created
|
||||
AFTER INSERT ON auth.users
|
||||
FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();
|
||||
@@ -0,0 +1,34 @@
|
||||
CREATE TYPE "public"."conversation-type" AS ENUM (
|
||||
'parametric',
|
||||
'creative'
|
||||
);
|
||||
|
||||
CREATE TYPE "public"."generation-status" AS ENUM (
|
||||
'pending',
|
||||
'success',
|
||||
'failure'
|
||||
);
|
||||
|
||||
CREATE TYPE "public"."mesh_model_type" AS ENUM (
|
||||
'quality',
|
||||
'fast'
|
||||
);
|
||||
|
||||
CREATE TYPE "public"."mesh_file_type" AS ENUM (
|
||||
'glb',
|
||||
'stl',
|
||||
'obj',
|
||||
'fbx'
|
||||
);
|
||||
|
||||
CREATE TYPE "public"."privacy_type" AS ENUM (
|
||||
'public',
|
||||
'private'
|
||||
);
|
||||
|
||||
CREATE TYPE "public"."prompt_type" AS ENUM (
|
||||
'mesh',
|
||||
'image',
|
||||
'chat'
|
||||
);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- THIS TEST USER IS USED FOR TESTING PURPOSES ONLY ON LOCAL DEVELOPMENT DONT RUN THIS IN PRODUCTION OR MOM WILL BE SAD
|
||||
INSERT INTO auth.users ( instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, recovery_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, created_at, updated_at, confirmation_token, email_change, email_change_token_new, recovery_token)
|
||||
VALUES
|
||||
('00000000-0000-0000-0000-000000000000', uuid_generate_v4(), 'authenticated', 'authenticated', 'test@adamcad.com', crypt('password', gen_salt('bf')), current_timestamp, current_timestamp, current_timestamp, '{"provider":"email","providers":["email"]}', '{}', current_timestamp, current_timestamp, '', '', '', '');
|
||||
|
||||
|
||||
-- test user email identity
|
||||
INSERT INTO auth.identities (id, user_id, identity_data, provider, provider_id, last_sign_in_at, created_at, updated_at)
|
||||
VALUES
|
||||
(uuid_generate_v4(), (SELECT id FROM auth.users WHERE email = 'test@adamcad.com'), format('{"sub":"%s","email":"%s"}', (SELECT id FROM auth.users WHERE email = 'test@adamcad.com')::text, 'test@adamcad.com')::jsonb, 'email', uuid_generate_v4(), current_timestamp, current_timestamp, current_timestamp);
|
||||
Reference in New Issue
Block a user