feat(artifact) : add tier-list conditions
CI / Build immutable images and deploy (push) Successful in 6m36s
CI / Build immutable images and deploy (push) Successful in 6m36s
This commit is contained in:
+11
-6
@@ -5,6 +5,7 @@ import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
||||
import { getDb } from "@/db";
|
||||
import {
|
||||
artifactGroups,
|
||||
artifactConditions,
|
||||
artifactProfiles,
|
||||
artifactRecommendations,
|
||||
catalogArtifacts,
|
||||
@@ -96,9 +97,10 @@ export async function createGuide(input: unknown) {
|
||||
sortOrder: order.maximum + 10,
|
||||
}).returning();
|
||||
await tx.insert(guideSections).values(DEFAULT_SECTIONS.map((section) => ({ ...section, guideId: guide.id })));
|
||||
const [condition] = await tx.insert(artifactConditions).values({ guideId: guide.id, sortOrder: 0 }).returning();
|
||||
await tx.insert(artifactGroups).values([
|
||||
{ guideId: guide.id, name: "Late Game", sortOrder: 0 },
|
||||
{ guideId: guide.id, name: "Early Game", sortOrder: 1 },
|
||||
{ conditionId: condition.id, name: "Late Game", sortOrder: 0 },
|
||||
{ conditionId: condition.id, name: "Early Game", sortOrder: 1 },
|
||||
]);
|
||||
await tx.insert(artifactProfiles).values({ guideId: guide.id });
|
||||
await tx.update(media).set({ currentReferenceCount: sql`${media.currentReferenceCount} + 1` }).where(eq(media.id, parsed.coverMediaId));
|
||||
@@ -213,10 +215,13 @@ export async function saveArtifacts(guideId: string, input: unknown, details?: G
|
||||
if (!guide) throw new Error("ไม่พบ Guide");
|
||||
return getDb().transaction(async (tx) => {
|
||||
const updated = await advanceGuideVersion(tx, guideId, data.expectedVersion, false, "artifact", details);
|
||||
await tx.delete(artifactGroups).where(eq(artifactGroups.guideId, guideId));
|
||||
for (const [sortOrder, group] of data.groups.entries()) {
|
||||
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, sortOrder: rowOrder })));
|
||||
await tx.delete(artifactConditions).where(eq(artifactConditions.guideId, guideId));
|
||||
for (const [conditionOrder, condition] of data.groups.entries()) {
|
||||
const [createdCondition] = await tx.insert(artifactConditions).values({ guideId, name: condition.name, note: condition.note, sortOrder: conditionOrder }).returning();
|
||||
for (const [sortOrder, group] of condition.tiers.entries()) {
|
||||
const [created] = await tx.insert(artifactGroups).values({ conditionId: createdCondition.id, 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, sortOrder: rowOrder })));
|
||||
}
|
||||
}
|
||||
await tx.insert(artifactProfiles).values({ guideId, sands: data.sands, goblet: data.goblet, circlet: data.circlet, substats: data.substats, recommendedConditions: data.recommendedGroups ?? [{ name: null, note: null, sands: data.sands, goblet: data.goblet, circlet: data.circlet, substats: data.substats }], targets: data.targetGroups[0]?.rows ?? [], targetConditions: data.targetGroups, talents: data.talents, note: data.note }).onConflictDoUpdate({ target: artifactProfiles.guideId, set: { sands: data.sands, goblet: data.goblet, circlet: data.circlet, substats: data.substats, recommendedConditions: data.recommendedGroups ?? [{ name: null, note: null, sands: data.sands, goblet: data.goblet, circlet: data.circlet, substats: data.substats }], targets: data.targetGroups[0]?.rows ?? [], targetConditions: data.targetGroups, talents: data.talents, note: data.note } });
|
||||
return updated;
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as z from "zod";
|
||||
import { getDb } from "@/db";
|
||||
import {
|
||||
artifactGroups,
|
||||
artifactConditions,
|
||||
artifactProfiles,
|
||||
artifactRecommendations,
|
||||
catalogArtifacts,
|
||||
@@ -329,10 +330,10 @@ async function getGuideContent(slug: string, publicOnly: boolean) {
|
||||
const materialKeys = [...new Set(
|
||||
[...row.character.ascension, ...row.character.leveling].flatMap((group) => group.items.map((item) => item.key)),
|
||||
)];
|
||||
const [sections, weaponConditionsRows, groups, profile, constellationGroups, teamConditionRows, teams, aliases, materials, poster, bleeding] = await runWithConcurrency([
|
||||
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(artifactGroups).where(eq(artifactGroups.guideId, row.guide.id)).orderBy(asc(artifactGroups.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)),
|
||||
@@ -350,6 +351,7 @@ async function getGuideContent(slug: string, publicOnly: boolean) {
|
||||
: 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]));
|
||||
|
||||
@@ -404,7 +406,7 @@ async function getGuideContent(slug: string, publicOnly: boolean) {
|
||||
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: 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, aliases: groupedAliases.artifact.get(artifact.key) ?? [], imageUrl: publicMediaUrl(artifact.imageKey) }, second: second ? { ...second, aliases: groupedAliases.artifact.get(second.key) ?? [], imageUrl: publicMediaUrl(second.imageKey) } : null }; }) })),
|
||||
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,
|
||||
|
||||
@@ -21,10 +21,11 @@ import {
|
||||
} from "@/lib/guides/stats";
|
||||
|
||||
describe("structured guide validation", () => {
|
||||
const artifactGroups = [
|
||||
const artifactTiers = [
|
||||
{ name: "Late Game", rows: [] },
|
||||
{ name: "Early Game", rows: [] },
|
||||
] as const;
|
||||
const artifactGroups = [{ name: null, note: null, tiers: artifactTiers }] as const;
|
||||
it("allows creating a guide without an overview", () => {
|
||||
expect(createGuideSchema.safeParse({ characterKey: "1", coverMediaId: "550e8400-e29b-41d4-a716-446655440000", overview: "" }).success).toBe(true);
|
||||
});
|
||||
@@ -120,8 +121,8 @@ describe("structured guide validation", () => {
|
||||
it("limits an optional note on each artifact recommendation", () => {
|
||||
const row = { firstArtifactKey: "1", secondArtifactKey: null, percent: 100 };
|
||||
const base = { expectedVersion: 1, sands: [], goblet: [], circlet: [], substats: [], targetGroups: [], talents: [], note: null };
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: "Late Game", rows: [{ ...row, note: null }] }, artifactGroups[1]] }).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: "Late Game", rows: [{ ...row, note: "x".repeat(2_001) }] }, artifactGroups[1]] }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: null, note: null, tiers: [{ name: "Late Game", rows: [{ ...row, note: null }] }, artifactTiers[1]] }] }).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: null, note: null, tiers: [{ name: "Late Game", rows: [{ ...row, note: "x".repeat(2_001) }] }, artifactTiers[1]] }] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts fixed target stats with text values", () => {
|
||||
@@ -140,8 +141,20 @@ describe("structured guide validation", () => {
|
||||
it("requires the fixed Late Game and Early Game artifact groups", () => {
|
||||
const base = { expectedVersion: 1, sands: [], goblet: [], circlet: [], substats: [], targetGroups: [], talents: [], note: null };
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: artifactGroups }).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [...artifactGroups].reverse() }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [...artifactGroups, { name: "Other", rows: [] }] }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: null, note: null, tiers: [...artifactTiers].reverse() }] }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: null, note: null, tiers: [...artifactTiers, { name: "Other", rows: [] }] }] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("supports named artifact tier-list conditions", () => {
|
||||
const base = { expectedVersion: 1, sands: [], goblet: [], circlet: [], substats: [], targetGroups: [], talents: [], note: null };
|
||||
const result = artifactSectionSchema.parse({ ...base, groups: [
|
||||
{ name: " On field ", note: "Best damage", tiers: artifactTiers },
|
||||
{ name: "", note: null, tiers: artifactTiers },
|
||||
] });
|
||||
|
||||
expect(result.groups.map((group) => group.name)).toEqual(["On field", null]);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [] }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: "x".repeat(121), note: null, tiers: artifactTiers }] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts only talent target levels from 1 through 10", () => {
|
||||
|
||||
@@ -89,10 +89,14 @@ const artifactGroupSchema = (name: "Late Game" | "Early Game") => z.strictObject
|
||||
|
||||
export const artifactSectionSchema = z.strictObject({
|
||||
expectedVersion: z.number().int().positive(),
|
||||
groups: z.tuple([
|
||||
artifactGroupSchema("Late Game"),
|
||||
artifactGroupSchema("Early Game"),
|
||||
]),
|
||||
groups: z.array(z.strictObject({
|
||||
name: conditionName,
|
||||
note: z.string().trim().max(2_000).nullable(),
|
||||
tiers: z.tuple([
|
||||
artifactGroupSchema("Late Game"),
|
||||
artifactGroupSchema("Early Game"),
|
||||
]),
|
||||
})).min(1).max(20),
|
||||
sands: z.array(z.enum(mainStatOptions.sands)).max(3),
|
||||
goblet: z.array(z.enum(mainStatOptions.goblet)).max(3),
|
||||
circlet: z.array(z.enum(mainStatOptions.circlet)).max(3),
|
||||
|
||||
Reference in New Issue
Block a user