SPB Git
20.4 KB · 230 lines typescript
1/**2 * Note SPBoucher AI — relational schema (Drizzle / PostgreSQL 17 + pgvector).3 * Conventions: uuid PKs, timestamptz, soft delete via deleted_at on content tables, jsonb for flexible content.4 */5import { pgTable, uuid, text, timestamp, boolean, integer, jsonb, doublePrecision, index, uniqueIndex, customType, primaryKey, bigint } from "drizzle-orm/pg-core";6import { sql, relations as rel } from "drizzle-orm";78export const vector = customType<{ data: number[]; driverData: string }>({9  dataType() { return "vector(1536)"; },10  toDriver(v: number[]) { return `[${v.join(",")}]`; },11  fromDriver(v: string) { return v.slice(1, -1).split(",").map(Number); },12});13const tsvector = customType<{ data: string }>({ dataType() { return "tsvector"; } });1415const ts = (name?: string) => (name ? timestamp(name, { withTimezone: true, mode: "date" }) : timestamp({ withTimezone: true, mode: "date" }));16const created = () => ts().notNull().defaultNow();1718// ---------- identity & auth19export const users = pgTable("users", {20  id: uuid().primaryKey().defaultRandom(),21  username: text().notNull().unique(),22  displayName: text("display_name").notNull(),23  email: text(),24  passwordHash: text("password_hash").notNull(),25  totpSecret: text("totp_secret"),26  totpEnabled: boolean("totp_enabled").notNull().default(false),27  preferences: jsonb().$type<Record<string, unknown>>().notNull().default({}),28  createdAt: created(), updatedAt: created(),29});30export const sessions = pgTable("sessions", {31  id: uuid().primaryKey().defaultRandom(),32  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),33  tokenHash: text("token_hash").notNull().unique(),34  userAgent: text("user_agent"), ip: text(),35  createdAt: created(), lastSeenAt: created(),36  expiresAt: ts().notNull(), absoluteExpiresAt: ts("absolute_expires_at").notNull(),37  revokedAt: ts("revoked_at"), mfaPassed: boolean("mfa_passed").notNull().default(false),38}, (t) => [index("sessions_user_idx").on(t.userId)]);39export const loginAttempts = pgTable("login_attempts", {40  id: uuid().primaryKey().defaultRandom(),41  ip: text().notNull(), username: text(), success: boolean().notNull(), createdAt: created(),42}, (t) => [index("login_attempts_ip_idx").on(t.ip, t.createdAt)]);43export const webauthnCredentials = pgTable("webauthn_credentials", {44  id: text().primaryKey(), userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),45  publicKey: text("public_key").notNull(), counter: bigint({ mode: "number" }).notNull().default(0),46  transports: jsonb().$type<string[]>().default([]), deviceType: text("device_type"), backedUp: boolean("backed_up").default(false),47  label: text(), createdAt: created(), lastUsedAt: ts("last_used_at"),48});49export const webauthnChallenges = pgTable("webauthn_challenges", {50  id: uuid().primaryKey().defaultRandom(), userId: uuid("user_id"), challenge: text().notNull(), kind: text().notNull(), createdAt: created(),51});5253// ---------- workspace & pages54export const workspaces = pgTable("workspaces", {55  id: uuid().primaryKey().defaultRandom(), ownerId: uuid("owner_id").notNull().references(() => users.id),56  name: text().notNull(), slug: text().notNull().unique(), settings: jsonb().$type<Record<string, unknown>>().notNull().default({}),57  createdAt: created(), updatedAt: created(),58});59export const pages = pgTable("pages", {60  id: uuid().primaryKey().defaultRandom(),61  workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),62  parentId: uuid("parent_id"),63  databaseId: uuid("database_id"), // set when the page is a row page of a database64  type: text().notNull().default("page"), // page | database | research | dashboard65  section: text().notNull().default("notes"), // notes | documents | databases | projects | research66  title: text().notNull().default("Untitled"), icon: text(), cover: text(),67  content: jsonb().$type<Record<string, unknown>>().notNull().default({ type: "doc", content: [] }),68  plainText: text("plain_text").notNull().default(""),69  tsv: tsvector("tsv").generatedAlwaysAs(sql`setweight(to_tsvector('simple', coalesce(title,'')), 'A') || setweight(to_tsvector('simple', coalesce(plain_text,'')), 'B')`),70  properties: jsonb().$type<Record<string, unknown>>().notNull().default({}),71  position: doublePrecision().notNull().default(0),72  pinned: boolean().notNull().default(false), archived: boolean().notNull().default(false),73  version: integer().notNull().default(1), wordCount: integer("word_count").notNull().default(0),74  createdAt: created(), updatedAt: created(), lastOpenedAt: ts("last_opened_at"), deletedAt: ts("deleted_at"),75}, (t) => [index("pages_ws_parent_idx").on(t.workspaceId, t.parentId), index("pages_tsv_idx").using("gin", t.tsv), index("pages_updated_idx").on(t.updatedAt), index("pages_db_idx").on(t.databaseId), index("pages_title_trgm_idx").using("gin", sql`${t.title} gin_trgm_ops`)]);76export const pageVersions = pgTable("page_versions", {77  id: uuid().primaryKey().defaultRandom(), pageId: uuid("page_id").notNull().references(() => pages.id, { onDelete: "cascade" }),78  version: integer().notNull(), title: text().notNull(), content: jsonb().notNull(), properties: jsonb().default({}),79  reason: text().notNull().default("autosave"), createdAt: created(), wordCount: integer("word_count").default(0),80}, (t) => [uniqueIndex("page_versions_unique").on(t.pageId, t.version)]);8182// ---------- databases83export const databases = pgTable("databases", {84  id: uuid().primaryKey().defaultRandom(), workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),85  pageId: uuid("page_id").notNull().references(() => pages.id, { onDelete: "cascade" }), // the page that hosts the database86  name: text().notNull(), description: text(), icon: text(), isTemplate: boolean("is_template").notNull().default(false),87  templateOf: uuid("template_of"), createdAt: created(), updatedAt: created(), deletedAt: ts("deleted_at"),88});89export const databaseProperties = pgTable("database_properties", {90  id: uuid().primaryKey().defaultRandom(), databaseId: uuid("database_id").notNull().references(() => databases.id, { onDelete: "cascade" }),91  name: text().notNull(), type: text().notNull(), // text number date checkbox status select multi_select url file relation rollup formula created_time last_edited_time ai92  config: jsonb().$type<Record<string, unknown>>().notNull().default({}), position: integer().notNull().default(0), width: integer().default(180),93  createdAt: created(),94}, (t) => [index("db_props_db_idx").on(t.databaseId)]);95export const databaseRows = pgTable("database_rows", {96  id: uuid().primaryKey().defaultRandom(), databaseId: uuid("database_id").notNull().references(() => databases.id, { onDelete: "cascade" }),97  pageId: uuid("page_id").notNull().references(() => pages.id, { onDelete: "cascade" }),98  values: jsonb().$type<Record<string, unknown>>().notNull().default({}), position: doublePrecision().notNull().default(0),99  createdAt: created(), updatedAt: created(), deletedAt: ts("deleted_at"),100}, (t) => [index("db_rows_db_idx").on(t.databaseId), index("db_rows_values_idx").using("gin", t.values)]);101export const databaseViews = pgTable("database_views", {102  id: uuid().primaryKey().defaultRandom(), databaseId: uuid("database_id").notNull().references(() => databases.id, { onDelete: "cascade" }),103  name: text().notNull(), type: text().notNull().default("table"), // table board calendar gallery timeline list104  config: jsonb().$type<Record<string, unknown>>().notNull().default({}), position: integer().notNull().default(0), isDefault: boolean("is_default").notNull().default(false), createdAt: created(),105});106export const relationsTable = pgTable("relations", {107  id: uuid().primaryKey().defaultRandom(), propertyId: uuid("property_id").notNull().references(() => databaseProperties.id, { onDelete: "cascade" }),108  fromRowId: uuid("from_row_id").notNull().references(() => databaseRows.id, { onDelete: "cascade" }), toRowId: uuid("to_row_id").notNull().references(() => databaseRows.id, { onDelete: "cascade" }),109  createdAt: created(),110}, (t) => [uniqueIndex("relations_unique").on(t.propertyId, t.fromRowId, t.toRowId), index("relations_to_idx").on(t.toRowId)]);111112// ---------- attachments, tags, tasks, links113export const attachments = pgTable("attachments", {114  id: uuid().primaryKey().defaultRandom(), workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),115  pageId: uuid("page_id").references(() => pages.id, { onDelete: "set null" }),116  filename: text().notNull(), mime: text().notNull(), size: bigint({ mode: "number" }).notNull(), sha256: text().notNull(), storagePath: text("storage_path").notNull(),117  extractedText: text("extracted_text"), pageCount: integer("page_count"), meta: jsonb().default({}), createdAt: created(), deletedAt: ts("deleted_at"),118}, (t) => [index("attachments_page_idx").on(t.pageId), index("attachments_sha_idx").on(t.sha256)]);119export const tags = pgTable("tags", {120  id: uuid().primaryKey().defaultRandom(), workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),121  name: text().notNull(), color: text().default("gray"), createdAt: created(),122}, (t) => [uniqueIndex("tags_unique").on(t.workspaceId, t.name)]);123export const pageTags = pgTable("page_tags", { pageId: uuid("page_id").notNull().references(() => pages.id, { onDelete: "cascade" }), tagId: uuid("tag_id").notNull().references(() => tags.id, { onDelete: "cascade" }) }, (t) => [primaryKey({ columns: [t.pageId, t.tagId] })]);124export const tasks = pgTable("tasks", {125  id: uuid().primaryKey().defaultRandom(), workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),126  pageId: uuid("page_id").references(() => pages.id, { onDelete: "set null" }), title: text().notNull(), status: text().notNull().default("todo"), priority: text().default("medium"),127  dueAt: ts("due_at"), completedAt: ts("completed_at"), project: text(), createdAt: created(), updatedAt: created(), deletedAt: ts("deleted_at"),128}, (t) => [index("tasks_due_idx").on(t.dueAt)]);129export const links = pgTable("links", {130  id: uuid().primaryKey().defaultRandom(), fromPageId: uuid("from_page_id").notNull().references(() => pages.id, { onDelete: "cascade" }), toPageId: uuid("to_page_id").notNull().references(() => pages.id, { onDelete: "cascade" }),131  kind: text().notNull().default("link"), createdAt: created(),132}, (t) => [uniqueIndex("links_unique").on(t.fromPageId, t.toPageId, t.kind), index("links_to_idx").on(t.toPageId)]);133export const knowledgeEdges = pgTable("knowledge_edges", {134  id: uuid().primaryKey().defaultRandom(), workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),135  fromId: uuid("from_id").notNull(), toId: uuid("to_id").notNull(), fromType: text("from_type").notNull().default("page"), toType: text("to_type").notNull().default("page"),136  kind: text().notNull(), // link | backlink | relation | ai_suggested | concept137  status: text().notNull().default("accepted"), // suggested | accepted | rejected138  confidence: doublePrecision(), rationale: text(), model: text(), createdAt: created(), decidedAt: ts("decided_at"),139}, (t) => [uniqueIndex("kedges_unique").on(t.fromId, t.toId, t.kind), index("kedges_ws_idx").on(t.workspaceId, t.status)]);140141// ---------- search142export const chunks = pgTable("chunks", {143  id: uuid().primaryKey().defaultRandom(), workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),144  sourceType: text("source_type").notNull(), // page | attachment | row145  sourceId: uuid("source_id").notNull(), pageId: uuid("page_id"), ordinal: integer().notNull(), blockId: text("block_id"), heading: text(),146  content: text().notNull(), tokenCount: integer("token_count").notNull().default(0), contentHash: text("content_hash").notNull(),147  tsv: tsvector("tsv").generatedAlwaysAs(sql`to_tsvector('simple', coalesce(heading,'') || ' ' || content)`),148  createdAt: created(),149}, (t) => [index("chunks_source_idx").on(t.sourceType, t.sourceId), index("chunks_tsv_idx").using("gin", t.tsv), uniqueIndex("chunks_unique").on(t.sourceType, t.sourceId, t.ordinal)]);150export const embeddings = pgTable("embeddings", {151  chunkId: uuid("chunk_id").primaryKey().references(() => chunks.id, { onDelete: "cascade" }),152  model: text().notNull(), version: integer().notNull().default(1), embedding: vector("embedding").notNull(), createdAt: created(),153}, (t) => [index("embeddings_hnsw_idx").using("hnsw", sql`${t.embedding} vector_cosine_ops`)]);154export const indexState = pgTable("index_state", {155  sourceType: text("source_type").notNull(), sourceId: uuid("source_id").notNull(), contentHash: text("content_hash").notNull(), model: text().notNull(), version: integer().notNull().default(1), indexedAt: created(),156}, (t) => [primaryKey({ columns: [t.sourceType, t.sourceId] })]);157158// ---------- AI159export const aiProviders = pgTable("ai_providers", {160  id: text().primaryKey(), name: text().notNull(), enabled: boolean().notNull().default(true), lastDiscoveryAt: ts("last_discovery_at"), status: text().default("unknown"), statusDetail: text("status_detail"),161});162export const aiModels = pgTable("ai_models", {163  id: text().primaryKey(), provider: text().notNull().references(() => aiProviders.id), displayName: text("display_name"),164  category: text().notNull().default("text"), available: boolean().notNull().default(true), enabled: boolean().notNull().default(true),165  contextWindow: integer("context_window"), maxOutput: integer("max_output"),166  inputPrice: doublePrecision("input_price"), cachedPrice: doublePrecision("cached_price"), outputPrice: doublePrecision("output_price"), pricingSource: text("pricing_source"),167  probes: jsonb().$type<Record<string, { status: string; detail?: string; ms?: number }>>().default({}),168  benchmark: jsonb().$type<Record<string, unknown>>().default({}), // summary.json aggregate for this model169  capabilities: jsonb().default({}), notes: jsonb().$type<string[]>().default([]), discoveredAt: ts("discovered_at"), probedAt: ts("probed_at"), benchmarkedAt: ts("benchmarked_at"), createdAt: created(), updatedAt: created(),170});171export const aiConversations = pgTable("ai_conversations", {172  id: uuid().primaryKey().defaultRandom(), workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),173  title: text().notNull().default("New conversation"), pageId: uuid("page_id").references(() => pages.id, { onDelete: "set null" }), model: text(), routing: text().default("auto"),174  pinned: boolean().notNull().default(false), createdAt: created(), updatedAt: created(), deletedAt: ts("deleted_at"),175}, (t) => [index("ai_conv_ws_idx").on(t.workspaceId, t.updatedAt)]);176export const aiMessages = pgTable("ai_messages", {177  id: uuid().primaryKey().defaultRandom(), conversationId: uuid("conversation_id").notNull().references(() => aiConversations.id, { onDelete: "cascade" }),178  role: text().notNull(), content: text().notNull(), model: text(), provider: text(),179  sources: jsonb().$type<{ chunkId: string; pageId: string; title: string; excerpt: string; score: number }[]>().default([]),180  usage: jsonb().default({}), costUsd: doublePrecision("cost_usd"), latencyMs: integer("latency_ms"), ttftMs: integer("ttft_ms"), meta: jsonb().default({}), createdAt: created(),181}, (t) => [index("ai_msgs_conv_idx").on(t.conversationId, t.createdAt)]);182export const aiExecutions = pgTable("ai_executions", {183  id: uuid().primaryKey().defaultRandom(), workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),184  agent: text().notNull(), task: text().notNull(), model: text(), status: text().notNull().default("pending"), // pending running awaiting_approval approved rejected completed failed185  scopes: jsonb().$type<string[]>().notNull().default([]), toolsInvoked: jsonb("tools_invoked").$type<{ tool: string; input: unknown; output?: unknown; at: string }[]>().default([]),186  documentsAccessed: jsonb("documents_accessed").$type<string[]>().default([]), proposedChanges: jsonb("proposed_changes").$type<unknown[]>().default([]),187  approvals: jsonb().$type<{ change: number; approved: boolean; at: string }[]>().default([]), outcome: text(), result: text(), error: text(),188  costUsd: doublePrecision("cost_usd").default(0), tokensIn: integer("tokens_in").default(0), tokensOut: integer("tokens_out").default(0),189  createdAt: created(), startedAt: ts("started_at"), finishedAt: ts("finished_at"),190}, (t) => [index("ai_exec_ws_idx").on(t.workspaceId, t.createdAt)]);191export const aiUsage = pgTable("ai_usage", {192  id: uuid().primaryKey().defaultRandom(), workspaceId: uuid("workspace_id"), provider: text().notNull(), model: text().notNull(), task: text().notNull(),193  inputTokens: integer("input_tokens").notNull().default(0), outputTokens: integer("output_tokens").notNull().default(0), cachedTokens: integer("cached_tokens").default(0), reasoningTokens: integer("reasoning_tokens").default(0),194  costUsd: doublePrecision("cost_usd"), latencyMs: integer("latency_ms"), ttftMs: integer("ttft_ms"), ok: boolean().notNull().default(true), error: text(), refId: uuid("ref_id"), createdAt: created(),195}, (t) => [index("ai_usage_created_idx").on(t.createdAt)]);196export const aiComparisons = pgTable("ai_comparisons", {197  id: uuid().primaryKey().defaultRandom(), workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),198  prompt: text().notNull(), models: jsonb().$type<string[]>().notNull(), results: jsonb().$type<unknown[]>().notNull().default([]), createdAt: created(),199});200201// ---------- research202export const researchReferences = pgTable("references", {203  id: uuid().primaryKey().defaultRandom(), workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),204  pageId: uuid("page_id").references(() => pages.id, { onDelete: "set null" }), citeKey: text("cite_key").notNull(), entryType: text("entry_type").notNull().default("article"),205  title: text().notNull(), authors: jsonb().$type<string[]>().default([]), year: integer(), journal: text(), doi: text(), url: text(), bibtex: text(), fields: jsonb().default({}),206  verified: text().notNull().default("user"), // user | doi_verified | ai_suggested207  attachmentId: uuid("attachment_id").references(() => attachments.id, { onDelete: "set null" }), createdAt: created(), updatedAt: created(), deletedAt: ts("deleted_at"),208}, (t) => [uniqueIndex("references_key_unique").on(t.workspaceId, t.citeKey)]);209export const experiments = pgTable("experiments", {210  id: uuid().primaryKey().defaultRandom(), workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),211  pageId: uuid("page_id").references(() => pages.id, { onDelete: "set null" }), name: text().notNull(), status: text().notNull().default("planned"),212  params: jsonb().default({}), metrics: jsonb().default({}), datasetRef: text("dataset_ref"), repoUrl: text("repo_url"), notes: text(), startedAt: ts("started_at"), finishedAt: ts("finished_at"), createdAt: created(), updatedAt: created(),213});214215// ---------- ops216export const auditLogs = pgTable("audit_logs", {217  id: uuid().primaryKey().defaultRandom(), userId: uuid("user_id"), action: text().notNull(), targetType: text("target_type"), targetId: text("target_id"), detail: jsonb().default({}), ip: text(), createdAt: created(),218}, (t) => [index("audit_created_idx").on(t.createdAt)]);219export const backupJobs = pgTable("backup_jobs", {220  id: uuid().primaryKey().defaultRandom(), kind: text().notNull().default("full"), status: text().notNull().default("running"), path: text(), sizeBytes: bigint("size_bytes", { mode: "number" }), sha256: text(), offsite: boolean().default(false), error: text(), startedAt: created(), finishedAt: ts("finished_at"),221});222export const dashboardLayouts = pgTable("dashboard_layouts", {223  id: uuid().primaryKey().defaultRandom(), userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), name: text().notNull().default("Home"),224  widgets: jsonb().$type<{ id: string; type: string; size: string; config?: Record<string, unknown> }[]>().notNull().default([]), isDefault: boolean("is_default").notNull().default(true), updatedAt: created(),225});226export const settings = pgTable("settings", { key: text().primaryKey(), value: jsonb().notNull(), updatedAt: created() });227228export const pagesRelations = rel(pages, ({ many, one }) => ({ versions: many(pageVersions), tags: many(pageTags), workspace: one(workspaces, { fields: [pages.workspaceId], references: [workspaces.id] }) }));229export const databasesRelations = rel(databases, ({ many }) => ({ properties: many(databaseProperties), rows: many(databaseRows), views: many(databaseViews) }));230
Files · 2c37cb5a3e2af18e5ba361996d453cab97da82c2
Menu

Theme