feat(guides): add structured image-first editor
This commit is contained in:
+232
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user