718 lines
24 KiB
TypeScript
718 lines
24 KiB
TypeScript
import { relations, sql } from "drizzle-orm";
|
|
import {
|
|
bigint,
|
|
boolean,
|
|
index,
|
|
integer,
|
|
jsonb,
|
|
numeric,
|
|
pgSchema,
|
|
primaryKey,
|
|
text,
|
|
timestamp,
|
|
uniqueIndex,
|
|
uuid,
|
|
varchar,
|
|
} from "drizzle-orm/pg-core";
|
|
|
|
import type { CatalogConstellation, CatalogTalent } from "@/lib/catalog/lunaris";
|
|
|
|
const authSchema = pgSchema("auth");
|
|
const mediaSchema = pgSchema("media");
|
|
const editorSchema = pgSchema("editor");
|
|
const catalogSchema = pgSchema("catalog");
|
|
const guidesSchema = pgSchema("guides");
|
|
const operationsSchema = pgSchema("operations");
|
|
|
|
const timestamps = {
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.$onUpdate(() => new Date())
|
|
.notNull(),
|
|
};
|
|
|
|
// Better Auth core tables. Credential registration is restricted to
|
|
// ADMIN_EMAIL, which is rechecked at every protected application boundary.
|
|
export const users = authSchema.table(
|
|
"user",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
name: text("name").notNull(),
|
|
email: text("email").notNull(),
|
|
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
image: text("image"),
|
|
...timestamps,
|
|
},
|
|
(table) => [uniqueIndex("user_email_unique").on(table.email)],
|
|
);
|
|
|
|
export const sessions = authSchema.table(
|
|
"session",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
token: text("token").notNull(),
|
|
ipAddress: text("ip_address"),
|
|
userAgent: text("user_agent"),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: "cascade" }),
|
|
...timestamps,
|
|
},
|
|
(table) => [
|
|
uniqueIndex("session_token_unique").on(table.token),
|
|
index("session_user_id_idx").on(table.userId),
|
|
],
|
|
);
|
|
|
|
export const accounts = authSchema.table(
|
|
"account",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
issuer: text("issuer").notNull(),
|
|
accountId: text("account_id").notNull(),
|
|
providerId: text("provider_id").notNull(),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: "cascade" }),
|
|
accessToken: text("access_token"),
|
|
refreshToken: text("refresh_token"),
|
|
idToken: text("id_token"),
|
|
accessTokenExpiresAt: timestamp("access_token_expires_at", {
|
|
withTimezone: true,
|
|
}),
|
|
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", {
|
|
withTimezone: true,
|
|
}),
|
|
scope: text("scope"),
|
|
password: text("password"),
|
|
...timestamps,
|
|
},
|
|
(table) => [
|
|
uniqueIndex("account_issuer_unique").on(table.issuer, table.accountId),
|
|
index("account_user_id_idx").on(table.userId),
|
|
],
|
|
);
|
|
|
|
export const verifications = authSchema.table(
|
|
"verification",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
identifier: text("identifier").notNull(),
|
|
value: text("value").notNull(),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
...timestamps,
|
|
},
|
|
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
|
);
|
|
|
|
export const media = mediaSchema.table(
|
|
"media",
|
|
{
|
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
objectKey: text("object_key").notNull(),
|
|
fileName: text("file_name").notNull(),
|
|
mimeType: varchar("mime_type", { length: 64 }).notNull(),
|
|
byteSize: bigint("byte_size", { mode: "number" }).notNull(),
|
|
checksum: varchar("checksum", { length: 128 }),
|
|
width: integer("width"),
|
|
height: integer("height"),
|
|
status: varchar("status", { length: 24 }).default("pending").notNull(),
|
|
currentReferenceCount: integer("current_reference_count")
|
|
.default(0)
|
|
.notNull(),
|
|
revisionReferenceCount: integer("revision_reference_count")
|
|
.default(0)
|
|
.notNull(),
|
|
createdById: text("created_by_id").references(() => users.id, {
|
|
onDelete: "set null",
|
|
}),
|
|
...timestamps,
|
|
},
|
|
(table) => [
|
|
uniqueIndex("media_object_key_unique").on(table.objectKey),
|
|
index("media_visibility_idx").on(
|
|
table.status,
|
|
table.currentReferenceCount,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const characters = editorSchema.table(
|
|
"character",
|
|
{
|
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
slug: varchar("slug", { length: 120 }).notNull(),
|
|
nameTh: text("name_th").notNull(),
|
|
nameEn: text("name_en"),
|
|
description: text("description"),
|
|
portraitMediaId: uuid("portrait_media_id").references(() => media.id, {
|
|
onDelete: "set null",
|
|
}),
|
|
element: varchar("element", { length: 48 }),
|
|
role: varchar("role", { length: 80 }),
|
|
rarity: integer("rarity"),
|
|
sortOrder: integer("sort_order").default(0).notNull(),
|
|
visible: boolean("visible").default(false).notNull(),
|
|
...timestamps,
|
|
},
|
|
(table) => [
|
|
uniqueIndex("character_slug_unique").on(table.slug),
|
|
index("character_directory_idx").on(table.visible, table.sortOrder),
|
|
],
|
|
);
|
|
|
|
export const pages = editorSchema.table(
|
|
"page",
|
|
{
|
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
characterId: uuid("character_id")
|
|
.notNull()
|
|
.references(() => characters.id, { onDelete: "cascade" }),
|
|
slug: varchar("slug", { length: 120 }).notNull(),
|
|
title: text("title").notNull(),
|
|
navOrder: integer("nav_order").default(0).notNull(),
|
|
visible: boolean("visible").default(false).notNull(),
|
|
publicNote: text("public_note"),
|
|
version: integer("version").default(1).notNull(),
|
|
...timestamps,
|
|
},
|
|
(table) => [
|
|
uniqueIndex("page_character_slug_unique").on(
|
|
table.characterId,
|
|
table.slug,
|
|
),
|
|
index("page_navigation_idx").on(
|
|
table.characterId,
|
|
table.visible,
|
|
table.navOrder,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const blocks = editorSchema.table(
|
|
"block",
|
|
{
|
|
id: uuid("id").primaryKey(),
|
|
pageId: uuid("page_id")
|
|
.notNull()
|
|
.references(() => pages.id, { onDelete: "cascade" }),
|
|
type: varchar("type", { length: 64 }).notNull(),
|
|
schemaVersion: integer("schema_version").default(1).notNull(),
|
|
config: jsonb("config").$type<Record<string, unknown>>().notNull(),
|
|
responsiveLayout: jsonb("responsive_layout")
|
|
.$type<Record<string, unknown>>()
|
|
.default({})
|
|
.notNull(),
|
|
sortOrder: integer("sort_order").default(0).notNull(),
|
|
...timestamps,
|
|
},
|
|
(table) => [index("block_page_order_idx").on(table.pageId, table.sortOrder)],
|
|
);
|
|
|
|
export const dataSources = editorSchema.table(
|
|
"data_source",
|
|
{
|
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
name: text("name").notNull(),
|
|
description: text("description"),
|
|
fields: jsonb("fields").$type<unknown[]>().default([]).notNull(),
|
|
currentVersion: integer("current_version").default(1).notNull(),
|
|
...timestamps,
|
|
},
|
|
(table) => [index("data_source_name_idx").on(table.name)],
|
|
);
|
|
|
|
export const dataSourceVersions = editorSchema.table(
|
|
"data_source_version",
|
|
{
|
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
dataSourceId: uuid("data_source_id")
|
|
.notNull()
|
|
.references(() => dataSources.id, { onDelete: "cascade" }),
|
|
version: integer("version").notNull(),
|
|
rows: jsonb("rows").$type<unknown[]>().default([]).notNull(),
|
|
evaluation: jsonb("evaluation").$type<Record<string, unknown>>(),
|
|
createdById: text("created_by_id").references(() => users.id, {
|
|
onDelete: "set null",
|
|
}),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(table) => [
|
|
uniqueIndex("data_source_version_unique").on(
|
|
table.dataSourceId,
|
|
table.version,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const pageDataSources = editorSchema.table(
|
|
"page_data_source",
|
|
{
|
|
pageId: uuid("page_id")
|
|
.notNull()
|
|
.references(() => pages.id, { onDelete: "cascade" }),
|
|
dataSourceId: uuid("data_source_id")
|
|
.notNull()
|
|
.references(() => dataSources.id, { onDelete: "restrict" }),
|
|
pinnedVersion: integer("pinned_version"),
|
|
},
|
|
(table) => [
|
|
primaryKey({ columns: [table.pageId, table.dataSourceId] }),
|
|
index("page_data_source_dependency_idx").on(table.dataSourceId),
|
|
],
|
|
);
|
|
|
|
export const templates = editorSchema.table(
|
|
"template",
|
|
{
|
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
slug: varchar("slug", { length: 120 }).notNull(),
|
|
name: text("name").notNull(),
|
|
description: text("description"),
|
|
category: varchar("category", { length: 64 }).notNull(),
|
|
structure: jsonb("structure").$type<Record<string, unknown>>().notNull(),
|
|
builtIn: boolean("built_in").default(false).notNull(),
|
|
...timestamps,
|
|
},
|
|
(table) => [uniqueIndex("template_slug_unique").on(table.slug)],
|
|
);
|
|
|
|
export const revisions = editorSchema.table(
|
|
"revision",
|
|
{
|
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
pageId: uuid("page_id")
|
|
.notNull()
|
|
.references(() => pages.id, { onDelete: "cascade" }),
|
|
pageVersion: integer("page_version").notNull(),
|
|
kind: varchar("kind", { length: 24 }).notNull(),
|
|
label: text("label"),
|
|
authorId: text("author_id").references(() => users.id, {
|
|
onDelete: "set null",
|
|
}),
|
|
sourceRevisionId: uuid("source_revision_id"),
|
|
checkpointHour: timestamp("checkpoint_hour", { withTimezone: true }),
|
|
snapshot: jsonb("snapshot").$type<Record<string, unknown>>().notNull(),
|
|
dataSourceVersions: jsonb("data_source_versions")
|
|
.$type<Record<string, number>>()
|
|
.default({})
|
|
.notNull(),
|
|
mediaIds: jsonb("media_ids").$type<string[]>().default([]).notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
},
|
|
(table) => [
|
|
index("revision_page_created_idx").on(table.pageId, table.createdAt),
|
|
uniqueIndex("revision_checkpoint_hour_unique")
|
|
.on(table.pageId, table.checkpointHour)
|
|
.where(sql`${table.kind} = 'checkpoint'`),
|
|
],
|
|
);
|
|
|
|
export const publicSnapshots = editorSchema.table(
|
|
"public_snapshot",
|
|
{
|
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
pageId: uuid("page_id")
|
|
.notNull()
|
|
.references(() => pages.id, { onDelete: "cascade" }),
|
|
pageVersion: integer("page_version").notNull(),
|
|
versionVector: jsonb("version_vector")
|
|
.$type<Record<string, number>>()
|
|
.default({})
|
|
.notNull(),
|
|
vectorHash: varchar("vector_hash", { length: 64 }).notNull(),
|
|
dto: jsonb("dto").$type<Record<string, unknown>>().notNull(),
|
|
visible: boolean("visible").default(false).notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(table) => [
|
|
uniqueIndex("public_snapshot_version_unique").on(
|
|
table.pageId,
|
|
table.pageVersion,
|
|
table.vectorHash,
|
|
),
|
|
index("public_snapshot_lookup_idx").on(
|
|
table.pageId,
|
|
table.visible,
|
|
table.createdAt,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const slugAliases = editorSchema.table(
|
|
"slug_alias",
|
|
{
|
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
kind: varchar("kind", { length: 24 }).notNull(),
|
|
oldCharacterSlug: varchar("old_character_slug", { length: 120 }).notNull(),
|
|
oldPageSlug: varchar("old_page_slug", { length: 120 }),
|
|
targetCharacterId: uuid("target_character_id")
|
|
.notNull()
|
|
.references(() => characters.id, { onDelete: "cascade" }),
|
|
targetPageId: uuid("target_page_id").references(() => pages.id, {
|
|
onDelete: "cascade",
|
|
}),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(table) => [
|
|
uniqueIndex("slug_alias_path_unique").on(
|
|
table.oldCharacterSlug,
|
|
table.oldPageSlug,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const mediaReferences = mediaSchema.table(
|
|
"media_reference",
|
|
{
|
|
mediaId: uuid("media_id")
|
|
.notNull()
|
|
.references(() => media.id, { onDelete: "cascade" }),
|
|
ownerType: varchar("owner_type", { length: 24 }).notNull(),
|
|
ownerId: uuid("owner_id").notNull(),
|
|
referenceKind: varchar("reference_kind", { length: 24 }).notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(table) => [
|
|
primaryKey({
|
|
columns: [
|
|
table.mediaId,
|
|
table.ownerType,
|
|
table.ownerId,
|
|
table.referenceKind,
|
|
],
|
|
}),
|
|
index("media_reference_owner_idx").on(table.ownerType, table.ownerId),
|
|
],
|
|
);
|
|
|
|
export const outboxEvents = operationsSchema.table(
|
|
"outbox_event",
|
|
{
|
|
id: bigint("id", { mode: "number" })
|
|
.primaryKey()
|
|
.generatedAlwaysAsIdentity(),
|
|
topic: varchar("topic", { length: 160 }).notNull(),
|
|
aggregateId: uuid("aggregate_id").notNull(),
|
|
eventType: varchar("event_type", { length: 64 }).notNull(),
|
|
payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
|
|
status: varchar("status", { length: 24 }).default("pending").notNull(),
|
|
attempts: integer("attempts").default(0).notNull(),
|
|
availableAt: timestamp("available_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
processedAt: timestamp("processed_at", { withTimezone: true }),
|
|
lastError: text("last_error"),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(table) => [
|
|
index("outbox_pending_idx").on(
|
|
table.status,
|
|
table.availableAt,
|
|
table.id,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const catalogVersions = catalogSchema.table(
|
|
"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 = catalogSchema.table(
|
|
"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(),
|
|
elementIconKey: text("element_icon_key"),
|
|
coopImageKey: text("coop_image_key"),
|
|
nameCardImageKey: text("name_card_image_key"),
|
|
constellations: jsonb("constellations").$type<CatalogConstellation[]>().default([]).notNull(),
|
|
talents: jsonb("talents").$type<CatalogTalent[]>().default([]).notNull(),
|
|
},
|
|
(table) => [
|
|
primaryKey({ columns: [table.version, table.key] }),
|
|
index("catalog_character_name_idx").on(table.version, table.name),
|
|
],
|
|
);
|
|
|
|
export const catalogWeapons = catalogSchema.table(
|
|
"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 = catalogSchema.table(
|
|
"catalog_artifact",
|
|
{
|
|
version: varchar("version", { length: 32 })
|
|
.notNull()
|
|
.references(() => catalogVersions.version, { onDelete: "cascade" }),
|
|
key: varchar("key", { length: 64 }).notNull(),
|
|
name: text("name").notNull(),
|
|
rarity: integer("rarity"),
|
|
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 = catalogSchema.table(
|
|
"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 = guidesSchema.table(
|
|
"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 = guidesSchema.table(
|
|
"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 = guidesSchema.table(
|
|
"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 }),
|
|
note: text("note"),
|
|
sortOrder: integer("sort_order").default(0).notNull(),
|
|
},
|
|
(table) => [index("weapon_recommendation_order_idx").on(table.guideId, table.sortOrder)],
|
|
);
|
|
|
|
export const artifactGroups = guidesSchema.table(
|
|
"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 = guidesSchema.table(
|
|
"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(),
|
|
note: text("note"),
|
|
sortOrder: integer("sort_order").default(0).notNull(),
|
|
},
|
|
(table) => [index("artifact_recommendation_order_idx").on(table.groupId, table.sortOrder)],
|
|
);
|
|
|
|
export const artifactProfiles = guidesSchema.table("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; relationToNext?: "gt" | "gte" }>>().default([]).notNull(),
|
|
note: text("note"),
|
|
...timestamps,
|
|
});
|
|
|
|
export const constellationRecommendations = guidesSchema.table(
|
|
"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(),
|
|
isGreat: boolean("is_great").default(false).notNull(),
|
|
},
|
|
(table) => [primaryKey({ columns: [table.guideId, table.level] })],
|
|
);
|
|
|
|
export const teamCompositions = guidesSchema.table(
|
|
"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 = guidesSchema.table(
|
|
"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(),
|
|
weaponKey: varchar("weapon_key", { length: 64 }),
|
|
artifactKey: varchar("artifact_key", { length: 64 }),
|
|
constellation: integer("constellation").default(0).notNull(),
|
|
refinement: integer("refinement").default(1).notNull(),
|
|
damage: numeric("damage", { precision: 18, scale: 2 }).notNull(),
|
|
},
|
|
(table) => [primaryKey({ columns: [table.teamId, table.position] })],
|
|
);
|
|
|
|
export const extraSections = guidesSchema.table("extra_section", {
|
|
sectionId: uuid("section_id").primaryKey().references(() => guideSections.id, { onDelete: "cascade" }),
|
|
...timestamps,
|
|
});
|
|
|
|
export const extraSectionRows = guidesSchema.table(
|
|
"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" }),
|
|
ratio: varchar("ratio", { length: 5 }).default("16:9").notNull(),
|
|
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, {
|
|
fields: [characters.portraitMediaId],
|
|
references: [media.id],
|
|
}),
|
|
}));
|
|
|
|
export const pageRelations = relations(pages, ({ many, one }) => ({
|
|
character: one(characters, {
|
|
fields: [pages.characterId],
|
|
references: [characters.id],
|
|
}),
|
|
blocks: many(blocks),
|
|
dependencies: many(pageDataSources),
|
|
revisions: many(revisions),
|
|
}));
|
|
|
|
export const blockRelations = relations(blocks, ({ one }) => ({
|
|
page: one(pages, {
|
|
fields: [blocks.pageId],
|
|
references: [pages.id],
|
|
}),
|
|
}));
|
|
|
|
export const dataSourceRelations = relations(dataSources, ({ many }) => ({
|
|
versions: many(dataSourceVersions),
|
|
pageDependencies: many(pageDataSources),
|
|
}));
|
|
|
|
export const dataSourceVersionRelations = relations(
|
|
dataSourceVersions,
|
|
({ one }) => ({
|
|
dataSource: one(dataSources, {
|
|
fields: [dataSourceVersions.dataSourceId],
|
|
references: [dataSources.id],
|
|
}),
|
|
}),
|
|
);
|