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()} />
: เข้าสู่ระบบเพื่อแสดงความคิดเห็น ตอบกลับ และกดถูกใจ
router.push(commentSignInHref())}>เข้าสู่ระบบ }
ความคิดเห็น
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 <>
- setOpen(true)}> แจ้งเตือนเมื่อมีคนตอบกลับ
-
- รับการแจ้งเตือนเมื่อมีคนตอบกลับ? เราจะแจ้งเตือนเมื่อมีคนตอบกลับความคิดเห็นของคุณ แม้ปิดแท็บแล้ว กดที่การแจ้งเตือนเพื่อไปยังคำตอบนั้น
- {error && {error}
}
- setOpen(false)}>ไว้ก่อน void enable()}>เปิดการแจ้งเตือน
-
- >;
+'use client';
+import { BellIcon } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+export function CommentPushPrompt({ admin = false }: { userId: string; admin?: boolean }) {
+ return window.dispatchEvent(new Event('notifications:enable'))}> {admin ? 'แจ้งเตือนความคิดเห็นใหม่' : 'แจ้งเตือนเมื่อมีคนตอบกลับ'} ;
}
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}
}
-
- setRequest(null)}>ไว้ก่อน
- void enable()}>เปิดการแจ้งเตือน
-
-
- ;
-}
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} }
+
+
+
การแจ้งเตือน { setOpen(false); window.dispatchEvent(new Event('notifications:enable')); }}>
+ {!state.signedIn ? เข้าสู่ระบบเพื่อดูการแจ้งเตือน : <>
+ {state.unread > 0 && void state.read().catch(() => undefined)}>อ่านทั้งหมด }
+
+ {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 &&
void state.refresh(true)}>แสดงเพิ่มเติม }
+
+ >}
+
+ ;
+}
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}
}
+ ไว้ก่อน void enable()}>เปิดการแจ้งเตือน
+ ;
+}
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();
+ });
+});