import { sql } from "drizzle-orm" import { pgTable, uuid, bigint, text, timestamp, boolean, integer, jsonb, index, uniqueIndex, check } from "drizzle-orm/pg-core" import { tenants } from "./tenants" import { authUsers } from "./auth_users" import { projects } from "./projects" const time = (name: string) => timestamp(name, { withTimezone: true }) export const collaborationWorkspaces = pgTable("collaboration_workspaces", { tenantId: bigint("tenant_id", { mode: "number" }).primaryKey().references(() => tenants.id, { onDelete: "cascade" }), publicId: uuid("public_id").notNull().unique().defaultRandom(), guestsEnabled: boolean("guests_enabled").notNull().default(true), }) export const collaborationPrincipals = pgTable("collaboration_principals", { id: uuid("id").primaryKey().defaultRandom(), kind: text("kind").notNull(), tenantId: bigint("tenant_id", { mode: "number" }).references(() => tenants.id, { onDelete: "cascade" }), userId: uuid("user_id").references(() => authUsers.id, { onDelete: "cascade" }), remoteOrigin: text("remote_origin"), remoteActorId: uuid("remote_actor_id"), displayName: text("display_name").notNull(), email: text("email"), disabled: boolean("disabled").notNull().default(false), createdAt: time("created_at").notNull().defaultNow(), }, t => ({ local: uniqueIndex("collaboration_local_actor").on(t.tenantId, t.userId).where(sql`${t.kind}='user'`), guest: uniqueIndex("collaboration_guest_actor").on(t.tenantId, t.email).where(sql`${t.kind}='guest'`), remote: uniqueIndex("collaboration_remote_actor").on(t.remoteOrigin, t.remoteActorId).where(sql`${t.kind}='remote'`), kind: check("collaboration_principals_kind_check", sql`${t.kind} IN ('user','guest','remote')`), identity: check("collaboration_principals_check", sql`(${t.kind}='user' AND ${t.tenantId} IS NOT NULL AND ${t.userId} IS NOT NULL AND ${t.remoteOrigin} IS NULL) OR (${t.kind}='guest' AND ${t.tenantId} IS NOT NULL AND ${t.userId} IS NULL AND ${t.email} IS NOT NULL AND ${t.remoteOrigin} IS NULL) OR (${t.kind}='remote' AND ${t.tenantId} IS NULL AND ${t.userId} IS NULL AND ${t.remoteOrigin} IS NOT NULL AND ${t.remoteActorId} IS NOT NULL)`), })) export const collaborationRooms = pgTable("collaboration_rooms", { id: uuid("id").primaryKey().defaultRandom(), tenantId: bigint("tenant_id", { mode: "number" }).notNull().references(() => tenants.id, { onDelete: "cascade" }), projectId: bigint("project_id", { mode: "number" }).references(() => projects.id, { onDelete: "cascade" }), name: text("name").notNull(), topic: text("topic").notNull().default(""), sharedOverview: jsonb("shared_overview").notNull().default({}), status: text("status").notNull().default("active"), sequence: bigint("sequence", { mode: "number" }).notNull().default(0), createdBy: uuid("created_by").notNull().references(() => authUsers.id), createdAt: time("created_at").notNull().defaultNow(), updatedAt: time("updated_at").notNull().defaultNow(), }, t => ({ project: index("collaboration_project_rooms").on(t.tenantId, t.projectId), name: check("collaboration_rooms_name_check", sql`length(${t.name}) BETWEEN 1 AND 160`), status: check("collaboration_rooms_status_check", sql`${t.status} IN ('active','archived','blocked')`) })) export const collaborationMembers = pgTable("collaboration_members", { id: uuid("id").primaryKey().defaultRandom(), roomId: uuid("room_id").notNull().references(() => collaborationRooms.id, { onDelete: "cascade" }), principalId: uuid("principal_id").notNull().references(() => collaborationPrincipals.id), status: text("status").notNull().default("active"), role: text("role").notNull().default("participant"), overviewAllowed: boolean("overview_allowed").notNull().default(false), historyFromSeq: bigint("history_from_seq", { mode: "number" }).notNull(), lastReadSeq: bigint("last_read_seq", { mode: "number" }).notNull().default(0), revision: bigint("revision", { mode: "number" }).notNull().default(1), expiresAt: time("expires_at"), joinedAt: time("joined_at").notNull().defaultNow(), revokedAt: time("revoked_at"), }, t => ({ member: uniqueIndex("collaboration_members_room_id_principal_id_key").on(t.roomId, t.principalId), status: check("collaboration_members_status_check", sql`${t.status} IN ('active','revoked')`), role: check("collaboration_members_role_check", sql`${t.role} IN ('participant','reader')`) })) export const collaborationMessages = pgTable("collaboration_messages", { id: uuid("id").primaryKey().defaultRandom(), roomId: uuid("room_id").notNull().references(() => collaborationRooms.id, { onDelete: "cascade" }), seq: bigint("seq", { mode: "number" }).notNull(), principalId: uuid("principal_id").notNull().references(() => collaborationPrincipals.id), clientId: uuid("client_id").notNull(), body: text("body").notNull(), createdAt: time("created_at").notNull().defaultNow(), }, t => ({ seq: uniqueIndex("collaboration_messages_room_id_seq_key").on(t.roomId, t.seq), command: uniqueIndex("collaboration_messages_room_id_principal_id_client_id_key").on(t.roomId, t.principalId, t.clientId), body: check("collaboration_messages_body_check", sql`length(${t.body}) BETWEEN 1 AND 10000`) })) export const collaborationInvitations = pgTable("collaboration_invitations", { id: uuid("id").primaryKey().defaultRandom(), roomId: uuid("room_id").notNull().references(() => collaborationRooms.id, { onDelete: "cascade" }), targetPrincipalId: uuid("target_principal_id").notNull().references(() => collaborationPrincipals.id), tokenHash: text("token_hash").notNull().unique(), role: text("role").notNull(), overviewAllowed: boolean("overview_allowed").notNull().default(false), invitedBy: uuid("invited_by").notNull().references(() => authUsers.id), status: text("status").notNull().default("pending"), expiresAt: time("expires_at").notNull(), accessExpiresAt: time("access_expires_at"), createdAt: time("created_at").notNull().defaultNow(), acceptedAt: time("accepted_at"), }, t => ({ recipient: index("collaboration_recipient_invitations").on(t.targetPrincipalId, t.status), role: check("collaboration_invitations_role_check", sql`${t.role} IN ('participant','reader')`), status: check("collaboration_invitations_status_check", sql`${t.status} IN ('pending','accepted','declined','revoked')`) })) export const collaborationGuestChallenges = pgTable("collaboration_guest_challenges", { id: uuid("id").primaryKey(), principalId: uuid("principal_id").notNull().references(() => collaborationPrincipals.id, { onDelete: "cascade" }), codeHash: text("code_hash").notNull(), attempts: integer("attempts").notNull().default(0), expiresAt: time("expires_at").notNull(), consumedAt: time("consumed_at"), }) export const collaborationGuestSessions = pgTable("collaboration_guest_sessions", { tokenHash: text("token_hash").primaryKey(), principalId: uuid("principal_id").notNull().references(() => collaborationPrincipals.id, { onDelete: "cascade" }), expiresAt: time("expires_at").notNull(), createdAt: time("created_at").notNull().defaultNow(), }, t => ({ expiry: index("collaboration_guest_session_expiry").on(t.expiresAt) })) export const collaborationRateLimits = pgTable("collaboration_rate_limits", { key: text("key").primaryKey(), hits: integer("hits").notNull(), resetsAt: time("resets_at").notNull(), }) export const collaborationAudit = pgTable("collaboration_audit", { id: bigint("id", { mode: "number" }).primaryKey().generatedByDefaultAsIdentity(), tenantId: bigint("tenant_id", { mode: "number" }).notNull().references(() => tenants.id, { onDelete: "cascade" }), roomId: uuid("room_id").references(() => collaborationRooms.id, { onDelete: "set null" }), actorId: uuid("actor_id").references(() => collaborationPrincipals.id), action: text("action").notNull(), subjectId: uuid("subject_id"), createdAt: time("created_at").notNull().defaultNow(), })