import { boolean, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid, } from "drizzle-orm/pg-core"; import type { MessageAttachment } from "../../types/messaging.js"; import { organization, user } from "./auth.js"; // A conversation between clinic staff, scoped to a clinic (organization). `name` // is set for named/group conversations; a 1:1 DM leaves it null and the display // name is derived from the other participant. export const conversations = pgTable( "conversations", { id: uuid("id").primaryKey().defaultRandom(), organizationId: text("organization_id") .notNull() .references(() => organization.id, { onDelete: "cascade" }), name: text("name"), isGroup: boolean("is_group").notNull().default(false), createdBy: text("created_by").references(() => user.id, { onDelete: "set null", }), createdAt: timestamp("created_at").defaultNow().notNull(), // Bumped on each new message so the inbox can sort by recency. updatedAt: timestamp("updated_at") .defaultNow() .$onUpdate(() => new Date()) .notNull(), }, (t) => [index("conversations_org_idx").on(t.organizationId)], ); export const conversationParticipants = pgTable( "conversation_participants", { id: uuid("id").primaryKey().defaultRandom(), conversationId: uuid("conversation_id") .notNull() .references(() => conversations.id, { onDelete: "cascade" }), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), // Last time this participant read the conversation; drives unread state. lastReadAt: timestamp("last_read_at"), }, (t) => [ uniqueIndex("conv_participant_uidx").on(t.conversationId, t.userId), index("conv_participant_user_idx").on(t.userId), ], ); export const messages = pgTable( "messages", { id: uuid("id").primaryKey().defaultRandom(), conversationId: uuid("conversation_id") .notNull() .references(() => conversations.id, { onDelete: "cascade" }), senderId: text("sender_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), body: text("body").notNull().default(""), // File references / shared appointment snapshots. Null when none. attachments: jsonb("attachments").$type(), createdAt: timestamp("created_at").defaultNow().notNull(), }, (t) => [index("messages_conv_idx").on(t.conversationId, t.createdAt)], ); // Stored uploaded files referenced by a message attachment. Bytes are kept as // base64 text (simple, dependency-free; fine for the app's scale). Org-scoped. export const messageAttachments = pgTable( "message_attachments", { id: uuid("id").primaryKey().defaultRandom(), organizationId: text("organization_id") .notNull() .references(() => organization.id, { onDelete: "cascade" }), uploaderId: text("uploader_id").references(() => user.id, { onDelete: "set null", }), fileName: text("file_name").notNull(), mimeType: text("mime_type").notNull(), size: integer("size").notNull(), data: text("data").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), }, (t) => [index("message_attachments_org_idx").on(t.organizationId)], );