feat(guides): add structured image-first editor
CI / Verify (push) Successful in 1m0s
CI / Build immutable images and deploy (push) Failing after 2m38s

This commit is contained in:
2026-08-29 13:30:52 +00:00 Unverified
parent b8e3b99033
commit dc5c667b4e
36 changed files with 5611 additions and 381 deletions
+232
View File
@@ -5,6 +5,7 @@ import {
index,
integer,
jsonb,
numeric,
pgTable,
primaryKey,
text,
@@ -421,6 +422,237 @@ export const outboxEvents = pgTable(
],
);
export const catalogVersions = pgTable(
"catalog_version",
{
version: varchar("version", { length: 32 }).primaryKey(),
active: boolean("active").default(false).notNull(),
sourceTimestamp: bigint("source_timestamp", { mode: "number" }),
syncedAt: timestamp("synced_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [uniqueIndex("catalog_active_unique").on(table.active).where(sql`${table.active} = true`)],
);
export const catalogCharacters = pgTable(
"catalog_character",
{
version: varchar("version", { length: 32 })
.notNull()
.references(() => catalogVersions.version, { onDelete: "cascade" }),
key: varchar("key", { length: 64 }).notNull(),
name: text("name").notNull(),
element: varchar("element", { length: 48 }),
rarity: integer("rarity"),
weaponType: varchar("weapon_type", { length: 64 }),
imageKey: text("image_key").notNull(),
},
(table) => [
primaryKey({ columns: [table.version, table.key] }),
index("catalog_character_name_idx").on(table.version, table.name),
],
);
export const catalogWeapons = pgTable(
"catalog_weapon",
{
version: varchar("version", { length: 32 })
.notNull()
.references(() => catalogVersions.version, { onDelete: "cascade" }),
key: varchar("key", { length: 64 }).notNull(),
name: text("name").notNull(),
rarity: integer("rarity"),
weaponType: varchar("weapon_type", { length: 64 }),
imageKey: text("image_key").notNull(),
},
(table) => [
primaryKey({ columns: [table.version, table.key] }),
index("catalog_weapon_name_idx").on(table.version, table.name),
],
);
export const catalogArtifacts = pgTable(
"catalog_artifact",
{
version: varchar("version", { length: 32 })
.notNull()
.references(() => catalogVersions.version, { onDelete: "cascade" }),
key: varchar("key", { length: 64 }).notNull(),
name: text("name").notNull(),
imageKey: text("image_key").notNull(),
},
(table) => [
primaryKey({ columns: [table.version, table.key] }),
index("catalog_artifact_name_idx").on(table.version, table.name),
],
);
export const catalogSyncJobs = pgTable(
"catalog_sync_job",
{
id: uuid("id").defaultRandom().primaryKey(),
status: varchar("status", { length: 24 }).default("pending").notNull(),
version: varchar("version", { length: 32 }),
progress: integer("progress").default(0).notNull(),
total: integer("total").default(0).notNull(),
error: text("error"),
triggeredById: text("triggered_by_id").references(() => users.id, { onDelete: "set null" }),
startedAt: timestamp("started_at", { withTimezone: true }),
completedAt: timestamp("completed_at", { withTimezone: true }),
...timestamps,
},
(table) => [index("catalog_sync_status_idx").on(table.status, table.createdAt)],
);
export const guides = pgTable(
"guide",
{
id: uuid("id").defaultRandom().primaryKey(),
characterKey: varchar("character_key", { length: 64 }).notNull(),
catalogVersion: varchar("catalog_version", { length: 32 })
.notNull()
.references(() => catalogVersions.version, { onDelete: "restrict" }),
slug: varchar("slug", { length: 120 }).notNull(),
name: text("name").notNull(),
overview: text("overview").notNull(),
coverMediaId: uuid("cover_media_id")
.notNull()
.references(() => media.id, { onDelete: "restrict" }),
version: integer("version").default(1).notNull(),
sortOrder: integer("sort_order").default(0).notNull(),
trashedAt: timestamp("trashed_at", { withTimezone: true }),
...timestamps,
},
(table) => [
uniqueIndex("guide_character_key_unique").on(table.characterKey),
uniqueIndex("guide_slug_unique").on(table.slug),
index("guide_directory_idx").on(table.trashedAt, table.sortOrder),
],
);
export const guideSections = pgTable(
"guide_section",
{
id: uuid("id").defaultRandom().primaryKey(),
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
kind: varchar("kind", { length: 32 }).notNull(),
slug: varchar("slug", { length: 120 }).notNull(),
title: text("title").notNull(),
enabled: boolean("enabled").default(true).notNull(),
sortOrder: integer("sort_order").default(0).notNull(),
},
(table) => [
uniqueIndex("guide_section_slug_unique").on(table.guideId, table.slug),
index("guide_section_order_idx").on(table.guideId, table.sortOrder),
],
);
export const weaponRecommendations = pgTable(
"weapon_recommendation",
{
id: uuid("id").defaultRandom().primaryKey(),
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
catalogVersion: varchar("catalog_version", { length: 32 }).notNull(),
weaponKey: varchar("weapon_key", { length: 64 }).notNull(),
refinement: integer("refinement").notNull(),
overallPercent: numeric("overall_percent", { precision: 8, scale: 2 }),
personalPercent: numeric("personal_percent", { precision: 8, scale: 2 }),
sortOrder: integer("sort_order").default(0).notNull(),
},
(table) => [index("weapon_recommendation_order_idx").on(table.guideId, table.sortOrder)],
);
export const artifactGroups = pgTable(
"artifact_group",
{
id: uuid("id").defaultRandom().primaryKey(),
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
name: text("name").notNull(),
sortOrder: integer("sort_order").default(0).notNull(),
},
(table) => [index("artifact_group_order_idx").on(table.guideId, table.sortOrder)],
);
export const artifactRecommendations = pgTable(
"artifact_recommendation",
{
id: uuid("id").defaultRandom().primaryKey(),
groupId: uuid("group_id").notNull().references(() => artifactGroups.id, { onDelete: "cascade" }),
catalogVersion: varchar("catalog_version", { length: 32 }).notNull(),
firstArtifactKey: varchar("first_artifact_key", { length: 64 }).notNull(),
secondArtifactKey: varchar("second_artifact_key", { length: 64 }),
percent: numeric("percent", { precision: 8, scale: 2 }).notNull(),
sortOrder: integer("sort_order").default(0).notNull(),
},
(table) => [index("artifact_recommendation_order_idx").on(table.groupId, table.sortOrder)],
);
export const artifactProfiles = pgTable("artifact_profile", {
guideId: uuid("guide_id").primaryKey().references(() => guides.id, { onDelete: "cascade" }),
sands: jsonb("sands").$type<string[]>().default([]).notNull(),
goblet: jsonb("goblet").$type<string[]>().default([]).notNull(),
circlet: jsonb("circlet").$type<string[]>().default([]).notNull(),
substats: jsonb("substats").$type<string[]>().default([]).notNull(),
targets: jsonb("targets").$type<Array<{ stat: string; value: string }>>().default([]).notNull(),
talents: jsonb("talents").$type<Array<{ type: "NA" | "E" | "Q"; priority: number; level?: string }>>().default([]).notNull(),
note: text("note"),
...timestamps,
});
export const constellationRecommendations = pgTable(
"constellation_recommendation",
{
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
level: integer("level").notNull(),
detail: text("detail").notNull(),
cumulativePercent: numeric("cumulative_percent", { precision: 8, scale: 2 }).notNull(),
},
(table) => [primaryKey({ columns: [table.guideId, table.level] })],
);
export const teamCompositions = pgTable(
"team_composition",
{
id: uuid("id").defaultRandom().primaryKey(),
guideId: uuid("guide_id").notNull().references(() => guides.id, { onDelete: "cascade" }),
title: text("title").notNull(),
rotationSeconds: numeric("rotation_seconds", { precision: 8, scale: 2 }).notNull(),
combo: text("combo").notNull(),
sortOrder: integer("sort_order").default(0).notNull(),
},
(table) => [index("team_composition_order_idx").on(table.guideId, table.sortOrder)],
);
export const teamMembers = pgTable(
"team_member",
{
teamId: uuid("team_id").notNull().references(() => teamCompositions.id, { onDelete: "cascade" }),
position: integer("position").notNull(),
catalogVersion: varchar("catalog_version", { length: 32 }).notNull(),
characterKey: varchar("character_key", { length: 64 }).notNull(),
damage: numeric("damage", { precision: 18, scale: 2 }).notNull(),
},
(table) => [primaryKey({ columns: [table.teamId, table.position] })],
);
export const extraSections = pgTable("extra_section", {
sectionId: uuid("section_id").primaryKey().references(() => guideSections.id, { onDelete: "cascade" }),
...timestamps,
});
export const extraSectionRows = pgTable(
"extra_section_row",
{
id: uuid("id").defaultRandom().primaryKey(),
sectionId: uuid("section_id").notNull().references(() => extraSections.sectionId, { onDelete: "cascade" }),
mediaId: uuid("media_id").references(() => media.id, { onDelete: "set null" }),
heading: text("heading").notNull(),
body: text("body").notNull(),
percent: numeric("percent", { precision: 8, scale: 2 }),
sortOrder: integer("sort_order").default(0).notNull(),
},
(table) => [index("extra_section_row_order_idx").on(table.sectionId, table.sortOrder)],
);
export const characterRelations = relations(characters, ({ many, one }) => ({
pages: many(pages),
portrait: one(media, {