import "server-only"; import { and, asc, desc, eq, inArray, isNotNull, isNull } from "drizzle-orm"; import { getDb } from "@/db"; import { artifactGroups, artifactProfiles, artifactRecommendations, catalogArtifacts, catalogCharacters, catalogSyncJobs, catalogVersions, catalogWeapons, constellationRecommendations, extraSectionRows, guides, guideSections, media, teamCompositions, teamMembers, weaponRecommendations, } from "@/db/schema"; import { publicMediaUrl } from "@/lib/media/storage"; export async function getActiveCatalog() { 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([ db.select().from(catalogCharacters).where(eq(catalogCharacters.version, version.version)).orderBy(asc(catalogCharacters.name)), db.select().from(catalogWeapons).where(eq(catalogWeapons.version, version.version)).orderBy(asc(catalogWeapons.name)), db.select().from(catalogArtifacts).where(eq(catalogArtifacts.version, version.version)).orderBy(asc(catalogArtifacts.name)), ]); return { version: version.version, characters: characters.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) })), }; } export async function listGuideCards(includeTrash = false) { const rows = await getDb() .select({ guide: guides, cover: media }) .from(guides) .innerJoin(media, eq(media.id, guides.coverMediaId)) .where(includeTrash ? isNotNull(guides.trashedAt) : isNull(guides.trashedAt)) .orderBy(asc(guides.sortOrder), asc(guides.name)); return rows.map(({ guide, cover }) => ({ ...guide, coverUrl: publicMediaUrl(cover.objectKey) })); } export async function getGuideByCharacterKey(characterKey: string) { const [row] = await getDb() .select({ guide: guides, cover: media }) .from(guides) .innerJoin(media, eq(media.id, guides.coverMediaId)) .where(and(eq(guides.characterKey, characterKey), isNull(guides.trashedAt))) .limit(1); if (!row) return null; const sections = await getDb().select().from(guideSections).where(eq(guideSections.guideId, row.guide.id)).orderBy(asc(guideSections.sortOrder)); return { ...row.guide, coverUrl: publicMediaUrl(row.cover.objectKey), sections }; } export async function getPublicGuide(slug: string) { const [row] = await getDb() .select({ guide: guides, cover: media, character: catalogCharacters }) .from(guides) .innerJoin(media, eq(media.id, guides.coverMediaId)) .innerJoin(catalogCharacters, and(eq(catalogCharacters.version, guides.catalogVersion), eq(catalogCharacters.key, guides.characterKey))) .where(and(eq(guides.slug, slug), isNull(guides.trashedAt))) .limit(1); if (!row) return null; 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({ 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().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(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)), ]); 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 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 members = teams.length ? await getDb().select({ member: teamMembers, character: catalogCharacters }).from(teamMembers).innerJoin(catalogCharacters, and(eq(catalogCharacters.version, teamMembers.catalogVersion), eq(catalogCharacters.key, teamMembers.characterKey))).where(inArray(teamMembers.teamId, teams.map((team) => team.id))).orderBy(asc(teamMembers.position)) : []; 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)) : []; return { ...row.guide, coverUrl: publicMediaUrl(row.cover.objectKey), character: { ...row.character, imageUrl: publicMediaUrl(row.character.imageKey) }, sections, weapons: weaponRows.map(({ recommendation, weapon }) => ({ ...recommendation, weapon: { ...weapon, imageUrl: publicMediaUrl(weapon.imageKey) } })), artifactGroups: groups.map((group) => ({ ...group, rows: artifactRows.filter(({ recommendation }) => recommendation.groupId === group.id).map(({ recommendation, artifact }) => { const second = secondArtifacts.find((item) => item.key === recommendation.secondArtifactKey); return { ...recommendation, first: { ...artifact, imageUrl: publicMediaUrl(artifact.imageKey) }, second: second ? { ...second, imageUrl: publicMediaUrl(second.imageKey) } : null }; }) })), artifactProfile: profile[0] ?? null, constellations, teams: teams.map((team) => ({ ...team, members: members.filter(({ member }) => member.teamId === team.id).map(({ member, character }) => ({ ...member, character: { ...character, imageUrl: publicMediaUrl(character.imageKey) } })) })), extras: sections.filter((section) => section.kind === "extra").map((section) => ({ ...section, rows: extraRows.filter(({ row: extraRow }) => extraRow.sectionId === section.id).map(({ row: extraRow, asset }) => ({ ...extraRow, imageUrl: asset ? publicMediaUrl(asset.objectKey) : null })) })), }; } export async function getLatestCatalogSyncJob() { const [job] = await getDb().select().from(catalogSyncJobs).orderBy(desc(catalogSyncJobs.createdAt)).limit(1); return job ?? null; } export type PublicGuide = NonNullable>>;