From 9bc710c94b31327ed8c0241e673f75534955425f Mon Sep 17 00:00:00 2001 From: gunshiz Date: Thu, 8 Oct 2026 02:23:59 +0700 Subject: [PATCH] feat : ask for noti --- README.md | 15 +- app/admin/comments/page.tsx | 8 +- app/api/comments/route.ts | 4 +- app/api/notifications/events/route.ts | 11 + app/api/notifications/route.ts | 27 + app/layout.tsx | 7 +- components/admin/admin-header.tsx | 8 +- components/comments/admin-comment-inbox.tsx | 12 +- components/comments/comment-board.tsx | 2 +- components/comments/comment-card.tsx | 1 + components/comments/comment-composer.tsx | 1 - components/comments/comment-push-prompt.tsx | 76 +- components/commission/push-client.ts | 19 +- components/commission/push-entry-prompt.tsx | 66 - components/commission/ticket-chat.tsx | 2 - .../notifications/notification-bell.tsx | 82 + components/notifications/push-client.ts | 18 + components/notifications/site-push-prompt.tsx | 60 + components/public/site-header.tsx | 2 + db/schema.ts | 21 + drizzle/0078_slow_black_bolt.sql | 21 + drizzle/0079_luxuriant_thunderball.sql | 2 + drizzle/meta/0078_snapshot.json | 5148 ++++++++++++++++ drizzle/meta/0079_snapshot.json | 5178 +++++++++++++++++ drizzle/meta/_journal.json | 14 + lib/comments/push.ts | 58 +- lib/comments/repository.ts | 12 +- lib/commission/push.ts | 9 +- lib/notifications/events.ts | 6 + lib/notifications/repository.ts | 41 + tests/comment-notification-worker.test.ts | 8 + tests/comments.integration.test.ts | 118 +- tests/notifications-route.test.ts | 30 + tests/site-push-client.test.ts | 27 + 34 files changed, 10913 insertions(+), 201 deletions(-) create mode 100644 app/api/notifications/events/route.ts create mode 100644 app/api/notifications/route.ts delete mode 100644 components/commission/push-entry-prompt.tsx create mode 100644 components/notifications/notification-bell.tsx create mode 100644 components/notifications/push-client.ts create mode 100644 components/notifications/site-push-prompt.tsx create mode 100644 drizzle/0078_slow_black_bolt.sql create mode 100644 drizzle/0079_luxuriant_thunderball.sql create mode 100644 drizzle/meta/0078_snapshot.json create mode 100644 drizzle/meta/0079_snapshot.json create mode 100644 lib/notifications/events.ts create mode 100644 lib/notifications/repository.ts create mode 100644 tests/notifications-route.test.ts create mode 100644 tests/site-push-client.test.ts diff --git a/README.md b/README.md index 63cb565..0fdd631 100644 --- a/README.md +++ b/README.md @@ -172,11 +172,22 @@ Replies expand inline with connector lines; replies to replies identify their re The Thai comment UI supports image paste (Ctrl+V) with previews and loading skeletons. Comment boards and the admin inbox receive Redis-backed SSE change events; reads after each event recheck authorization. Reconnecting refreshes missed updates. -Signed-in authors can enable browser notifications after posting. New replies notify -only the direct parent author, excluding self-replies. Notification clicks open the +One site-entry prompt enables browser notifications for comments, Commission, +and future notification features. “ไม่ต้องถามอีก” remembers dismissal in this +browser; the header bell settings can reopen the prompt. Permission granted before +sign-in is connected to the account after sign-in. New replies notify +only the direct parent author, excluding self-replies. Subscribed, verified admins +also receive new top-level comments from other authors, with a link that opens the +comment in an admin conversation dialog. Admins can enable this from the inbox +using “แจ้งเตือนความคิดเห็นใหม่”. Reply notification clicks open the root thread, load the relevant reply page, and highlight/focus the exact reply. This uses the existing `WEB_PUSH_PUBLIC_KEY`, `WEB_PUSH_PRIVATE_KEY`, and `WEB_PUSH_SUBJECT` configuration, with subscriptions in `comments.push_subscription`. +The header bell lists comments and Commission notifications from `public.notification`, +updates through one user-scoped SSE stream, and caps its red unread badge at `9+`. +Clicking an item marks it read; “อ่านทั้งหมด” marks the account's history read. +Read state persists across visits, admin notifications recheck current permissions, +and source deletion removes its notifications through cascading foreign keys. Likes, dislikes, and admin hearts update optimistically and save in the background; a failed save rolls the reaction back and shows an error. diff --git a/app/admin/comments/page.tsx b/app/admin/comments/page.tsx index ac32f0a..a5751ce 100644 --- a/app/admin/comments/page.tsx +++ b/app/admin/comments/page.tsx @@ -4,14 +4,16 @@ import { AdminHeader } from "@/components/admin/admin-header"; import { AdminCommentInbox } from "@/components/comments/admin-comment-inbox"; import { getCommentViewer, listComments, listCommentTargets } from "@/lib/comments/repository"; -export default async function AdminCommentsPage({ searchParams }: { searchParams: Promise<{ target?: string }> }) { +export default async function AdminCommentsPage({ searchParams }: { searchParams: Promise<{ target?: string; comment?: string }> }) { await connection(); const viewer = await getCommentViewer(); if (!viewer?.admin) redirect("/auth/login?next=%2Fadmin%2Fcomments"); - const initialTarget = (await searchParams).target; + const query = await searchParams; + const initialTarget = query.target; + const initialComment = initialTarget && query.comment && /^[0-9a-f-]{36}$/i.test(query.comment) ? query.comment : undefined; const [initial, targets] = await Promise.all([listComments({ viewer, inbox: true, target: initialTarget }), listCommentTargets()]); return <>

ความคิดเห็น

ตอบกลับและจัดการความคิดเห็นจากทุกไกด์

