import { relations, sql } from "drizzle-orm"; import { bigint, boolean, index, integer, jsonb, pgTable, primaryKey, text, timestamp, uniqueIndex, uuid, varchar, } from "drizzle-orm/pg-core"; 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. Application authorization still compares the // verified session email with ADMIN_EMAIL at every protected boundary. export const users = pgTable( "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 = pgTable( "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 = pgTable( "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 = pgTable( "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 = pgTable( "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 = pgTable( "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 = pgTable( "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 = pgTable( "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>().notNull(), responsiveLayout: jsonb("responsive_layout") .$type>() .default({}) .notNull(), sortOrder: integer("sort_order").default(0).notNull(), ...timestamps, }, (table) => [index("block_page_order_idx").on(table.pageId, table.sortOrder)], ); export const dataSources = pgTable( "data_source", { id: uuid("id").defaultRandom().primaryKey(), name: text("name").notNull(), description: text("description"), fields: jsonb("fields").$type().default([]).notNull(), currentVersion: integer("current_version").default(1).notNull(), ...timestamps, }, (table) => [index("data_source_name_idx").on(table.name)], ); export const dataSourceVersions = pgTable( "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().default([]).notNull(), evaluation: jsonb("evaluation").$type>(), 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 = pgTable( "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 = pgTable( "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>().notNull(), builtIn: boolean("built_in").default(false).notNull(), ...timestamps, }, (table) => [uniqueIndex("template_slug_unique").on(table.slug)], ); export const revisions = pgTable( "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>().notNull(), dataSourceVersions: jsonb("data_source_versions") .$type>() .default({}) .notNull(), mediaIds: jsonb("media_ids").$type().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 = pgTable( "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>() .default({}) .notNull(), vectorHash: varchar("vector_hash", { length: 64 }).notNull(), dto: jsonb("dto").$type>().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 = pgTable( "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 = pgTable( "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 = pgTable( "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>().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 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], }), }), );