refactor: remove catalog sync jobs and version history
This commit is contained in:
@@ -16,9 +16,9 @@ import { builtInTemplates } from "@/lib/templates";
|
|||||||
import { persistTemplateApplication } from "@/lib/templates/persist";
|
import { persistTemplateApplication } from "@/lib/templates/persist";
|
||||||
import {
|
import {
|
||||||
createGuide,
|
createGuide,
|
||||||
enqueueCatalogSync,
|
|
||||||
permanentlyDeleteGuide,
|
permanentlyDeleteGuide,
|
||||||
restoreGuide,
|
restoreGuide,
|
||||||
|
runCatalogSync,
|
||||||
saveArtifacts,
|
saveArtifacts,
|
||||||
saveConstellations,
|
saveConstellations,
|
||||||
saveExtraSections,
|
saveExtraSections,
|
||||||
@@ -274,10 +274,9 @@ export async function deleteStructuredGuide(guideId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function startCatalogSync() {
|
export async function startCatalogSync() {
|
||||||
const admin = await requireAdmin();
|
await requireAdmin();
|
||||||
try {
|
try {
|
||||||
const job = await enqueueCatalogSync(admin.user.id);
|
return await runCatalogSync();
|
||||||
return { status: "saved" as const, jobId: job.id };
|
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
return { status: "error" as const, message: cause instanceof Error ? cause.message : "เริ่ม Sync ไม่สำเร็จ" };
|
return { status: "error" as const, message: cause instanceof Error ? cause.message : "เริ่ม Sync ไม่สำเร็จ" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default async function CreateGuidePage() {
|
|||||||
<main className="mx-auto flex w-full max-w-7xl flex-col gap-8 p-4 sm:p-6 lg:p-8">
|
<main className="mx-auto flex w-full max-w-7xl flex-col gap-8 p-4 sm:p-6 lg:p-8">
|
||||||
<header className="flex items-center gap-4">
|
<header className="flex items-center gap-4">
|
||||||
<Button variant="outline" size="icon" render={<Link href="/admin" />} nativeButton={false} aria-label="กลับ Dashboard"><ArrowLeftIcon /></Button>
|
<Button variant="outline" size="icon" render={<Link href="/admin" />} nativeButton={false} aria-label="กลับ Dashboard"><ArrowLeftIcon /></Button>
|
||||||
<div><p className="text-sm text-muted-foreground">Catalog {catalog.version}</p><h1 className="font-heading text-3xl font-semibold">สร้าง Guide ใหม่</h1></div>
|
<div><p className="text-sm text-muted-foreground">Catalog ล่าสุดจาก Lunaris</p><h1 className="font-heading text-3xl font-semibold">สร้าง Guide ใหม่</h1></div>
|
||||||
</header>
|
</header>
|
||||||
<CreateGuideFlow characters={catalog.characters} existingKeys={guides.map((guide) => guide.characterKey)} />
|
<CreateGuideFlow characters={catalog.characters} existingKeys={guides.map((guide) => guide.characterKey)} />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+3
-4
@@ -4,15 +4,14 @@ import { connection } from "next/server";
|
|||||||
import { StructuredDashboard } from "@/components/admin/structured-dashboard";
|
import { StructuredDashboard } from "@/components/admin/structured-dashboard";
|
||||||
import { AdminHeader } from "@/components/admin/admin-header";
|
import { AdminHeader } from "@/components/admin/admin-header";
|
||||||
import { getAdminSession } from "@/lib/auth/server";
|
import { getAdminSession } from "@/lib/auth/server";
|
||||||
import { getActiveCatalog, getLatestCatalogSyncJob, listGuideCards } from "@/lib/guides/queries";
|
import { getActiveCatalog, listGuideCards } from "@/lib/guides/queries";
|
||||||
|
|
||||||
export default async function AdminPage() {
|
export default async function AdminPage() {
|
||||||
await connection();
|
await connection();
|
||||||
if (!(await getAdminSession())) redirect("/admin/login");
|
if (!(await getAdminSession())) redirect("/admin/login");
|
||||||
const [guides, trash, syncJob, catalog] = await Promise.all([
|
const [guides, trash, catalog] = await Promise.all([
|
||||||
listGuideCards(),
|
listGuideCards(),
|
||||||
listGuideCards(true),
|
listGuideCards(true),
|
||||||
getLatestCatalogSyncJob(),
|
|
||||||
getActiveCatalog(),
|
getActiveCatalog(),
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
@@ -24,7 +23,7 @@ export default async function AdminPage() {
|
|||||||
<h1 className="font-heading text-3xl font-semibold">Guide Dashboard</h1>
|
<h1 className="font-heading text-3xl font-semibold">Guide Dashboard</h1>
|
||||||
<p className="mt-2 text-muted-foreground">จัดการ Guide แบบภาพ-first และบันทึกขึ้นเว็บไซต์ทันที</p>
|
<p className="mt-2 text-muted-foreground">จัดการ Guide แบบภาพ-first และบันทึกขึ้นเว็บไซต์ทันที</p>
|
||||||
</header>
|
</header>
|
||||||
<StructuredDashboard guides={guides} trash={trash} syncJob={syncJob} catalogVersion={catalog?.version ?? null} />
|
<StructuredDashboard guides={guides} trash={trash} catalogReady={Boolean(catalog)} />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { PlusIcon, RefreshCwIcon, RotateCcwIcon, Trash2Icon } from "lucide-react";
|
import { PlusIcon, RefreshCwIcon, RotateCcwIcon, Trash2Icon } from "lucide-react";
|
||||||
import { useEffect, useTransition } from "react";
|
import { useTransition } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { deleteStructuredGuide, moveGuideToTrash, restoreStructuredGuide, startCatalogSync } from "@/app/admin/actions";
|
import { deleteStructuredGuide, moveGuideToTrash, restoreStructuredGuide, startCatalogSync } from "@/app/admin/actions";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -17,17 +16,10 @@ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "
|
|||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
|
||||||
type GuideCard = { id: string; characterKey: string; name: string; overview: string; coverUrl: string; version: number; updatedAt: Date; trashedAt: Date | null };
|
type GuideCard = { id: string; characterKey: string; name: string; overview: string; coverUrl: string; version: number; updatedAt: Date; trashedAt: Date | null };
|
||||||
type SyncJob = { id: string; status: string; version: string | null; progress: number; total: number; error: string | null } | null;
|
|
||||||
|
|
||||||
export function StructuredDashboard({ guides, trash, syncJob, catalogVersion }: { guides: GuideCard[]; trash: GuideCard[]; syncJob: SyncJob; catalogVersion: string | null }) {
|
export function StructuredDashboard({ guides, trash, catalogReady }: { guides: GuideCard[]; trash: GuideCard[]; catalogReady: boolean }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const syncing = syncJob?.status === "pending" || syncJob?.status === "processing";
|
|
||||||
useEffect(() => {
|
|
||||||
if (!syncing) return;
|
|
||||||
const timer = window.setInterval(() => router.refresh(), 3_000);
|
|
||||||
return () => window.clearInterval(timer);
|
|
||||||
}, [router, syncing]);
|
|
||||||
|
|
||||||
function run(action: () => Promise<{ status: string; message?: string }>, success: string) {
|
function run(action: () => Promise<{ status: string; message?: string }>, success: string) {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
@@ -40,10 +32,9 @@ export function StructuredDashboard({ guides, trash, syncJob, catalogVersion }:
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-2"><Badge variant="outline">Lunaris {catalogVersion ?? "ยังไม่ Sync"}</Badge>{syncJob ? <Badge variant={syncJob.status === "failed" ? "destructive" : "secondary"}>{syncJob.status}{syncing && syncJob.total ? ` ${syncJob.progress}/${syncJob.total}` : ""}</Badge> : null}</div>
|
<div className="flex items-center gap-2"><Badge variant="outline">Lunaris {catalogReady ? "พร้อมใช้งาน" : "ยังไม่ Sync"}</Badge></div>
|
||||||
<div className="flex gap-2"><Button variant="outline" disabled={pending || syncing} onClick={() => run(startCatalogSync, "เริ่ม Sync แล้ว")}>{pending || syncing ? <Spinner data-icon="inline-start" /> : <RefreshCwIcon data-icon="inline-start" />}Sync Catalog</Button><Button render={<Link href="/admin/create" />} nativeButton={false} disabled={!catalogVersion}><PlusIcon data-icon="inline-start" />สร้าง Guide</Button></div>
|
<div className="flex gap-2"><Button variant="outline" disabled={pending} onClick={() => run(startCatalogSync, "Sync สำเร็จแล้ว")}>{pending ? <Spinner data-icon="inline-start" /> : <RefreshCwIcon data-icon="inline-start" />}Sync Catalog</Button><Button render={<Link href="/admin/create" />} nativeButton={false} disabled={!catalogReady}><PlusIcon data-icon="inline-start" />สร้าง Guide</Button></div>
|
||||||
</div>
|
</div>
|
||||||
{syncJob?.status === "failed" ? <Alert variant="destructive"><AlertTitle>Sync ไม่สำเร็จ</AlertTitle><AlertDescription>{syncJob.error}</AlertDescription></Alert> : null}
|
|
||||||
{guides.length ? (
|
{guides.length ? (
|
||||||
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">{guides.map((guide) => (
|
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">{guides.map((guide) => (
|
||||||
<Card key={guide.id} className="overflow-hidden py-0">
|
<Card key={guide.id} className="overflow-hidden py-0">
|
||||||
@@ -53,7 +44,7 @@ export function StructuredDashboard({ guides, trash, syncJob, catalogVersion }:
|
|||||||
<CardFooter className="justify-between pb-5"><Button render={<Link href={`/admin/${guide.characterKey}`} />} nativeButton={false}>แก้ไข Guide</Button><AlertDialog><AlertDialogTrigger render={<Button variant="ghost" size="icon" />}><Trash2Icon /><span className="sr-only">ย้าย {guide.name} ไปถังขยะ</span></AlertDialogTrigger><AlertDialogContent><AlertDialogHeader><AlertDialogTitle>ย้าย {guide.name} ไปถังขยะ?</AlertDialogTitle><AlertDialogDescription>Guide จะหายจากหน้าเว็บไซต์ทันที และสามารถกู้คืนได้ภายหลัง</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogCancel>ยกเลิก</AlertDialogCancel><AlertDialogAction onClick={() => run(() => moveGuideToTrash(guide.id), "ย้ายไปถังขยะแล้ว")}>ย้ายไปถังขยะ</AlertDialogAction></AlertDialogFooter></AlertDialogContent></AlertDialog></CardFooter>
|
<CardFooter className="justify-between pb-5"><Button render={<Link href={`/admin/${guide.characterKey}`} />} nativeButton={false}>แก้ไข Guide</Button><AlertDialog><AlertDialogTrigger render={<Button variant="ghost" size="icon" />}><Trash2Icon /><span className="sr-only">ย้าย {guide.name} ไปถังขยะ</span></AlertDialogTrigger><AlertDialogContent><AlertDialogHeader><AlertDialogTitle>ย้าย {guide.name} ไปถังขยะ?</AlertDialogTitle><AlertDialogDescription>Guide จะหายจากหน้าเว็บไซต์ทันที และสามารถกู้คืนได้ภายหลัง</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogCancel>ยกเลิก</AlertDialogCancel><AlertDialogAction onClick={() => run(() => moveGuideToTrash(guide.id), "ย้ายไปถังขยะแล้ว")}>ย้ายไปถังขยะ</AlertDialogAction></AlertDialogFooter></AlertDialogContent></AlertDialog></CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
))}</div>
|
))}</div>
|
||||||
) : <Empty className="border"><EmptyHeader><EmptyTitle>ยังไม่มี Guide</EmptyTitle><EmptyDescription>Sync Catalog แล้วเลือกตัวละครเพื่อสร้าง Guide แรก</EmptyDescription></EmptyHeader><EmptyContent><Button render={<Link href="/admin/create" />} nativeButton={false} disabled={!catalogVersion}>สร้าง Guide</Button></EmptyContent></Empty>}
|
) : <Empty className="border"><EmptyHeader><EmptyTitle>ยังไม่มี Guide</EmptyTitle><EmptyDescription>Sync Catalog แล้วเลือกตัวละครเพื่อสร้าง Guide แรก</EmptyDescription></EmptyHeader><EmptyContent><Button render={<Link href="/admin/create" />} nativeButton={false} disabled={!catalogReady}>สร้าง Guide</Button></EmptyContent></Empty>}
|
||||||
{trash.length ? <section className="flex flex-col gap-4"><div><h2 className="font-heading text-xl font-semibold">ถังขยะ</h2><p className="text-sm text-muted-foreground">Guide ที่ซ่อนไว้จากเว็บไซต์</p></div><div className="grid gap-3 md:grid-cols-2">{trash.map((guide) => <Card key={guide.id}><CardHeader><CardTitle>{guide.name}</CardTitle><CardDescription>{guide.overview}</CardDescription><CardAction><div className="flex gap-2"><Button variant="outline" size="sm" disabled={pending} onClick={() => run(() => restoreStructuredGuide(guide.id), "กู้คืน Guide แล้ว")}><RotateCcwIcon data-icon="inline-start" />กู้คืน</Button><AlertDialog><AlertDialogTrigger render={<Button variant="destructive" size="sm" />}><Trash2Icon data-icon="inline-start" />ลบถาวร</AlertDialogTrigger><AlertDialogContent><AlertDialogHeader><AlertDialogTitle>ลบ {guide.name} ถาวร?</AlertDialogTitle><AlertDialogDescription>ข้อมูล Weapons, Artifacts, Constellations, Team และ Extra ทั้งหมดจะกู้คืนไม่ได้</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogCancel>ยกเลิก</AlertDialogCancel><AlertDialogAction onClick={() => run(() => deleteStructuredGuide(guide.id), "ลบ Guide ถาวรแล้ว")}>ยืนยันลบถาวร</AlertDialogAction></AlertDialogFooter></AlertDialogContent></AlertDialog></div></CardAction></CardHeader></Card>)}</div></section> : null}
|
{trash.length ? <section className="flex flex-col gap-4"><div><h2 className="font-heading text-xl font-semibold">ถังขยะ</h2><p className="text-sm text-muted-foreground">Guide ที่ซ่อนไว้จากเว็บไซต์</p></div><div className="grid gap-3 md:grid-cols-2">{trash.map((guide) => <Card key={guide.id}><CardHeader><CardTitle>{guide.name}</CardTitle><CardDescription>{guide.overview}</CardDescription><CardAction><div className="flex gap-2"><Button variant="outline" size="sm" disabled={pending} onClick={() => run(() => restoreStructuredGuide(guide.id), "กู้คืน Guide แล้ว")}><RotateCcwIcon data-icon="inline-start" />กู้คืน</Button><AlertDialog><AlertDialogTrigger render={<Button variant="destructive" size="sm" />}><Trash2Icon data-icon="inline-start" />ลบถาวร</AlertDialogTrigger><AlertDialogContent><AlertDialogHeader><AlertDialogTitle>ลบ {guide.name} ถาวร?</AlertDialogTitle><AlertDialogDescription>ข้อมูล Weapons, Artifacts, Constellations, Team และ Extra ทั้งหมดจะกู้คืนไม่ได้</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogCancel>ยกเลิก</AlertDialogCancel><AlertDialogAction onClick={() => run(() => deleteStructuredGuide(guide.id), "ลบ Guide ถาวรแล้ว")}>ยืนยันลบถาวร</AlertDialogAction></AlertDialogFooter></AlertDialogContent></AlertDialog></div></CardAction></CardHeader></Card>)}</div></section> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+5
-75
@@ -431,23 +431,9 @@ export const outboxEvents = pgTable(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const catalogVersions = catalogSchema.table(
|
|
||||||
"version",
|
|
||||||
{
|
|
||||||
version: varchar("version", { length: 32 }).primaryKey(),
|
|
||||||
active: boolean("active").default(false).notNull(),
|
|
||||||
sourceTimestamp: bigint("source_timestamp", { mode: "number" }),
|
|
||||||
syncedAt: timestamp("synced_at", { withTimezone: true }).defaultNow().notNull(),
|
|
||||||
},
|
|
||||||
(table) => [uniqueIndex("catalog_active_unique").on(table.active).where(sql`${table.active} = true`)],
|
|
||||||
);
|
|
||||||
|
|
||||||
export const catalogCharacters = catalogSchema.table(
|
export const catalogCharacters = catalogSchema.table(
|
||||||
"character",
|
"character",
|
||||||
{
|
{
|
||||||
version: varchar("version", { length: 32 })
|
|
||||||
.notNull()
|
|
||||||
.references(() => catalogVersions.version, { onDelete: "cascade" }),
|
|
||||||
key: varchar("key", { length: 64 }).notNull(),
|
key: varchar("key", { length: 64 }).notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
element: varchar("element", { length: 48 }),
|
element: varchar("element", { length: 48 }),
|
||||||
@@ -460,45 +446,15 @@ export const catalogCharacters = catalogSchema.table(
|
|||||||
constellations: jsonb("constellations").$type<CatalogConstellation[]>().default([]).notNull(),
|
constellations: jsonb("constellations").$type<CatalogConstellation[]>().default([]).notNull(),
|
||||||
talents: jsonb("talents").$type<CatalogTalent[]>().default([]).notNull(),
|
talents: jsonb("talents").$type<CatalogTalent[]>().default([]).notNull(),
|
||||||
},
|
},
|
||||||
(table) => [
|
|
||||||
primaryKey({ columns: [table.version, table.key] }),
|
|
||||||
index("catalog_character_name_idx").on(table.version, table.name),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
export const catalogCharacterCurrent = catalogSchema.table(
|
|
||||||
"character_current",
|
|
||||||
{
|
|
||||||
key: varchar("key", { length: 64 }).notNull(),
|
|
||||||
name: text("name").notNull(),
|
|
||||||
element: varchar("element", { length: 48 }),
|
|
||||||
rarity: integer("rarity"),
|
|
||||||
weaponType: varchar("weapon_type", { length: 64 }),
|
|
||||||
imageKey: text("image_key").notNull(),
|
|
||||||
elementIconKey: text("element_icon_key"),
|
|
||||||
splashImageKey: text("splash_image_key"),
|
|
||||||
nameCardImageKey: text("name_card_image_key"),
|
|
||||||
constellations: jsonb("constellations")
|
|
||||||
.$type<CatalogConstellation[]>()
|
|
||||||
.default([])
|
|
||||||
.notNull(),
|
|
||||||
talents: jsonb("talents")
|
|
||||||
.$type<CatalogTalent[]>()
|
|
||||||
.default([])
|
|
||||||
.notNull(),
|
|
||||||
},
|
|
||||||
(table) => [
|
(table) => [
|
||||||
primaryKey({ columns: [table.key] }),
|
primaryKey({ columns: [table.key] }),
|
||||||
index("catalog_character_current_name_idx").on(table.name),
|
index("catalog_character_name_idx").on(table.name),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const catalogWeapons = catalogSchema.table(
|
export const catalogWeapons = catalogSchema.table(
|
||||||
"weapon",
|
"weapon",
|
||||||
{
|
{
|
||||||
version: varchar("version", { length: 32 })
|
|
||||||
.notNull()
|
|
||||||
.references(() => catalogVersions.version, { onDelete: "cascade" }),
|
|
||||||
key: varchar("key", { length: 64 }).notNull(),
|
key: varchar("key", { length: 64 }).notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
rarity: integer("rarity"),
|
rarity: integer("rarity"),
|
||||||
@@ -506,53 +462,30 @@ export const catalogWeapons = catalogSchema.table(
|
|||||||
imageKey: text("image_key").notNull(),
|
imageKey: text("image_key").notNull(),
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
primaryKey({ columns: [table.version, table.key] }),
|
primaryKey({ columns: [table.key] }),
|
||||||
index("catalog_weapon_name_idx").on(table.version, table.name),
|
index("catalog_weapon_name_idx").on(table.name),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const catalogArtifacts = catalogSchema.table(
|
export const catalogArtifacts = catalogSchema.table(
|
||||||
"artifact",
|
"artifact",
|
||||||
{
|
{
|
||||||
version: varchar("version", { length: 32 })
|
|
||||||
.notNull()
|
|
||||||
.references(() => catalogVersions.version, { onDelete: "cascade" }),
|
|
||||||
key: varchar("key", { length: 64 }).notNull(),
|
key: varchar("key", { length: 64 }).notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
rarity: integer("rarity"),
|
rarity: integer("rarity"),
|
||||||
imageKey: text("image_key").notNull(),
|
imageKey: text("image_key").notNull(),
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
primaryKey({ columns: [table.version, table.key] }),
|
primaryKey({ columns: [table.key] }),
|
||||||
index("catalog_artifact_name_idx").on(table.version, table.name),
|
index("catalog_artifact_name_idx").on(table.name),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const catalogSyncJobs = catalogSchema.table(
|
|
||||||
"sync_job",
|
|
||||||
{
|
|
||||||
id: uuid("id").defaultRandom().primaryKey(),
|
|
||||||
status: varchar("status", { length: 24 }).default("pending").notNull(),
|
|
||||||
version: varchar("version", { length: 32 }),
|
|
||||||
progress: integer("progress").default(0).notNull(),
|
|
||||||
total: integer("total").default(0).notNull(),
|
|
||||||
error: text("error"),
|
|
||||||
triggeredById: text("triggered_by_id").references(() => users.id, { onDelete: "set null" }),
|
|
||||||
startedAt: timestamp("started_at", { withTimezone: true }),
|
|
||||||
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
||||||
...timestamps,
|
|
||||||
},
|
|
||||||
(table) => [index("catalog_sync_status_idx").on(table.status, table.createdAt)],
|
|
||||||
);
|
|
||||||
|
|
||||||
export const guides = guidesSchema.table(
|
export const guides = guidesSchema.table(
|
||||||
"guide",
|
"guide",
|
||||||
{
|
{
|
||||||
id: uuid("id").defaultRandom().primaryKey(),
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
characterKey: varchar("character_key", { length: 64 }).notNull(),
|
characterKey: varchar("character_key", { length: 64 }).notNull(),
|
||||||
catalogVersion: varchar("catalog_version", { length: 32 })
|
|
||||||
.notNull()
|
|
||||||
.references(() => catalogVersions.version, { onDelete: "restrict" }),
|
|
||||||
slug: varchar("slug", { length: 120 }).notNull(),
|
slug: varchar("slug", { length: 120 }).notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
overview: text("overview").notNull(),
|
overview: text("overview").notNull(),
|
||||||
@@ -593,7 +526,6 @@ export const weaponRecommendations = guidesSchema.table(
|
|||||||
{
|
{
|
||||||
id: uuid("id").defaultRandom().primaryKey(),
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
|
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
|
||||||
catalogVersion: varchar("catalog_version", { length: 32 }).notNull(),
|
|
||||||
weaponKey: varchar("weapon_key", { length: 64 }).notNull(),
|
weaponKey: varchar("weapon_key", { length: 64 }).notNull(),
|
||||||
refinement: integer("refinement").notNull(),
|
refinement: integer("refinement").notNull(),
|
||||||
overallPercent: numeric("overall_percent", { precision: 8, scale: 2 }),
|
overallPercent: numeric("overall_percent", { precision: 8, scale: 2 }),
|
||||||
@@ -620,7 +552,6 @@ export const artifactRecommendations = guidesSchema.table(
|
|||||||
{
|
{
|
||||||
id: uuid("id").defaultRandom().primaryKey(),
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
groupId: uuid("group_id").notNull().references(() => artifactGroups.id, { onDelete: "cascade" }),
|
groupId: uuid("group_id").notNull().references(() => artifactGroups.id, { onDelete: "cascade" }),
|
||||||
catalogVersion: varchar("catalog_version", { length: 32 }).notNull(),
|
|
||||||
firstArtifactKey: varchar("first_artifact_key", { length: 64 }).notNull(),
|
firstArtifactKey: varchar("first_artifact_key", { length: 64 }).notNull(),
|
||||||
secondArtifactKey: varchar("second_artifact_key", { length: 64 }),
|
secondArtifactKey: varchar("second_artifact_key", { length: 64 }),
|
||||||
percent: numeric("percent", { precision: 8, scale: 2 }).notNull(),
|
percent: numeric("percent", { precision: 8, scale: 2 }).notNull(),
|
||||||
@@ -672,7 +603,6 @@ export const teamMembers = guidesSchema.table(
|
|||||||
{
|
{
|
||||||
teamId: uuid("team_id").notNull().references(() => teamCompositions.id, { onDelete: "cascade" }),
|
teamId: uuid("team_id").notNull().references(() => teamCompositions.id, { onDelete: "cascade" }),
|
||||||
position: integer("position").notNull(),
|
position: integer("position").notNull(),
|
||||||
catalogVersion: varchar("catalog_version", { length: 32 }).notNull(),
|
|
||||||
characterKey: varchar("character_key", { length: 64 }).notNull(),
|
characterKey: varchar("character_key", { length: 64 }).notNull(),
|
||||||
weaponKey: varchar("weapon_key", { length: 64 }),
|
weaponKey: varchar("weapon_key", { length: 64 }),
|
||||||
artifactKey: varchar("artifact_key", { length: 64 }),
|
artifactKey: varchar("artifact_key", { length: 64 }),
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE "catalog"."character_current" CASCADE;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
ALTER TABLE "catalog"."artifact" DROP CONSTRAINT IF EXISTS "artifact_version_version_version_fk";--> statement-breakpoint
|
||||||
|
ALTER TABLE "catalog"."character" DROP CONSTRAINT IF EXISTS "character_version_version_version_fk";--> statement-breakpoint
|
||||||
|
ALTER TABLE "catalog"."weapon" DROP CONSTRAINT IF EXISTS "weapon_version_version_version_fk";--> statement-breakpoint
|
||||||
|
ALTER TABLE "guides"."guide" DROP CONSTRAINT IF EXISTS "guide_catalog_version_version_version_fk";--> statement-breakpoint
|
||||||
|
DELETE FROM "catalog"."artifact" WHERE "version" IS DISTINCT FROM (SELECT "version" FROM "catalog"."version" WHERE "active" = true LIMIT 1);--> statement-breakpoint
|
||||||
|
DELETE FROM "catalog"."character" WHERE "version" IS DISTINCT FROM (SELECT "version" FROM "catalog"."version" WHERE "active" = true LIMIT 1);--> statement-breakpoint
|
||||||
|
DELETE FROM "catalog"."weapon" WHERE "version" IS DISTINCT FROM (SELECT "version" FROM "catalog"."version" WHERE "active" = true LIMIT 1);--> statement-breakpoint
|
||||||
|
DROP INDEX IF EXISTS "catalog"."catalog_artifact_name_idx";--> statement-breakpoint
|
||||||
|
DROP INDEX IF EXISTS "catalog"."catalog_character_name_idx";--> statement-breakpoint
|
||||||
|
DROP INDEX IF EXISTS "catalog"."catalog_weapon_name_idx";--> statement-breakpoint
|
||||||
|
ALTER TABLE "catalog"."artifact" DROP CONSTRAINT IF EXISTS "artifact_version_key_pk";--> statement-breakpoint
|
||||||
|
ALTER TABLE "catalog"."character" DROP CONSTRAINT IF EXISTS "character_version_key_pk";--> statement-breakpoint
|
||||||
|
ALTER TABLE "catalog"."weapon" DROP CONSTRAINT IF EXISTS "weapon_version_key_pk";--> statement-breakpoint
|
||||||
|
ALTER TABLE "catalog"."artifact" ADD CONSTRAINT "artifact_key_pk" PRIMARY KEY("key");--> statement-breakpoint
|
||||||
|
ALTER TABLE "catalog"."character" ADD CONSTRAINT "character_key_pk" PRIMARY KEY("key");--> statement-breakpoint
|
||||||
|
ALTER TABLE "catalog"."weapon" ADD CONSTRAINT "weapon_key_pk" PRIMARY KEY("key");--> statement-breakpoint
|
||||||
|
ALTER TABLE "guides"."artifact_recommendation" DROP COLUMN "catalog_version";--> statement-breakpoint
|
||||||
|
ALTER TABLE "catalog"."artifact" DROP COLUMN "version";--> statement-breakpoint
|
||||||
|
ALTER TABLE "catalog"."character" DROP COLUMN "version";--> statement-breakpoint
|
||||||
|
ALTER TABLE "catalog"."weapon" DROP COLUMN "version";--> statement-breakpoint
|
||||||
|
ALTER TABLE "guides"."guide" DROP COLUMN "catalog_version";--> statement-breakpoint
|
||||||
|
ALTER TABLE "guides"."team_member" DROP COLUMN "catalog_version";--> statement-breakpoint
|
||||||
|
ALTER TABLE "guides"."weapon_recommendation" DROP COLUMN "catalog_version";--> statement-breakpoint
|
||||||
|
CREATE INDEX "catalog_artifact_name_idx" ON "catalog"."artifact" USING btree ("name");--> statement-breakpoint
|
||||||
|
CREATE INDEX "catalog_character_name_idx" ON "catalog"."character" USING btree ("name");--> statement-breakpoint
|
||||||
|
CREATE INDEX "catalog_weapon_name_idx" ON "catalog"."weapon" USING btree ("name");--> statement-breakpoint
|
||||||
|
DROP TABLE "catalog"."sync_job";--> statement-breakpoint
|
||||||
|
DROP TABLE "catalog"."version";
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -106,6 +106,20 @@
|
|||||||
"when": 1788190879635,
|
"when": 1788190879635,
|
||||||
"tag": "0014_redundant_black_crow",
|
"tag": "0014_redundant_black_crow",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 15,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1788192255590,
|
||||||
|
"tag": "0015_steady_switch",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1788192979231,
|
||||||
|
"tag": "0016_uneven_infant_terrible",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+21
-130
@@ -1,4 +1,3 @@
|
|||||||
import { asc, eq } from "drizzle-orm";
|
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
@@ -6,10 +5,7 @@ import * as z from "zod";
|
|||||||
import { getDb } from "@/db/client";
|
import { getDb } from "@/db/client";
|
||||||
import {
|
import {
|
||||||
catalogArtifacts,
|
catalogArtifacts,
|
||||||
catalogCharacterCurrent,
|
|
||||||
catalogCharacters,
|
catalogCharacters,
|
||||||
catalogSyncJobs,
|
|
||||||
catalogVersions,
|
|
||||||
catalogWeapons,
|
catalogWeapons,
|
||||||
} from "@/db/schema";
|
} from "@/db/schema";
|
||||||
import {
|
import {
|
||||||
@@ -63,7 +59,6 @@ const artifactSchema = z.record(
|
|||||||
);
|
);
|
||||||
|
|
||||||
interface SyncedCharacter {
|
interface SyncedCharacter {
|
||||||
version: string;
|
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
element: string | null;
|
element: string | null;
|
||||||
@@ -138,7 +133,6 @@ async function mapConcurrent<T, R>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function uploadAssets(
|
async function uploadAssets(
|
||||||
jobId: string,
|
|
||||||
assets: Array<{
|
assets: Array<{
|
||||||
source?: string;
|
source?: string;
|
||||||
body?: Uint8Array;
|
body?: Uint8Array;
|
||||||
@@ -148,12 +142,7 @@ async function uploadAssets(
|
|||||||
): Promise<Set<string>> {
|
): Promise<Set<string>> {
|
||||||
const storage = await getMediaStorage();
|
const storage = await getMediaStorage();
|
||||||
let next = 0;
|
let next = 0;
|
||||||
let completed = 0;
|
|
||||||
const missing = new Set<string>();
|
const missing = new Set<string>();
|
||||||
await getDb()
|
|
||||||
.update(catalogSyncJobs)
|
|
||||||
.set({ total: assets.length, progress: 0 })
|
|
||||||
.where(eq(catalogSyncJobs.id, jobId));
|
|
||||||
async function worker() {
|
async function worker() {
|
||||||
while (next < assets.length) {
|
while (next < assets.length) {
|
||||||
const asset = assets[next++];
|
const asset = assets[next++];
|
||||||
@@ -178,13 +167,6 @@ async function uploadAssets(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
completed += 1;
|
|
||||||
if (completed % 10 === 0 || completed === assets.length) {
|
|
||||||
await getDb()
|
|
||||||
.update(catalogSyncJobs)
|
|
||||||
.set({ progress: completed })
|
|
||||||
.where(eq(catalogSyncJobs.id, jobId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -193,40 +175,19 @@ async function uploadAssets(
|
|||||||
return missing;
|
return missing;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function claimCatalogSyncJob() {
|
export async function processCatalogSync(): Promise<void> {
|
||||||
const [pending] = await getDb()
|
const versionInfo = versionSchema.parse(await getJson(VERSION_URL));
|
||||||
.select()
|
const version = versionInfo.version;
|
||||||
.from(catalogSyncJobs)
|
const [charactersRaw, weaponsRaw, artifactsRaw] = await Promise.all([
|
||||||
.where(eq(catalogSyncJobs.status, "pending"))
|
|
||||||
.orderBy(asc(catalogSyncJobs.createdAt))
|
|
||||||
.limit(1);
|
|
||||||
if (!pending) return null;
|
|
||||||
const [claimed] = await getDb()
|
|
||||||
.update(catalogSyncJobs)
|
|
||||||
.set({ status: "processing", startedAt: new Date(), error: null })
|
|
||||||
.where(eq(catalogSyncJobs.id, pending.id))
|
|
||||||
.returning();
|
|
||||||
return claimed ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processCatalogSyncJob(jobId: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
const versionInfo = versionSchema.parse(await getJson(VERSION_URL));
|
|
||||||
const version = versionInfo.version;
|
|
||||||
await getDb()
|
|
||||||
.update(catalogSyncJobs)
|
|
||||||
.set({ version })
|
|
||||||
.where(eq(catalogSyncJobs.id, jobId));
|
|
||||||
const [charactersRaw, weaponsRaw, artifactsRaw] = await Promise.all([
|
|
||||||
getJson(`${API_ROOT}/${version}/charlist.json`),
|
getJson(`${API_ROOT}/${version}/charlist.json`),
|
||||||
getJson(`${API_ROOT}/${version}/weaponlist.json`),
|
getJson(`${API_ROOT}/${version}/weaponlist.json`),
|
||||||
getJson(`${API_ROOT}/${version}/artifactlist.json`),
|
getJson(`${API_ROOT}/${version}/artifactlist.json`),
|
||||||
]);
|
]);
|
||||||
const characterData = characterSchema.parse(charactersRaw);
|
const characterData = characterSchema.parse(charactersRaw);
|
||||||
const weaponData = weaponSchema.parse(weaponsRaw);
|
const weaponData = weaponSchema.parse(weaponsRaw);
|
||||||
const artifactData = artifactSchema.parse(artifactsRaw);
|
const artifactData = artifactSchema.parse(artifactsRaw);
|
||||||
|
|
||||||
const remoteCharacters: SyncedCharacter[] = await mapConcurrent(
|
const remoteCharacters: SyncedCharacter[] = await mapConcurrent(
|
||||||
Object.entries(characterData).filter(
|
Object.entries(characterData).filter(
|
||||||
([, value]) =>
|
([, value]) =>
|
||||||
value.enName &&
|
value.enName &&
|
||||||
@@ -246,7 +207,6 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
|
|||||||
const nameCardImage = nameCardImageName(value.CardImg);
|
const nameCardImage = nameCardImageName(value.CardImg);
|
||||||
const element = detail?.info?.element ?? value.element ?? null;
|
const element = detail?.info?.element ?? value.element ?? null;
|
||||||
return {
|
return {
|
||||||
version,
|
|
||||||
key,
|
key,
|
||||||
name: value.enName,
|
name: value.enName,
|
||||||
element,
|
element,
|
||||||
@@ -276,7 +236,7 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
|
|||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const customCharacters: SyncedCharacter[] = await mapConcurrent(
|
const customCharacters: SyncedCharacter[] = await mapConcurrent(
|
||||||
[...customTravelers],
|
[...customTravelers],
|
||||||
async (traveler) => {
|
async (traveler) => {
|
||||||
@@ -286,7 +246,6 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
version,
|
|
||||||
key: traveler.key,
|
key: traveler.key,
|
||||||
name: traveler.name,
|
name: traveler.name,
|
||||||
element: traveler.element,
|
element: traveler.element,
|
||||||
@@ -318,12 +277,11 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
|
|||||||
value.enName && value.enName !== "???" && value.weaponIcon,
|
value.enName && value.enName !== "???" && value.weaponIcon,
|
||||||
)
|
)
|
||||||
.map(([key, value]) => ({
|
.map(([key, value]) => ({
|
||||||
version,
|
|
||||||
key,
|
key,
|
||||||
name: value.enName,
|
name: value.enName,
|
||||||
rarity: rarity(value.qualityType),
|
rarity: rarity(value.qualityType),
|
||||||
weaponType: value.weaponType ?? null,
|
weaponType: value.weaponType ?? null,
|
||||||
imageKey: `catalog/${version}/weapons/${webpName(value.weaponIcon)}`,
|
imageKey: `catalog/weapons/${webpName(value.weaponIcon)}`,
|
||||||
sourceImage: `${API_ROOT}/assets/weaponicon/${encodeURIComponent(webpName(value.weaponIcon))}`,
|
sourceImage: `${API_ROOT}/assets/weaponicon/${encodeURIComponent(webpName(value.weaponIcon))}`,
|
||||||
}));
|
}));
|
||||||
let artifacts = Object.entries(artifactData)
|
let artifacts = Object.entries(artifactData)
|
||||||
@@ -331,11 +289,10 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
|
|||||||
([, value]) => value.enName && value.enName !== "???" && value.setIcon,
|
([, value]) => value.enName && value.enName !== "???" && value.setIcon,
|
||||||
)
|
)
|
||||||
.map(([key, value]) => ({
|
.map(([key, value]) => ({
|
||||||
version,
|
|
||||||
key,
|
key,
|
||||||
name: value.enName,
|
name: value.enName,
|
||||||
rarity: rarity(value.qualityType),
|
rarity: rarity(value.qualityType),
|
||||||
imageKey: `catalog/${version}/artifacts/${webpName(value.setIcon)}`,
|
imageKey: `catalog/artifacts/${webpName(value.setIcon)}`,
|
||||||
sourceImage: `${API_ROOT}/assets/artifacts/${encodeURIComponent(webpName(value.setIcon))}`,
|
sourceImage: `${API_ROOT}/assets/artifacts/${encodeURIComponent(webpName(value.setIcon))}`,
|
||||||
}));
|
}));
|
||||||
const uniqueAssets = new Map<
|
const uniqueAssets = new Map<
|
||||||
@@ -398,7 +355,7 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const missingAssets = await uploadAssets(jobId, [...uniqueAssets.values()]);
|
const missingAssets = await uploadAssets([...uniqueAssets.values()]);
|
||||||
characters = characters.filter((item) => !missingAssets.has(item.imageKey));
|
characters = characters.filter((item) => !missingAssets.has(item.imageKey));
|
||||||
characters = characters.map((item) => ({
|
characters = characters.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
@@ -454,10 +411,9 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
|
|||||||
{ type: "application/json", acl: "public-read" },
|
{ type: "application/json", acl: "public-read" },
|
||||||
),
|
),
|
||||||
storage.write(
|
storage.write(
|
||||||
`catalog/${version}/weapons.json`,
|
"catalog/weapons.json",
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
weapons.map((item) => ({
|
weapons.map((item) => ({
|
||||||
version: item.version,
|
|
||||||
key: item.key,
|
key: item.key,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
rarity: item.rarity,
|
rarity: item.rarity,
|
||||||
@@ -468,10 +424,9 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
|
|||||||
{ type: "application/json", acl: "public-read" },
|
{ type: "application/json", acl: "public-read" },
|
||||||
),
|
),
|
||||||
storage.write(
|
storage.write(
|
||||||
`catalog/${version}/artifacts.json`,
|
"catalog/artifacts.json",
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
artifacts.map((item) => ({
|
artifacts.map((item) => ({
|
||||||
version: item.version,
|
|
||||||
key: item.key,
|
key: item.key,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
rarity: item.rarity,
|
rarity: item.rarity,
|
||||||
@@ -483,32 +438,14 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await getDb().transaction(async (tx) => {
|
await getDb().transaction(async (tx) => {
|
||||||
await tx
|
await tx.delete(catalogCharacters);
|
||||||
.insert(catalogVersions)
|
await tx.delete(catalogWeapons);
|
||||||
.values({ version, sourceTimestamp: versionInfo.timestamp ?? null })
|
await tx.delete(catalogArtifacts);
|
||||||
.onConflictDoUpdate({
|
|
||||||
target: catalogVersions.version,
|
|
||||||
set: {
|
|
||||||
sourceTimestamp: versionInfo.timestamp ?? null,
|
|
||||||
syncedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await tx
|
|
||||||
.delete(catalogCharacters)
|
|
||||||
.where(eq(catalogCharacters.version, version));
|
|
||||||
await tx.delete(catalogCharacterCurrent);
|
|
||||||
await tx
|
|
||||||
.delete(catalogWeapons)
|
|
||||||
.where(eq(catalogWeapons.version, version));
|
|
||||||
await tx
|
|
||||||
.delete(catalogArtifacts)
|
|
||||||
.where(eq(catalogArtifacts.version, version));
|
|
||||||
if (characters.length)
|
if (characters.length)
|
||||||
await tx
|
await tx
|
||||||
.insert(catalogCharacters)
|
.insert(catalogCharacters)
|
||||||
.values(
|
.values(
|
||||||
characters.map((item) => ({
|
characters.map((item) => ({
|
||||||
version: item.version,
|
|
||||||
key: item.key,
|
key: item.key,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
element: item.element,
|
element: item.element,
|
||||||
@@ -522,28 +459,11 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
|
|||||||
talents: item.talents,
|
talents: item.talents,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
if (characters.length)
|
|
||||||
await tx.insert(catalogCharacterCurrent).values(
|
|
||||||
characters.map((item) => ({
|
|
||||||
key: item.key,
|
|
||||||
name: item.name,
|
|
||||||
element: item.element,
|
|
||||||
rarity: item.rarity,
|
|
||||||
weaponType: item.weaponType,
|
|
||||||
imageKey: item.imageKey,
|
|
||||||
elementIconKey: item.elementIconKey,
|
|
||||||
splashImageKey: item.splashImageKey,
|
|
||||||
nameCardImageKey: item.nameCardImageKey,
|
|
||||||
constellations: item.constellations,
|
|
||||||
talents: item.talents,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
if (weapons.length)
|
if (weapons.length)
|
||||||
await tx
|
await tx
|
||||||
.insert(catalogWeapons)
|
.insert(catalogWeapons)
|
||||||
.values(
|
.values(
|
||||||
weapons.map((item) => ({
|
weapons.map((item) => ({
|
||||||
version: item.version,
|
|
||||||
key: item.key,
|
key: item.key,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
rarity: item.rarity,
|
rarity: item.rarity,
|
||||||
@@ -556,44 +476,15 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
|
|||||||
.insert(catalogArtifacts)
|
.insert(catalogArtifacts)
|
||||||
.values(
|
.values(
|
||||||
artifacts.map((item) => ({
|
artifacts.map((item) => ({
|
||||||
version: item.version,
|
|
||||||
key: item.key,
|
key: item.key,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
rarity: item.rarity,
|
rarity: item.rarity,
|
||||||
imageKey: item.imageKey,
|
imageKey: item.imageKey,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
await tx.update(catalogVersions).set({ active: false });
|
|
||||||
await tx
|
|
||||||
.update(catalogVersions)
|
|
||||||
.set({ active: true, syncedAt: new Date() })
|
|
||||||
.where(eq(catalogVersions.version, version));
|
|
||||||
await tx
|
|
||||||
.update(catalogSyncJobs)
|
|
||||||
.set({
|
|
||||||
status: "completed",
|
|
||||||
completedAt: new Date(),
|
|
||||||
progress: uniqueAssets.size,
|
|
||||||
total: uniqueAssets.size,
|
|
||||||
})
|
|
||||||
.where(eq(catalogSyncJobs.id, jobId));
|
|
||||||
});
|
});
|
||||||
await storage.write("catalog/current.json", JSON.stringify({ version }), {
|
await storage.write("catalog/current.json", JSON.stringify({ version }), {
|
||||||
type: "application/json",
|
type: "application/json",
|
||||||
acl: "public-read",
|
acl: "public-read",
|
||||||
});
|
});
|
||||||
} catch (cause) {
|
|
||||||
await getDb()
|
|
||||||
.update(catalogSyncJobs)
|
|
||||||
.set({
|
|
||||||
status: "failed",
|
|
||||||
completedAt: new Date(),
|
|
||||||
error:
|
|
||||||
cause instanceof Error
|
|
||||||
? cause.message.slice(0, 4_000)
|
|
||||||
: "Unknown sync failure",
|
|
||||||
})
|
|
||||||
.where(eq(catalogSyncJobs.id, jobId));
|
|
||||||
throw cause;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-19
@@ -8,9 +8,7 @@ import {
|
|||||||
artifactProfiles,
|
artifactProfiles,
|
||||||
artifactRecommendations,
|
artifactRecommendations,
|
||||||
catalogArtifacts,
|
catalogArtifacts,
|
||||||
catalogCharacterCurrent,
|
catalogCharacters,
|
||||||
catalogSyncJobs,
|
|
||||||
catalogVersions,
|
|
||||||
catalogWeapons,
|
catalogWeapons,
|
||||||
constellationRecommendations,
|
constellationRecommendations,
|
||||||
extraSectionRows,
|
extraSectionRows,
|
||||||
@@ -33,6 +31,7 @@ import {
|
|||||||
updateGuideSchema,
|
updateGuideSchema,
|
||||||
weaponSectionSchema,
|
weaponSectionSchema,
|
||||||
} from "@/lib/guides/schemas";
|
} from "@/lib/guides/schemas";
|
||||||
|
import { processCatalogSync } from "@/lib/catalog/sync";
|
||||||
|
|
||||||
const DEFAULT_SECTIONS = [
|
const DEFAULT_SECTIONS = [
|
||||||
{ kind: "weapon", slug: "weapon", title: "Weapons", sortOrder: 0 },
|
{ kind: "weapon", slug: "weapon", title: "Weapons", sortOrder: 0 },
|
||||||
@@ -52,10 +51,8 @@ async function emitGuideEvents(tx: Parameters<Parameters<ReturnType<typeof getDb
|
|||||||
export async function createGuide(input: unknown) {
|
export async function createGuide(input: unknown) {
|
||||||
const parsed = createGuideSchema.parse(input);
|
const parsed = createGuideSchema.parse(input);
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const [version] = await db.select().from(catalogVersions).where(eq(catalogVersions.active, true)).limit(1);
|
const [character] = await db.select().from(catalogCharacters).where(eq(catalogCharacters.key, parsed.characterKey)).limit(1);
|
||||||
if (!version) throw new Error("กรุณา Sync ข้อมูล Lunaris ก่อนสร้างคู่มือ");
|
if (!character) throw new Error("ไม่พบตัวละครใน Catalog");
|
||||||
const [character] = await db.select().from(catalogCharacterCurrent).where(eq(catalogCharacterCurrent.key, parsed.characterKey)).limit(1);
|
|
||||||
if (!character) throw new Error("ไม่พบตัวละครใน Catalog เวอร์ชันปัจจุบัน");
|
|
||||||
const [cover] = await db.select().from(media).where(and(eq(media.id, parsed.coverMediaId), eq(media.status, "ready"))).limit(1);
|
const [cover] = await db.select().from(media).where(and(eq(media.id, parsed.coverMediaId), eq(media.status, "ready"))).limit(1);
|
||||||
if (!cover) throw new Error("กรุณาอัปโหลดภาพ Guide ให้สำเร็จ");
|
if (!cover) throw new Error("กรุณาอัปโหลดภาพ Guide ให้สำเร็จ");
|
||||||
const baseSlug = slugFromCharacterName(character.name, character.key);
|
const baseSlug = slugFromCharacterName(character.name, character.key);
|
||||||
@@ -65,7 +62,6 @@ export async function createGuide(input: unknown) {
|
|||||||
return db.transaction(async (tx) => {
|
return db.transaction(async (tx) => {
|
||||||
const [guide] = await tx.insert(guides).values({
|
const [guide] = await tx.insert(guides).values({
|
||||||
characterKey: character.key,
|
characterKey: character.key,
|
||||||
catalogVersion: version.version,
|
|
||||||
slug,
|
slug,
|
||||||
name: character.name,
|
name: character.name,
|
||||||
overview: parsed.overview,
|
overview: parsed.overview,
|
||||||
@@ -97,7 +93,7 @@ export async function saveWeapons(guideId: string, input: unknown) {
|
|||||||
return getDb().transaction(async (tx) => {
|
return getDb().transaction(async (tx) => {
|
||||||
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion);
|
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion);
|
||||||
await tx.delete(weaponRecommendations).where(eq(weaponRecommendations.guideId, guideId));
|
await tx.delete(weaponRecommendations).where(eq(weaponRecommendations.guideId, guideId));
|
||||||
if (data.rows.length) await tx.insert(weaponRecommendations).values(data.rows.map((row, sortOrder) => ({ ...row, overallPercent: row.overallPercent?.toString() ?? null, personalPercent: row.personalPercent?.toString() ?? null, guideId, catalogVersion: guide.catalogVersion, sortOrder })));
|
if (data.rows.length) await tx.insert(weaponRecommendations).values(data.rows.map((row, sortOrder) => ({ ...row, overallPercent: row.overallPercent?.toString() ?? null, personalPercent: row.personalPercent?.toString() ?? null, guideId, sortOrder })));
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -128,7 +124,7 @@ export async function saveArtifacts(guideId: string, input: unknown) {
|
|||||||
await tx.delete(artifactGroups).where(eq(artifactGroups.guideId, guideId));
|
await tx.delete(artifactGroups).where(eq(artifactGroups.guideId, guideId));
|
||||||
for (const [sortOrder, group] of data.groups.entries()) {
|
for (const [sortOrder, group] of data.groups.entries()) {
|
||||||
const [created] = await tx.insert(artifactGroups).values({ guideId, name: group.name, sortOrder }).returning();
|
const [created] = await tx.insert(artifactGroups).values({ guideId, name: group.name, sortOrder }).returning();
|
||||||
if (group.rows.length) await tx.insert(artifactRecommendations).values(group.rows.map((row, rowOrder) => ({ ...row, percent: row.percent.toString(), groupId: created.id, catalogVersion: guide.catalogVersion, sortOrder: rowOrder })));
|
if (group.rows.length) await tx.insert(artifactRecommendations).values(group.rows.map((row, rowOrder) => ({ ...row, percent: row.percent.toString(), groupId: created.id, sortOrder: rowOrder })));
|
||||||
}
|
}
|
||||||
await tx.insert(artifactProfiles).values({ guideId, sands: data.sands, goblet: data.goblet, circlet: data.circlet, substats: data.substats, targets: data.targets, talents: data.talents, note: data.note }).onConflictDoUpdate({ target: artifactProfiles.guideId, set: { sands: data.sands, goblet: data.goblet, circlet: data.circlet, substats: data.substats, targets: data.targets, talents: data.talents, note: data.note } });
|
await tx.insert(artifactProfiles).values({ guideId, sands: data.sands, goblet: data.goblet, circlet: data.circlet, substats: data.substats, targets: data.targets, talents: data.talents, note: data.note }).onConflictDoUpdate({ target: artifactProfiles.guideId, set: { sands: data.sands, goblet: data.goblet, circlet: data.circlet, substats: data.substats, targets: data.targets, talents: data.talents, note: data.note } });
|
||||||
return updated;
|
return updated;
|
||||||
@@ -154,9 +150,9 @@ export async function saveTeams(guideId: string, input: unknown) {
|
|||||||
const weaponKeys = [...new Set(members.map((member) => member.weaponKey).filter((key): key is string => Boolean(key)))];
|
const weaponKeys = [...new Set(members.map((member) => member.weaponKey).filter((key): key is string => Boolean(key)))];
|
||||||
const artifactKeys = [...new Set(members.map((member) => member.artifactKey).filter((key): key is string => Boolean(key)))];
|
const artifactKeys = [...new Set(members.map((member) => member.artifactKey).filter((key): key is string => Boolean(key)))];
|
||||||
const [characters, weapons, artifacts] = await Promise.all([
|
const [characters, weapons, artifacts] = await Promise.all([
|
||||||
characterKeys.length ? getDb().select().from(catalogCharacterCurrent).where(inArray(catalogCharacterCurrent.key, characterKeys)) : [],
|
characterKeys.length ? getDb().select().from(catalogCharacters).where(inArray(catalogCharacters.key, characterKeys)) : [],
|
||||||
weaponKeys.length ? getDb().select().from(catalogWeapons).where(and(eq(catalogWeapons.version, guide.catalogVersion), inArray(catalogWeapons.key, weaponKeys))) : [],
|
weaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, weaponKeys)) : [],
|
||||||
artifactKeys.length ? getDb().select().from(catalogArtifacts).where(and(eq(catalogArtifacts.version, guide.catalogVersion), inArray(catalogArtifacts.key, artifactKeys))) : [],
|
artifactKeys.length ? getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, artifactKeys)) : [],
|
||||||
]);
|
]);
|
||||||
if (characters.length !== characterKeys.length) throw new Error("ไม่พบตัวละครบางรายการใน Catalog");
|
if (characters.length !== characterKeys.length) throw new Error("ไม่พบตัวละครบางรายการใน Catalog");
|
||||||
if (weapons.length !== weaponKeys.length) throw new Error("ไม่พบอาวุธบางรายการใน Catalog");
|
if (weapons.length !== weaponKeys.length) throw new Error("ไม่พบอาวุธบางรายการใน Catalog");
|
||||||
@@ -173,7 +169,7 @@ export async function saveTeams(guideId: string, input: unknown) {
|
|||||||
await tx.delete(teamCompositions).where(eq(teamCompositions.guideId, guideId));
|
await tx.delete(teamCompositions).where(eq(teamCompositions.guideId, guideId));
|
||||||
for (const [sortOrder, team] of data.teams.entries()) {
|
for (const [sortOrder, team] of data.teams.entries()) {
|
||||||
const [created] = await tx.insert(teamCompositions).values({ guideId, title: team.title, rotationSeconds: team.rotationSeconds.toString(), combo: team.combo, sortOrder }).returning();
|
const [created] = await tx.insert(teamCompositions).values({ guideId, title: team.title, rotationSeconds: team.rotationSeconds.toString(), combo: team.combo, sortOrder }).returning();
|
||||||
await tx.insert(teamMembers).values(team.members.map((member, position) => ({ ...member, teamId: created.id, position, catalogVersion: guide.catalogVersion, damage: member.damage.toString() })));
|
await tx.insert(teamMembers).values(team.members.map((member, position) => ({ ...member, teamId: created.id, position, damage: member.damage.toString() })));
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
@@ -222,9 +218,7 @@ export async function permanentlyDeleteGuide(guideId: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function enqueueCatalogSync(userId: string) {
|
export async function runCatalogSync() {
|
||||||
const [running] = await getDb().select().from(catalogSyncJobs).where(sql`${catalogSyncJobs.status} in ('pending', 'processing')`).limit(1);
|
await processCatalogSync();
|
||||||
if (running) return running;
|
return { status: "saved" as const };
|
||||||
const [job] = await getDb().insert(catalogSyncJobs).values({ triggeredById: userId }).returning();
|
|
||||||
return job;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-42
@@ -1,6 +1,6 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
|
|
||||||
import { and, asc, desc, eq, inArray, isNotNull, isNull } from "drizzle-orm";
|
import { and, asc, eq, inArray, isNotNull, isNull } from "drizzle-orm";
|
||||||
|
|
||||||
import { getDb } from "@/db";
|
import { getDb } from "@/db";
|
||||||
import {
|
import {
|
||||||
@@ -8,9 +8,7 @@ import {
|
|||||||
artifactProfiles,
|
artifactProfiles,
|
||||||
artifactRecommendations,
|
artifactRecommendations,
|
||||||
catalogArtifacts,
|
catalogArtifacts,
|
||||||
catalogCharacterCurrent,
|
catalogCharacters,
|
||||||
catalogSyncJobs,
|
|
||||||
catalogVersions,
|
|
||||||
catalogWeapons,
|
catalogWeapons,
|
||||||
constellationRecommendations,
|
constellationRecommendations,
|
||||||
extraSectionRows,
|
extraSectionRows,
|
||||||
@@ -23,11 +21,11 @@ import {
|
|||||||
} from "@/db/schema";
|
} from "@/db/schema";
|
||||||
import { publicMediaUrl } from "@/lib/media/storage";
|
import { publicMediaUrl } from "@/lib/media/storage";
|
||||||
|
|
||||||
function catalogCharacterWithUrls(item: typeof catalogCharacterCurrent.$inferSelect) {
|
function catalogCharacterWithUrls(item: typeof catalogCharacters.$inferSelect) {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
imageUrl: publicMediaUrl(item.imageKey),
|
imageUrl: publicMediaUrl(item.imageKey),
|
||||||
splashImageUrl: item.splashImageKey ? publicMediaUrl(item.splashImageKey) : null,
|
splashImageUrl: item.coopImageKey ? publicMediaUrl(item.coopImageKey) : null,
|
||||||
nameCardImageUrl: item.nameCardImageKey ? publicMediaUrl(item.nameCardImageKey) : null,
|
nameCardImageUrl: item.nameCardImageKey ? publicMediaUrl(item.nameCardImageKey) : null,
|
||||||
elementIconUrl: item.elementIconKey ? publicMediaUrl(item.elementIconKey) : null,
|
elementIconUrl: item.elementIconKey ? publicMediaUrl(item.elementIconKey) : null,
|
||||||
constellations: item.constellations.map((constellation) => ({
|
constellations: item.constellations.map((constellation) => ({
|
||||||
@@ -43,15 +41,13 @@ function catalogCharacterWithUrls(item: typeof catalogCharacterCurrent.$inferSel
|
|||||||
|
|
||||||
export async function getActiveCatalog() {
|
export async function getActiveCatalog() {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const [version] = await db.select().from(catalogVersions).where(eq(catalogVersions.active, true)).limit(1);
|
|
||||||
if (!version) return null;
|
|
||||||
const [characters, weapons, artifacts] = await Promise.all([
|
const [characters, weapons, artifacts] = await Promise.all([
|
||||||
db.select().from(catalogCharacterCurrent).orderBy(asc(catalogCharacterCurrent.name)),
|
db.select().from(catalogCharacters).orderBy(asc(catalogCharacters.name)),
|
||||||
db.select().from(catalogWeapons).where(eq(catalogWeapons.version, version.version)).orderBy(asc(catalogWeapons.name)),
|
db.select().from(catalogWeapons).orderBy(asc(catalogWeapons.name)),
|
||||||
db.select().from(catalogArtifacts).where(eq(catalogArtifacts.version, version.version)).orderBy(asc(catalogArtifacts.name)),
|
db.select().from(catalogArtifacts).orderBy(asc(catalogArtifacts.name)),
|
||||||
]);
|
]);
|
||||||
|
if (!characters.length) return null;
|
||||||
return {
|
return {
|
||||||
version: version.version,
|
|
||||||
characters: characters.map(catalogCharacterWithUrls),
|
characters: characters.map(catalogCharacterWithUrls),
|
||||||
weapons: weapons.map((item) => ({ ...item, imageUrl: publicMediaUrl(item.imageKey) })),
|
weapons: weapons.map((item) => ({ ...item, imageUrl: publicMediaUrl(item.imageKey) })),
|
||||||
artifacts: artifacts.map((item) => ({ ...item, imageUrl: publicMediaUrl(item.imageKey) })),
|
artifacts: artifacts.map((item) => ({ ...item, imageUrl: publicMediaUrl(item.imageKey) })),
|
||||||
@@ -82,31 +78,31 @@ export async function getGuideByCharacterKey(characterKey: string) {
|
|||||||
|
|
||||||
export async function getPublicGuide(slug: string) {
|
export async function getPublicGuide(slug: string) {
|
||||||
const [row] = await getDb()
|
const [row] = await getDb()
|
||||||
.select({ guide: guides, cover: media, character: catalogCharacterCurrent })
|
.select({ guide: guides, cover: media, character: catalogCharacters })
|
||||||
.from(guides)
|
.from(guides)
|
||||||
.innerJoin(media, eq(media.id, guides.coverMediaId))
|
.innerJoin(media, eq(media.id, guides.coverMediaId))
|
||||||
.innerJoin(catalogCharacterCurrent, eq(catalogCharacterCurrent.key, guides.characterKey))
|
.innerJoin(catalogCharacters, eq(catalogCharacters.key, guides.characterKey))
|
||||||
.where(and(eq(guides.slug, slug), isNull(guides.trashedAt)))
|
.where(and(eq(guides.slug, slug), isNull(guides.trashedAt)))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
const [sections, weaponRows, groups, profile, constellations, teams] = await Promise.all([
|
const [sections, weaponRows, groups, profile, constellations, teams] = await Promise.all([
|
||||||
getDb().select().from(guideSections).where(and(eq(guideSections.guideId, row.guide.id), eq(guideSections.enabled, true))).orderBy(asc(guideSections.sortOrder)),
|
getDb().select().from(guideSections).where(and(eq(guideSections.guideId, row.guide.id), eq(guideSections.enabled, true))).orderBy(asc(guideSections.sortOrder)),
|
||||||
getDb().select({ recommendation: weaponRecommendations, weapon: catalogWeapons }).from(weaponRecommendations).innerJoin(catalogWeapons, and(eq(catalogWeapons.version, weaponRecommendations.catalogVersion), eq(catalogWeapons.key, weaponRecommendations.weaponKey))).where(eq(weaponRecommendations.guideId, row.guide.id)).orderBy(asc(weaponRecommendations.sortOrder)),
|
getDb().select({ recommendation: weaponRecommendations, weapon: catalogWeapons }).from(weaponRecommendations).innerJoin(catalogWeapons, eq(catalogWeapons.key, weaponRecommendations.weaponKey)).where(eq(weaponRecommendations.guideId, row.guide.id)).orderBy(asc(weaponRecommendations.sortOrder)),
|
||||||
getDb().select().from(artifactGroups).where(eq(artifactGroups.guideId, row.guide.id)).orderBy(asc(artifactGroups.sortOrder)),
|
getDb().select().from(artifactGroups).where(eq(artifactGroups.guideId, row.guide.id)).orderBy(asc(artifactGroups.sortOrder)),
|
||||||
getDb().select().from(artifactProfiles).where(eq(artifactProfiles.guideId, row.guide.id)).limit(1),
|
getDb().select().from(artifactProfiles).where(eq(artifactProfiles.guideId, row.guide.id)).limit(1),
|
||||||
getDb().select().from(constellationRecommendations).where(eq(constellationRecommendations.guideId, row.guide.id)).orderBy(asc(constellationRecommendations.level)),
|
getDb().select().from(constellationRecommendations).where(eq(constellationRecommendations.guideId, row.guide.id)).orderBy(asc(constellationRecommendations.level)),
|
||||||
getDb().select().from(teamCompositions).where(eq(teamCompositions.guideId, row.guide.id)).orderBy(asc(teamCompositions.sortOrder)),
|
getDb().select().from(teamCompositions).where(eq(teamCompositions.guideId, row.guide.id)).orderBy(asc(teamCompositions.sortOrder)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const artifactRows = groups.length ? await getDb().select({ recommendation: artifactRecommendations, artifact: catalogArtifacts }).from(artifactRecommendations).innerJoin(catalogArtifacts, and(eq(catalogArtifacts.version, artifactRecommendations.catalogVersion), eq(catalogArtifacts.key, artifactRecommendations.firstArtifactKey))).where(inArray(artifactRecommendations.groupId, groups.map((group) => group.id))).orderBy(asc(artifactRecommendations.sortOrder)) : [];
|
const artifactRows = groups.length ? await getDb().select({ recommendation: artifactRecommendations, artifact: catalogArtifacts }).from(artifactRecommendations).innerJoin(catalogArtifacts, eq(catalogArtifacts.key, artifactRecommendations.firstArtifactKey)).where(inArray(artifactRecommendations.groupId, groups.map((group) => group.id))).orderBy(asc(artifactRecommendations.sortOrder)) : [];
|
||||||
const secondKeys = artifactRows.map(({ recommendation }) => recommendation.secondArtifactKey).filter((key): key is string => Boolean(key));
|
const secondKeys = artifactRows.map(({ recommendation }) => recommendation.secondArtifactKey).filter((key): key is string => Boolean(key));
|
||||||
const secondArtifacts = secondKeys.length ? await getDb().select().from(catalogArtifacts).where(and(eq(catalogArtifacts.version, row.guide.catalogVersion), inArray(catalogArtifacts.key, secondKeys))) : [];
|
const secondArtifacts = secondKeys.length ? await getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, secondKeys)) : [];
|
||||||
const members = teams.length ? await getDb().select({ member: teamMembers, character: catalogCharacterCurrent }).from(teamMembers).innerJoin(catalogCharacterCurrent, eq(catalogCharacterCurrent.key, teamMembers.characterKey)).where(inArray(teamMembers.teamId, teams.map((team) => team.id))).orderBy(asc(teamMembers.position)) : [];
|
const members = teams.length ? await getDb().select({ member: teamMembers, character: catalogCharacters }).from(teamMembers).innerJoin(catalogCharacters, eq(catalogCharacters.key, teamMembers.characterKey)).where(inArray(teamMembers.teamId, teams.map((team) => team.id))).orderBy(asc(teamMembers.position)) : [];
|
||||||
const memberWeaponKeys = members.map(({ member }) => member.weaponKey).filter((key): key is string => Boolean(key));
|
const memberWeaponKeys = members.map(({ member }) => member.weaponKey).filter((key): key is string => Boolean(key));
|
||||||
const memberArtifactKeys = members.map(({ member }) => member.artifactKey).filter((key): key is string => Boolean(key));
|
const memberArtifactKeys = members.map(({ member }) => member.artifactKey).filter((key): key is string => Boolean(key));
|
||||||
const [memberWeapons, memberArtifacts] = await Promise.all([
|
const [memberWeapons, memberArtifacts] = await Promise.all([
|
||||||
memberWeaponKeys.length ? getDb().select().from(catalogWeapons).where(and(eq(catalogWeapons.version, row.guide.catalogVersion), inArray(catalogWeapons.key, memberWeaponKeys))) : [],
|
memberWeaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, memberWeaponKeys)) : [],
|
||||||
memberArtifactKeys.length ? getDb().select().from(catalogArtifacts).where(and(eq(catalogArtifacts.version, row.guide.catalogVersion), inArray(catalogArtifacts.key, memberArtifactKeys))) : [],
|
memberArtifactKeys.length ? getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, memberArtifactKeys)) : [],
|
||||||
]);
|
]);
|
||||||
const extraIds = sections.filter((section) => section.kind === "extra").map((section) => section.id);
|
const extraIds = sections.filter((section) => section.kind === "extra").map((section) => section.id);
|
||||||
const extraRows = extraIds.length ? await getDb().select({ row: extraSectionRows, asset: media }).from(extraSectionRows).leftJoin(media, eq(media.id, extraSectionRows.mediaId)).where(inArray(extraSectionRows.sectionId, extraIds)).orderBy(asc(extraSectionRows.sortOrder)) : [];
|
const extraRows = extraIds.length ? await getDb().select({ row: extraSectionRows, asset: media }).from(extraSectionRows).leftJoin(media, eq(media.id, extraSectionRows.mediaId)).where(inArray(extraSectionRows.sectionId, extraIds)).orderBy(asc(extraSectionRows.sortOrder)) : [];
|
||||||
@@ -133,11 +129,6 @@ export async function getPublicGuide(slug: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLatestCatalogSyncJob() {
|
|
||||||
const [job] = await getDb().select().from(catalogSyncJobs).orderBy(desc(catalogSyncJobs.createdAt)).limit(1);
|
|
||||||
return job ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPublicTeamExport(teamId: string) {
|
export async function getPublicTeamExport(teamId: string) {
|
||||||
const [row] = await getDb()
|
const [row] = await getDb()
|
||||||
.select({ team: teamCompositions, guide: guides })
|
.select({ team: teamCompositions, guide: guides })
|
||||||
@@ -156,11 +147,11 @@ export async function getPublicTeamExport(teamId: string) {
|
|||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
|
|
||||||
const members = await getDb()
|
const members = await getDb()
|
||||||
.select({ member: teamMembers, character: catalogCharacterCurrent })
|
.select({ member: teamMembers, character: catalogCharacters })
|
||||||
.from(teamMembers)
|
.from(teamMembers)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
catalogCharacterCurrent,
|
catalogCharacters,
|
||||||
eq(catalogCharacterCurrent.key, teamMembers.characterKey),
|
eq(catalogCharacters.key, teamMembers.characterKey),
|
||||||
)
|
)
|
||||||
.where(eq(teamMembers.teamId, teamId))
|
.where(eq(teamMembers.teamId, teamId))
|
||||||
.orderBy(asc(teamMembers.position));
|
.orderBy(asc(teamMembers.position));
|
||||||
@@ -168,8 +159,8 @@ export async function getPublicTeamExport(teamId: string) {
|
|||||||
const weaponKeys = members.map(({ member }) => member.weaponKey).filter((key): key is string => Boolean(key));
|
const weaponKeys = members.map(({ member }) => member.weaponKey).filter((key): key is string => Boolean(key));
|
||||||
const artifactKeys = members.map(({ member }) => member.artifactKey).filter((key): key is string => Boolean(key));
|
const artifactKeys = members.map(({ member }) => member.artifactKey).filter((key): key is string => Boolean(key));
|
||||||
const [weapons, artifacts] = await Promise.all([
|
const [weapons, artifacts] = await Promise.all([
|
||||||
weaponKeys.length ? getDb().select().from(catalogWeapons).where(and(eq(catalogWeapons.version, row.guide.catalogVersion), inArray(catalogWeapons.key, weaponKeys))) : [],
|
weaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, weaponKeys)) : [],
|
||||||
artifactKeys.length ? getDb().select().from(catalogArtifacts).where(and(eq(catalogArtifacts.version, row.guide.catalogVersion), inArray(catalogArtifacts.key, artifactKeys))) : [],
|
artifactKeys.length ? getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, artifactKeys)) : [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -210,10 +201,7 @@ export async function getPublicWeaponExport(guideId: string) {
|
|||||||
.from(weaponRecommendations)
|
.from(weaponRecommendations)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
catalogWeapons,
|
catalogWeapons,
|
||||||
and(
|
eq(catalogWeapons.key, weaponRecommendations.weaponKey),
|
||||||
eq(catalogWeapons.version, weaponRecommendations.catalogVersion),
|
|
||||||
eq(catalogWeapons.key, weaponRecommendations.weaponKey),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.where(eq(weaponRecommendations.guideId, guideId))
|
.where(eq(weaponRecommendations.guideId, guideId))
|
||||||
.orderBy(asc(weaponRecommendations.sortOrder));
|
.orderBy(asc(weaponRecommendations.sortOrder));
|
||||||
@@ -229,7 +217,7 @@ export async function getPublicWeaponExport(guideId: string) {
|
|||||||
|
|
||||||
export async function getPublicArtifactExport(guideId: string) {
|
export async function getPublicArtifactExport(guideId: string) {
|
||||||
const [row] = await getDb()
|
const [row] = await getDb()
|
||||||
.select({ guide: guides, character: catalogCharacterCurrent })
|
.select({ guide: guides, character: catalogCharacters })
|
||||||
.from(guides)
|
.from(guides)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
guideSections,
|
guideSections,
|
||||||
@@ -240,8 +228,8 @@ export async function getPublicArtifactExport(guideId: string) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
catalogCharacterCurrent,
|
catalogCharacters,
|
||||||
eq(catalogCharacterCurrent.key, guides.characterKey),
|
eq(catalogCharacters.key, guides.characterKey),
|
||||||
)
|
)
|
||||||
.where(and(eq(guides.id, guideId), isNull(guides.trashedAt)))
|
.where(and(eq(guides.id, guideId), isNull(guides.trashedAt)))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
@@ -257,10 +245,7 @@ export async function getPublicArtifactExport(guideId: string) {
|
|||||||
.from(artifactRecommendations)
|
.from(artifactRecommendations)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
catalogArtifacts,
|
catalogArtifacts,
|
||||||
and(
|
eq(catalogArtifacts.key, artifactRecommendations.firstArtifactKey),
|
||||||
eq(catalogArtifacts.version, artifactRecommendations.catalogVersion),
|
|
||||||
eq(catalogArtifacts.key, artifactRecommendations.firstArtifactKey),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.where(inArray(artifactRecommendations.groupId, groups.map((group) => group.id)))
|
.where(inArray(artifactRecommendations.groupId, groups.map((group) => group.id)))
|
||||||
.orderBy(asc(artifactRecommendations.sortOrder))
|
.orderBy(asc(artifactRecommendations.sortOrder))
|
||||||
@@ -269,7 +254,7 @@ export async function getPublicArtifactExport(guideId: string) {
|
|||||||
.map(({ recommendation }) => recommendation.secondArtifactKey)
|
.map(({ recommendation }) => recommendation.secondArtifactKey)
|
||||||
.filter((key): key is string => Boolean(key));
|
.filter((key): key is string => Boolean(key));
|
||||||
const secondArtifacts = secondKeys.length
|
const secondArtifacts = secondKeys.length
|
||||||
? await getDb().select().from(catalogArtifacts).where(and(eq(catalogArtifacts.version, row.guide.catalogVersion), inArray(catalogArtifacts.key, secondKeys)))
|
? await getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, secondKeys))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
processOutboxEvent,
|
processOutboxEvent,
|
||||||
} from "@/lib/outbox/processor";
|
} from "@/lib/outbox/processor";
|
||||||
import { closeRedisClient, getRedisClient } from "@/lib/redis/client";
|
import { closeRedisClient, getRedisClient } from "@/lib/redis/client";
|
||||||
import { claimCatalogSyncJob, processCatalogSyncJob } from "@/lib/catalog/sync";
|
|
||||||
|
|
||||||
let stopping = false;
|
let stopping = false;
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
@@ -41,15 +40,6 @@ async function run(): Promise<void> {
|
|||||||
const leaseMs = positiveInteger("OUTBOX_LEASE_MS", 30_000, 300_000);
|
const leaseMs = positiveInteger("OUTBOX_LEASE_MS", 30_000, 300_000);
|
||||||
|
|
||||||
while (!stopping) {
|
while (!stopping) {
|
||||||
const catalogJob = await claimCatalogSyncJob();
|
|
||||||
if (catalogJob) {
|
|
||||||
try {
|
|
||||||
await processCatalogSyncJob(catalogJob.id);
|
|
||||||
} catch (cause) {
|
|
||||||
console.error("Catalog sync failed", cause);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const events = await claimOutboxEvents(batchSize, leaseMs);
|
const events = await claimOutboxEvents(batchSize, leaseMs);
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
await wait(pollMs);
|
await wait(pollMs);
|
||||||
|
|||||||
Reference in New Issue
Block a user