import { sql } from "drizzle-orm"; import { boolean, customType, index, integer, jsonb, pgTable, primaryKey, text, timestamp, uniqueIndex, uuid, } from "drizzle-orm/pg-core"; export const tsvector = customType<{ data: string }>({ dataType() { return "tsvector"; }, }); export const series = pgTable("series", { id: uuid("id").primaryKey().defaultRandom(), slug: text("slug").notNull().unique(), name: text("name").notNull(), description: text("description").notNull().default(""), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }); export const topics = pgTable("topics", { id: uuid("id").primaryKey().defaultRandom(), slug: text("slug").notNull().unique(), name: text("name").notNull(), description: text("description").notNull().default(""), parentId: uuid("parent_id"), sortOrder: integer("sort_order").notNull().default(0), }); export const papers = pgTable( "papers", { id: uuid("id").primaryKey().defaultRandom(), publicId: text("public_id").notNull().unique(), // AS-WP-2026-0001 slug: text("slug").notNull().unique(), title: text("title").notNull(), status: text("status").notNull().default("published"), discipline: text("discipline").notNull(), researchMode: text("research_mode").notNull(), currentVersionId: uuid("current_version_id"), seriesId: uuid("series_id").references(() => series.id), firstPublishedAt: timestamp("first_published_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), retractedAt: timestamp("retracted_at", { withTimezone: true }), synthetic: boolean("synthetic").notNull().default(false), }, (t) => [ index("papers_status_idx").on(t.status, t.firstPublishedAt), index("papers_discipline_idx").on(t.discipline), ], ); export const paperVersions = pgTable( "paper_versions", { id: uuid("id").primaryKey().defaultRandom(), paperId: uuid("paper_id") .notNull() .references(() => papers.id, { onDelete: "cascade" }), versionNo: integer("version_no").notNull(), runId: uuid("run_id").notNull(), title: text("title").notNull(), abstract: text("abstract").notNull(), keywords: text("keywords").array().notNull().default([]), metadata: jsonb("metadata").$type>().notNull().default({}), artifactSet: jsonb("artifact_set") .$type< Array<{ path: string; kind: string; sha256: string; bytes: number; contentType: string; publicPath: string; }> >() .notNull() .default([]), pdfPublicPath: text("pdf_public_path").notNull(), qaSummary: jsonb("qa_summary").$type>().notNull().default({}), provenance: jsonb("provenance").$type>().notNull().default({}), changelog: text("changelog").notNull().default(""), publishedAt: timestamp("published_at", { withTimezone: true }).notNull().defaultNow(), search: tsvector("search") .notNull() .generatedAlwaysAs( sql`setweight(to_tsvector('english', coalesce(title, '')), 'A') || setweight(to_tsvector('english', coalesce(abstract, '')), 'B') || setweight(to_tsvector('english', as_immutable_array_to_string(keywords)), 'A')`, ), }, (t) => [ uniqueIndex("paper_versions_uq").on(t.paperId, t.versionNo), uniqueIndex("paper_versions_run_uq").on(t.runId), index("paper_versions_search_idx").using("gin", t.search), ], ); export const paperTopics = pgTable( "paper_topics", { paperId: uuid("paper_id") .notNull() .references(() => papers.id, { onDelete: "cascade" }), topicId: uuid("topic_id") .notNull() .references(() => topics.id, { onDelete: "cascade" }), }, (t) => [primaryKey({ columns: [t.paperId, t.topicId] })], ); export const paperSeries = pgTable( "paper_series", { paperId: uuid("paper_id") .notNull() .references(() => papers.id, { onDelete: "cascade" }), seriesId: uuid("series_id") .notNull() .references(() => series.id, { onDelete: "cascade" }), }, (t) => [primaryKey({ columns: [t.paperId, t.seriesId] })], ); export const publicationEvents = pgTable( "publication_events", { id: uuid("id").primaryKey().defaultRandom(), paperId: uuid("paper_id") .notNull() .references(() => papers.id, { onDelete: "cascade" }), versionId: uuid("version_id"), kind: text("kind").notNull(), // published | corrected | retracted | metadata_updated | unpublished actor: text("actor").notNull(), detail: jsonb("detail").$type>().notNull().default({}), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }, (t) => [index("publication_events_paper_idx").on(t.paperId, t.createdAt)], ); export const corrections = pgTable("corrections", { id: uuid("id").primaryKey().defaultRandom(), paperId: uuid("paper_id") .notNull() .references(() => papers.id, { onDelete: "cascade" }), fromVersionNo: integer("from_version_no").notNull(), toVersionNo: integer("to_version_no").notNull(), notice: text("notice").notNull(), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }); export const retractions = pgTable("retractions", { id: uuid("id").primaryKey().defaultRandom(), paperId: uuid("paper_id") .notNull() .references(() => papers.id, { onDelete: "cascade" }), reason: text("reason").notNull(), notice: text("notice").notNull(), retractedBy: text("retracted_by").notNull(), retractedAt: timestamp("retracted_at", { withTimezone: true }).notNull().defaultNow(), }); export const paperSequences = pgTable("paper_sequences", { year: integer("year").primaryKey(), last: integer("last").notNull().default(0), });