Files
buzz-sheet/lib/guides/queries.ts
T
gunshiz ea12c85c21
CI / Verify (push) Successful in 1m23s
CI / Build immutable images and deploy (push) Successful in 2m5s
fix(guides) : publish created extra sections
2026-09-23 17:31:20 +07:00

520 lines
29 KiB
TypeScript

import "server-only";
import { and, asc, desc, eq, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
import * as z from "zod";
import { getDb } from "@/db";
import {
artifactGroups,
artifactConditions,
artifactProfiles,
artifactRecommendations,
catalogArtifacts,
catalogCharacters,
catalogMaterials,
catalogWeapons,
constellationConditions,
constellationRecommendations,
extraSectionRows,
guides,
guidePosterBleeding,
guideSections,
glossaryAliases,
media,
teamCompositions,
teamConditions,
teamMembers,
weaponRecommendations,
weaponConditions,
} from "@/db/schema";
import { getMediaStorage, publicMediaUrl } from "@/lib/media/storage";
import { aliasesByItem } from "@/lib/glossary";
import type { PosterBlendMode } from "@/components/public/material-poster";
const catalogVersionSchema = z.object({ version: z.string() });
const posterBlendModes = new Set(["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"]);
// Leave most of the per-pod pool available to other visitors during cache misses.
const PUBLIC_QUERY_CONCURRENCY = 3;
async function runWithConcurrency<
const T extends ReadonlyArray<() => Promise<unknown>>,
>(
tasks: T,
concurrency = PUBLIC_QUERY_CONCURRENCY,
): Promise<{ [K in keyof T]: Awaited<ReturnType<T[K]>> }> {
const results = new Array<unknown>(tasks.length);
let next = 0;
await Promise.all(
Array.from({ length: Math.min(concurrency, tasks.length) }, async () => {
while (next < tasks.length) {
const index = next++;
results[index] = await tasks[index]();
}
}),
);
return results as { [K in keyof T]: Awaited<ReturnType<T[K]>> };
}
function catalogCharacterWithUrls(
item: typeof catalogCharacters.$inferSelect,
materialByKey = new Map<string, typeof catalogMaterials.$inferSelect>(),
) {
return {
...item,
imageUrl: publicMediaUrl(item.imageKey),
splashImageUrl: item.coopImageKey ? publicMediaUrl(item.coopImageKey) : null,
nameCardImageUrl: item.nameCardImageKey ? publicMediaUrl(item.nameCardImageKey) : null,
elementIconUrl: item.elementIconKey ? publicMediaUrl(item.elementIconKey) : null,
constellations: item.constellations.map((constellation) => ({
...constellation,
iconUrl: constellation.iconKey ? publicMediaUrl(constellation.iconKey) : null,
})),
talents: item.talents.map((talent) => ({
...talent,
iconUrl: talent.iconKey ? publicMediaUrl(talent.iconKey) : null,
})),
ascension: item.ascension.map((group) => ({
...group,
items: group.items.map((entry) => {
const material = materialByKey.get(entry.key);
return {
...entry,
name: material?.name ?? entry.key,
description: material?.description ?? null,
rarity: material?.rarity ?? null,
type: material?.type ?? null,
imageUrl: material ? publicMediaUrl(material.imageKey) : null,
lunarisUrl: `https://lunaris.moe/material/${encodeURIComponent(entry.key)}`,
available: Boolean(material),
};
}),
})),
leveling: item.leveling.map((group) => ({
...group,
items: group.items.map((entry) => {
const material = materialByKey.get(entry.key);
return {
...entry,
name: material?.name ?? entry.key,
description: material?.description ?? null,
rarity: material?.rarity ?? null,
type: material?.type ?? null,
imageUrl: material ? publicMediaUrl(material.imageKey) : null,
lunarisUrl: `https://lunaris.moe/material/${encodeURIComponent(entry.key)}`,
available: Boolean(material),
};
}),
})),
};
}
export async function getActiveCatalog() {
const db = getDb();
const [characters, weapons, artifacts, materials, aliases] = await Promise.all([
db.select().from(catalogCharacters).orderBy(asc(catalogCharacters.name)),
db.select().from(catalogWeapons).orderBy(asc(catalogWeapons.name)),
db.select().from(catalogArtifacts).orderBy(asc(catalogArtifacts.name)),
db.select().from(catalogMaterials).orderBy(asc(catalogMaterials.name)),
db.select().from(glossaryAliases),
]);
if (!characters.length) return null;
const groupedAliases = aliasesByItem(aliases);
const materialByKey = new Map(materials.map((item) => [item.key, item]));
const storage = await getMediaStorage();
const { version } = catalogVersionSchema.parse(
await storage.file("catalog/current.json").json(),
);
return {
version,
characters: characters.map((item) => ({ ...catalogCharacterWithUrls(item, materialByKey), aliases: groupedAliases.character.get(item.key) ?? [] })),
weapons: weapons.map((item) => ({ ...item, aliases: groupedAliases.weapon.get(item.key) ?? [], imageUrl: publicMediaUrl(item.imageKey) })),
artifacts: artifacts.map((item) => ({ ...item, aliases: groupedAliases.artifact.get(item.key) ?? [], imageUrl: publicMediaUrl(item.imageKey) })),
materials: materials.map((item) => ({ ...item, imageUrl: publicMediaUrl(item.imageKey), lunarisUrl: `https://lunaris.moe/material/${encodeURIComponent(item.key)}` })),
};
}
export async function listGuideCards(includeTrash = false) {
const rows = await getDb()
.select({
id: guides.id,
characterKey: guides.characterKey,
name: guides.name,
overview: guides.overview,
version: guides.version,
updatedAt: guides.updatedAt,
trashedAt: guides.trashedAt,
isPublic: guides.isPublic,
coverObjectKey: media.objectKey,
})
.from(guides)
.innerJoin(media, eq(media.id, guides.coverMediaId))
.where(includeTrash ? isNotNull(guides.trashedAt) : isNull(guides.trashedAt))
.orderBy(desc(guides.sortOrder), desc(guides.createdAt), asc(guides.name));
return rows.map(({ coverObjectKey, ...guide }) => ({
...guide,
coverUrl: publicMediaUrl(coverObjectKey),
}));
}
export async function listAdminTeamImportGuides(excludeGuideId: string) {
const rows = await getDb()
.select({
id: guides.id,
name: guides.name,
characterKey: guides.characterKey,
isPublic: guides.isPublic,
coverObjectKey: media.objectKey,
})
.from(guides)
.innerJoin(media, eq(media.id, guides.coverMediaId))
.where(and(isNull(guides.trashedAt), ne(guides.id, excludeGuideId)))
.orderBy(desc(guides.sortOrder), desc(guides.createdAt), asc(guides.name));
return rows.map(({ coverObjectKey, ...guide }) => ({
...guide,
coverUrl: publicMediaUrl(coverObjectKey),
}));
}
export async function getAdminTeamImportGuide(guideId: string) {
const [guide] = await getDb()
.select({ id: guides.id, name: guides.name })
.from(guides)
.where(and(eq(guides.id, guideId), isNull(guides.trashedAt)))
.limit(1);
if (!guide) return null;
const [conditions, teams] = await Promise.all([
getDb().select().from(teamConditions)
.where(eq(teamConditions.guideId, guideId))
.orderBy(asc(teamConditions.sortOrder)),
getDb().select().from(teamCompositions)
.where(eq(teamCompositions.guideId, guideId))
.orderBy(asc(teamCompositions.sortOrder)),
]);
const members = teams.length
? await getDb()
.select({ member: teamMembers, characterName: catalogCharacters.name })
.from(teamMembers)
.leftJoin(catalogCharacters, eq(catalogCharacters.key, teamMembers.characterKey))
.where(inArray(teamMembers.teamId, teams.map((team) => team.id)))
.orderBy(asc(teamMembers.position))
: [];
const hydratedTeams = teams.map((team) => ({
id: team.id,
title: team.title,
rotationSeconds: team.rotationSeconds,
combo: team.combo,
members: members.filter(({ member }) => member.teamId === team.id).map(({ member, characterName }) => ({
characterKey: member.characterKey,
characterName: characterName ?? member.characterKey,
weaponKey: member.weaponKey,
artifactKey: member.artifactKey,
secondArtifactKey: member.secondArtifactKey,
constellation: member.constellation,
refinement: member.refinement,
damage: member.damage,
})),
}));
return {
...guide,
groups: conditions.length
? conditions.map((condition) => ({
id: condition.id,
name: condition.name,
teams: hydratedTeams.filter((team) => teams.find((candidate) => candidate.id === team.id)?.conditionId === condition.id),
}))
: [{ id: "team-condition-default", name: null, teams: hydratedTeams }],
};
}
export async function listPublicGuideCards() {
const [rows, aliases] = await Promise.all([getDb()
.select({
id: guides.id,
characterKey: guides.characterKey,
slug: guides.slug,
name: guides.name,
overview: guides.overview,
version: guides.version,
updatedAt: guides.updatedAt,
publishedAt: guides.publishedAt,
publishedCatalogVersion: guides.publishedCatalogVersion,
element: catalogCharacters.element,
weaponType: catalogCharacters.weaponType,
rarity: catalogCharacters.rarity,
coverObjectKey: media.objectKey,
})
.from(guides)
.innerJoin(media, eq(media.id, guides.coverMediaId))
.innerJoin(catalogCharacters, eq(catalogCharacters.key, guides.characterKey))
.where(and(eq(guides.isPublic, true), isNull(guides.trashedAt)))
.orderBy(desc(guides.sortOrder), desc(guides.createdAt), asc(guides.name)), getDb().select({
kind: glossaryAliases.kind,
catalogKey: glossaryAliases.catalogKey,
alias: glossaryAliases.alias,
}).from(glossaryAliases)]);
const grouped = aliasesByItem(aliases);
return rows.map(({ coverObjectKey, ...guide }) => ({
...guide,
aliases: grouped.character.get(guide.characterKey) ?? [],
coverUrl: publicMediaUrl(coverObjectKey),
}));
}
export async function getGuideEditorHeader(characterKey: string) {
const [row] = await getDb()
.select({
id: guides.id,
characterKey: guides.characterKey,
slug: guides.slug,
name: guides.name,
version: guides.version,
coverObjectKey: media.objectKey,
})
.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.id))
.orderBy(asc(guideSections.sortOrder));
const { coverObjectKey, ...guide } = row;
return { ...guide, coverUrl: publicMediaUrl(coverObjectKey), sections };
}
export async function getGuideByCharacterKey(characterKey: string) {
const [row] = await getDb()
.select({ guide: guides, cover: media, character: catalogCharacters })
.from(guides)
.innerJoin(media, eq(media.id, guides.coverMediaId))
.innerJoin(catalogCharacters, eq(catalogCharacters.key, guides.characterKey))
.where(and(eq(guides.characterKey, characterKey), isNull(guides.trashedAt)))
.limit(1);
if (!row) return null;
const materialKeys = [...new Set(
[...row.character.ascension, ...row.character.leveling].flatMap((group) => group.items.map((item) => item.key)),
)];
const [materials, poster, bleeding] = await Promise.all([
materialKeys.length
? getDb().select().from(catalogMaterials).where(inArray(catalogMaterials.key, materialKeys))
: [],
row.guide.posterArtMediaId
? getDb().select().from(media).where(eq(media.id, row.guide.posterArtMediaId)).limit(1)
: Promise.resolve([]),
getDb().select({ layer: guidePosterBleeding, asset: media }).from(guidePosterBleeding).leftJoin(media, eq(media.id, guidePosterBleeding.mediaId)).where(eq(guidePosterBleeding.guideId, row.guide.id)).orderBy(asc(guidePosterBleeding.sortOrder)),
]);
const materialByKey = new Map(materials.map((item) => [item.key, item]));
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),
coverStatus: row.cover.status,
posterArtUrl: poster[0] ? publicMediaUrl(poster[0].objectKey) : null,
posterBleeding: bleeding.map(({ layer, asset }) => ({ ...layer, blendMode: (posterBlendModes.has(layer.blendMode) ? layer.blendMode : "multiply") as PosterBlendMode, imageUrl: asset ? publicMediaUrl(asset.objectKey) : null })),
character: catalogCharacterWithUrls(row.character, materialByKey),
sections,
};
}
/** Navigation probes return presence flags, never the content of other sections. */
async function populatedSections(guideId: string) {
const rows = await getDb().select({
section: guideSections,
populated: sql<boolean>`case ${guideSections.kind}
when 'weapon' then exists (select 1 from ${weaponRecommendations} r join ${weaponConditions} c on c.id = r.condition_id where c.guide_id = ${guideId})
when 'artifact' then exists (select 1 from ${artifactRecommendations} r join ${artifactGroups} g on g.id = r.group_id join ${artifactConditions} c on c.id = g.condition_id where c.guide_id = ${guideId})
or exists (select 1 from ${artifactProfiles} p where p.guide_id = ${guideId} and (
p.sands <> '[]'::jsonb or p.goblet <> '[]'::jsonb or p.circlet <> '[]'::jsonb or p.substats <> '[]'::jsonb or p.talents <> '[]'::jsonb or p.targets <> '[]'::jsonb
or exists (select 1 from jsonb_array_elements(p.recommended_conditions) c where c->'sands' <> '[]'::jsonb or c->'goblet' <> '[]'::jsonb or c->'circlet' <> '[]'::jsonb or c->'substats' <> '[]'::jsonb)
or exists (select 1 from jsonb_array_elements(p.target_conditions) c where c->'rows' <> '[]'::jsonb)))
when 'constellations' then exists (select 1 from ${constellationRecommendations} r join ${constellationConditions} c on c.id = r.condition_id where c.guide_id = ${guideId})
when 'team' then exists (select 1 from ${teamCompositions} t where t.guide_id = ${guideId})
when 'extra' then true
else exists (select 1 from ${extraSectionRows} r where r.section_id = ${guideSections.id}) end`,
}).from(guideSections).where(and(
eq(guideSections.guideId, guideId),
or(eq(guideSections.enabled, true), eq(guideSections.kind, "extra")),
))
.orderBy(asc(guideSections.sortOrder));
return rows.filter((row) => row.populated).map((row) => row.section);
}
export async function getPublicGuideSummary(slug: string) {
const [row] = await getDb().select({
id: guides.id, slug: guides.slug, name: guides.name, overview: guides.overview,
characterKey: guides.characterKey, coverObjectKey: media.objectKey,
}).from(guides).innerJoin(media, eq(media.id, guides.coverMediaId))
.where(and(eq(guides.slug, slug), eq(guides.isPublic, true), isNull(guides.trashedAt))).limit(1);
if (!row) return null;
const { coverObjectKey, ...summary } = row;
return { ...summary, coverUrl: publicMediaUrl(coverObjectKey), sections: await populatedSections(row.id) };
}
async function getGuideContent(slug: string, publicOnly: boolean, selectedSection?: string) {
const [row] = await getDb()
.select({ guide: guides, cover: media, character: catalogCharacters })
.from(guides)
.innerJoin(media, eq(media.id, guides.coverMediaId))
.innerJoin(catalogCharacters, eq(catalogCharacters.key, guides.characterKey))
.where(and(
eq(guides.slug, slug),
isNull(guides.trashedAt),
...(publicOnly ? [eq(guides.isPublic, true)] : []),
))
.limit(1);
if (!row) return null;
const availableSections = publicOnly ? await populatedSections(row.guide.id) : undefined;
if (publicOnly && selectedSection !== "overview" && !availableSections?.some((section) => section.slug === selectedSection)) return null;
const wants = (section: string) => !publicOnly || selectedSection === section;
const materialKeys = [...new Set(
[...row.character.ascension, ...row.character.leveling].flatMap((group) => group.items.map((item) => item.key)),
)];
const [sections, weaponConditionsRows, artifactConditionRows, profile, constellationGroups, teamConditionRows, teams, aliases, materials, poster, bleeding] = await runWithConcurrency([
() => getDb().select().from(guideSections).where(and(
eq(guideSections.guideId, row.guide.id),
...(publicOnly
? [or(eq(guideSections.enabled, true), eq(guideSections.kind, "extra"))]
: [eq(guideSections.enabled, true)]),
)).orderBy(asc(guideSections.sortOrder)),
() => wants("weapon") ? getDb().select().from(weaponConditions).where(eq(weaponConditions.guideId, row.guide.id)).orderBy(asc(weaponConditions.sortOrder)) : Promise.resolve([]),
() => wants("artifact") ? getDb().select().from(artifactConditions).where(eq(artifactConditions.guideId, row.guide.id)).orderBy(asc(artifactConditions.sortOrder)) : Promise.resolve([]),
() => wants("artifact") ? getDb().select().from(artifactProfiles).where(eq(artifactProfiles.guideId, row.guide.id)).limit(1) : Promise.resolve([]),
() => wants("constellations") ? getDb().select().from(constellationConditions).where(eq(constellationConditions.guideId, row.guide.id)).orderBy(asc(constellationConditions.sortOrder)) : Promise.resolve([]),
() => wants("team") ? getDb().select().from(teamConditions).where(eq(teamConditions.guideId, row.guide.id)).orderBy(asc(teamConditions.sortOrder)) : Promise.resolve([]),
() => wants("team") ? getDb().select().from(teamCompositions).where(eq(teamCompositions.guideId, row.guide.id)).orderBy(asc(teamCompositions.sortOrder)) : Promise.resolve([]),
() => getDb().select({
kind: glossaryAliases.kind,
catalogKey: glossaryAliases.catalogKey,
alias: glossaryAliases.alias,
}).from(glossaryAliases),
() => wants("overview") && materialKeys.length
? getDb().select().from(catalogMaterials).where(inArray(catalogMaterials.key, materialKeys))
: Promise.resolve([]),
() => wants("overview") && row.guide.posterArtMediaId
? getDb().select().from(media).where(eq(media.id, row.guide.posterArtMediaId)).limit(1)
: Promise.resolve([]),
() => wants("overview") ? getDb().select({ layer: guidePosterBleeding, asset: media }).from(guidePosterBleeding).leftJoin(media, eq(media.id, guidePosterBleeding.mediaId)).where(eq(guidePosterBleeding.guideId, row.guide.id)).orderBy(asc(guidePosterBleeding.sortOrder)) : Promise.resolve([]),
]);
const groups = artifactConditionRows.length ? await getDb().select().from(artifactGroups).where(inArray(artifactGroups.conditionId, artifactConditionRows.map((condition) => condition.id))).orderBy(asc(artifactGroups.sortOrder)) : [];
const groupedAliases = aliasesByItem(aliases);
const materialByKey = new Map(materials.map((item) => [item.key, item]));
const elementIconUrls = Object.fromEntries(
(wants("artifact") ? await getDb()
.select({
element: catalogCharacters.element,
elementIconKey: catalogCharacters.elementIconKey,
})
.from(catalogCharacters)
.where(isNotNull(catalogCharacters.elementIconKey)) : [])
.flatMap((character) =>
character.element && character.elementIconKey
? [[character.element, publicMediaUrl(character.elementIconKey)] as const]
: [],
),
);
const weaponRows = weaponConditionsRows.length ? await getDb().select({ recommendation: weaponRecommendations, weapon: catalogWeapons }).from(weaponRecommendations).innerJoin(catalogWeapons, eq(catalogWeapons.key, weaponRecommendations.weaponKey)).where(inArray(weaponRecommendations.conditionId, weaponConditionsRows.map((condition) => condition.id))).orderBy(asc(weaponRecommendations.sortOrder)) : [];
const constellations = constellationGroups.length ? await getDb().select().from(constellationRecommendations).where(inArray(constellationRecommendations.conditionId, constellationGroups.map((group) => group.id))).orderBy(asc(constellationRecommendations.level)) : [];
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 secondArtifacts = secondKeys.length ? await getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, secondKeys)) : [];
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 memberArtifactKeys = members.flatMap(({ member }) => [member.artifactKey, member.secondArtifactKey]).filter((key): key is string => Boolean(key));
const [memberWeapons, memberArtifacts] = await Promise.all([
memberWeaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, memberWeaponKeys)) : [],
memberArtifactKeys.length ? getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, memberArtifactKeys)) : [],
]);
const extraIds = sections.filter((section) => section.kind === "extra" && wants(section.slug)).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 extraCharacterKeys = [...new Set(extraRows.flatMap(({ row: extraRow }) => extraRow.catalogItems.filter((item) => item.kind === "character").map((item) => item.key)))];
const extraWeaponKeys = [...new Set(extraRows.flatMap(({ row: extraRow }) => extraRow.catalogItems.filter((item) => item.kind === "weapon").map((item) => item.key)))];
const extraArtifactKeys = [...new Set(extraRows.flatMap(({ row: extraRow }) => extraRow.catalogItems.filter((item) => item.kind === "artifact").map((item) => item.key)))];
const [extraCharacters, extraWeapons, extraArtifacts] = await Promise.all([
extraCharacterKeys.length ? getDb().select().from(catalogCharacters).where(inArray(catalogCharacters.key, extraCharacterKeys)) : [],
extraWeaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, extraWeaponKeys)) : [],
extraArtifactKeys.length ? getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, extraArtifactKeys)) : [],
]);
const extraCatalogs = { character: extraCharacters, weapon: extraWeapons, artifact: extraArtifacts };
const memberWeaponByKey = new Map(memberWeapons.map((item) => [item.key, item]));
const memberArtifactByKey = new Map(memberArtifacts.map((item) => [item.key, item]));
const secondArtifactByKey = new Map(secondArtifacts.map((item) => [item.key, item]));
const membersByTeam = Map.groupBy(members, ({ member }) => member.teamId);
const hydratedTeams = teams.map((team) => ({ ...team, members: (membersByTeam.get(team.id) ?? []).map(({ member, character }) => { const weapon = memberWeaponByKey.get(member.weaponKey ?? ""); const artifact = memberArtifactByKey.get(member.artifactKey ?? ""); const secondArtifact = memberArtifactByKey.get(member.secondArtifactKey ?? ""); return { ...member, character: { ...catalogCharacterWithUrls(character), aliases: groupedAliases.character.get(character.key) ?? [] }, weapon: weapon ? { ...weapon, aliases: groupedAliases.weapon.get(weapon.key) ?? [], imageUrl: publicMediaUrl(weapon.imageKey) } : null, artifact: artifact ? { ...artifact, aliases: groupedAliases.artifact.get(artifact.key) ?? [], imageUrl: publicMediaUrl(artifact.imageKey) } : null, secondArtifact: secondArtifact ? { ...secondArtifact, aliases: groupedAliases.artifact.get(secondArtifact.key) ?? [], imageUrl: publicMediaUrl(secondArtifact.imageKey) } : null }; }) }));
return {
...row.guide,
coverUrl: publicMediaUrl(row.cover.objectKey),
coverStatus: row.cover.status,
posterArtUrl: poster[0] ? publicMediaUrl(poster[0].objectKey) : null,
posterBleeding: bleeding.map(({ layer, asset }) => ({ ...layer, blendMode: (posterBlendModes.has(layer.blendMode) ? layer.blendMode : "multiply") as PosterBlendMode, imageUrl: asset ? publicMediaUrl(asset.objectKey) : null })),
character: { ...catalogCharacterWithUrls(row.character, materialByKey), aliases: groupedAliases.character.get(row.character.key) ?? [] },
elementIconUrls,
sections,
availableSections,
weapons: weaponRows.map(({ recommendation, weapon }) => ({ ...recommendation, weapon: { ...weapon, aliases: groupedAliases.weapon.get(weapon.key) ?? [], imageUrl: publicMediaUrl(weapon.imageKey) } })),
weaponGroups: weaponConditionsRows.map((condition) => ({ ...condition, rows: weaponRows.filter(({ recommendation }) => recommendation.conditionId === condition.id).map(({ recommendation, weapon }) => ({ ...recommendation, weapon: { ...weapon, aliases: groupedAliases.weapon.get(weapon.key) ?? [], imageUrl: publicMediaUrl(weapon.imageKey) } })) })),
artifactGroups: artifactConditionRows.map((condition) => ({ ...condition, tiers: groups.filter((group) => group.conditionId === condition.id).map((group) => ({ ...group, rows: artifactRows.filter(({ recommendation }) => recommendation.groupId === group.id).map(({ recommendation, artifact }) => { const second = secondArtifactByKey.get(recommendation.secondArtifactKey ?? ""); return { ...recommendation, first: { ...artifact, aliases: groupedAliases.artifact.get(artifact.key) ?? [], imageUrl: publicMediaUrl(artifact.imageKey) }, second: second ? { ...second, aliases: groupedAliases.artifact.get(second.key) ?? [], imageUrl: publicMediaUrl(second.imageKey) } : null }; }) })) })),
artifactProfile: profile[0] ? { ...profile[0], recommendedConditions: profile[0].recommendedConditions.length ? profile[0].recommendedConditions : [{ name: null, note: null, sands: profile[0].sands, goblet: profile[0].goblet, circlet: profile[0].circlet, substats: profile[0].substats }], targetConditions: profile[0].targetConditions.length ? profile[0].targetConditions : [{ name: null, note: null, rows: profile[0].targets }] } : null,
constellationGroups: constellationGroups.map((group) => ({
...group,
rows: constellations.filter((recommendation) => recommendation.conditionId === group.id).map((recommendation) => {
const catalog = row.character.constellations.find((item) => item.level === recommendation.level);
return {
...recommendation,
name: catalog?.name ?? `C${recommendation.level}`,
detail: catalog?.description ?? recommendation.detail,
iconUrl: catalog?.iconKey ? publicMediaUrl(catalog.iconKey) : null,
};
}),
})),
teams: hydratedTeams,
teamGroups: teamConditionRows.length ? teamConditionRows.map((condition) => ({ ...condition, teams: hydratedTeams.filter((team) => team.conditionId === condition.id) })) : [{ id: "team-condition-default", guideId: row.guide.id, name: null, note: null, sortOrder: 0, teams: hydratedTeams }],
extras: sections.filter((section) => section.kind === "extra" && wants(section.slug)).map((section) => ({ ...section, rows: extraRows.filter(({ row: extraRow }) => extraRow.sectionId === section.id).map(({ row: extraRow, asset }) => ({
...extraRow,
catalogItems: extraRow.catalogItems.map((reference) => {
const item = extraCatalogs[reference.kind].find((candidate) => candidate.key === reference.key);
return {
...reference,
name: item?.name ?? reference.key,
imageUrl: item ? publicMediaUrl(item.imageKey) : null,
rarity: item?.rarity ?? null,
available: Boolean(item),
};
}),
imageUrl: asset ? publicMediaUrl(asset.objectKey) : null,
imageWidth: asset?.width ?? null,
imageHeight: asset?.height ?? null,
})) })),
};
}
export function getPublicGuide(slug: string, section = "overview") {
return getGuideContent(slug, true, section);
}
export function getAdminGuideContent(slug: string) {
return getGuideContent(slug, false);
}
export async function getAdminGuideContentById(guideId: string) {
const [guide] = await getDb()
.select({ slug: guides.slug })
.from(guides)
.where(and(eq(guides.id, guideId), isNull(guides.trashedAt)))
.limit(1);
return guide ? getGuideContent(guide.slug, false) : null;
}
export type PublicGuide = NonNullable<Awaited<ReturnType<typeof getPublicGuide>>>;
export type PublicGuideSummary = NonNullable<Awaited<ReturnType<typeof getPublicGuideSummary>>>;