- +
; } diff --git a/app/api/comments/route.ts b/app/api/comments/route.ts index 4588d74..2ded514 100644 --- a/app/api/comments/route.ts +++ b/app/api/comments/route.ts @@ -1,5 +1,5 @@ import { after } from "next/server"; -import { sendCommentReplyPush } from "@/lib/comments/push"; +import { sendCommentPush } from "@/lib/comments/push"; import { getCommentViewer, listComments, requireCommentViewer } from "@/lib/comments/repository"; import { publishComment } from "@/lib/comments/publish"; import { errorResponse, HttpError, requireSameOrigin } from "@/lib/security/http"; @@ -21,7 +21,7 @@ export async function POST(request: Request) { const target = new URL(request.url).searchParams.get("target"); if (!target) throw new HttpError(400, "target-required"); const result = await publishComment(request, viewer, { target }); - after(async () => { await sendCommentReplyPush(result.id).catch(() => console.error("Comment reply notification failed")); }); + after(async () => { await sendCommentPush(result.id).catch(() => console.error("Comment notification failed")); }); return Response.json(result, { status: 201 }); } catch (cause) { return errorResponse(cause); } } diff --git a/app/api/notifications/events/route.ts b/app/api/notifications/events/route.ts new file mode 100644 index 0000000..df23eef --- /dev/null +++ b/app/api/notifications/events/route.ts @@ -0,0 +1,11 @@ +import { requireCommissionUser } from '@/lib/commission/server'; +import { createRedisNamedEventResponse } from '@/lib/events/redis-stream'; +import { errorResponse } from '@/lib/security/http'; +import { limitRequest, trustedClientAddress } from '@/lib/security/rate-limit'; +export async function GET(request: Request) { + try { + const user = await requireCommissionUser(); + await limitRequest('stream-open', trustedClientAddress(request.headers), 60); + return await createRedisNamedEventResponse(`notifications:user:${user.id}`, 'changed', request.signal); + } catch (cause) { return errorResponse(cause); } +} diff --git a/app/api/notifications/route.ts b/app/api/notifications/route.ts new file mode 100644 index 0000000..70321b7 --- /dev/null +++ b/app/api/notifications/route.ts @@ -0,0 +1,27 @@ +import * as z from 'zod'; +import { requireCommissionUser } from '@/lib/commission/server'; +import { listNotifications, readNotifications } from '@/lib/notifications/repository'; +import { errorResponse, HttpError, readJson, requireSameOrigin } from '@/lib/security/http'; +import { limitRequest } from '@/lib/security/rate-limit'; +export async function GET(request: Request) { + try { + const user = await requireCommissionUser(); + const raw = new URL(request.url).searchParams.get('before'); + const parts = raw?.split('|'); + const parsed = parts ? z.object({ time: z.iso.datetime(), id: z.uuid() }).safeParse({ time: parts[0], id: parts[1] }) : undefined; + if (raw && (!parsed?.success || parts?.length !== 2)) throw new HttpError(400, 'invalid-cursor'); + const before = parsed?.success ? parsed.data : undefined; + return Response.json(await listNotifications(user, before), { headers: { 'Cache-Control': 'private, no-store' } }); + } catch (cause) { return errorResponse(cause); } +} +export async function POST(request: Request) { + try { + requireSameOrigin(request); + const user = await requireCommissionUser(); + await limitRequest('notification-read', user.id, 120); + const parsed = z.object({ id: z.uuid().optional() }).safeParse(await readJson(request, 1024)); + if (!parsed.success) throw new HttpError(400, 'invalid-notification'); + await readNotifications(user.id, parsed.data.id); + return new Response(null, { status: 204 }); + } catch (cause) { return errorResponse(cause); } +} diff --git a/app/layout.tsx b/app/layout.tsx index 7d43870..ed7338e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,8 @@ import { Anuphan, Geist_Mono } from "next/font/google"; import { DeploymentUpdateNotifier } from "@/components/deployment-update-notifier"; import { PageProgress } from "@/components/page-progress"; import { Toaster } from "@/components/ui/sonner"; +import { NotificationProvider } from "@/components/notifications/notification-bell"; +import { SitePushPrompt } from "@/components/notifications/site-push-prompt"; import { siteUrl } from "@/lib/site-url"; import "./globals.css"; @@ -47,7 +49,10 @@ export default function RootLayout({ children }: LayoutProps<"/">) { > - {children} + + {children} + + - {pathname.startsWith("/admin/commission") && }
- + +
diff --git a/components/comments/admin-comment-inbox.tsx b/components/comments/admin-comment-inbox.tsx index 9981165..6ccab11 100644 --- a/components/comments/admin-comment-inbox.tsx +++ b/components/comments/admin-comment-inbox.tsx @@ -9,18 +9,20 @@ import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; import { FuzzyCombobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from "@/components/ui/combobox"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Empty, EmptyHeader, EmptyTitle, EmptyDescription } from "@/components/ui/empty"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import type { CommentPage } from "@/lib/comments/types"; import { characterRarityGradientClass } from "@/lib/catalog/rarity"; import { cn } from "@/lib/utils"; import { useMarkCommentsRead } from "./admin-unread-comments"; import { CommentPushPrompt } from "./comment-push-prompt"; -import { CommentCard } from "./comment-card"; +import { CommentCard, CommentThread } from "./comment-card"; import { CommentSkeleton } from "./comment-skeleton"; import { useCommentEvents } from "./use-comment-events"; import { useCommentFeed } from "./use-comment-feed"; type TargetOption = { value: string; label: string; image: string | null; rarity: number | null; characterKey: string | null }; -export function AdminCommentInbox({ initial, targets, initialTarget }: { initial: CommentPage; targets: TargetOption[]; initialTarget?: string }) { +export function AdminCommentInbox({ initial, targets, initialTarget, initialComment }: { initial: CommentPage; targets: TargetOption[]; initialTarget?: string; initialComment?: string }) { + const [notificationOpen, setNotificationOpen] = useState(Boolean(initialComment)); const [target, setTarget] = useState(targets.find((item) => item.value === initialTarget) ?? null); const [status, setStatus] = useState("all"); const [answered, setAnswered] = useState("all"); @@ -31,7 +33,11 @@ export function AdminCommentInbox({ initial, targets, initialTarget }: { initial const markRead = useMarkCommentsRead(); useEffect(() => { void markRead(target?.value ?? "all"); }, [markRead, target, feed.revision]); return
- {feed.page.viewer && } + {feed.page.viewer && } + {initialComment && initialTarget && + การสนทนาความคิดเห็นจากการแจ้งเตือนและคำตอบกลับ + {notificationOpen && feed.refresh({ silent: true })} />} + } ไกด์หรือรอบ Stygian item.label} itemToStringValue={(item) => item.value} diff --git a/components/comments/comment-board.tsx b/components/comments/comment-board.tsx index a3236df..b178f4b 100644 --- a/components/comments/comment-board.tsx +++ b/components/comments/comment-board.tsx @@ -39,7 +39,7 @@ export function CommentBoard({ target, initial }: { target: string; initial: Com return () => window.removeEventListener("hashchange", showThread); }, []); return
- {feed.page.viewer && } + {feed.page.viewer && }
{feed.page.viewer?.name?.slice(0, 2) || "คุณ"}
{feed.page.viewer ? void feed.refresh()} /> :

เข้าสู่ระบบเพื่อแสดงความคิดเห็น ตอบกลับ และกดถูกใจ

}

ความคิดเห็น

diff --git a/components/comments/comment-card.tsx b/components/comments/comment-card.tsx index 907eae9..601c5f7 100644 --- a/components/comments/comment-card.tsx +++ b/components/comments/comment-card.tsx @@ -256,6 +256,7 @@ export function CommentCard({ item, viewer, onChange, revision = 0, inbox = fals {/* eslint-disable-next-line @next/next/no-img-element */} {`ภาพ } + {item.target.startsWith("stygian:") && {item.targetName}} }
ประวัติการแก้ไขดูข้อความและรูปภาพในแต่ละเวอร์ชันของความคิดเห็น diff --git a/components/comments/comment-composer.tsx b/components/comments/comment-composer.tsx index aed9613..f60adf7 100644 --- a/components/comments/comment-composer.tsx +++ b/components/comments/comment-composer.tsx @@ -96,7 +96,6 @@ export function CommentComposer({ target, replyTo, editing, onSaved, onCancel }: }); setText(""); setFiles([]); setKept([]); setExpanded(false); toast.success(editing ? "แก้ไขความคิดเห็นแล้ว" : "ส่งความคิดเห็นแล้ว"); - window.dispatchEvent(new Event("comment:posted")); onSaved(); } catch (cause) { setError(cause instanceof Error ? cause.message : "ไม่สามารถบันทึกความคิดเห็นได้"); } finally { setPending(false); uploadRef.current = null; } diff --git a/components/comments/comment-push-prompt.tsx b/components/comments/comment-push-prompt.tsx index 5b204be..ecb3da6 100644 --- a/components/comments/comment-push-prompt.tsx +++ b/components/comments/comment-push-prompt.tsx @@ -1,72 +1,6 @@ -"use client"; - -import { useCallback, useEffect, useState } from "react"; -import { BellIcon } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; - -export function CommentPushPrompt({ userId }: { userId: string }) { - const [publicKey, setPublicKey] = useState(null); - const [open, setOpen] = useState(false); - const [enabled, setEnabled] = useState(false); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); - const askedKey = `comment-push-asked-${userId}`; - const syncedKey = `comment-push-synced-${userId}`; - const syncPush = useCallback(async (key: string) => { - // Reuse the site's existing root service worker so ticket notifications keep working. - const registration = await navigator.serviceWorker.register("/commission-push-sw.js", { scope: "/" }); - const subscription = await registration.pushManager.getSubscription() ?? await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key }); - const response = await fetch("/api/comments/push-subscription", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(subscription.toJSON()) }); - if (!response.ok) throw new Error("subscription-failed"); - sessionStorage.setItem(syncedKey, "1"); - }, [syncedKey]); - useEffect(() => { - if (!window.isSecureContext || !("Notification" in window) || !("PushManager" in window) || !("serviceWorker" in navigator)) return; - let active = true; - void fetch("/api/comments/push-subscription", { cache: "no-store" }).then(async (response) => { - if (!response.ok) return; - const config = await response.json() as { publicKey: string }; - if (!active) return; - setPublicKey(config.publicKey); - if (Notification.permission === "granted") { - if (!sessionStorage.getItem(syncedKey)) await syncPush(config.publicKey); - if (active) setEnabled(true); - } - }).catch(() => undefined); - return () => { active = false; }; - }, [userId, syncedKey, syncPush]); - - useEffect(() => { - function ask() { - if (!publicKey || Notification.permission !== "default" || localStorage.getItem(askedKey)) return; - localStorage.setItem(askedKey, "1"); - setOpen(true); - } - window.addEventListener("comment:posted", ask); - return () => window.removeEventListener("comment:posted", ask); - }, [publicKey, askedKey]); - - async function enable() { - if (!publicKey) return; - setBusy(true); setError(""); - try { - if (await Notification.requestPermission() !== "granted") { - setError("ยังไม่ได้อนุญาตการแจ้งเตือน เปิดสิทธิ์การแจ้งเตือนในการตั้งค่าเว็บไซต์ของเบราว์เซอร์"); - return; - } - await syncPush(publicKey); - setEnabled(true); setOpen(false); - } catch { setError("เปิดการแจ้งเตือนไม่สำเร็จ กรุณาลองอีกครั้ง"); } - finally { setBusy(false); } - } - if (!publicKey || enabled) return null; - return <> - - - รับการแจ้งเตือนเมื่อมีคนตอบกลับ?เราจะแจ้งเตือนเมื่อมีคนตอบกลับความคิดเห็นของคุณ แม้ปิดแท็บแล้ว กดที่การแจ้งเตือนเพื่อไปยังคำตอบนั้น - {error &&

{error}

} - -
- ; +'use client'; +import { BellIcon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +export function CommentPushPrompt({ admin = false }: { userId: string; admin?: boolean }) { + return ; } diff --git a/components/commission/push-client.ts b/components/commission/push-client.ts index d42004d..cdec311 100644 --- a/components/commission/push-client.ts +++ b/components/commission/push-client.ts @@ -1,26 +1,9 @@ "use client"; -export async function subscribeCommissionPush(scope: "customer" | "admin", publicKey: string) { - if (await Notification.requestPermission() !== "granted") return false; - await syncCommissionPush(scope, publicKey); - return true; -} - -export async function syncCommissionPush(scope: "customer" | "admin", publicKey: string) { - const registration = await navigator.serviceWorker.register("/commission-push-sw.js", { scope: "/" }); - const subscription = await registration.pushManager.getSubscription() ?? - await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: publicKey }); - const response = await fetch("/api/commission/push-subscription", { method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ ...subscription.toJSON(), scope }) }); - if (!response.ok) throw new Error("Could not save push subscription"); - sessionStorage.setItem(`commission-push-synced-${scope}`, "1"); -} - export async function unsubscribeCommissionPush() { sessionStorage.removeItem("commission-push-synced-customer"); sessionStorage.removeItem("commission-push-synced-admin"); - for (const key of Object.keys(sessionStorage)) if (key.startsWith("comment-push-synced-")) sessionStorage.removeItem(key); + for (const key of Object.keys(sessionStorage)) if (key.startsWith("comment-push-synced-") || key.startsWith("site-push-synced-")) sessionStorage.removeItem(key); if (!("serviceWorker" in navigator)) return; const registration = await navigator.serviceWorker.getRegistration("/"); const subscription = await registration?.pushManager.getSubscription(); diff --git a/components/commission/push-entry-prompt.tsx b/components/commission/push-entry-prompt.tsx deleted file mode 100644 index 93eec55..0000000 --- a/components/commission/push-entry-prompt.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { subscribeCommissionPush, syncCommissionPush } from "./push-client"; - -type PushRequest = { scope: "customer" | "admin"; publicKey: string }; -const askedKey = "commission-push-asked"; - -export function CommissionPushEntryPrompt({ scope }: { scope: "customer" | "admin" }) { - const [request, setRequest] = useState(null); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); - - useEffect(() => { - if (!window.isSecureContext || - !("Notification" in window) || !("PushManager" in window) || !("serviceWorker" in navigator) || - Notification.permission === "denied" || - (Notification.permission === "default" && sessionStorage.getItem(askedKey)) || - (Notification.permission === "granted" && (sessionStorage.getItem(`commission-push-synced-${scope}`) || - (scope === "customer" && sessionStorage.getItem("commission-push-synced-admin"))))) return; - - let active = true; - void fetch("/api/commission/push-subscription", { cache: "no-store" }).then(async (response) => { - if (!response.ok) return; - const config = await response.json() as { publicKey: string; canAdmin: boolean }; - if (!active || (scope === "admin" && !config.canAdmin)) return; - const subscriptionScope = config.canAdmin ? "admin" : scope; - if (Notification.permission === "granted") { - await syncCommissionPush(subscriptionScope, config.publicKey); - return; - } - if (Notification.permission !== "default") return; - sessionStorage.setItem(askedKey, "1"); - setRequest({ scope: subscriptionScope, publicKey: config.publicKey }); - }).catch(() => undefined); - return () => { active = false; }; - }, [scope]); - - async function enable() { - if (!request) return; - setBusy(true); - setError(""); - try { - await subscribeCommissionPush(request.scope, request.publicKey); - setRequest(null); - } catch { - setError("เปิดการแจ้งเตือนไม่สำเร็จ กรุณาลองอีกครั้ง"); - } finally { setBusy(false); } - } - - return { if (!open) setRequest(null); }}> - - - รับการแจ้งเตือนข้อความใหม่? - เมื่อมีข้อความใหม่ใน Ticket เราจะแจ้งเตือนพร้อมชื่อผู้ส่งและข้อความ แม้ปิดแท็บแล้ว - - {error &&

{error}

} - - - - -
-
; -} diff --git a/components/commission/ticket-chat.tsx b/components/commission/ticket-chat.tsx index cad7706..d9cb341 100644 --- a/components/commission/ticket-chat.tsx +++ b/components/commission/ticket-chat.tsx @@ -80,7 +80,6 @@ import { import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { parseCommissionMessageEvent } from "@/lib/commission/message-event"; -import { CommissionPushEntryPrompt } from "./push-entry-prompt"; import { keepCommissionTitle } from "./keep-title"; type ChatMessage = { @@ -626,7 +625,6 @@ export function CommissionTicketChat({ return ( <> - {!admin && } diff --git a/components/notifications/notification-bell.tsx b/components/notifications/notification-bell.tsx new file mode 100644 index 0000000..2e8a02c --- /dev/null +++ b/components/notifications/notification-bell.tsx @@ -0,0 +1,82 @@ +'use client'; +import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react'; +import Link from 'next/link'; +import { BellIcon, SettingsIcon } from 'lucide-react'; +import { authClient } from '@/lib/auth/client'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Skeleton } from '@/components/ui/skeleton'; +import { cn } from '@/lib/utils'; + +type Item = { id: string; title: string; body: string; url: string; readAt: string | null }; +type Feed = { items: Item[]; unread: number; nextCursor: string | null }; +const empty: Feed = { items: [], unread: 0, nextCursor: null }; +type State = Feed & { signedIn: boolean; busy: boolean; error: string; refresh: (more?: boolean) => Promise; read: (id?: string) => Promise }; +const Context = createContext({ ...empty, signedIn: false, busy: false, error: '', refresh: async () => {}, read: async () => {} }); +export function NotificationProvider({ children }: { children: ReactNode }) { + const { data } = authClient.useSession(); + const userId = data?.user.id; + const [feed, setFeed] = useState(empty); + const [owner, setOwner] = useState(undefined); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(''); + const current = useRef(feed); + useEffect(() => { current.current = feed; }, [feed]); + const sequence = useRef(0); + const invalidate = useCallback(() => { ++sequence.current; }, []); + const refresh = useCallback(async (more = false) => { + if (!userId) return; + const request = ++sequence.current; + const cursor = more ? current.current.nextCursor : null; + setBusy(true); + try { + const response = await fetch(`/api/notifications${cursor ? `?before=${encodeURIComponent(cursor)}` : ''}`, { cache: 'no-store' }); + if (!response.ok) throw new Error('โหลดการแจ้งเตือนไม่สำเร็จ'); + const result = await response.json() as Feed; + if (sequence.current !== request) return; + setFeed(previous => ({ ...result, items: more ? [...previous.items, ...result.items.filter(item => !previous.items.some(old => old.id === item.id))] : result.items })); + setOwner(userId); setError(''); + } catch (cause) { if (sequence.current === request) setError(cause instanceof Error ? cause.message : 'โหลดการแจ้งเตือนไม่สำเร็จ'); } + finally { if (sequence.current === request) setBusy(false); } + }, [userId]); + useEffect(() => { + if (!userId) return; + void Promise.resolve().then(() => refresh()); + const source = new EventSource('/api/notifications/events'); + source.addEventListener('changed', () => void refresh()); + source.addEventListener('open', () => void refresh()); + return () => { invalidate(); source.close(); }; + }, [userId, refresh, invalidate]); + const read = useCallback(async (id?: string) => { + if (!userId) return; + setFeed(previous => ({ ...previous, unread: id ? Math.max(0, previous.unread - (previous.items.some(item => item.id === id && !item.readAt) ? 1 : 0)) : 0, items: previous.items.map(item => !id || item.id === id ? { ...item, readAt: new Date().toISOString() } : item) })); + try { + const response = await fetch('/api/notifications', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(id ? { id } : {}) }); + if (!response.ok) throw new Error('บันทึกการอ่านไม่สำเร็จ'); + } finally { await refresh(); } + }, [userId, refresh]); + return {children}; +} +export function NotificationBell() { + const state = useContext(Context); + const [open, setOpen] = useState(false); + return { setOpen(value); if (value) void state.refresh(); }}> + }> + {state.unread > 0 && {state.unread > 9 ? '9+' : state.unread}} + + +

การแจ้งเตือน

+ {!state.signedIn ? เข้าสู่ระบบเพื่อดูการแจ้งเตือน : <> + {state.unread > 0 && } +
+ {state.items.map(item => { setOpen(false); void state.read(item.id).catch(() => undefined); }}>

{!item.readAt && ●}{item.title}

{item.body}

)} + {state.busy && !state.items.length &&
} + {!state.busy && !state.items.length && !state.error &&

ยังไม่มีการแจ้งเตือน

} + {state.error &&

{state.error}

} + {state.nextCursor && } +
+ } +
+
; +} diff --git a/components/notifications/push-client.ts b/components/notifications/push-client.ts new file mode 100644 index 0000000..4b779c4 --- /dev/null +++ b/components/notifications/push-client.ts @@ -0,0 +1,18 @@ +'use client'; + +// All notification features share one browser permission and root service worker. +export async function syncSitePush(userId: string) { + const configResponse = await fetch('/api/commission/push-subscription', { cache: 'no-store' }); + if (!configResponse.ok) throw new Error('push-config-failed'); + const config = await configResponse.json() as { publicKey: string; canAdmin: boolean }; + const registration = await navigator.serviceWorker.register('/commission-push-sw.js', { scope: '/' }); + const subscription = await registration.pushManager.getSubscription() ?? await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: config.publicKey }); + const data = subscription.toJSON(); + const registrations = [ + { url: '/api/comments/push-subscription', body: data }, + { url: '/api/commission/push-subscription', body: { ...data, scope: config.canAdmin ? 'admin' : 'customer' } }, + ]; + const responses = await Promise.all(registrations.map(feature => fetch(feature.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(feature.body) }))); + if (responses.some(response => !response.ok)) throw new Error('push-subscription-failed'); + sessionStorage.setItem(`site-push-synced-${userId}`, '1'); +} diff --git a/components/notifications/site-push-prompt.tsx b/components/notifications/site-push-prompt.tsx new file mode 100644 index 0000000..93ad4ca --- /dev/null +++ b/components/notifications/site-push-prompt.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useEffect, useId, useState } from 'react'; +import { authClient } from '@/lib/auth/client'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Field, FieldLabel } from '@/components/ui/field'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { syncSitePush } from './push-client'; + +const dismissedKey = 'site-push-dont-ask'; +const askedKey = 'site-push-asked'; +export function SitePushPrompt({ configured }: { configured: boolean }) { + const { data } = authClient.useSession(); + const userId = data?.user.id; + const id = useId(); + const [open, setOpen] = useState(false); + const [dontAsk, setDontAsk] = useState(false); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(''); + useEffect(() => { + if (!configured || !window.isSecureContext || !('Notification' in window) || !('PushManager' in window) || !('serviceWorker' in navigator)) return; + let active = true; + function show() { setError(''); setOpen(true); } + window.addEventListener('notifications:enable', show); + void Promise.resolve().then(async () => { + if (!active) return; + if (Notification.permission === 'granted') { + if (userId && !sessionStorage.getItem(`site-push-synced-${userId}`)) await syncSitePush(userId); + } else if (Notification.permission === 'default' && !localStorage.getItem(dismissedKey) && !sessionStorage.getItem(askedKey)) { + sessionStorage.setItem(askedKey, '1'); + setOpen(true); + } + }).catch(() => undefined); + return () => { active = false; window.removeEventListener('notifications:enable', show); }; + }, [configured, userId]); + function close() { + if (dontAsk) localStorage.setItem(dismissedKey, '1'); + setOpen(false); + } + async function enable() { + setBusy(true); setError(''); + try { + if (await Notification.requestPermission() !== 'granted') { + setError('ยังไม่ได้อนุญาตการแจ้งเตือน เปิดสิทธิ์ในการตั้งค่าเว็บไซต์ของเบราว์เซอร์'); + return; + } + if (userId) await syncSitePush(userId); + close(); + } catch { setError('เปิดการแจ้งเตือนไม่สำเร็จ กรุณาลองอีกครั้ง'); } + finally { setBusy(false); } + } + return { if (!value && !busy) close(); }}> + รับการแจ้งเตือนจากเว็บไซต์?รับข้อความ Commission คำตอบกลับความคิดเห็น และการแจ้งเตือนอื่นๆ ของเว็บไซต์ในอนาคต แม้ปิดแท็บแล้ว + {!userId &&

หลังเข้าสู่ระบบ เราจะเชื่อมการแจ้งเตือนกับบัญชีของคุณ

} + ไม่ต้องถามอีก + {error &&

{error}

} + +
; +} diff --git a/components/public/site-header.tsx b/components/public/site-header.tsx index 2bfcb97..8937daa 100644 --- a/components/public/site-header.tsx +++ b/components/public/site-header.tsx @@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"; import { VisitorPresence } from "@/components/public/visitor-presence"; import { PublicNavigation } from "@/components/public/public-navigation"; import { PublicAccountMenu } from "@/components/public/public-account-menu"; +import { NotificationBell } from "@/components/notifications/notification-bell"; import { getAdminSession } from "@/lib/auth/server"; import { HttpError } from "@/lib/security/http"; @@ -45,6 +46,7 @@ export function SiteHeader({ editHref = "/admin", guideId }: { editHref?: string +
diff --git a/db/schema.ts b/db/schema.ts index 8e23a15..e51600e 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -793,3 +793,24 @@ export const commentReadState = commentsSchema.table("read_state", { target: text("target").notNull(), seenAt: timestamp("seen_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [primaryKey({ columns: [table.userId, table.target] })]); + +export const notifications = pgTable("notification", { + id: uuid("id").defaultRandom().primaryKey(), + userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), + eventKey: text("event_key").notNull(), + kind: text("kind").notNull(), + title: text("title").notNull(), + body: text("body").notNull(), + url: text("url").notNull(), + adminOnly: boolean("admin_only").default(false).notNull(), + commentId: uuid("comment_id").references(() => comments.id, { onDelete: "cascade" }), + messageId: uuid("message_id").references(() => commissionMessages.id, { onDelete: "cascade" }), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + readAt: timestamp("read_at", { withTimezone: true }), +}, (table) => [ + uniqueIndex("notification_user_event_unique").on(table.userId, table.eventKey), + index("notification_user_time_idx").on(table.userId, table.createdAt, table.id), + index("notification_user_unread_idx").on(table.userId).where(sql`${table.readAt} is null`), + index("notification_comment_idx").on(table.commentId), + index("notification_message_idx").on(table.messageId), +]); diff --git a/drizzle/0078_slow_black_bolt.sql b/drizzle/0078_slow_black_bolt.sql new file mode 100644 index 0000000..1eb23f5 --- /dev/null +++ b/drizzle/0078_slow_black_bolt.sql @@ -0,0 +1,21 @@ +CREATE TABLE "notification" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "event_key" text NOT NULL, + "kind" text NOT NULL, + "title" text NOT NULL, + "body" text NOT NULL, + "url" text NOT NULL, + "admin_only" boolean DEFAULT false NOT NULL, + "comment_id" uuid, + "message_id" uuid, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "read_at" timestamp with time zone +); +--> statement-breakpoint +ALTER TABLE "notification" ADD CONSTRAINT "notification_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notification" ADD CONSTRAINT "notification_comment_id_comment_id_fk" FOREIGN KEY ("comment_id") REFERENCES "comments"."comment"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notification" ADD CONSTRAINT "notification_message_id_message_id_fk" FOREIGN KEY ("message_id") REFERENCES "commission"."message"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "notification_user_event_unique" ON "notification" USING btree ("user_id","event_key");--> statement-breakpoint +CREATE INDEX "notification_user_time_idx" ON "notification" USING btree ("user_id","created_at","id");--> statement-breakpoint +CREATE INDEX "notification_user_unread_idx" ON "notification" USING btree ("user_id") WHERE "notification"."read_at" is null; \ No newline at end of file diff --git a/drizzle/0079_luxuriant_thunderball.sql b/drizzle/0079_luxuriant_thunderball.sql new file mode 100644 index 0000000..93f335b --- /dev/null +++ b/drizzle/0079_luxuriant_thunderball.sql @@ -0,0 +1,2 @@ +CREATE INDEX "notification_comment_idx" ON "notification" USING btree ("comment_id");--> statement-breakpoint +CREATE INDEX "notification_message_idx" ON "notification" USING btree ("message_id"); \ No newline at end of file diff --git a/drizzle/meta/0078_snapshot.json b/drizzle/meta/0078_snapshot.json new file mode 100644 index 0000000..939344e --- /dev/null +++ b/drizzle/meta/0078_snapshot.json @@ -0,0 +1,5148 @@ +{ + "id": "f183423d-8b2a-40c5-8376-24d0bd78ece6", + "prevId": "cce69c3a-0dd5-4fb6-86b9-0bf806914efc", + "version": "7", + "dialect": "postgresql", + "tables": { + "auth.account": { + "name": "account", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "password": { + "name": "password", + "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": { + "account_provider_unique": { + "name": "account_provider_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_condition": { + "name": "artifact_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_media_id": { + "name": "icon_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "artifact_condition_order_idx": { + "name": "artifact_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_condition_guide_id_guide_id_fk": { + "name": "artifact_condition_guide_id_guide_id_fk", + "tableFrom": "artifact_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "artifact_condition_icon_media_id_media_id_fk": { + "name": "artifact_condition_icon_media_id_media_id_fk", + "tableFrom": "artifact_condition", + "tableTo": "media", + "columnsFrom": [ + "icon_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_group": { + "name": "artifact_group", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "artifact_group_order_idx": { + "name": "artifact_group_order_idx", + "columns": [ + { + "expression": "condition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_group_condition_id_artifact_condition_id_fk": { + "name": "artifact_group_condition_id_artifact_condition_id_fk", + "tableFrom": "artifact_group", + "tableTo": "artifact_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_profile": { + "name": "artifact_profile", + "schema": "guides", + "columns": { + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "sands": { + "name": "sands", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "goblet": { + "name": "goblet", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "circlet": { + "name": "circlet", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "substats": { + "name": "substats", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recommended_conditions": { + "name": "recommended_conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "targets": { + "name": "targets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "target_conditions": { + "name": "target_conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "talents": { + "name": "talents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "note": { + "name": "note", + "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": { + "artifact_profile_guide_id_guide_id_fk": { + "name": "artifact_profile_guide_id_guide_id_fk", + "tableFrom": "artifact_profile", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_recommendation": { + "name": "artifact_recommendation", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "first_artifact_key": { + "name": "first_artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "second_artifact_key": { + "name": "second_artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "percent": { + "name": "percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "artifact_recommendation_order_idx": { + "name": "artifact_recommendation_order_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_recommendation_group_id_artifact_group_id_fk": { + "name": "artifact_recommendation_group_id_artifact_group_id_fk", + "tableFrom": "artifact_recommendation", + "tableTo": "artifact_group", + "schemaTo": "guides", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "audit_log_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "time": { + "name": "time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_time_idx": { + "name": "audit_log_time_idx", + "columns": [ + { + "expression": "time", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_author_time_idx": { + "name": "audit_log_author_time_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.artifact": { + "name": "artifact", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "catalog_artifact_name_idx": { + "name": "catalog_artifact_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "artifact_key_pk": { + "name": "artifact_key_pk", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.character": { + "name": "character", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "element": { + "name": "element", + "type": "varchar(48)", + "primaryKey": false, + "notNull": false + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "weapon_type": { + "name": "weapon_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "element_icon_key": { + "name": "element_icon_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coop_image_key": { + "name": "coop_image_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_card_image_key": { + "name": "name_card_image_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constellations": { + "name": "constellations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "talents": { + "name": "talents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "ascension": { + "name": "ascension", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "leveling": { + "name": "leveling", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "base_stats": { + "name": "base_stats", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "catalog_character_name_idx": { + "name": "catalog_character_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "character_key_pk": { + "name": "character_key_pk", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.material": { + "name": "material", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "catalog_material_name_idx": { + "name": "catalog_material_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.weapon": { + "name": "weapon", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "weapon_type": { + "name": "weapon_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "catalog_weapon_name_idx": { + "name": "catalog_weapon_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "weapon_key_pk": { + "name": "weapon_key_pk", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "comments.attachment": { + "name": "attachment", + "schema": "comments", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "comment_attachment_key_unique": { + "name": "comment_attachment_key_unique", + "columns": [ + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_attachment_comment_idx": { + "name": "comment_attachment_comment_idx", + "columns": [ + { + "expression": "comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "attachment_comment_id_comment_id_fk": { + "name": "attachment_comment_id_comment_id_fk", + "tableFrom": "attachment", + "tableTo": "comment", + "schemaTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "comment_image_size": { + "name": "comment_image_size", + "value": "\"comments\".\"attachment\".\"byte_size\" > 0 and \"comments\".\"attachment\".\"byte_size\" <= 10485760" + }, + "comment_image_type": { + "name": "comment_image_type", + "value": "\"comments\".\"attachment\".\"mime_type\" in ('image/jpeg', 'image/png', 'image/webp')" + } + }, + "isRLSEnabled": false + }, + "comments.push_subscription": { + "name": "push_subscription", + "schema": "comments", + "columns": { + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "comment_push_subscription_user_idx": { + "name": "comment_push_subscription_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_subscription_user_id_user_id_fk": { + "name": "push_subscription_user_id_user_id_fk", + "tableFrom": "push_subscription", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "comments.reaction": { + "name": "reaction", + "schema": "comments", + "columns": { + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "reaction_comment_id_comment_id_fk": { + "name": "reaction_comment_id_comment_id_fk", + "tableFrom": "reaction", + "tableTo": "comment", + "schemaTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reaction_user_id_user_id_fk": { + "name": "reaction_user_id_user_id_fk", + "tableFrom": "reaction", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reaction_comment_id_user_id_pk": { + "name": "reaction_comment_id_user_id_pk", + "columns": [ + "comment_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "comment_reaction_value": { + "name": "comment_reaction_value", + "value": "\"comments\".\"reaction\".\"value\" in (-1, 1)" + } + }, + "isRLSEnabled": false + }, + "comments.read_state": { + "name": "read_state", + "schema": "comments", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seen_at": { + "name": "seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "read_state_user_id_user_id_fk": { + "name": "read_state_user_id_user_id_fk", + "tableFrom": "read_state", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "read_state_user_id_target_pk": { + "name": "read_state_user_id_target_pk", + "columns": [ + "user_id", + "target" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "comments.revision_attachment": { + "name": "revision_attachment", + "schema": "comments", + "columns": { + "revision_id": { + "name": "revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attachment_id": { + "name": "attachment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "comment_revision_attachment_position_unique": { + "name": "comment_revision_attachment_position_unique", + "columns": [ + { + "expression": "revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revision_attachment_revision_id_revision_id_fk": { + "name": "revision_attachment_revision_id_revision_id_fk", + "tableFrom": "revision_attachment", + "tableTo": "revision", + "schemaTo": "comments", + "columnsFrom": [ + "revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revision_attachment_attachment_id_attachment_id_fk": { + "name": "revision_attachment_attachment_id_attachment_id_fk", + "tableFrom": "revision_attachment", + "tableTo": "attachment", + "schemaTo": "comments", + "columnsFrom": [ + "attachment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revision_attachment_revision_id_attachment_id_pk": { + "name": "revision_attachment_revision_id_attachment_id_pk", + "columns": [ + "revision_id", + "attachment_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "comment_attachment_position": { + "name": "comment_attachment_position", + "value": "\"comments\".\"revision_attachment\".\"position\" >= 0 and \"comments\".\"revision_attachment\".\"position\" < 5" + } + }, + "isRLSEnabled": false + }, + "comments.revision": { + "name": "revision", + "schema": "comments", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "comment_revision_version_unique": { + "name": "comment_revision_version_unique", + "columns": [ + { + "expression": "comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revision_comment_id_comment_id_fk": { + "name": "revision_comment_id_comment_id_fk", + "tableFrom": "revision", + "tableTo": "comment", + "schemaTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "comment_text_length": { + "name": "comment_text_length", + "value": "length(\"comments\".\"revision\".\"text\") <= 4000" + } + }, + "isRLSEnabled": false + }, + "comments.thread": { + "name": "thread", + "schema": "comments", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "comment_thread_guide_unique": { + "name": "comment_thread_guide_unique", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_thread_schedule_unique": { + "name": "comment_thread_schedule_unique", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "thread_guide_id_guide_id_fk": { + "name": "thread_guide_id_guide_id_fk", + "tableFrom": "thread", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "thread_schedule_id_stygian_schedule_schedule_id_fk": { + "name": "thread_schedule_id_stygian_schedule_schedule_id_fk", + "tableFrom": "thread", + "tableTo": "stygian_schedule", + "schemaTo": "guides", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "schedule_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "comment_thread_one_target": { + "name": "comment_thread_one_target", + "value": "(\"comments\".\"thread\".\"guide_id\" is null) <> (\"comments\".\"thread\".\"schedule_id\" is null)" + } + }, + "isRLSEnabled": false + }, + "comments.comment": { + "name": "comment", + "schema": "comments", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "root_id": { + "name": "root_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reply_to_id": { + "name": "reply_to_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hearted_by_id": { + "name": "hearted_by_id", + "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": { + "comment_thread_id_unique": { + "name": "comment_thread_id_unique", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_thread_root_time_idx": { + "name": "comment_thread_root_time_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "root_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_root_time_idx": { + "name": "comment_root_time_idx", + "columns": [ + { + "expression": "root_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_inbox_time_idx": { + "name": "comment_inbox_time_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comment_thread_id_thread_id_fk": { + "name": "comment_thread_id_thread_id_fk", + "tableFrom": "comment", + "tableTo": "thread", + "schemaTo": "comments", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comment_author_id_user_id_fk": { + "name": "comment_author_id_user_id_fk", + "tableFrom": "comment", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comment_hearted_by_id_user_id_fk": { + "name": "comment_hearted_by_id_user_id_fk", + "tableFrom": "comment", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "hearted_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "comment_thread_id_root_id_comment_thread_id_id_fk": { + "name": "comment_thread_id_root_id_comment_thread_id_id_fk", + "tableFrom": "comment", + "tableTo": "comment", + "schemaTo": "comments", + "columnsFrom": [ + "thread_id", + "root_id" + ], + "columnsTo": [ + "thread_id", + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comment_thread_id_reply_to_id_comment_thread_id_id_fk": { + "name": "comment_thread_id_reply_to_id_comment_thread_id_id_fk", + "tableFrom": "comment", + "tableTo": "comment", + "schemaTo": "comments", + "columnsFrom": [ + "thread_id", + "reply_to_id" + ], + "columnsTo": [ + "thread_id", + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "comment_reply_shape": { + "name": "comment_reply_shape", + "value": "(\"comments\".\"comment\".\"root_id\" is null) = (\"comments\".\"comment\".\"reply_to_id\" is null)" + }, + "comment_version_positive": { + "name": "comment_version_positive", + "value": "\"comments\".\"comment\".\"version\" > 0" + } + }, + "isRLSEnabled": false + }, + "commission.checkout": { + "name": "checkout", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "amount_baht": { + "name": "amount_baht", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_checkout_user_idx": { + "name": "commission_checkout_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "checkout_user_id_user_id_fk": { + "name": "checkout_user_id_user_id_fk", + "tableFrom": "checkout", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.message": { + "name": "message", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "ticket_id": { + "name": "ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_to_id": { + "name": "reply_to_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_object_key": { + "name": "image_object_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_mime_type": { + "name": "image_mime_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "image_object_keys": { + "name": "image_object_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_message_ticket_time_idx": { + "name": "commission_message_ticket_time_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_message_reply_idx": { + "name": "commission_message_reply_idx", + "columns": [ + { + "expression": "reply_to_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_ticket_id_ticket_id_fk": { + "name": "message_ticket_id_ticket_id_fk", + "tableFrom": "message", + "tableTo": "ticket", + "schemaTo": "commission", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_author_id_user_id_fk": { + "name": "message_author_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_reply_to_id_message_id_fk": { + "name": "message_reply_to_id_message_id_fk", + "tableFrom": "message", + "tableTo": "message", + "schemaTo": "commission", + "columnsFrom": [ + "reply_to_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.payment": { + "name": "payment", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "checkout_id": { + "name": "checkout_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trans_ref": { + "name": "trans_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_baht": { + "name": "amount_baht", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "transferred_at": { + "name": "transferred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "slip_object_key": { + "name": "slip_object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slip_mime_type": { + "name": "slip_mime_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "provider_data": { + "name": "provider_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_payment_checkout_unique": { + "name": "commission_payment_checkout_unique", + "columns": [ + { + "expression": "checkout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_payment_reference_unique": { + "name": "commission_payment_reference_unique", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_payment_trans_ref_unique": { + "name": "commission_payment_trans_ref_unique", + "columns": [ + { + "expression": "trans_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_payment_transferred_at_idx": { + "name": "commission_payment_transferred_at_idx", + "columns": [ + { + "expression": "transferred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_checkout_id_checkout_id_fk": { + "name": "payment_checkout_id_checkout_id_fk", + "tableFrom": "payment", + "tableTo": "checkout", + "schemaTo": "commission", + "columnsFrom": [ + "checkout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payment_ticket_id_ticket_id_fk": { + "name": "payment_ticket_id_ticket_id_fk", + "tableFrom": "payment", + "tableTo": "ticket", + "schemaTo": "commission", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.push_subscription": { + "name": "push_subscription", + "schema": "commission", + "columns": { + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_push_subscription_user_idx": { + "name": "commission_push_subscription_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_subscription_user_id_user_id_fk": { + "name": "push_subscription_user_id_user_id_fk", + "tableFrom": "push_subscription", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.reaction": { + "name": "reaction", + "schema": "commission", + "columns": { + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "reaction_message_id_message_id_fk": { + "name": "reaction_message_id_message_id_fk", + "tableFrom": "reaction", + "tableTo": "message", + "schemaTo": "commission", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reaction_user_id_user_id_fk": { + "name": "reaction_user_id_user_id_fk", + "tableFrom": "reaction", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reaction_message_id_user_id_emoji_pk": { + "name": "reaction_message_id_user_id_emoji_pk", + "columns": [ + "message_id", + "user_id", + "emoji" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.settings": { + "name": "settings", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "is_open": { + "name": "is_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "closure_message": { + "name": "closure_message", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.ticket": { + "name": "ticket", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "checkout_id": { + "name": "checkout_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "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": { + "commission_ticket_checkout_unique": { + "name": "commission_ticket_checkout_unique", + "columns": [ + { + "expression": "checkout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_ticket_user_idx": { + "name": "commission_ticket_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_ticket_status_idx": { + "name": "commission_ticket_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ticket_checkout_id_checkout_id_fk": { + "name": "ticket_checkout_id_checkout_id_fk", + "tableFrom": "ticket", + "tableTo": "checkout", + "schemaTo": "commission", + "columnsFrom": [ + "checkout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ticket_user_id_user_id_fk": { + "name": "ticket_user_id_user_id_fk", + "tableFrom": "ticket", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.constellation_condition": { + "name": "constellation_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_media_id": { + "name": "icon_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "constellation_condition_order_idx": { + "name": "constellation_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "constellation_condition_guide_id_guide_id_fk": { + "name": "constellation_condition_guide_id_guide_id_fk", + "tableFrom": "constellation_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "constellation_condition_icon_media_id_media_id_fk": { + "name": "constellation_condition_icon_media_id_media_id_fk", + "tableFrom": "constellation_condition", + "tableTo": "media", + "columnsFrom": [ + "icon_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.constellation_recommendation": { + "name": "constellation_recommendation", + "schema": "guides", + "columns": { + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cumulative_percent": { + "name": "cumulative_percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "is_great": { + "name": "is_great", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "constellation_recommendation_condition_id_constellation_condition_id_fk": { + "name": "constellation_recommendation_condition_id_constellation_condition_id_fk", + "tableFrom": "constellation_recommendation", + "tableTo": "constellation_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "constellation_recommendation_condition_id_level_pk": { + "name": "constellation_recommendation_condition_id_level_pk", + "columns": [ + "condition_id", + "level" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.extra_section_row": { + "name": "extra_section_row", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "section_id": { + "name": "section_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heading": { + "name": "heading", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "percent": { + "name": "percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "background_color": { + "name": "background_color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "text_color": { + "name": "text_color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "highlights": { + "name": "highlights", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "catalog_items": { + "name": "catalog_items", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sheet": { + "name": "sheet", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "extra_section_row_order_idx": { + "name": "extra_section_row_order_idx", + "columns": [ + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "extra_section_row_section_id_extra_section_section_id_fk": { + "name": "extra_section_row_section_id_extra_section_section_id_fk", + "tableFrom": "extra_section_row", + "tableTo": "extra_section", + "schemaTo": "guides", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "section_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "extra_section_row_media_id_media_id_fk": { + "name": "extra_section_row_media_id_media_id_fk", + "tableFrom": "extra_section_row", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.extra_section": { + "name": "extra_section", + "schema": "guides", + "columns": { + "section_id": { + "name": "section_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "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": { + "extra_section_section_id_guide_section_id_fk": { + "name": "extra_section_section_id_guide_section_id_fk", + "tableFrom": "extra_section", + "tableTo": "guide_section", + "schemaTo": "guides", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.glossary_alias": { + "name": "glossary_alias", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "catalog_key": { + "name": "catalog_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "normalized_alias": { + "name": "normalized_alias", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "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": { + "glossary_alias_item_normalized_unique": { + "name": "glossary_alias_item_normalized_unique", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "catalog_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "normalized_alias", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "glossary_alias_item_idx": { + "name": "glossary_alias_item_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "catalog_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.guide_poster_bleeding": { + "name": "guide_poster_bleeding", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "scale": { + "name": "scale", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "blend_mode": { + "name": "blend_mode", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'multiply'" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "guide_poster_bleeding_order_idx": { + "name": "guide_poster_bleeding_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "guide_poster_bleeding_guide_id_guide_id_fk": { + "name": "guide_poster_bleeding_guide_id_guide_id_fk", + "tableFrom": "guide_poster_bleeding", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "guide_poster_bleeding_media_id_media_id_fk": { + "name": "guide_poster_bleeding_media_id_media_id_fk", + "tableFrom": "guide_poster_bleeding", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.guide_section": { + "name": "guide_section", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "guide_section_slug_unique": { + "name": "guide_section_slug_unique", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "guide_section_order_idx": { + "name": "guide_section_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "guide_section_guide_id_guide_id_fk": { + "name": "guide_section_guide_id_guide_id_fk", + "tableFrom": "guide_section", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.guide": { + "name": "guide", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_key": { + "name": "character_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overview": { + "name": "overview", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "youtube_urls": { + "name": "youtube_urls", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cover_media_id": { + "name": "cover_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "poster_art_media_id": { + "name": "poster_art_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "poster_art_offset_x": { + "name": "poster_art_offset_x", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "poster_art_offset_y": { + "name": "poster_art_offset_y", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "poster_art_scale": { + "name": "poster_art_scale", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "poster_art_rotation": { + "name": "poster_art_rotation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "poster_top_colors": { + "name": "poster_top_colors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"#000000\",\"#000000\"]'::jsonb" + }, + "poster_bottom_colors": { + "name": "poster_bottom_colors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"#000000\",\"#000000\"]'::jsonb" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "trashed_at": { + "name": "trashed_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": { + "guide_character_key_unique": { + "name": "guide_character_key_unique", + "columns": [ + { + "expression": "character_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "guide_slug_unique": { + "name": "guide_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "guide_directory_idx": { + "name": "guide_directory_idx", + "columns": [ + { + "expression": "trashed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "guide_cover_media_id_media_id_fk": { + "name": "guide_cover_media_id_media_id_fk", + "tableFrom": "guide", + "tableTo": "media", + "columnsFrom": [ + "cover_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "guide_poster_art_media_id_media_id_fk": { + "name": "guide_poster_art_media_id_media_id_fk", + "tableFrom": "guide", + "tableTo": "media", + "columnsFrom": [ + "poster_art_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "current_reference_count": { + "name": "current_reference_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_id": { + "name": "created_by_id", + "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": { + "media_object_key_unique": { + "name": "media_object_key_unique", + "columns": [ + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_visibility_idx": { + "name": "media_visibility_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_reference_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_created_by_id_user_id_fk": { + "name": "media_created_by_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admin_only": { + "name": "admin_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "notification_user_event_unique": { + "name": "notification_user_event_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_user_time_idx": { + "name": "notification_user_time_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_user_unread_idx": { + "name": "notification_user_unread_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"notification\".\"read_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_user_id_user_id_fk": { + "name": "notification_user_id_user_id_fk", + "tableFrom": "notification", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_comment_id_comment_id_fk": { + "name": "notification_comment_id_comment_id_fk", + "tableFrom": "notification", + "tableTo": "comment", + "schemaTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_message_id_message_id_fk": { + "name": "notification_message_id_message_id_fk", + "tableFrom": "notification", + "tableTo": "message", + "schemaTo": "commission", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "outbox_event_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "topic": { + "name": "topic", + "type": "varchar(160)", + "primaryKey": false, + "notNull": true + }, + "aggregate_id": { + "name": "aggregate_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "outbox_pending_idx": { + "name": "outbox_pending_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.session": { + "name": "session", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.stygian_boss": { + "name": "stygian_boss", + "schema": "guides", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hp": { + "name": "hp", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mechanics": { + "name": "mechanics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recommended_mechanics": { + "name": "recommended_mechanics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recommended_mechanic_types": { + "name": "recommended_mechanic_types", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "stygian_boss_schedule_idx": { + "name": "stygian_boss_schedule_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stygian_boss_schedule_id_stygian_schedule_schedule_id_fk": { + "name": "stygian_boss_schedule_id_stygian_schedule_schedule_id_fk", + "tableFrom": "stygian_boss", + "tableTo": "stygian_schedule", + "schemaTo": "guides", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "schedule_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "stygian_boss_schedule_id_level_position_pk": { + "name": "stygian_boss_schedule_id_level_position_pk", + "columns": [ + "schedule_id", + "level", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.stygian_recommendation_group": { + "name": "stygian_recommendation_group", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "boss_position": { + "name": "boss_position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "stygian_recommendation_group_order_idx": { + "name": "stygian_recommendation_group_order_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "boss_position", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stygian_recommendation_group_schedule_id_stygian_schedule_schedule_id_fk": { + "name": "stygian_recommendation_group_schedule_id_stygian_schedule_schedule_id_fk", + "tableFrom": "stygian_recommendation_group", + "tableTo": "stygian_schedule", + "schemaTo": "guides", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "schedule_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.stygian_recommendation_item": { + "name": "stygian_recommendation_item", + "schema": "guides", + "columns": { + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true + }, + "catalog_key": { + "name": "catalog_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "weapon_key": { + "name": "weapon_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "artifact_key": { + "name": "artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "refinement": { + "name": "refinement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "constellation_level": { + "name": "constellation_level", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "element": { + "name": "element", + "type": "varchar(24)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stygian_recommendation_item_group_id_stygian_recommendation_group_id_fk": { + "name": "stygian_recommendation_item_group_id_stygian_recommendation_group_id_fk", + "tableFrom": "stygian_recommendation_item", + "tableTo": "stygian_recommendation_group", + "schemaTo": "guides", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "stygian_recommendation_item_group_id_position_pk": { + "name": "stygian_recommendation_item_group_id_position_pk", + "columns": [ + "group_id", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.stygian_schedule": { + "name": "stygian_schedule", + "schema": "guides", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "challenge_name": { + "name": "challenge_name", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "schedule_start_time": { + "name": "schedule_start_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "schedule_end_time": { + "name": "schedule_end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.team_composition": { + "name": "team_composition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rotation_seconds": { + "name": "rotation_seconds", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "combo": { + "name": "combo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "team_composition_order_idx": { + "name": "team_composition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_composition_guide_id_guide_id_fk": { + "name": "team_composition_guide_id_guide_id_fk", + "tableFrom": "team_composition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_composition_condition_id_team_condition_id_fk": { + "name": "team_composition_condition_id_team_condition_id_fk", + "tableFrom": "team_composition", + "tableTo": "team_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.team_condition": { + "name": "team_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_media_id": { + "name": "icon_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "team_condition_order_idx": { + "name": "team_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_condition_guide_id_guide_id_fk": { + "name": "team_condition_guide_id_guide_id_fk", + "tableFrom": "team_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_condition_icon_media_id_media_id_fk": { + "name": "team_condition_icon_media_id_media_id_fk", + "tableFrom": "team_condition", + "tableTo": "media", + "columnsFrom": [ + "icon_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.team_member": { + "name": "team_member", + "schema": "guides", + "columns": { + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "character_key": { + "name": "character_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "weapon_key": { + "name": "weapon_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "artifact_key": { + "name": "artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "second_artifact_key": { + "name": "second_artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "constellation": { + "name": "constellation", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "refinement": { + "name": "refinement", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "damage": { + "name": "damage", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "team_member_team_id_team_composition_id_fk": { + "name": "team_member_team_id_team_composition_id_fk", + "tableFrom": "team_member", + "tableTo": "team_composition", + "schemaTo": "guides", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_member_team_id_position_pk": { + "name": "team_member_team_id_position_pk", + "columns": [ + "team_id", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user": { + "name": "user", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "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": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verification": { + "name": "verification", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "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_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.weapon_condition": { + "name": "weapon_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_media_id": { + "name": "icon_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "weapon_condition_order_idx": { + "name": "weapon_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weapon_condition_guide_id_guide_id_fk": { + "name": "weapon_condition_guide_id_guide_id_fk", + "tableFrom": "weapon_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weapon_condition_icon_media_id_media_id_fk": { + "name": "weapon_condition_icon_media_id_media_id_fk", + "tableFrom": "weapon_condition", + "tableTo": "media", + "columnsFrom": [ + "icon_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.weapon_recommendation": { + "name": "weapon_recommendation", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "weapon_key": { + "name": "weapon_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "refinement": { + "name": "refinement", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + }, + "overall_percent": { + "name": "overall_percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "personal_percent": { + "name": "personal_percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "weapon_recommendation_order_idx": { + "name": "weapon_recommendation_order_idx", + "columns": [ + { + "expression": "condition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weapon_recommendation_condition_id_weapon_condition_id_fk": { + "name": "weapon_recommendation_condition_id_weapon_condition_id_fk", + "tableFrom": "weapon_recommendation", + "tableTo": "weapon_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0079_snapshot.json b/drizzle/meta/0079_snapshot.json new file mode 100644 index 0000000..dccbade --- /dev/null +++ b/drizzle/meta/0079_snapshot.json @@ -0,0 +1,5178 @@ +{ + "id": "4a884ac3-555c-42cc-8d49-4fec960a7c0d", + "prevId": "f183423d-8b2a-40c5-8376-24d0bd78ece6", + "version": "7", + "dialect": "postgresql", + "tables": { + "auth.account": { + "name": "account", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "password": { + "name": "password", + "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": { + "account_provider_unique": { + "name": "account_provider_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_condition": { + "name": "artifact_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_media_id": { + "name": "icon_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "artifact_condition_order_idx": { + "name": "artifact_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_condition_guide_id_guide_id_fk": { + "name": "artifact_condition_guide_id_guide_id_fk", + "tableFrom": "artifact_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "artifact_condition_icon_media_id_media_id_fk": { + "name": "artifact_condition_icon_media_id_media_id_fk", + "tableFrom": "artifact_condition", + "tableTo": "media", + "columnsFrom": [ + "icon_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_group": { + "name": "artifact_group", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "artifact_group_order_idx": { + "name": "artifact_group_order_idx", + "columns": [ + { + "expression": "condition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_group_condition_id_artifact_condition_id_fk": { + "name": "artifact_group_condition_id_artifact_condition_id_fk", + "tableFrom": "artifact_group", + "tableTo": "artifact_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_profile": { + "name": "artifact_profile", + "schema": "guides", + "columns": { + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "sands": { + "name": "sands", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "goblet": { + "name": "goblet", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "circlet": { + "name": "circlet", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "substats": { + "name": "substats", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recommended_conditions": { + "name": "recommended_conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "targets": { + "name": "targets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "target_conditions": { + "name": "target_conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "talents": { + "name": "talents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "note": { + "name": "note", + "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": { + "artifact_profile_guide_id_guide_id_fk": { + "name": "artifact_profile_guide_id_guide_id_fk", + "tableFrom": "artifact_profile", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.artifact_recommendation": { + "name": "artifact_recommendation", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "first_artifact_key": { + "name": "first_artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "second_artifact_key": { + "name": "second_artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "percent": { + "name": "percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "artifact_recommendation_order_idx": { + "name": "artifact_recommendation_order_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_recommendation_group_id_artifact_group_id_fk": { + "name": "artifact_recommendation_group_id_artifact_group_id_fk", + "tableFrom": "artifact_recommendation", + "tableTo": "artifact_group", + "schemaTo": "guides", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "audit_log_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "time": { + "name": "time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_time_idx": { + "name": "audit_log_time_idx", + "columns": [ + { + "expression": "time", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_author_time_idx": { + "name": "audit_log_author_time_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.artifact": { + "name": "artifact", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "catalog_artifact_name_idx": { + "name": "catalog_artifact_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "artifact_key_pk": { + "name": "artifact_key_pk", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.character": { + "name": "character", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "element": { + "name": "element", + "type": "varchar(48)", + "primaryKey": false, + "notNull": false + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "weapon_type": { + "name": "weapon_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "element_icon_key": { + "name": "element_icon_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coop_image_key": { + "name": "coop_image_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_card_image_key": { + "name": "name_card_image_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constellations": { + "name": "constellations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "talents": { + "name": "talents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "ascension": { + "name": "ascension", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "leveling": { + "name": "leveling", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "base_stats": { + "name": "base_stats", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "catalog_character_name_idx": { + "name": "catalog_character_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "character_key_pk": { + "name": "character_key_pk", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.material": { + "name": "material", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "catalog_material_name_idx": { + "name": "catalog_material_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "catalog.weapon": { + "name": "weapon", + "schema": "catalog", + "columns": { + "key": { + "name": "key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "weapon_type": { + "name": "weapon_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "catalog_weapon_name_idx": { + "name": "catalog_weapon_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "weapon_key_pk": { + "name": "weapon_key_pk", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "comments.attachment": { + "name": "attachment", + "schema": "comments", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "comment_attachment_key_unique": { + "name": "comment_attachment_key_unique", + "columns": [ + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_attachment_comment_idx": { + "name": "comment_attachment_comment_idx", + "columns": [ + { + "expression": "comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "attachment_comment_id_comment_id_fk": { + "name": "attachment_comment_id_comment_id_fk", + "tableFrom": "attachment", + "tableTo": "comment", + "schemaTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "comment_image_size": { + "name": "comment_image_size", + "value": "\"comments\".\"attachment\".\"byte_size\" > 0 and \"comments\".\"attachment\".\"byte_size\" <= 10485760" + }, + "comment_image_type": { + "name": "comment_image_type", + "value": "\"comments\".\"attachment\".\"mime_type\" in ('image/jpeg', 'image/png', 'image/webp')" + } + }, + "isRLSEnabled": false + }, + "comments.push_subscription": { + "name": "push_subscription", + "schema": "comments", + "columns": { + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "comment_push_subscription_user_idx": { + "name": "comment_push_subscription_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_subscription_user_id_user_id_fk": { + "name": "push_subscription_user_id_user_id_fk", + "tableFrom": "push_subscription", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "comments.reaction": { + "name": "reaction", + "schema": "comments", + "columns": { + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "reaction_comment_id_comment_id_fk": { + "name": "reaction_comment_id_comment_id_fk", + "tableFrom": "reaction", + "tableTo": "comment", + "schemaTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reaction_user_id_user_id_fk": { + "name": "reaction_user_id_user_id_fk", + "tableFrom": "reaction", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reaction_comment_id_user_id_pk": { + "name": "reaction_comment_id_user_id_pk", + "columns": [ + "comment_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "comment_reaction_value": { + "name": "comment_reaction_value", + "value": "\"comments\".\"reaction\".\"value\" in (-1, 1)" + } + }, + "isRLSEnabled": false + }, + "comments.read_state": { + "name": "read_state", + "schema": "comments", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seen_at": { + "name": "seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "read_state_user_id_user_id_fk": { + "name": "read_state_user_id_user_id_fk", + "tableFrom": "read_state", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "read_state_user_id_target_pk": { + "name": "read_state_user_id_target_pk", + "columns": [ + "user_id", + "target" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "comments.revision_attachment": { + "name": "revision_attachment", + "schema": "comments", + "columns": { + "revision_id": { + "name": "revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attachment_id": { + "name": "attachment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "comment_revision_attachment_position_unique": { + "name": "comment_revision_attachment_position_unique", + "columns": [ + { + "expression": "revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revision_attachment_revision_id_revision_id_fk": { + "name": "revision_attachment_revision_id_revision_id_fk", + "tableFrom": "revision_attachment", + "tableTo": "revision", + "schemaTo": "comments", + "columnsFrom": [ + "revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revision_attachment_attachment_id_attachment_id_fk": { + "name": "revision_attachment_attachment_id_attachment_id_fk", + "tableFrom": "revision_attachment", + "tableTo": "attachment", + "schemaTo": "comments", + "columnsFrom": [ + "attachment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revision_attachment_revision_id_attachment_id_pk": { + "name": "revision_attachment_revision_id_attachment_id_pk", + "columns": [ + "revision_id", + "attachment_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "comment_attachment_position": { + "name": "comment_attachment_position", + "value": "\"comments\".\"revision_attachment\".\"position\" >= 0 and \"comments\".\"revision_attachment\".\"position\" < 5" + } + }, + "isRLSEnabled": false + }, + "comments.revision": { + "name": "revision", + "schema": "comments", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "comment_revision_version_unique": { + "name": "comment_revision_version_unique", + "columns": [ + { + "expression": "comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revision_comment_id_comment_id_fk": { + "name": "revision_comment_id_comment_id_fk", + "tableFrom": "revision", + "tableTo": "comment", + "schemaTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "comment_text_length": { + "name": "comment_text_length", + "value": "length(\"comments\".\"revision\".\"text\") <= 4000" + } + }, + "isRLSEnabled": false + }, + "comments.thread": { + "name": "thread", + "schema": "comments", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "comment_thread_guide_unique": { + "name": "comment_thread_guide_unique", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_thread_schedule_unique": { + "name": "comment_thread_schedule_unique", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "thread_guide_id_guide_id_fk": { + "name": "thread_guide_id_guide_id_fk", + "tableFrom": "thread", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "thread_schedule_id_stygian_schedule_schedule_id_fk": { + "name": "thread_schedule_id_stygian_schedule_schedule_id_fk", + "tableFrom": "thread", + "tableTo": "stygian_schedule", + "schemaTo": "guides", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "schedule_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "comment_thread_one_target": { + "name": "comment_thread_one_target", + "value": "(\"comments\".\"thread\".\"guide_id\" is null) <> (\"comments\".\"thread\".\"schedule_id\" is null)" + } + }, + "isRLSEnabled": false + }, + "comments.comment": { + "name": "comment", + "schema": "comments", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "root_id": { + "name": "root_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reply_to_id": { + "name": "reply_to_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hearted_by_id": { + "name": "hearted_by_id", + "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": { + "comment_thread_id_unique": { + "name": "comment_thread_id_unique", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_thread_root_time_idx": { + "name": "comment_thread_root_time_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "root_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_root_time_idx": { + "name": "comment_root_time_idx", + "columns": [ + { + "expression": "root_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comment_inbox_time_idx": { + "name": "comment_inbox_time_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comment_thread_id_thread_id_fk": { + "name": "comment_thread_id_thread_id_fk", + "tableFrom": "comment", + "tableTo": "thread", + "schemaTo": "comments", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comment_author_id_user_id_fk": { + "name": "comment_author_id_user_id_fk", + "tableFrom": "comment", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comment_hearted_by_id_user_id_fk": { + "name": "comment_hearted_by_id_user_id_fk", + "tableFrom": "comment", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "hearted_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "comment_thread_id_root_id_comment_thread_id_id_fk": { + "name": "comment_thread_id_root_id_comment_thread_id_id_fk", + "tableFrom": "comment", + "tableTo": "comment", + "schemaTo": "comments", + "columnsFrom": [ + "thread_id", + "root_id" + ], + "columnsTo": [ + "thread_id", + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "comment_thread_id_reply_to_id_comment_thread_id_id_fk": { + "name": "comment_thread_id_reply_to_id_comment_thread_id_id_fk", + "tableFrom": "comment", + "tableTo": "comment", + "schemaTo": "comments", + "columnsFrom": [ + "thread_id", + "reply_to_id" + ], + "columnsTo": [ + "thread_id", + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "comment_reply_shape": { + "name": "comment_reply_shape", + "value": "(\"comments\".\"comment\".\"root_id\" is null) = (\"comments\".\"comment\".\"reply_to_id\" is null)" + }, + "comment_version_positive": { + "name": "comment_version_positive", + "value": "\"comments\".\"comment\".\"version\" > 0" + } + }, + "isRLSEnabled": false + }, + "commission.checkout": { + "name": "checkout", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "amount_baht": { + "name": "amount_baht", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_checkout_user_idx": { + "name": "commission_checkout_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "checkout_user_id_user_id_fk": { + "name": "checkout_user_id_user_id_fk", + "tableFrom": "checkout", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.message": { + "name": "message", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "ticket_id": { + "name": "ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_to_id": { + "name": "reply_to_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_object_key": { + "name": "image_object_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_mime_type": { + "name": "image_mime_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "image_object_keys": { + "name": "image_object_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_message_ticket_time_idx": { + "name": "commission_message_ticket_time_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_message_reply_idx": { + "name": "commission_message_reply_idx", + "columns": [ + { + "expression": "reply_to_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_ticket_id_ticket_id_fk": { + "name": "message_ticket_id_ticket_id_fk", + "tableFrom": "message", + "tableTo": "ticket", + "schemaTo": "commission", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_author_id_user_id_fk": { + "name": "message_author_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_reply_to_id_message_id_fk": { + "name": "message_reply_to_id_message_id_fk", + "tableFrom": "message", + "tableTo": "message", + "schemaTo": "commission", + "columnsFrom": [ + "reply_to_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.payment": { + "name": "payment", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "checkout_id": { + "name": "checkout_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trans_ref": { + "name": "trans_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_baht": { + "name": "amount_baht", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "transferred_at": { + "name": "transferred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "slip_object_key": { + "name": "slip_object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slip_mime_type": { + "name": "slip_mime_type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "provider_data": { + "name": "provider_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_payment_checkout_unique": { + "name": "commission_payment_checkout_unique", + "columns": [ + { + "expression": "checkout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_payment_reference_unique": { + "name": "commission_payment_reference_unique", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_payment_trans_ref_unique": { + "name": "commission_payment_trans_ref_unique", + "columns": [ + { + "expression": "trans_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_payment_transferred_at_idx": { + "name": "commission_payment_transferred_at_idx", + "columns": [ + { + "expression": "transferred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_checkout_id_checkout_id_fk": { + "name": "payment_checkout_id_checkout_id_fk", + "tableFrom": "payment", + "tableTo": "checkout", + "schemaTo": "commission", + "columnsFrom": [ + "checkout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payment_ticket_id_ticket_id_fk": { + "name": "payment_ticket_id_ticket_id_fk", + "tableFrom": "payment", + "tableTo": "ticket", + "schemaTo": "commission", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.push_subscription": { + "name": "push_subscription", + "schema": "commission", + "columns": { + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "commission_push_subscription_user_idx": { + "name": "commission_push_subscription_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_subscription_user_id_user_id_fk": { + "name": "push_subscription_user_id_user_id_fk", + "tableFrom": "push_subscription", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.reaction": { + "name": "reaction", + "schema": "commission", + "columns": { + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "reaction_message_id_message_id_fk": { + "name": "reaction_message_id_message_id_fk", + "tableFrom": "reaction", + "tableTo": "message", + "schemaTo": "commission", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reaction_user_id_user_id_fk": { + "name": "reaction_user_id_user_id_fk", + "tableFrom": "reaction", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reaction_message_id_user_id_emoji_pk": { + "name": "reaction_message_id_user_id_emoji_pk", + "columns": [ + "message_id", + "user_id", + "emoji" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.settings": { + "name": "settings", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "is_open": { + "name": "is_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "closure_message": { + "name": "closure_message", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "commission.ticket": { + "name": "ticket", + "schema": "commission", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "checkout_id": { + "name": "checkout_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "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": { + "commission_ticket_checkout_unique": { + "name": "commission_ticket_checkout_unique", + "columns": [ + { + "expression": "checkout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_ticket_user_idx": { + "name": "commission_ticket_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commission_ticket_status_idx": { + "name": "commission_ticket_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ticket_checkout_id_checkout_id_fk": { + "name": "ticket_checkout_id_checkout_id_fk", + "tableFrom": "ticket", + "tableTo": "checkout", + "schemaTo": "commission", + "columnsFrom": [ + "checkout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ticket_user_id_user_id_fk": { + "name": "ticket_user_id_user_id_fk", + "tableFrom": "ticket", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.constellation_condition": { + "name": "constellation_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_media_id": { + "name": "icon_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "constellation_condition_order_idx": { + "name": "constellation_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "constellation_condition_guide_id_guide_id_fk": { + "name": "constellation_condition_guide_id_guide_id_fk", + "tableFrom": "constellation_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "constellation_condition_icon_media_id_media_id_fk": { + "name": "constellation_condition_icon_media_id_media_id_fk", + "tableFrom": "constellation_condition", + "tableTo": "media", + "columnsFrom": [ + "icon_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.constellation_recommendation": { + "name": "constellation_recommendation", + "schema": "guides", + "columns": { + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cumulative_percent": { + "name": "cumulative_percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "is_great": { + "name": "is_great", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "constellation_recommendation_condition_id_constellation_condition_id_fk": { + "name": "constellation_recommendation_condition_id_constellation_condition_id_fk", + "tableFrom": "constellation_recommendation", + "tableTo": "constellation_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "constellation_recommendation_condition_id_level_pk": { + "name": "constellation_recommendation_condition_id_level_pk", + "columns": [ + "condition_id", + "level" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.extra_section_row": { + "name": "extra_section_row", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "section_id": { + "name": "section_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heading": { + "name": "heading", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "percent": { + "name": "percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "background_color": { + "name": "background_color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "text_color": { + "name": "text_color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": false + }, + "highlights": { + "name": "highlights", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "catalog_items": { + "name": "catalog_items", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sheet": { + "name": "sheet", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "extra_section_row_order_idx": { + "name": "extra_section_row_order_idx", + "columns": [ + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "extra_section_row_section_id_extra_section_section_id_fk": { + "name": "extra_section_row_section_id_extra_section_section_id_fk", + "tableFrom": "extra_section_row", + "tableTo": "extra_section", + "schemaTo": "guides", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "section_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "extra_section_row_media_id_media_id_fk": { + "name": "extra_section_row_media_id_media_id_fk", + "tableFrom": "extra_section_row", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.extra_section": { + "name": "extra_section", + "schema": "guides", + "columns": { + "section_id": { + "name": "section_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "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": { + "extra_section_section_id_guide_section_id_fk": { + "name": "extra_section_section_id_guide_section_id_fk", + "tableFrom": "extra_section", + "tableTo": "guide_section", + "schemaTo": "guides", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.glossary_alias": { + "name": "glossary_alias", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "catalog_key": { + "name": "catalog_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "normalized_alias": { + "name": "normalized_alias", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "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": { + "glossary_alias_item_normalized_unique": { + "name": "glossary_alias_item_normalized_unique", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "catalog_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "normalized_alias", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "glossary_alias_item_idx": { + "name": "glossary_alias_item_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "catalog_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.guide_poster_bleeding": { + "name": "guide_poster_bleeding", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "scale": { + "name": "scale", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "blend_mode": { + "name": "blend_mode", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'multiply'" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "guide_poster_bleeding_order_idx": { + "name": "guide_poster_bleeding_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "guide_poster_bleeding_guide_id_guide_id_fk": { + "name": "guide_poster_bleeding_guide_id_guide_id_fk", + "tableFrom": "guide_poster_bleeding", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "guide_poster_bleeding_media_id_media_id_fk": { + "name": "guide_poster_bleeding_media_id_media_id_fk", + "tableFrom": "guide_poster_bleeding", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.guide_section": { + "name": "guide_section", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "guide_section_slug_unique": { + "name": "guide_section_slug_unique", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "guide_section_order_idx": { + "name": "guide_section_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "guide_section_guide_id_guide_id_fk": { + "name": "guide_section_guide_id_guide_id_fk", + "tableFrom": "guide_section", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.guide": { + "name": "guide", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_key": { + "name": "character_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overview": { + "name": "overview", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "youtube_urls": { + "name": "youtube_urls", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cover_media_id": { + "name": "cover_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "poster_art_media_id": { + "name": "poster_art_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "poster_art_offset_x": { + "name": "poster_art_offset_x", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "poster_art_offset_y": { + "name": "poster_art_offset_y", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "poster_art_scale": { + "name": "poster_art_scale", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "poster_art_rotation": { + "name": "poster_art_rotation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "poster_top_colors": { + "name": "poster_top_colors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"#000000\",\"#000000\"]'::jsonb" + }, + "poster_bottom_colors": { + "name": "poster_bottom_colors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"#000000\",\"#000000\"]'::jsonb" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "trashed_at": { + "name": "trashed_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": { + "guide_character_key_unique": { + "name": "guide_character_key_unique", + "columns": [ + { + "expression": "character_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "guide_slug_unique": { + "name": "guide_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "guide_directory_idx": { + "name": "guide_directory_idx", + "columns": [ + { + "expression": "trashed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "guide_cover_media_id_media_id_fk": { + "name": "guide_cover_media_id_media_id_fk", + "tableFrom": "guide", + "tableTo": "media", + "columnsFrom": [ + "cover_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "guide_poster_art_media_id_media_id_fk": { + "name": "guide_poster_art_media_id_media_id_fk", + "tableFrom": "guide", + "tableTo": "media", + "columnsFrom": [ + "poster_art_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "current_reference_count": { + "name": "current_reference_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_id": { + "name": "created_by_id", + "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": { + "media_object_key_unique": { + "name": "media_object_key_unique", + "columns": [ + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_visibility_idx": { + "name": "media_visibility_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_reference_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_created_by_id_user_id_fk": { + "name": "media_created_by_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admin_only": { + "name": "admin_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "comment_id": { + "name": "comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "notification_user_event_unique": { + "name": "notification_user_event_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_user_time_idx": { + "name": "notification_user_time_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_user_unread_idx": { + "name": "notification_user_unread_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"notification\".\"read_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_comment_idx": { + "name": "notification_comment_idx", + "columns": [ + { + "expression": "comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_message_idx": { + "name": "notification_message_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_user_id_user_id_fk": { + "name": "notification_user_id_user_id_fk", + "tableFrom": "notification", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_comment_id_comment_id_fk": { + "name": "notification_comment_id_comment_id_fk", + "tableFrom": "notification", + "tableTo": "comment", + "schemaTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_message_id_message_id_fk": { + "name": "notification_message_id_message_id_fk", + "tableFrom": "notification", + "tableTo": "message", + "schemaTo": "commission", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "outbox_event_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "topic": { + "name": "topic", + "type": "varchar(160)", + "primaryKey": false, + "notNull": true + }, + "aggregate_id": { + "name": "aggregate_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "outbox_pending_idx": { + "name": "outbox_pending_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.session": { + "name": "session", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.stygian_boss": { + "name": "stygian_boss", + "schema": "guides", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hp": { + "name": "hp", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mechanics": { + "name": "mechanics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recommended_mechanics": { + "name": "recommended_mechanics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recommended_mechanic_types": { + "name": "recommended_mechanic_types", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "image_key": { + "name": "image_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "stygian_boss_schedule_idx": { + "name": "stygian_boss_schedule_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stygian_boss_schedule_id_stygian_schedule_schedule_id_fk": { + "name": "stygian_boss_schedule_id_stygian_schedule_schedule_id_fk", + "tableFrom": "stygian_boss", + "tableTo": "stygian_schedule", + "schemaTo": "guides", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "schedule_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "stygian_boss_schedule_id_level_position_pk": { + "name": "stygian_boss_schedule_id_level_position_pk", + "columns": [ + "schedule_id", + "level", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.stygian_recommendation_group": { + "name": "stygian_recommendation_group", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "boss_position": { + "name": "boss_position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "stygian_recommendation_group_order_idx": { + "name": "stygian_recommendation_group_order_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "boss_position", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stygian_recommendation_group_schedule_id_stygian_schedule_schedule_id_fk": { + "name": "stygian_recommendation_group_schedule_id_stygian_schedule_schedule_id_fk", + "tableFrom": "stygian_recommendation_group", + "tableTo": "stygian_schedule", + "schemaTo": "guides", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "schedule_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.stygian_recommendation_item": { + "name": "stygian_recommendation_item", + "schema": "guides", + "columns": { + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true + }, + "catalog_key": { + "name": "catalog_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "weapon_key": { + "name": "weapon_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "artifact_key": { + "name": "artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "refinement": { + "name": "refinement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "constellation_level": { + "name": "constellation_level", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "element": { + "name": "element", + "type": "varchar(24)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stygian_recommendation_item_group_id_stygian_recommendation_group_id_fk": { + "name": "stygian_recommendation_item_group_id_stygian_recommendation_group_id_fk", + "tableFrom": "stygian_recommendation_item", + "tableTo": "stygian_recommendation_group", + "schemaTo": "guides", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "stygian_recommendation_item_group_id_position_pk": { + "name": "stygian_recommendation_item_group_id_position_pk", + "columns": [ + "group_id", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.stygian_schedule": { + "name": "stygian_schedule", + "schema": "guides", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "challenge_name": { + "name": "challenge_name", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "schedule_start_time": { + "name": "schedule_start_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "schedule_end_time": { + "name": "schedule_end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.team_composition": { + "name": "team_composition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rotation_seconds": { + "name": "rotation_seconds", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true + }, + "combo": { + "name": "combo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "team_composition_order_idx": { + "name": "team_composition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_composition_guide_id_guide_id_fk": { + "name": "team_composition_guide_id_guide_id_fk", + "tableFrom": "team_composition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_composition_condition_id_team_condition_id_fk": { + "name": "team_composition_condition_id_team_condition_id_fk", + "tableFrom": "team_composition", + "tableTo": "team_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.team_condition": { + "name": "team_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_media_id": { + "name": "icon_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "team_condition_order_idx": { + "name": "team_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_condition_guide_id_guide_id_fk": { + "name": "team_condition_guide_id_guide_id_fk", + "tableFrom": "team_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_condition_icon_media_id_media_id_fk": { + "name": "team_condition_icon_media_id_media_id_fk", + "tableFrom": "team_condition", + "tableTo": "media", + "columnsFrom": [ + "icon_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.team_member": { + "name": "team_member", + "schema": "guides", + "columns": { + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "character_key": { + "name": "character_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "weapon_key": { + "name": "weapon_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "artifact_key": { + "name": "artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "second_artifact_key": { + "name": "second_artifact_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "constellation": { + "name": "constellation", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "refinement": { + "name": "refinement", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "damage": { + "name": "damage", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "team_member_team_id_team_composition_id_fk": { + "name": "team_member_team_id_team_composition_id_fk", + "tableFrom": "team_member", + "tableTo": "team_composition", + "schemaTo": "guides", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_member_team_id_position_pk": { + "name": "team_member_team_id_position_pk", + "columns": [ + "team_id", + "position" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user": { + "name": "user", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "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": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verification": { + "name": "verification", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "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_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.weapon_condition": { + "name": "weapon_condition", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guide_id": { + "name": "guide_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_media_id": { + "name": "icon_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "weapon_condition_order_idx": { + "name": "weapon_condition_order_idx", + "columns": [ + { + "expression": "guide_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weapon_condition_guide_id_guide_id_fk": { + "name": "weapon_condition_guide_id_guide_id_fk", + "tableFrom": "weapon_condition", + "tableTo": "guide", + "schemaTo": "guides", + "columnsFrom": [ + "guide_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weapon_condition_icon_media_id_media_id_fk": { + "name": "weapon_condition_icon_media_id_media_id_fk", + "tableFrom": "weapon_condition", + "tableTo": "media", + "columnsFrom": [ + "icon_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "guides.weapon_recommendation": { + "name": "weapon_recommendation", + "schema": "guides", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "condition_id": { + "name": "condition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "weapon_key": { + "name": "weapon_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "refinement": { + "name": "refinement", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + }, + "overall_percent": { + "name": "overall_percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "personal_percent": { + "name": "personal_percent", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "weapon_recommendation_order_idx": { + "name": "weapon_recommendation_order_idx", + "columns": [ + { + "expression": "condition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weapon_recommendation_condition_id_weapon_condition_id_fk": { + "name": "weapon_recommendation_condition_id_weapon_condition_id_fk", + "tableFrom": "weapon_recommendation", + "tableTo": "weapon_condition", + "schemaTo": "guides", + "columnsFrom": [ + "condition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 8397643..6a35607 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -547,6 +547,20 @@ "when": 1791395148797, "tag": "0077_reflective_chimera", "breakpoints": true + }, + { + "idx": 78, + "version": "7", + "when": 1791400048838, + "tag": "0078_slow_black_bolt", + "breakpoints": true + }, + { + "idx": 79, + "version": "7", + "when": 1791400712755, + "tag": "0079_luxuriant_thunderball", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/comments/push.ts b/lib/comments/push.ts index 7694a1c..3b0a704 100644 --- a/lib/comments/push.ts +++ b/lib/comments/push.ts @@ -1,33 +1,59 @@ import "server-only"; -import { and, eq } from "drizzle-orm"; +import { and, eq, inArray, ne, sql } from "drizzle-orm"; import webPush from "web-push"; import { getDb } from "@/db"; import { comments, commentPushSubscriptions, commentRevisions, users } from "@/db/schema"; import { pushPublicKey, validPushEndpoint } from "@/lib/commission/push"; +import { createNotifications } from "@/lib/notifications/repository"; import { authorizeComment } from "./repository"; -export async function sendCommentReplyPush(id: string) { - if (!pushPublicKey()) return; +export async function sendCommentPush(id: string) { const [reply] = await getDb().select().from(comments).where(eq(comments.id, id)).limit(1); - if (!reply?.replyToId || reply.hidden || reply.deletedAt) return; - const [parent] = await getDb().select().from(comments).where(eq(comments.id, reply.replyToId)).limit(1); - if (!parent || parent.authorId === reply.authorId || parent.hidden || parent.deletedAt) return; - const [recipient] = await getDb().select().from(users).where(eq(users.id, parent.authorId)).limit(1); - if (!recipient || recipient.banned) return; - let destination; - try { - ({ destination } = await authorizeComment(id, { id: recipient.id, admin: recipient.role === "admin" && recipient.emailVerified })); - } catch { return; } + if (!reply || reply.hidden || reply.deletedAt) return; + let recipients: { id: string; admin: boolean }[]; + let url: string; + let title: string; + if (reply.replyToId) { + const [parent] = await getDb().select().from(comments).where(eq(comments.id, reply.replyToId)).limit(1); + if (!parent || parent.authorId === reply.authorId || parent.hidden || parent.deletedAt) return; + const [recipient] = await getDb().select().from(users).where(eq(users.id, parent.authorId)).limit(1); + if (!recipient || recipient.banned) return; + let destination; + try { + ({ destination } = await authorizeComment(id, { id: recipient.id, admin: recipient.role === "admin" && recipient.emailVerified })); + } catch { return; } + recipients = [{ id: recipient.id, admin: false }]; + url = `${destination.href}${destination.href.includes("?") ? "&" : "?"}reply=${id}#comment-${reply.rootId}`; + title = "ตอบกลับความคิดเห็นของคุณ"; + } else { + recipients = (await getDb().select({ id: users.id }).from(users) + .where(and(eq(users.role, "admin"), eq(users.emailVerified, true), ne(users.id, reply.authorId), sql`${users.banned} is not true`))) + .map((user) => ({ ...user, admin: true })); + if (!recipients.length) return; + let destination; + try { + ({ destination } = await authorizeComment(id, { id: recipients[0].id, admin: true })); + } catch { return; } + if (!destination.writable) return; + url = `/admin/comments?target=${encodeURIComponent(destination.target)}&comment=${id}`; + title = `แสดงความคิดเห็นใหม่ใน ${destination.name}`; + } const [author] = await getDb().select({ name: users.name }).from(users).where(eq(users.id, reply.authorId)).limit(1); const [revision] = await getDb().select({ text: commentRevisions.text }).from(commentRevisions) .where(and(eq(commentRevisions.commentId, id), eq(commentRevisions.version, reply.version))).limit(1); - const subscriptions = await getDb().select().from(commentPushSubscriptions).where(eq(commentPushSubscriptions.userId, recipient.id)); + const notificationTitle = `${author?.name ?? "ผู้ใช้"} ${title}`; + const body = revision?.text.slice(0, 300) || "ส่งรูปภาพ"; + await createNotifications(recipients.map(recipient => ({ userId: recipient.id, adminOnly: recipient.admin, url })), { + eventKey: `comment:${id}`, kind: reply.replyToId ? "comment_reply" : "comment_new", title: notificationTitle, body, commentId: id, + }); + if (!pushPublicKey()) return; + const subscriptions = await getDb().select().from(commentPushSubscriptions).where(inArray(commentPushSubscriptions.userId, recipients.map(recipient => recipient.id))); if (!subscriptions.length) return; webPush.setVapidDetails(process.env.WEB_PUSH_SUBJECT!, process.env.WEB_PUSH_PUBLIC_KEY!, process.env.WEB_PUSH_PRIVATE_KEY!); - const payload = JSON.stringify({ id: `comment-${id}`, title: `${author?.name ?? "ผู้ใช้"} ตอบกลับความคิดเห็นของคุณ`, - body: revision?.text.slice(0, 300) || "ส่งรูปภาพ", icon: "/icon/nav/Comment.webp", - url: `${destination.href}${destination.href.includes("?") ? "&" : "?"}reply=${id}#comment-${reply.rootId}` }); + const payload = JSON.stringify({ id: `comment-${id}`, title: notificationTitle, + body, icon: "/icon/nav/Comment.webp", + url }); for (let offset = 0; offset < subscriptions.length; offset += 10) { await Promise.all(subscriptions.slice(offset, offset + 10).map(async (subscription) => { if (!validPushEndpoint(subscription.endpoint)) return; diff --git a/lib/comments/repository.ts b/lib/comments/repository.ts index ab1f987..cc66e7d 100644 --- a/lib/comments/repository.ts +++ b/lib/comments/repository.ts @@ -5,7 +5,8 @@ import { notifyCommentChange } from "./events"; import { and, asc, desc, eq, inArray, isNull, lt, or, sql, type SQL } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; import { getDb, type Database } from "@/db"; -import { commentThreads, comments, commentRevisions, commentAttachments, commentRevisionAttachments, commentReactions, catalogCharacters, guides, stygianSchedules, users } from "@/db/schema"; +import { commentThreads, comments, commentRevisions, commentAttachments, commentRevisionAttachments, commentReactions, catalogCharacters, guides, stygianSchedules, users, notifications } from "@/db/schema"; +import { notifyNotificationChange } from "@/lib/notifications/events"; import { getMediaStorage, publicMediaUrl } from "@/lib/media/storage"; import { getCustomerSession } from "@/lib/auth/server"; import { isAuthorizedAdmin } from "@/lib/auth/authorization"; @@ -14,7 +15,7 @@ import { HttpError } from "@/lib/security/http"; import { decodeCursor, encodeCursor, parseTarget, uuidSchema } from "./validation"; import type { CommentHistoryItem, CommentImage, CommentItem, CommentPage, CommentViewer } from "./types"; -type Reader = Pick; +type Reader = Pick; export async function getCommentViewer(): Promise { const session = await getCustomerSession(); if (!session) return null; @@ -199,11 +200,14 @@ export async function withCommentLock(id: string, viewer: CommentViewer, task export async function mutateComment(id: string, viewer: CommentViewer, action: "delete" | "reaction" | "moderation" | "heart", value?: number | boolean) { let target = ""; let deletedImages: { objectKey: string }[] = []; + let notificationUsers: { userId: string }[] = []; const result = await withCommentLock(id, viewer, async (tx, context) => { target = context.destination.target; const c = context.comment; if (action === "moderation") { requireCommentAdmin(viewer); + notificationUsers = await tx.selectDistinct({ userId: notifications.userId }).from(notifications) + .innerJoin(comments, eq(comments.id, notifications.commentId)).where(or(eq(comments.id, id), eq(comments.rootId, id))); await tx.update(comments).set({ hidden: value as boolean }).where(eq(comments.id, id)); const [actor] = await tx.select({ id: users.id, name: users.name }).from(users).where(eq(users.id, viewer.id)); await writeAuditLog(tx, auditActor(actor), { action: value ? "comment.hidden" : "comment.restored", targetType: "comment", targetId: id, @@ -221,6 +225,8 @@ export async function mutateComment(id: string, viewer: CommentViewer, action: " ) select id from descendants`; deletedImages = await tx.select({ objectKey: commentAttachments.objectKey }).from(commentAttachments) .where(sql`${commentAttachments.commentId} in (${descendants})`); + notificationUsers = await tx.selectDistinct({ userId: notifications.userId }).from(notifications) + .where(sql`${notifications.commentId} in (${descendants})`); // Delete the entire subtree in one statement so self-referencing foreign keys remain valid. // Revisions, attachment metadata, revision links, and reactions cascade automatically. await tx.delete(comments).where(sql`${comments.id} in (${descendants})`); @@ -234,6 +240,8 @@ export async function mutateComment(id: string, viewer: CommentViewer, action: " .onConflictDoUpdate({ target: [commentReactions.commentId, commentReactions.userId], set: { value: value as number } }); } }); + for (let offset = 0; offset < notificationUsers.length; offset += 10) + await Promise.all(notificationUsers.slice(offset, offset + 10).map(user => notifyNotificationChange(user.userId))); await notifyCommentChange(target); if (deletedImages.length) { try { diff --git a/lib/commission/push.ts b/lib/commission/push.ts index ca46ec0..8e97bc8 100644 --- a/lib/commission/push.ts +++ b/lib/commission/push.ts @@ -1,7 +1,8 @@ import "server-only"; -import { and, eq, ne, or } from "drizzle-orm"; +import { and, eq, ne, or, sql } from "drizzle-orm"; import webPush from "web-push"; +import { createNotifications } from "@/lib/notifications/repository"; import { getDb } from "@/db"; import { commissionPushSubscriptions, users } from "@/db/schema"; @@ -25,6 +26,12 @@ export async function sendCommissionMessagePush(message: { id: string; ticketId: string; ticketTitle: string; customerId: string; authorId: string; authorName: string; text: string | null; }) { + const recipients = await getDb().select({ id: users.id, role: users.role, verified: users.emailVerified }).from(users) + .where(and(ne(users.id, message.authorId), sql`${users.banned} is not true`, or(eq(users.id, message.customerId), and(eq(users.role, "admin"), eq(users.emailVerified, true))))); + await createNotifications(recipients.map(user => { + const admin = user.role === "admin" && user.verified && user.id !== message.customerId; + return { userId: user.id, adminOnly: admin, url: `/${admin ? "admin/commission" : "commission/tickets"}/${message.ticketId}` }; + }), { eventKey: `commission:${message.id}`, kind: "commission_message", title: `${message.authorName} - ${message.ticketTitle}`, body: message.text?.slice(0, 300) || "ส่งรูปภาพ", messageId: message.id }); if (!pushPublicKey()) return; const subscriptions = await getDb().select({ subscription: commissionPushSubscriptions }) .from(commissionPushSubscriptions) diff --git a/lib/notifications/events.ts b/lib/notifications/events.ts new file mode 100644 index 0000000..dafb8be --- /dev/null +++ b/lib/notifications/events.ts @@ -0,0 +1,6 @@ +import 'server-only'; +import { getRedisClient, redisEventChannel } from '@/lib/redis/client'; +export async function notifyNotificationChange(userId: string) { + try { await (await getRedisClient()).publish(redisEventChannel(`notifications:user:${userId}`), 'changed'); } + catch { /* Saved notifications remain available after reconnect. */ } +} diff --git a/lib/notifications/repository.ts b/lib/notifications/repository.ts new file mode 100644 index 0000000..2d0e06f --- /dev/null +++ b/lib/notifications/repository.ts @@ -0,0 +1,41 @@ +import 'server-only'; +import { and, desc, eq, isNull, or, sql } from 'drizzle-orm'; +import { alias } from 'drizzle-orm/pg-core'; +import { getDb } from '@/db'; +import { notifications, comments, commentThreads, guides } from '@/db/schema'; +import { notifyNotificationChange } from './events'; + +type Recipient = { userId: string; adminOnly?: boolean; url: string }; +type Message = { eventKey: string; kind: string; title: string; body: string; commentId?: string; messageId?: string }; +export async function createNotifications(recipients: Recipient[], message: Message) { + for (let offset = 0; offset < recipients.length; offset += 100) { + const inserted = await getDb().insert(notifications).values(recipients.slice(offset, offset + 100).map(recipient => ({ ...recipient, ...message }))) + .onConflictDoNothing({ target: [notifications.userId, notifications.eventKey] }).returning({ userId: notifications.userId }); + for (let start = 0; start < inserted.length; start += 10) await Promise.all(inserted.slice(start, start + 10).map(row => notifyNotificationChange(row.userId))); + } +} +const root = alias(comments, 'notification_root'); +export async function listNotifications(user: { id: string; role: string | null; emailVerified: boolean }, before?: { time: string; id: string }) { + const admin = user.role === 'admin' && user.emailVerified; + const visible = and(eq(notifications.userId, user.id), admin ? undefined : and(eq(notifications.adminOnly, false), or(isNull(notifications.commentId), and( + eq(comments.hidden, false), sql`coalesce(${root.hidden}, false) = false`, + or(isNull(commentThreads.guideId), and(eq(guides.isPublic, true), isNull(guides.trashedAt))), + )))); + const query = () => getDb().select({ notification: notifications, cursorTime: sql`to_char(${notifications.createdAt} at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')` }).from(notifications) + .leftJoin(comments, eq(comments.id, notifications.commentId)).leftJoin(root, eq(root.id, comments.rootId)) + .leftJoin(commentThreads, eq(commentThreads.id, comments.threadId)).leftJoin(guides, eq(guides.id, commentThreads.guideId)); + const [rows, count] = await Promise.all([ + query().where(and(visible, before ? sql`(${notifications.createdAt}, ${notifications.id}) < (${before.time}::timestamptz, ${before.id}::uuid)` : undefined)).orderBy(desc(notifications.createdAt), desc(notifications.id)).limit(21), + getDb().select({ count: sql`count(*)::int` }).from(notifications) + .leftJoin(comments, eq(comments.id, notifications.commentId)).leftJoin(root, eq(root.id, comments.rootId)) + .leftJoin(commentThreads, eq(commentThreads.id, comments.threadId)).leftJoin(guides, eq(guides.id, commentThreads.guideId)) + .where(and(visible, isNull(notifications.readAt))), + ]); + const items = rows.slice(0, 20).map(row => row.notification); + const last = rows[19]; + return { items, unread: count[0]?.count ?? 0, nextCursor: rows.length > 20 ? `${last.cursorTime}|${last.notification.id}` : null }; +} +export async function readNotifications(userId: string, id?: string) { + await getDb().update(notifications).set({ readAt: sql`now()` }).where(and(eq(notifications.userId, userId), isNull(notifications.readAt), id ? eq(notifications.id, id) : undefined)); + await notifyNotificationChange(userId); +} diff --git a/tests/comment-notification-worker.test.ts b/tests/comment-notification-worker.test.ts index 1558a23..0c02e82 100644 --- a/tests/comment-notification-worker.test.ts +++ b/tests/comment-notification-worker.test.ts @@ -22,6 +22,14 @@ describe("reply notification navigation", () => { expect(close).toHaveBeenCalled(); expect(self.clients.openWindow).toHaveBeenCalledWith(`https://guide.example.test${url}`); }); + it("opens the admin inbox at the notified comment", async () => { + const { self, handlers } = worker(); + const url = "/admin/comments?target=stygian%3A123&comment=comment-id"; + let task: Promise | undefined; + handlers.notificationclick({ notification: { close: vi.fn(), data: { url } }, waitUntil: (promise: Promise) => { task = promise; } }); + await task; + expect(self.clients.openWindow).toHaveBeenCalledWith(`https://guide.example.test${url}`); + }); it("uses the comment icon and rejects off-site notification destinations", async () => { const { self, handlers } = worker(); const message = { id: "comment-id", title: "Reply", body: "Text", icon: "/icon/nav/Comment.webp", url: "/guide/comment#comment-root" }; diff --git a/tests/comments.integration.test.ts b/tests/comments.integration.test.ts index 61d973a..8e7885a 100644 --- a/tests/comments.integration.test.ts +++ b/tests/comments.integration.test.ts @@ -4,6 +4,7 @@ import sharp from "sharp"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("server-only", () => ({})); +vi.mock("@/lib/notifications/events", () => ({ notifyNotificationChange: vi.fn() })); vi.mock("next/server", () => ({ after: vi.fn() })); const pushTransport = vi.hoisted(() => ({ sendNotification: vi.fn(), setVapidDetails: vi.fn() })); vi.mock("web-push", () => ({ default: pushTransport })); @@ -24,7 +25,10 @@ const commentsRoute = await import("@/app/api/comments/route"); const itemRoute = await import("@/app/api/comments/[id]/route"); const actionRoute = await import("@/app/api/comments/[id]/[action]/route"); const inboxRoute = await import("@/app/api/admin/comments/route"); -const { sendCommentReplyPush } = await import("@/lib/comments/push"); +const { sendCommentPush } = await import("@/lib/comments/push"); +const { notifyNotificationChange } = await import("@/lib/notifications/events"); +const { createNotifications, listNotifications, readNotifications } = await import("@/lib/notifications/repository"); +const { sendCommissionMessagePush } = await import("@/lib/commission/push"); const { unreadCommentCounts, markCommentsRead } = await import("@/lib/comments/unread"); const subscriptionRoute = await import("@/app/api/comments/push-subscription/route"); const imageRoute = await import("@/app/api/comments/images/[id]/route"); @@ -73,6 +77,7 @@ describeDatabase("guide comments against PostgreSQL", { timeout: 30000 }, () => pushTransport.sendNotification.mockReset().mockResolvedValue(undefined); pushTransport.setVapidDetails.mockReset(); await getDb().delete(schema.commentPushSubscriptions).where(inArray(schema.commentPushSubscriptions.userId, [author.id, other.id, admin.id])); + await getDb().delete(schema.notifications).where(inArray(schema.notifications.userId, [author.id, other.id, admin.id])); await getDb().delete(schema.commentReadState).where(inArray(schema.commentReadState.userId, [author.id, other.id, admin.id])); await getDb().delete(schema.commentThreads).where(inArray(schema.commentThreads.guideId, [guideId, otherGuideId, privateGuideId])); await getDb().delete(schema.commentThreads).where(inArray(schema.commentThreads.scheduleId, [scheduleId, scheduleId + 1])); @@ -359,7 +364,7 @@ describeDatabase("guide comments against PostgreSQL", { timeout: 30000 }, () => await saveSubscription(other.id, `${subscription.endpoint}-other`); const root = await post("Root"); const reply = await publishComment(request("Reply", { reply: root.id }), other, { target }); - await sendCommentReplyPush(reply.id); + await sendCommentPush(reply.id); expect(pushTransport.sendNotification).toHaveBeenCalledTimes(1); expect(pushTransport.sendNotification.mock.calls[0][0].endpoint).toBe(subscription.endpoint); expect(JSON.parse(pushTransport.sendNotification.mock.calls[0][1])).toMatchObject({ body: "Reply", url: `/comment-test-${guideId}/comment?reply=${reply.id}#comment-${root.id}` }); @@ -367,31 +372,128 @@ describeDatabase("guide comments against PostgreSQL", { timeout: 30000 }, () => const nested = await post("Nested", { reply: reply.id }); expect((await listComments({ viewer: admin, rootId: root.id })).items.find((item) => item.id === reply.id)?.hasReplies).toBe(true); expect((await listComments({ viewer: admin, rootId: root.id })).items.find((item) => item.id === nested.id)?.hasReplies).toBe(false); - await sendCommentReplyPush(nested.id); + await sendCommentPush(nested.id); expect(pushTransport.sendNotification).toHaveBeenCalledTimes(1); expect(pushTransport.sendNotification.mock.calls[0][0].endpoint).toBe(`${subscription.endpoint}-other`); expect(JSON.parse(pushTransport.sendNotification.mock.calls[0][1]).url).toContain(`reply=${nested.id}#comment-${root.id}`); pushTransport.sendNotification.mockClear(); - await sendCommentReplyPush((await post("Self", { reply: root.id })).id); - await sendCommentReplyPush(root.id); + await sendCommentPush((await post("Self", { reply: root.id })).id); expect(pushTransport.sendNotification).not.toHaveBeenCalled(); }); + it("notifies subscribed admins about new comments, checks current roles, and skips the author", async () => { + await saveSubscription(admin.id, `${subscription.endpoint}-admin`); + await saveSubscription(other.id, `${subscription.endpoint}-reader`); + const root = await post("New root"); + const adminCalls = () => pushTransport.sendNotification.mock.calls.filter(([recipient]) => recipient.endpoint === `${subscription.endpoint}-admin`); + await sendCommentPush(root.id); + expect(adminCalls()).toHaveLength(1); + expect(JSON.parse(adminCalls()[0][1])).toMatchObject({ body: "New root", icon: "/icon/nav/Comment.webp", url: `/admin/comments?target=${encodeURIComponent(target)}&comment=${root.id}` }); + expect(pushTransport.sendNotification.mock.calls.some(([recipient]) => recipient.endpoint === `${subscription.endpoint}-reader`)).toBe(false); + pushTransport.sendNotification.mockClear(); + await sendCommentPush((await publishComment(request("Admin's own"), admin, { target })).id); + expect(adminCalls()).toHaveLength(0); + try { + for (const state of [{ role: "user" }, { role: "admin", emailVerified: false }, { role: "admin", emailVerified: true, banned: true }]) { + await getDb().update(schema.users).set(state).where(eq(schema.users.id, admin.id)); + await sendCommentPush(root.id); + expect(adminCalls()).toHaveLength(0); + } + } finally { + await getDb().update(schema.users).set({ role: "admin", emailVerified: true, banned: false }).where(eq(schema.users.id, admin.id)); + } + }, 60000); + it("links Stygian admin notifications and suppresses removed comments and expired subscriptions", async () => { + const endpoint = `${subscription.endpoint}-admin`; + await saveSubscription(admin.id, endpoint); + const root = await publishComment(request("New Stygian comment"), author, { target: `stygian:${scheduleId}` }); + const adminCalls = () => pushTransport.sendNotification.mock.calls.filter(([recipient]) => recipient.endpoint === endpoint); + await sendCommentPush(root.id); + expect(JSON.parse(adminCalls()[0][1]).url).toBe(`/admin/comments?target=stygian%3A${scheduleId}&comment=${root.id}`); + pushTransport.sendNotification.mockClear(); + await mutateComment(root.id, admin, "moderation", true); + await sendCommentPush(root.id); + expect(adminCalls()).toHaveLength(0); + await mutateComment(root.id, admin, "moderation", false); + pushTransport.sendNotification.mockImplementation(async (recipient) => { if (recipient.endpoint === endpoint) throw { statusCode: 410 }; }); + await sendCommentPush(root.id); + expect(await getDb().select().from(schema.commentPushSubscriptions).where(eq(schema.commentPushSubscriptions.endpoint, endpoint))).toHaveLength(0); + await saveSubscription(admin.id, endpoint); + await mutateComment(root.id, author, "delete"); + pushTransport.sendNotification.mockClear(); + await sendCommentPush(root.id); + expect(adminCalls()).toHaveLength(0); + }, 60000); it("does not push hidden or deleted replies and removes expired browser subscriptions", async () => { await saveSubscription(author.id); const root = await post("Root"); const reply = await publishComment(request("Reply", { reply: root.id }), other, { target }); await mutateComment(root.id, admin, "moderation", true); - await sendCommentReplyPush(reply.id); + await sendCommentPush(reply.id); expect(pushTransport.sendNotification).not.toHaveBeenCalled(); await mutateComment(root.id, admin, "moderation", false); pushTransport.sendNotification.mockRejectedValueOnce({ statusCode: 410 }); - await sendCommentReplyPush(reply.id); + await sendCommentPush(reply.id); expect(await getDb().select().from(schema.commentPushSubscriptions).where(eq(schema.commentPushSubscriptions.userId, author.id))).toHaveLength(0); await mutateComment(root.id, author, "delete"); pushTransport.sendNotification.mockClear(); - await sendCommentReplyPush(reply.id); + await sendCommentPush(reply.id); expect(pushTransport.sendNotification).not.toHaveBeenCalled(); }); + it("persists comment notifications without push permission and keeps history scoped to its owner", async () => { + const root = await post("Bell root"); + await sendCommentPush(root.id); + const adminUser = { ...admin, role: "admin", emailVerified: true }; + const authorUser = { ...author, role: "user", emailVerified: true }; + expect((await listNotifications(adminUser)).items[0]).toMatchObject({ commentId: root.id, kind: "comment_new", readAt: null }); + expect((await listNotifications(authorUser)).unread).toBe(0); + const reply = await publishComment(request("Bell reply", { reply: root.id }), other, { target }); + const key = process.env.WEB_PUSH_PUBLIC_KEY; + delete process.env.WEB_PUSH_PUBLIC_KEY; + try { await sendCommentPush(reply.id); await sendCommentPush(reply.id); } finally { process.env.WEB_PUSH_PUBLIC_KEY = key; } + const inbox = await listNotifications(authorUser); + expect(inbox.unread).toBe(1); expect(inbox.items).toHaveLength(1); + await readNotifications(other.id, inbox.items[0].id); + expect((await listNotifications(authorUser)).unread).toBe(1); + await readNotifications(author.id, inbox.items[0].id); + expect((await listNotifications(authorUser)).unread).toBe(0); + expect((await listNotifications({ ...adminUser, role: "user" })).items).toHaveLength(0); + await mutateComment(root.id, admin, "moderation", true); + expect((await listNotifications(authorUser)).items).toHaveLength(0); + await mutateComment(root.id, admin, "moderation", false); + vi.mocked(notifyNotificationChange).mockClear(); + await mutateComment(root.id, author, "delete"); + expect(notifyNotificationChange).toHaveBeenCalledWith(author.id); + expect(notifyNotificationChange).toHaveBeenCalledWith(admin.id); + expect((await listNotifications(adminUser)).items).toHaveLength(0); + }, 60000); + it("pages notification history without dropping notifications sharing a timestamp", async () => { + const user = { ...author, role: "user", emailVerified: true }; + for (let index = 0; index < 25; index++) await createNotifications([{ userId: author.id, url: "/" }], { eventKey: `fixture:${index}`, kind: "future", title: "Future notification", body: "Text" }); + await getDb().update(schema.notifications).set({ createdAt: new Date("2026-01-01T00:00:00Z") }).where(eq(schema.notifications.userId, author.id)); + const first = await listNotifications(user); + const [time, id] = first.nextCursor!.split("|"); + const second = await listNotifications(user, { time, id }); + expect(first.items).toHaveLength(20); expect(second.items).toHaveLength(5); + expect(new Set([...first.items, ...second.items].map(item => item.id)).size).toBe(25); + await readNotifications(author.id); + expect((await listNotifications(user)).unread).toBe(0); + }, 60000); + it("records commission notifications for customers and admins without browser subscriptions", async () => { + const ticketId = randomUUID(), messageId = randomUUID(), checkoutId = randomUUID(); + await getDb().insert(schema.commissionCheckouts).values({ id: checkoutId, userId: author.id, amountBaht: 20, request: { teams: [], weapons: [], constellations: [{ characterKey: "test", levels: [0] }] } }); + await getDb().insert(schema.commissionTickets).values({ id: ticketId, checkoutId, userId: author.id, title: "Notification ticket" }); + await getDb().insert(schema.commissionMessages).values({ id: messageId, ticketId, authorId: other.id, text: "Commission bell" }); + try { + await sendCommissionMessagePush({ id: messageId, ticketId, customerId: author.id, authorId: other.id, authorName: "Reader", ticketTitle: "Ticket", text: "Commission bell" }); + expect((await listNotifications({ ...author, role: "user", emailVerified: true })).items[0]).toMatchObject({ kind: "commission_message", url: `/commission/tickets/${ticketId}` }); + expect((await listNotifications({ ...admin, role: "admin", emailVerified: true })).items[0]).toMatchObject({ kind: "commission_message", url: `/admin/commission/${ticketId}` }); + expect((await listNotifications({ ...other, role: "user", emailVerified: true })).items).toHaveLength(0); + } finally { + await getDb().delete(schema.commissionMessages).where(eq(schema.commissionMessages.id, messageId)); + await getDb().delete(schema.commissionTickets).where(eq(schema.commissionTickets.id, ticketId)); + await getDb().delete(schema.commissionCheckouts).where(eq(schema.commissionCheckouts.id, checkoutId)); + } + }, 60000); it("counts unread comments per guide for admins and clears only the selected guide", async () => { await markCommentsRead(admin, "all"); await post("Unread"); diff --git a/tests/notifications-route.test.ts b/tests/notifications-route.test.ts new file mode 100644 index 0000000..d50cef2 --- /dev/null +++ b/tests/notifications-route.test.ts @@ -0,0 +1,30 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { HttpError } from '@/lib/security/http'; +vi.mock('server-only', () => ({})); +const mocks = vi.hoisted(() => ({ user: vi.fn(), list: vi.fn(), read: vi.fn(), limit: vi.fn() })); +vi.mock('@/lib/commission/server', () => ({ requireCommissionUser: mocks.user })); +vi.mock('@/lib/notifications/repository', () => ({ listNotifications: mocks.list, readNotifications: mocks.read })); +vi.mock('@/lib/security/rate-limit', () => ({ limitRequest: mocks.limit })); +const { GET, POST } = await import('@/app/api/notifications/route'); +beforeEach(() => { + vi.clearAllMocks(); process.env.BETTER_AUTH_URL = 'https://guide.example.test'; + mocks.user.mockResolvedValue({ id: 'owner', role: 'user', emailVerified: true }); + mocks.list.mockResolvedValue({ items: [], unread: 0, nextCursor: null }); +}); +describe('notification endpoints', () => { + it('requires an authenticated account and disables caching', async () => { + const request = new Request('https://guide.example.test/api/notifications'); + expect((await GET(request)).headers.get('cache-control')).toBe('private, no-store'); + mocks.user.mockRejectedValueOnce(new HttpError(401, 'unauthorized')); + expect((await GET(request)).status).toBe(401); + }); + it('validates cursors and uses the current account for mark-read', async () => { + expect((await GET(new Request('https://guide.example.test/api/notifications?before=bad'))).status).toBe(400); + const id = '00000000-0000-4000-8000-000000000001'; + const request = (origin: string) => new Request('https://guide.example.test/api/notifications', { method: 'POST', headers: { Origin: origin, 'Content-Type': 'application/json' }, body: JSON.stringify({ id, userId: 'victim' }) }); + expect((await POST(request('https://evil.example'))).status).toBe(403); + expect(mocks.read).not.toHaveBeenCalled(); + expect((await POST(request('https://guide.example.test'))).status).toBe(204); + expect(mocks.read).toHaveBeenCalledWith('owner', id); + }); +}); diff --git a/tests/site-push-client.test.ts b/tests/site-push-client.test.ts new file mode 100644 index 0000000..54b8fca --- /dev/null +++ b/tests/site-push-client.test.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { syncSitePush } from '@/components/notifications/push-client'; +afterEach(() => vi.unstubAllGlobals()); +describe('shared site push subscriptions', () => { + for (const admin of [false, true]) it(`registers comments and commission with one ${admin ? 'admin' : 'customer'} browser subscription`, async () => { + const json = { endpoint: 'https://fcm.googleapis.com/fcm/send/test', keys: { auth: 'test', p256dh: 'test' } }; + const register = vi.fn().mockResolvedValue({ pushManager: { getSubscription: async () => ({ toJSON: () => json }) } }); + const setItem = vi.fn(); + const fetcher = vi.fn(async (_url: string, options?: RequestInit) => options?.method === "POST" ? new Response(null, { status: 204 }) : Response.json({ publicKey: 'key', canAdmin: admin })); + vi.stubGlobal('navigator', { serviceWorker: { register } }); + vi.stubGlobal('fetch', fetcher); vi.stubGlobal('sessionStorage', { setItem }); + await syncSitePush('user'); + expect(register).toHaveBeenCalledTimes(1); + expect(fetcher.mock.calls[1][0]).toBe('/api/comments/push-subscription'); + expect(JSON.parse(fetcher.mock.calls[1][1]!.body as string)).toEqual(json); + expect(JSON.parse(fetcher.mock.calls[2][1]!.body as string)).toEqual({ ...json, scope: admin ? 'admin' : 'customer' }); + expect(setItem).toHaveBeenCalledWith('site-push-synced-user', '1'); + }); + it('keeps failed registration retryable', async () => { + const setItem = vi.fn(); + vi.stubGlobal('navigator', { serviceWorker: { register: async () => ({ pushManager: { getSubscription: async () => ({ toJSON: () => ({ endpoint: 'endpoint' }) }) } }) } }); + vi.stubGlobal('sessionStorage', { setItem }); + vi.stubGlobal('fetch', vi.fn(async (url: string, options?: RequestInit) => options?.method === "POST" ? new Response(null, { status: url.includes('comments') ? 503 : 204 }) : Response.json({ publicKey: 'key', canAdmin: false }))); + await expect(syncSitePush('user')).rejects.toThrow('push-subscription-failed'); + expect(setItem).not.toHaveBeenCalled(); + }); +});