471 lines
25 KiB
TypeScript
471 lines
25 KiB
TypeScript
import "server-only";
|
|
|
|
import { and, asc, desc, eq, inArray, isNotNull, isNull, ne } 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,
|
|
};
|
|
}
|
|
|
|
async function getGuideContent(slug: string, publicOnly: boolean) {
|
|
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 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), eq(guideSections.enabled, true))).orderBy(asc(guideSections.sortOrder)),
|
|
() => getDb().select().from(weaponConditions).where(eq(weaponConditions.guideId, row.guide.id)).orderBy(asc(weaponConditions.sortOrder)),
|
|
() => getDb().select().from(artifactConditions).where(eq(artifactConditions.guideId, row.guide.id)).orderBy(asc(artifactConditions.sortOrder)),
|
|
() => getDb().select().from(artifactProfiles).where(eq(artifactProfiles.guideId, row.guide.id)).limit(1),
|
|
() => getDb().select().from(constellationConditions).where(eq(constellationConditions.guideId, row.guide.id)).orderBy(asc(constellationConditions.sortOrder)),
|
|
() => getDb().select().from(teamConditions).where(eq(teamConditions.guideId, row.guide.id)).orderBy(asc(teamConditions.sortOrder)),
|
|
() => getDb().select().from(teamCompositions).where(eq(teamCompositions.guideId, row.guide.id)).orderBy(asc(teamCompositions.sortOrder)),
|
|
() => getDb().select({
|
|
kind: glossaryAliases.kind,
|
|
catalogKey: glossaryAliases.catalogKey,
|
|
alias: glossaryAliases.alias,
|
|
}).from(glossaryAliases),
|
|
() => materialKeys.length
|
|
? getDb().select().from(catalogMaterials).where(inArray(catalogMaterials.key, materialKeys))
|
|
: Promise.resolve([]),
|
|
() => 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 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(
|
|
(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").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 hydratedTeams = teams.map((team) => ({ ...team, members: members.filter(({ member }) => member.teamId === team.id).map(({ member, character }) => { const weapon = memberWeapons.find((item) => item.key === member.weaponKey); const artifact = memberArtifacts.find((item) => item.key === member.artifactKey); const secondArtifact = memberArtifacts.find((item) => item.key === 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,
|
|
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 = secondArtifacts.find((item) => item.key === 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").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) {
|
|
return getGuideContent(slug, true);
|
|
}
|
|
|
|
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>>>;
|