perf : increase admin page speed
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({ reads: [] as unknown[], rows: new Map<unknown, unknown[]>() }));
|
||||
vi.mock("server-only", () => ({}));
|
||||
vi.mock("@/lib/media/storage", () => ({ publicMediaUrl: (key: string) => `/media/${key}` }));
|
||||
vi.mock("@/db", () => ({
|
||||
getDb: () => ({
|
||||
select: () => {
|
||||
let table: unknown;
|
||||
const query = {
|
||||
from(value: unknown) { table = value; return query; },
|
||||
innerJoin() { return query; },
|
||||
leftJoin() { return query; },
|
||||
where() { return query; },
|
||||
orderBy() { return query; },
|
||||
limit() { return query; },
|
||||
then(resolve: (rows: unknown[]) => unknown) {
|
||||
mocks.reads.push(table);
|
||||
return Promise.resolve(mocks.rows.get(table) ?? []).then(resolve);
|
||||
},
|
||||
};
|
||||
return query;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
import {
|
||||
artifactConditions, constellationConditions, extraSectionRows,
|
||||
guides, guideSections, teamConditions, weaponConditions,
|
||||
catalogMaterials, guidePosterBleeding,
|
||||
} from "@/db/schema";
|
||||
import { getAdminGuideContent } from "./queries";
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.reads.length = 0;
|
||||
mocks.rows.clear();
|
||||
mocks.rows.set(guides, [{
|
||||
guide: { id: "guide", slug: "test", posterArtMediaId: null },
|
||||
cover: { objectKey: "cover", status: "ready" },
|
||||
character: { key: "test", imageKey: "character", constellations: [], talents: [], ascension: [], leveling: [] },
|
||||
}]);
|
||||
mocks.rows.set(guideSections, [
|
||||
{ id: "extra-a", slug: "extra-a", kind: "extra" },
|
||||
{ id: "extra-b", slug: "extra-b", kind: "extra" },
|
||||
]);
|
||||
});
|
||||
|
||||
describe("admin section reads", () => {
|
||||
it.each([
|
||||
["weapon", weaponConditions],
|
||||
["artifact", artifactConditions],
|
||||
["constellations", constellationConditions],
|
||||
["team", teamConditions],
|
||||
])("loads only the %s section's conditions", async (section, expectedTable) => {
|
||||
const result = await getAdminGuideContent("test", section);
|
||||
expect(result).not.toBeNull();
|
||||
expect(mocks.reads).toContain(expectedTable);
|
||||
for (const table of [weaponConditions, artifactConditions, constellationConditions, teamConditions]) {
|
||||
if (table !== expectedTable) expect(mocks.reads).not.toContain(table);
|
||||
}
|
||||
expect(mocks.reads).not.toContain(extraSectionRows);
|
||||
expect(mocks.reads).not.toContain(catalogMaterials);
|
||||
expect(mocks.reads).not.toContain(guidePosterBleeding);
|
||||
});
|
||||
|
||||
it("loads just the selected extra section", async () => {
|
||||
const result = await getAdminGuideContent("test", "extra-b");
|
||||
expect(result?.extras.map((extra) => extra.id)).toEqual(["extra-b"]);
|
||||
expect(mocks.reads).toContain(extraSectionRows);
|
||||
expect(mocks.reads).not.toContain(weaponConditions);
|
||||
expect(mocks.reads).not.toContain(teamConditions);
|
||||
});
|
||||
|
||||
it("preserves full-guide reads for existing preview and validation callers", async () => {
|
||||
const result = await getAdminGuideContent("test");
|
||||
for (const table of [weaponConditions, artifactConditions, constellationConditions, teamConditions, extraSectionRows]) {
|
||||
expect(mocks.reads).toContain(table);
|
||||
}
|
||||
expect(result?.extras).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
+35
-16
@@ -1,7 +1,6 @@
|
||||
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 {
|
||||
@@ -27,11 +26,10 @@ import {
|
||||
weaponRecommendations,
|
||||
weaponConditions,
|
||||
} from "@/db/schema";
|
||||
import { getMediaStorage, publicMediaUrl } from "@/lib/media/storage";
|
||||
import { 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.
|
||||
@@ -121,17 +119,7 @@ export async function getActiveCatalog() {
|
||||
if (!characters.length) return null;
|
||||
const groupedAliases = aliasesByItem(aliases);
|
||||
const materialByKey = new Map(materials.map((item) => [item.key, item]));
|
||||
let version = "unknown";
|
||||
try {
|
||||
const storage = await getMediaStorage();
|
||||
version = catalogVersionSchema.parse(
|
||||
await storage.file("catalog/current.json").json(),
|
||||
).version;
|
||||
} catch (error) {
|
||||
console.error("Unable to read the active catalog version", error);
|
||||
}
|
||||
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) })),
|
||||
@@ -139,6 +127,37 @@ export async function getActiveCatalog() {
|
||||
};
|
||||
}
|
||||
|
||||
// Picker data deliberately excludes character descriptions, talents and materials.
|
||||
// Editors obtain the selected character's details from their guide content.
|
||||
export async function getCatalogOptions() {
|
||||
const [characters, weapons, artifacts, aliases] = await runWithConcurrency([
|
||||
() => getDb().select({
|
||||
key: catalogCharacters.key,
|
||||
name: catalogCharacters.name,
|
||||
rarity: catalogCharacters.rarity,
|
||||
weaponType: catalogCharacters.weaponType,
|
||||
element: catalogCharacters.element,
|
||||
imageKey: catalogCharacters.imageKey,
|
||||
elementIconKey: catalogCharacters.elementIconKey,
|
||||
}).from(catalogCharacters).orderBy(asc(catalogCharacters.name)),
|
||||
() => getDb().select().from(catalogWeapons).orderBy(asc(catalogWeapons.name)),
|
||||
() => getDb().select().from(catalogArtifacts).orderBy(asc(catalogArtifacts.name)),
|
||||
() => getDb().select().from(glossaryAliases),
|
||||
]);
|
||||
if (!characters.length) return null;
|
||||
const groupedAliases = aliasesByItem(aliases);
|
||||
return {
|
||||
characters: characters.map((item) => ({
|
||||
...item,
|
||||
imageUrl: publicMediaUrl(item.imageKey),
|
||||
elementIconUrl: item.elementIconKey ? publicMediaUrl(item.elementIconKey) : null,
|
||||
aliases: groupedAliases.character.get(item.key) ?? [],
|
||||
})),
|
||||
weapons: weapons.map((item) => ({ ...item, imageUrl: publicMediaUrl(item.imageKey), aliases: groupedAliases.weapon.get(item.key) ?? [] })),
|
||||
artifacts: artifacts.map((item) => ({ ...item, imageUrl: publicMediaUrl(item.imageKey), aliases: groupedAliases.artifact.get(item.key) ?? [] })),
|
||||
};
|
||||
}
|
||||
|
||||
export async function listGuideCards(includeTrash = false) {
|
||||
const rows = await getDb()
|
||||
.select({
|
||||
@@ -375,7 +394,7 @@ async function getGuideContent(slug: string, publicOnly: boolean, selectedSectio
|
||||
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 wants = (section: string) => selectedSection === undefined || selectedSection === section;
|
||||
const materialKeys = [...new Set(
|
||||
[...row.character.ascension, ...row.character.leveling].flatMap((group) => group.items.map((item) => item.key)),
|
||||
)];
|
||||
@@ -506,8 +525,8 @@ export function getPublicGuide(slug: string, section = "overview") {
|
||||
return getGuideContent(slug, true, section);
|
||||
}
|
||||
|
||||
export function getAdminGuideContent(slug: string) {
|
||||
return getGuideContent(slug, false);
|
||||
export function getAdminGuideContent(slug: string, section?: string) {
|
||||
return getGuideContent(slug, false, section);
|
||||
}
|
||||
|
||||
export async function getAdminGuideContentById(guideId: string) {
|
||||
|
||||
Reference in New Issue
Block a user