From a86609aa5c567ebf0f3636cb356eb237ac0d0f57 Mon Sep 17 00:00:00 2001 From: wass08 Date: Wed, 4 Mar 2026 13:18:18 +0100 Subject: [PATCH] community presets draft --- apps/editor/app/api/presets/[id]/route.ts | 80 ++ apps/editor/app/api/presets/route.ts | 81 ++ .../components/ui/panels/door-panel.tsx | 44 +- .../ui/panels/presets/presets-popover.tsx | 395 +++++ .../components/ui/panels/window-panel.tsx | 39 +- apps/editor/instrumentation.ts | 9 + packages/db/src/schema/index.ts | 3 + packages/db/src/schema/presets/presets.ts | 30 + packages/db/src/types.ts | 35 + .../20260304121520_daffy_gideon.sql | 17 + .../20260304130720_create_presets.sql | 58 + .../meta/20260304121520_snapshot.json | 1271 +++++++++++++++++ supabase/migrations/meta/_journal.json | 7 + 13 files changed, 2067 insertions(+), 2 deletions(-) create mode 100644 apps/editor/app/api/presets/[id]/route.ts create mode 100644 apps/editor/app/api/presets/route.ts create mode 100644 apps/editor/components/ui/panels/presets/presets-popover.tsx create mode 100644 packages/db/src/schema/presets/presets.ts create mode 100644 supabase/migrations/20260304121520_daffy_gideon.sql create mode 100644 supabase/migrations/20260304130720_create_presets.sql create mode 100644 supabase/migrations/meta/20260304121520_snapshot.json diff --git a/apps/editor/app/api/presets/[id]/route.ts b/apps/editor/app/api/presets/[id]/route.ts new file mode 100644 index 00000000..3fede668 --- /dev/null +++ b/apps/editor/app/api/presets/[id]/route.ts @@ -0,0 +1,80 @@ +import { NextRequest, NextResponse } from 'next/server' +import { headers } from 'next/headers' +import { auth } from '@/lib/auth' +import { supabaseAdmin } from '@/lib/supabase/server' + +// PUT /api/presets/[id] +export async function PUT( + req: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const session = await auth.api.getSession({ headers: await headers() }) + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + const body = await req.json() + const { name } = body + + if (!name) { + return NextResponse.json({ error: 'Missing name' }, { status: 400 }) + } + + const { data: existing } = await supabaseAdmin + .from('presets') + .select('user_id, is_community') + .eq('id', id) + .single() + + if (!existing || existing.user_id !== session.user.id || existing.is_community) { + return NextResponse.json({ error: 'Not found or forbidden' }, { status: 403 }) + } + + const { data: preset, error } = await supabaseAdmin + .from('presets') + .update({ name }) + .eq('id', id) + .select() + .single() + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }) + return NextResponse.json({ preset }) +} + +// DELETE /api/presets/[id] +export async function DELETE( + req: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const session = await auth.api.getSession({ headers: await headers() }) + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + + const { data: existing } = await supabaseAdmin + .from('presets') + .select('user_id, is_community, thumbnail_url') + .eq('id', id) + .single() + + if (!existing || existing.user_id !== session.user.id || existing.is_community) { + return NextResponse.json({ error: 'Not found or forbidden' }, { status: 403 }) + } + + // Delete thumbnail from storage if present + if (existing.thumbnail_url) { + const url = existing.thumbnail_url as string + const match = url.match(/preset-thumbnails\/(.+)$/) + if (match?.[1]) { + await supabaseAdmin.storage.from('preset-thumbnails').remove([match[1]]) + } + } + + const { error } = await supabaseAdmin.from('presets').delete().eq('id', id) + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }) + return NextResponse.json({ success: true }) +} diff --git a/apps/editor/app/api/presets/route.ts b/apps/editor/app/api/presets/route.ts new file mode 100644 index 00000000..eb5d9367 --- /dev/null +++ b/apps/editor/app/api/presets/route.ts @@ -0,0 +1,81 @@ +import { NextRequest, NextResponse } from 'next/server' +import { headers } from 'next/headers' +import { auth } from '@/lib/auth' +import { supabaseAdmin } from '@/lib/supabase/server' +import { createId } from '@pascal-app/db' + +// GET /api/presets?type=door|window&tab=community|mine +export async function GET(req: NextRequest) { + const { searchParams } = req.nextUrl + const type = searchParams.get('type') + const tab = searchParams.get('tab') ?? 'community' + + if (!type || (type !== 'door' && type !== 'window')) { + return NextResponse.json({ error: 'Invalid type' }, { status: 400 }) + } + + if (tab === 'mine') { + const session = await auth.api.getSession({ headers: await headers() }) + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { data, error } = await supabaseAdmin + .from('presets') + .select('*') + .eq('type', type) + .eq('user_id', session.user.id) + .eq('is_community', false) + .order('created_at', { ascending: false }) + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }) + return NextResponse.json({ presets: data }) + } + + // community tab + const { data, error } = await supabaseAdmin + .from('presets') + .select('*') + .eq('type', type) + .eq('is_community', true) + .order('created_at', { ascending: false }) + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }) + return NextResponse.json({ presets: data }) +} + +// POST /api/presets +export async function POST(req: NextRequest) { + const session = await auth.api.getSession({ headers: await headers() }) + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const body = await req.json() + const { type, name, data, thumbnailUrl } = body + + if (!type || !name || !data) { + return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }) + } + + if (type !== 'door' && type !== 'window') { + return NextResponse.json({ error: 'Invalid type' }, { status: 400 }) + } + + const { data: preset, error } = await supabaseAdmin + .from('presets') + .insert({ + id: createId('preset'), + type, + name, + data, + thumbnail_url: thumbnailUrl ?? null, + user_id: session.user.id, + is_community: false, + }) + .select() + .single() + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }) + return NextResponse.json({ preset }, { status: 201 }) +} diff --git a/apps/editor/components/ui/panels/door-panel.tsx b/apps/editor/components/ui/panels/door-panel.tsx index 2c6d8457..386b0e2a 100644 --- a/apps/editor/components/ui/panels/door-panel.tsx +++ b/apps/editor/components/ui/panels/door-panel.tsx @@ -2,7 +2,7 @@ import { type AnyNode, type AnyNodeId, DoorNode, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' -import { Copy, FlipHorizontal2, Move, Trash2 } from 'lucide-react' +import { BookMarked, Copy, FlipHorizontal2, Move, Trash2 } from 'lucide-react' import { useCallback } from 'react' import { sfxEmitter } from '@/lib/sfx-bus' import useEditor from '@/store/use-editor' @@ -14,6 +14,7 @@ import { MetricControl } from '../controls/metric-control' import { ToggleControl } from '../controls/toggle-control' import { SegmentedControl } from '../controls/segmented-control' import { ActionButton, ActionGroup } from '../controls/action-button' +import { PresetsPopover } from './presets/presets-popover' export function DoorPanel() { const selectedIds = useViewer((s) => s.selection.selectedIds) @@ -116,6 +117,37 @@ export function DoorPanel() { handleUpdate({ segments: updated }) } + const handleSavePreset = useCallback(async (name: string) => { + if (!node) return + const data = { + width: node.width, + height: node.height, + frameThickness: node.frameThickness, + frameDepth: node.frameDepth, + contentPadding: node.contentPadding, + hingesSide: node.hingesSide, + swingDirection: node.swingDirection, + threshold: node.threshold, + thresholdHeight: node.thresholdHeight, + handle: node.handle, + handleHeight: node.handleHeight, + handleSide: node.handleSide, + doorCloser: node.doorCloser, + panicBar: node.panicBar, + panicBarHeight: node.panicBarHeight, + segments: node.segments, + } + await fetch('/api/presets', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: 'door', name, data }), + }) + }, [node]) + + const handleApplyPreset = useCallback((data: Record) => { + handleUpdate(data as Partial) + }, [handleUpdate]) + if (!node || node.type !== 'door' || selectedIds.length !== 1) return null const hSum = node.segments.reduce((s, seg) => s + seg.heightRatio, 0) @@ -128,6 +160,16 @@ export function DoorPanel() { onClose={handleClose} width={320} > + {/* Presets strip */} +
+ + + +
+ Xwall} diff --git a/apps/editor/components/ui/panels/presets/presets-popover.tsx b/apps/editor/components/ui/panels/presets/presets-popover.tsx new file mode 100644 index 00000000..dacf29cd --- /dev/null +++ b/apps/editor/components/ui/panels/presets/presets-popover.tsx @@ -0,0 +1,395 @@ +'use client' + +import { useEffect, useState, useCallback } from 'react' +import { BookMarked, Pencil, Plus, Trash2, Users, Check, X } from 'lucide-react' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/primitives/popover' +import { useAuth } from '@/features/community/lib/auth/hooks' +import { cn } from '@/lib/utils' + +export type PresetType = 'door' | 'window' + +export interface PresetData { + id: string + type: string + name: string + data: Record + thumbnail_url: string | null + user_id: string | null + is_community: boolean + created_at: string +} + +type Tab = 'community' | 'mine' + +interface PresetsPopoverProps { + type: PresetType + onApply: (data: Record) => void + onSave: (name: string) => Promise + children: React.ReactNode +} + +export function PresetsPopover({ type, onApply, onSave, children }: PresetsPopoverProps) { + const { isAuthenticated } = useAuth() + const [open, setOpen] = useState(false) + const [tab, setTab] = useState('community') + const [presets, setPresets] = useState([]) + const [loading, setLoading] = useState(false) + + // Save dialog state + const [showSaveInput, setShowSaveInput] = useState(false) + const [saveName, setSaveName] = useState('') + const [saving, setSaving] = useState(false) + + // Rename state + const [renamingId, setRenamingId] = useState(null) + const [renameValue, setRenameValue] = useState('') + + // Delete confirmation + const [deletingId, setDeletingId] = useState(null) + + const fetchPresets = useCallback(async () => { + setLoading(true) + try { + const res = await fetch(`/api/presets?type=${type}&tab=${tab}`) + if (res.ok) { + const json = await res.json() + setPresets(json.presets ?? []) + } + } finally { + setLoading(false) + } + }, [type, tab]) + + useEffect(() => { + if (open) fetchPresets() + }, [open, fetchPresets]) + + // Switch tab to community if user signs out while on mine tab + useEffect(() => { + if (!isAuthenticated && tab === 'mine') setTab('community') + }, [isAuthenticated, tab]) + + const handleSave = async () => { + if (!saveName.trim()) return + setSaving(true) + try { + await onSave(saveName.trim()) + setSaveName('') + setShowSaveInput(false) + if (tab === 'mine') fetchPresets() + else setTab('mine') + } finally { + setSaving(false) + } + } + + const handleRename = async (id: string) => { + if (!renameValue.trim()) return + const res = await fetch(`/api/presets/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: renameValue.trim() }), + }) + if (res.ok) { + setPresets((prev) => prev.map((p) => (p.id === id ? { ...p, name: renameValue.trim() } : p))) + setRenamingId(null) + } + } + + const handleDelete = async (id: string) => { + const res = await fetch(`/api/presets/${id}`, { method: 'DELETE' }) + if (res.ok) { + setPresets((prev) => prev.filter((p) => p.id !== id)) + setDeletingId(null) + } + } + + return ( + + {children} + + {/* Header */} +
+
+ + + {type === 'door' ? 'Door' : 'Window'} Presets + +
+ {isAuthenticated && ( + + )} +
+ + {/* Save input */} + {showSaveInput && ( +
+ setSaveName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSave() + if (e.key === 'Escape') { setShowSaveInput(false); setSaveName('') } + }} + placeholder="Preset name…" + className="flex-1 min-w-0 rounded-md border border-border/50 bg-background/50 px-2 py-1 text-xs text-foreground placeholder:text-muted-foreground/60 outline-none focus:border-ring focus:ring-1 focus:ring-ring/30" + /> + + +
+ )} + + {/* Tabs */} +
+ setTab('community')}> + + Community + + { + if (!isAuthenticated) return + setTab('mine') + }} + disabled={!isAuthenticated} + > + + My presets + +
+ + {/* Content */} +
+ {loading ? ( +
+
+
+ ) : presets.length === 0 ? ( + + ) : ( +
    + {presets.map((preset) => ( + { onApply(preset.data); setOpen(false) }} + onStartRename={() => { setRenamingId(preset.id); setRenameValue(preset.name) }} + onRenameChange={setRenameValue} + onRenameConfirm={() => handleRename(preset.id)} + onRenameCancel={() => setRenamingId(null)} + onDeleteRequest={() => setDeletingId(preset.id)} + onDeleteConfirm={() => handleDelete(preset.id)} + onDeleteCancel={() => setDeletingId(null)} + /> + ))} +
+ )} +
+ + + ) +} + +function TabButton({ + active, + onClick, + disabled, + children, +}: { + active: boolean + onClick: () => void + disabled?: boolean + children: React.ReactNode +}) { + return ( + + ) +} + +function EmptyState({ tab, isAuthenticated }: { tab: Tab; isAuthenticated: boolean }) { + return ( +
+ +

+ {tab === 'community' + ? 'No community presets yet.' + : isAuthenticated + ? 'No presets saved yet. Use "Save preset" to save the current configuration.' + : 'Sign in to save and view your presets.'} +

+
+ ) +} + +interface PresetRowProps { + preset: PresetData + isMine: boolean + renamingId: string | null + renameValue: string + deletingId: string | null + onApply: () => void + onStartRename: () => void + onRenameChange: (v: string) => void + onRenameConfirm: () => void + onRenameCancel: () => void + onDeleteRequest: () => void + onDeleteConfirm: () => void + onDeleteCancel: () => void +} + +function PresetRow({ + preset, + isMine, + renamingId, + renameValue, + deletingId, + onApply, + onStartRename, + onRenameChange, + onRenameConfirm, + onRenameCancel, + onDeleteRequest, + onDeleteConfirm, + onDeleteCancel, +}: PresetRowProps) { + const isRenaming = renamingId === preset.id + const isDeleting = deletingId === preset.id + + if (isDeleting) { + return ( +
  • + Delete "{preset.name}"? +
    + + +
    +
  • + ) + } + + if (isRenaming) { + return ( +
  • + onRenameChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') onRenameConfirm() + if (e.key === 'Escape') onRenameCancel() + }} + className="flex-1 min-w-0 rounded-md border border-border/50 bg-background/50 px-2 py-1 text-xs text-foreground outline-none focus:border-ring focus:ring-1 focus:ring-ring/30" + /> + + +
  • + ) + } + + return ( +
  • + {/* Thumbnail placeholder */} +
    + {preset.thumbnail_url ? ( + // eslint-disable-next-line @next/next/no-img-element + {preset.name} + ) : ( +
    +
    +
    + )} +
    + + + + {isMine && ( +
    + + +
    + )} +
  • + ) +} diff --git a/apps/editor/components/ui/panels/window-panel.tsx b/apps/editor/components/ui/panels/window-panel.tsx index a721b8a4..80f64679 100644 --- a/apps/editor/components/ui/panels/window-panel.tsx +++ b/apps/editor/components/ui/panels/window-panel.tsx @@ -2,7 +2,7 @@ import { type AnyNode, type AnyNodeId, WindowNode, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' -import { Copy, FlipHorizontal2, Move, Trash2 } from 'lucide-react' +import { BookMarked, Copy, FlipHorizontal2, Move, Trash2 } from 'lucide-react' import { useCallback } from 'react' import { sfxEmitter } from '@/lib/sfx-bus' import useEditor from '@/store/use-editor' @@ -13,6 +13,7 @@ import { SliderControl } from '../controls/slider-control' import { MetricControl } from '../controls/metric-control' import { ToggleControl } from '../controls/toggle-control' import { ActionButton, ActionGroup } from '../controls/action-button' +import { PresetsPopover } from './presets/presets-popover' export function WindowPanel() { const selectedIds = useViewer((s) => s.selection.selectedIds) @@ -91,6 +92,32 @@ export function WindowPanel() { setSelection({ selectedIds: [] }) }, [node, setMovingNode, setSelection]) + const handleSavePreset = useCallback(async (name: string) => { + if (!node) return + const data = { + width: node.width, + height: node.height, + frameThickness: node.frameThickness, + frameDepth: node.frameDepth, + columnRatios: node.columnRatios, + rowRatios: node.rowRatios, + columnDividerThickness: node.columnDividerThickness, + rowDividerThickness: node.rowDividerThickness, + sill: node.sill, + sillDepth: node.sillDepth, + sillThickness: node.sillThickness, + } + await fetch('/api/presets', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: 'window', name, data }), + }) + }, [node]) + + const handleApplyPreset = useCallback((data: Record) => { + handleUpdate(data as Partial) + }, [handleUpdate]) + if (!node || node.type !== 'window' || selectedIds.length !== 1) return null const numCols = node.columnRatios.length @@ -134,6 +161,16 @@ export function WindowPanel() { onClose={handleClose} width={320} > + {/* Presets strip */} +
    + + + +
    + Xpos} diff --git a/apps/editor/instrumentation.ts b/apps/editor/instrumentation.ts index 81648c17..ffe74034 100644 --- a/apps/editor/instrumentation.ts +++ b/apps/editor/instrumentation.ts @@ -37,5 +37,14 @@ export async function register() { }) console.log('Created "project-assets" storage bucket') } + + if (!bucketNames.has('preset-thumbnails')) { + await supabase.storage.createBucket('preset-thumbnails', { + public: true, + fileSizeLimit: 5 * 1024 * 1024, // 5MB + allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'], + }) + console.log('Created "preset-thumbnails" storage bucket') + } } } diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 65773f88..b64110cc 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -14,3 +14,6 @@ export * from './projects/assets' export * from './projects/likes' export * from './projects/models' export * from './projects/projects' + +// Presets table +export * from './presets/presets' diff --git a/packages/db/src/schema/presets/presets.ts b/packages/db/src/schema/presets/presets.ts new file mode 100644 index 00000000..57c4ec62 --- /dev/null +++ b/packages/db/src/schema/presets/presets.ts @@ -0,0 +1,30 @@ +import { pgTable, index, text, boolean, jsonb } from 'drizzle-orm/pg-core' +import { createInsertSchema, createSelectSchema } from 'drizzle-zod' +import { id, timestampsColumns } from '../../helpers' +import { users } from '../auth/users' + +export const presets = pgTable( + 'presets', + (t) => ({ + id: id('preset'), + type: t.text('type').notNull(), // 'door' | 'window' + name: t.text('name').notNull(), + data: t.jsonb('data').notNull(), + thumbnailUrl: t.text('thumbnail_url'), + userId: t + .text('user_id') + .references(() => users.id, { onDelete: 'cascade' }), + isCommunity: t.boolean('is_community').notNull().default(false), + ...timestampsColumns, + }), + (t) => [ + index('presets_type_idx').on(t.type), + index('presets_user_id_idx').on(t.userId), + index('presets_is_community_idx').on(t.isCommunity), + ], +).enableRLS() + +export type Preset = typeof presets.$inferSelect +export type NewPreset = typeof presets.$inferInsert +export const insertPresetSchema = createInsertSchema(presets) +export const selectPresetSchema = createSelectSchema(presets) diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts index bcab2216..5c4b94b3 100644 --- a/packages/db/src/types.ts +++ b/packages/db/src/types.ts @@ -116,6 +116,41 @@ export interface Database { updated_at?: string } } + presets: { + Row: { + id: string + type: string + name: string + data: Json + thumbnail_url: string | null + user_id: string | null + is_community: boolean + created_at: string + updated_at: string + } + Insert: { + id?: string + type: string + name: string + data: Json + thumbnail_url?: string | null + user_id?: string | null + is_community?: boolean + created_at?: string + updated_at?: string + } + Update: { + id?: string + type?: string + name?: string + data?: Json + thumbnail_url?: string | null + user_id?: string | null + is_community?: boolean + created_at?: string + updated_at?: string + } + } } Views: {} Functions: {} diff --git a/supabase/migrations/20260304121520_daffy_gideon.sql b/supabase/migrations/20260304121520_daffy_gideon.sql new file mode 100644 index 00000000..d4496b77 --- /dev/null +++ b/supabase/migrations/20260304121520_daffy_gideon.sql @@ -0,0 +1,17 @@ +CREATE TABLE "presets" ( + "id" text PRIMARY KEY NOT NULL, + "type" text NOT NULL, + "name" text NOT NULL, + "data" jsonb NOT NULL, + "thumbnail_url" text, + "user_id" text, + "is_community" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "presets" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +ALTER TABLE "presets" ADD CONSTRAINT "presets_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "presets_type_idx" ON "presets" USING btree ("type");--> statement-breakpoint +CREATE INDEX "presets_user_id_idx" ON "presets" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "presets_is_community_idx" ON "presets" USING btree ("is_community"); \ No newline at end of file diff --git a/supabase/migrations/20260304130720_create_presets.sql b/supabase/migrations/20260304130720_create_presets.sql new file mode 100644 index 00000000..0fb841ae --- /dev/null +++ b/supabase/migrations/20260304130720_create_presets.sql @@ -0,0 +1,58 @@ +-- Presets table: stores door/window presets for community and user use +CREATE TABLE "presets" ( + "id" text PRIMARY KEY NOT NULL, + "type" text NOT NULL, -- 'door' | 'window' + "name" text NOT NULL, + "data" jsonb NOT NULL, + "thumbnail_url" text, + "user_id" text REFERENCES "auth_users"("id") ON DELETE CASCADE, + "is_community" boolean NOT NULL DEFAULT false, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); + +--> statement-breakpoint +CREATE INDEX "presets_type_idx" ON "presets"("type"); +--> statement-breakpoint +CREATE INDEX "presets_user_id_idx" ON "presets"("user_id"); +--> statement-breakpoint +CREATE INDEX "presets_is_community_idx" ON "presets"("is_community"); + +--> statement-breakpoint +ALTER TABLE "presets" ENABLE ROW LEVEL SECURITY; + +--> statement-breakpoint +-- Anyone can read community presets +CREATE POLICY "Anyone can view community presets" + ON "presets" FOR SELECT + USING ("is_community" = true); + +--> statement-breakpoint +-- Users can view their own presets +CREATE POLICY "Users can view own presets" + ON "presets" FOR SELECT + USING ("user_id" = current_setting('app.user_id', true)::TEXT); + +--> statement-breakpoint +-- Users can insert their own presets +CREATE POLICY "Users can insert own presets" + ON "presets" FOR INSERT + WITH CHECK ("user_id" = current_setting('app.user_id', true)::TEXT AND "is_community" = false); + +--> statement-breakpoint +-- Users can update their own presets +CREATE POLICY "Users can update own presets" + ON "presets" FOR UPDATE + USING ("user_id" = current_setting('app.user_id', true)::TEXT AND "is_community" = false); + +--> statement-breakpoint +-- Users can delete their own presets +CREATE POLICY "Users can delete own presets" + ON "presets" FOR DELETE + USING ("user_id" = current_setting('app.user_id', true)::TEXT AND "is_community" = false); + +--> statement-breakpoint +CREATE TRIGGER "update_presets_updated_at" + BEFORE UPDATE ON "presets" + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); diff --git a/supabase/migrations/meta/20260304121520_snapshot.json b/supabase/migrations/meta/20260304121520_snapshot.json new file mode 100644 index 00000000..773c1d38 --- /dev/null +++ b/supabase/migrations/meta/20260304121520_snapshot.json @@ -0,0 +1,1271 @@ +{ + "id": "75cf6a24-dec8-44c8-97a6-5dd4e8ad7fc5", + "prevId": "022423df-5615-44b1-a725-a9c1f4baa074", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.auth_accounts": { + "name": "auth_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_accounts_user_id_auth_users_id_fk": { + "name": "auth_accounts_user_id_auth_users_id_fk", + "tableFrom": "auth_accounts", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.auth_jwks": { + "name": "auth_jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.auth_sessions": { + "name": "auth_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_project_id": { + "name": "active_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_sessions_user_id_auth_users_id_fk": { + "name": "auth_sessions_user_id_auth_users_id_fk", + "tableFrom": "auth_sessions", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auth_sessions_impersonated_by_auth_users_id_fk": { + "name": "auth_sessions_impersonated_by_auth_users_id_fk", + "tableFrom": "auth_sessions", + "tableTo": "auth_users", + "columnsFrom": [ + "impersonated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.auth_users": { + "name": "auth_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_url": { + "name": "github_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "x_url": { + "name": "x_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "youtube_url": { + "name": "youtube_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_notifications": { + "name": "email_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "role": { + "name": "role", + "type": "auth_user_roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_unique_index": { + "name": "email_unique_index", + "columns": [ + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "username_unique_index": { + "name": "username_unique_index", + "columns": [ + { + "expression": "lower(\"username\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.auth_verifications": { + "name": "auth_verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_index": { + "name": "verification_identifier_index", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.feedback": { + "name": "feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scene_graph": { + "name": "scene_graph", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.projects_addresses": { + "name": "projects_addresses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "street_number": { + "name": "street_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "route_short": { + "name": "route_short", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "neighborhood": { + "name": "neighborhood", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "county": { + "name": "county", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_long": { + "name": "state_long", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code_suffix": { + "name": "postal_code_suffix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_long": { + "name": "country_long", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "raw_json": { + "name": "raw_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "address_components_unique": { + "name": "address_components_unique", + "nullsNotDistinct": false, + "columns": [ + "street_number", + "route", + "city", + "state", + "postal_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.project_assets": { + "name": "project_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_assets_project_id_projects_id_fk": { + "name": "project_assets_project_id_projects_id_fk", + "tableFrom": "project_assets", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.projects_likes": { + "name": "projects_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_likes_project_id_projects_id_fk": { + "name": "projects_likes_project_id_projects_id_fk", + "tableFrom": "projects_likes", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_likes_project_user_unique": { + "name": "projects_likes_project_user_unique", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.projects_models": { + "name": "projects_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft": { + "name": "draft", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scene_graph": { + "name": "scene_graph", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_models_project_id_projects_id_fk": { + "name": "projects_models_project_id_projects_id_fk", + "tableFrom": "projects_models", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_id": { + "name": "address_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details_json": { + "name": "details_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "published_model_version": { + "name": "published_model_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_empty": { + "name": "is_empty", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_scans_public": { + "name": "show_scans_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_guides_public": { + "name": "show_guides_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "likes": { + "name": "likes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_address_idx": { + "name": "project_address_idx", + "columns": [ + { + "expression": "address_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_owner_idx": { + "name": "project_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_is_private_idx": { + "name": "project_is_private_idx", + "columns": [ + { + "expression": "is_private", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_views_idx": { + "name": "project_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_likes_idx": { + "name": "project_likes_idx", + "columns": [ + { + "expression": "likes", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_address_id_projects_addresses_id_fk": { + "name": "projects_address_id_projects_addresses_id_fk", + "tableFrom": "projects", + "tableTo": "projects_addresses", + "columnsFrom": [ + "address_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "projects_owner_id_auth_users_id_fk": { + "name": "projects_owner_id_auth_users_id_fk", + "tableFrom": "projects", + "tableTo": "auth_users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.presets": { + "name": "presets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_community": { + "name": "is_community", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "presets_type_idx": { + "name": "presets_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "presets_user_id_idx": { + "name": "presets_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "presets_is_community_idx": { + "name": "presets_is_community_idx", + "columns": [ + { + "expression": "is_community", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "presets_user_id_auth_users_id_fk": { + "name": "presets_user_id_auth_users_id_fk", + "tableFrom": "presets", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": { + "public.auth_user_roles": { + "name": "auth_user_roles", + "schema": "public", + "values": [ + "user", + "admin" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/supabase/migrations/meta/_journal.json b/supabase/migrations/meta/_journal.json index 8c7bb9c5..69efeb8d 100644 --- a/supabase/migrations/meta/_journal.json +++ b/supabase/migrations/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1772573729680, "tag": "20260303213529_marvelous_mikhail_rasputin", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1772626520793, + "tag": "20260304121520_daffy_gideon", + "breakpoints": true } ] } \ No newline at end of file