84 lines
7.8 KiB
TypeScript
84 lines
7.8 KiB
TypeScript
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(),
|
|
})
|