mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: move session sharing and collab rooms into a plugin (B12)
This commit is contained in:
1 parent
791d8656a1
commit
a276f6898c
207 files changed
+35771
-11072
No files matched your search
@@ -500,69 +500,6 @@ async function initializeCompleteDatabase(): Promise<void> {
|
||||
FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_shares (
|
||||
id TEXT PRIMARY KEY,
|
||||
host_id INTEGER NOT NULL,
|
||||
owner_user_id TEXT NOT NULL,
|
||||
protocol TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
tab_instance_id TEXT,
|
||||
share_type TEXT NOT NULL,
|
||||
target_user_id TEXT,
|
||||
link_token TEXT UNIQUE,
|
||||
permission_level TEXT NOT NULL DEFAULT 'read-only',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TEXT NOT NULL,
|
||||
revoked_at TEXT,
|
||||
last_joined_at TEXT,
|
||||
join_count INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (owner_user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (target_user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_share_participants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
share_id TEXT NOT NULL,
|
||||
user_id TEXT,
|
||||
guest_label TEXT,
|
||||
joined_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
left_at TEXT,
|
||||
FOREIGN KEY (share_id) REFERENCES session_shares (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collab_rooms (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
owner_user_id TEXT NOT NULL,
|
||||
persistent INTEGER NOT NULL DEFAULT 0,
|
||||
presenter_user_id TEXT,
|
||||
stage_protocol TEXT,
|
||||
stage_host_id INTEGER,
|
||||
stage_share_id TEXT,
|
||||
guest_link_token TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ended_at TEXT,
|
||||
FOREIGN KEY (owner_user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (presenter_user_id) REFERENCES users (id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (stage_host_id) REFERENCES ssh_data (id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (stage_share_id) REFERENCES session_shares (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collab_room_members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
room_role TEXT NOT NULL DEFAULT 'member',
|
||||
added_by TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (room_id, user_id),
|
||||
FOREIGN KEY (room_id) REFERENCES collab_rooms (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (added_by) REFERENCES users (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS secret_sources (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
@@ -2060,10 +1997,6 @@ const migrateSchema = () => {
|
||||
}
|
||||
|
||||
addColumnIfNotExists("users", "sso_provider_id", "INTEGER");
|
||||
addColumnIfNotExists("collab_rooms", "guest_link_token", "TEXT");
|
||||
sqlite.exec(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_collab_rooms_guest_token ON collab_rooms (guest_link_token)",
|
||||
);
|
||||
|
||||
try {
|
||||
const usersTableInfo = sqlite.prepare("PRAGMA table_info(users)").all() as Array<{
|
||||
|
||||
@@ -135,16 +135,6 @@ export const PERFORMANCE_INDEXES: PerformanceIndex[] = [
|
||||
table: "session_recordings",
|
||||
columns: "host_id",
|
||||
},
|
||||
{
|
||||
name: "idx_session_shares_session_id",
|
||||
table: "session_shares",
|
||||
columns: "session_id",
|
||||
},
|
||||
{
|
||||
name: "idx_session_shares_host_id",
|
||||
table: "session_shares",
|
||||
columns: "host_id",
|
||||
},
|
||||
|
||||
// Automations.
|
||||
{
|
||||
|
||||
@@ -761,70 +761,6 @@ export const sessionRecordings = mysqlTable(
|
||||
],
|
||||
);
|
||||
|
||||
export const sessionShares = mysqlTable(
|
||||
"session_shares",
|
||||
{
|
||||
id: varchar("id", { length: 255 }).primaryKey(),
|
||||
|
||||
hostId: int("host_id")
|
||||
.notNull()
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
ownerUserId: varchar("owner_user_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
|
||||
protocol: varchar("protocol", { length: 255 }).notNull(),
|
||||
|
||||
// Live-session binding: TerminalSessionManager's session.id for SSH, or
|
||||
// guacd's own guacamoleConnectionId for rdp/vnc/telnet. Neither is a DB
|
||||
// row (process-local, in-memory) so this intentionally has no FK.
|
||||
sessionId: varchar("session_id", { length: 255 }).notNull(),
|
||||
tabInstanceId: text("tab_instance_id"),
|
||||
|
||||
shareType: text("share_type").notNull(), // "link" | "user"
|
||||
targetUserId: varchar("target_user_id", { length: 255 }).references(() => users.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
linkToken: varchar("link_token", { length: 255 }).unique(),
|
||||
|
||||
permissionLevel: text("permission_level").notNull().default("read-only"),
|
||||
|
||||
createdAt: varchar("created_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
expiresAt: varchar("expires_at", { length: 255 }).notNull(),
|
||||
revokedAt: text("revoked_at"),
|
||||
|
||||
lastJoinedAt: text("last_joined_at"),
|
||||
joinCount: int("join_count").notNull().default(0),
|
||||
},
|
||||
// Resolved from the live session on join, and listed per host.
|
||||
(table) => [
|
||||
index("idx_session_shares_session_id").on(table.sessionId),
|
||||
index("idx_session_shares_host_id").on(table.hostId),
|
||||
],
|
||||
);
|
||||
|
||||
export const sessionShareParticipants = mysqlTable(
|
||||
"session_share_participants",
|
||||
{
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
shareId: varchar("share_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => sessionShares.id, { onDelete: "cascade" }),
|
||||
|
||||
userId: varchar("user_id", { length: 255 }).references(() => users.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
guestLabel: text("guest_label"),
|
||||
|
||||
joinedAt: text("joined_at")
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
leftAt: text("left_at"),
|
||||
},
|
||||
);
|
||||
|
||||
export const opksshTokens = mysqlTable(
|
||||
"opkssh_tokens",
|
||||
{
|
||||
@@ -1569,81 +1505,6 @@ export const aiProposals = mysqlTable(
|
||||
|
||||
// --- collab rooms ---
|
||||
|
||||
/**
|
||||
* A collaboration room: a group of users watching one "stage" - the live
|
||||
* session the current presenter is showing. The stage points at a
|
||||
* shareType="room" row in session_shares, so transport, gating, recording and
|
||||
* expiry all reuse the session-sharing machinery.
|
||||
*/
|
||||
export const collabRooms = mysqlTable(
|
||||
"collab_rooms",
|
||||
{
|
||||
id: varchar("id", { length: 255 }).primaryKey(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
ownerUserId: varchar("owner_user_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
|
||||
// Persistent rooms survive being emptied and can be re-used; one-off
|
||||
// rooms are ended explicitly and never listed again.
|
||||
persistent: boolean("persistent")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
presenterUserId: varchar("presenter_user_id", { length: 255 }).references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
stageProtocol: text("stage_protocol"),
|
||||
stageHostId: int("stage_host_id").references(() => hosts.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
stageShareId: varchar("stage_share_id", { length: 255 }).references(() => sessionShares.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
|
||||
// Set = anonymous guests may watch the stage through this token.
|
||||
guestLinkToken: varchar("guest_link_token", { length: 255 }),
|
||||
|
||||
createdAt: varchar("created_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
endedAt: text("ended_at"),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_collab_rooms_owner").on(table.ownerUserId),
|
||||
uniqueIndex("idx_collab_rooms_guest_token").on(table.guestLinkToken),
|
||||
],
|
||||
);
|
||||
|
||||
export const collabRoomMembers = mysqlTable(
|
||||
"collab_room_members",
|
||||
{
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
roomId: varchar("room_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => collabRooms.id, { onDelete: "cascade" }),
|
||||
userId: varchar("user_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
|
||||
// "host" runs the room: invites, force-switches the presenter, ends it.
|
||||
roomRole: text("room_role").notNull().default("member"),
|
||||
addedBy: varchar("added_by", { length: 255 }).references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
createdAt: varchar("created_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`(CURRENT_TIMESTAMP)`),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("idx_collab_room_members_room_user").on(
|
||||
table.roomId,
|
||||
table.userId,
|
||||
),
|
||||
index("idx_collab_room_members_user").on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
// --- secret sources ---
|
||||
|
||||
/**
|
||||
|
||||
@@ -762,70 +762,6 @@ export const sessionRecordings = pgTable(
|
||||
],
|
||||
);
|
||||
|
||||
export const sessionShares = pgTable(
|
||||
"session_shares",
|
||||
{
|
||||
id: varchar("id", { length: 255 }).primaryKey(),
|
||||
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
ownerUserId: varchar("owner_user_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
|
||||
protocol: varchar("protocol", { length: 255 }).notNull(),
|
||||
|
||||
// Live-session binding: TerminalSessionManager's session.id for SSH, or
|
||||
// guacd's own guacamoleConnectionId for rdp/vnc/telnet. Neither is a DB
|
||||
// row (process-local, in-memory) so this intentionally has no FK.
|
||||
sessionId: varchar("session_id", { length: 255 }).notNull(),
|
||||
tabInstanceId: text("tab_instance_id"),
|
||||
|
||||
shareType: text("share_type").notNull(), // "link" | "user"
|
||||
targetUserId: varchar("target_user_id", { length: 255 }).references(() => users.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
linkToken: varchar("link_token", { length: 255 }).unique(),
|
||||
|
||||
permissionLevel: text("permission_level").notNull().default("read-only"),
|
||||
|
||||
createdAt: varchar("created_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
expiresAt: varchar("expires_at", { length: 255 }).notNull(),
|
||||
revokedAt: text("revoked_at"),
|
||||
|
||||
lastJoinedAt: text("last_joined_at"),
|
||||
joinCount: integer("join_count").notNull().default(0),
|
||||
},
|
||||
// Resolved from the live session on join, and listed per host.
|
||||
(table) => [
|
||||
index("idx_session_shares_session_id").on(table.sessionId),
|
||||
index("idx_session_shares_host_id").on(table.hostId),
|
||||
],
|
||||
);
|
||||
|
||||
export const sessionShareParticipants = pgTable(
|
||||
"session_share_participants",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
shareId: varchar("share_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => sessionShares.id, { onDelete: "cascade" }),
|
||||
|
||||
userId: varchar("user_id", { length: 255 }).references(() => users.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
guestLabel: text("guest_label"),
|
||||
|
||||
joinedAt: text("joined_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
leftAt: text("left_at"),
|
||||
},
|
||||
);
|
||||
|
||||
export const opksshTokens = pgTable(
|
||||
"opkssh_tokens",
|
||||
{
|
||||
@@ -1570,81 +1506,6 @@ export const aiProposals = pgTable(
|
||||
|
||||
// --- collab rooms ---
|
||||
|
||||
/**
|
||||
* A collaboration room: a group of users watching one "stage" - the live
|
||||
* session the current presenter is showing. The stage points at a
|
||||
* shareType="room" row in session_shares, so transport, gating, recording and
|
||||
* expiry all reuse the session-sharing machinery.
|
||||
*/
|
||||
export const collabRooms = pgTable(
|
||||
"collab_rooms",
|
||||
{
|
||||
id: varchar("id", { length: 255 }).primaryKey(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
ownerUserId: varchar("owner_user_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
|
||||
// Persistent rooms survive being emptied and can be re-used; one-off
|
||||
// rooms are ended explicitly and never listed again.
|
||||
persistent: boolean("persistent")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
presenterUserId: varchar("presenter_user_id", { length: 255 }).references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
stageProtocol: text("stage_protocol"),
|
||||
stageHostId: integer("stage_host_id").references(() => hosts.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
stageShareId: varchar("stage_share_id", { length: 255 }).references(() => sessionShares.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
|
||||
// Set = anonymous guests may watch the stage through this token.
|
||||
guestLinkToken: varchar("guest_link_token", { length: 255 }),
|
||||
|
||||
createdAt: varchar("created_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
endedAt: text("ended_at"),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_collab_rooms_owner").on(table.ownerUserId),
|
||||
uniqueIndex("idx_collab_rooms_guest_token").on(table.guestLinkToken),
|
||||
],
|
||||
);
|
||||
|
||||
export const collabRoomMembers = pgTable(
|
||||
"collab_room_members",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
roomId: varchar("room_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => collabRooms.id, { onDelete: "cascade" }),
|
||||
userId: varchar("user_id", { length: 255 })
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
|
||||
// "host" runs the room: invites, force-switches the presenter, ends it.
|
||||
roomRole: text("room_role").notNull().default("member"),
|
||||
addedBy: varchar("added_by", { length: 255 }).references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
createdAt: varchar("created_at", { length: 255 })
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("idx_collab_room_members_room_user").on(
|
||||
table.roomId,
|
||||
table.userId,
|
||||
),
|
||||
index("idx_collab_room_members_user").on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
// --- secret sources ---
|
||||
|
||||
/**
|
||||
|
||||
@@ -756,70 +756,6 @@ export const sessionRecordings = sqliteTable(
|
||||
],
|
||||
);
|
||||
|
||||
export const sessionShares = sqliteTable(
|
||||
"session_shares",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
|
||||
hostId: integer("host_id")
|
||||
.notNull()
|
||||
.references(() => hosts.id, { onDelete: "cascade" }),
|
||||
ownerUserId: text("owner_user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
|
||||
protocol: text("protocol").notNull(),
|
||||
|
||||
// Live-session binding: TerminalSessionManager's session.id for SSH, or
|
||||
// guacd's own guacamoleConnectionId for rdp/vnc/telnet. Neither is a DB
|
||||
// row (process-local, in-memory) so this intentionally has no FK.
|
||||
sessionId: text("session_id").notNull(),
|
||||
tabInstanceId: text("tab_instance_id"),
|
||||
|
||||
shareType: text("share_type").notNull(), // "link" | "user"
|
||||
targetUserId: text("target_user_id").references(() => users.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
linkToken: text("link_token").unique(),
|
||||
|
||||
permissionLevel: text("permission_level").notNull().default("read-only"),
|
||||
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
expiresAt: text("expires_at").notNull(),
|
||||
revokedAt: text("revoked_at"),
|
||||
|
||||
lastJoinedAt: text("last_joined_at"),
|
||||
joinCount: integer("join_count").notNull().default(0),
|
||||
},
|
||||
// Resolved from the live session on join, and listed per host.
|
||||
(table) => [
|
||||
index("idx_session_shares_session_id").on(table.sessionId),
|
||||
index("idx_session_shares_host_id").on(table.hostId),
|
||||
],
|
||||
);
|
||||
|
||||
export const sessionShareParticipants = sqliteTable(
|
||||
"session_share_participants",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
shareId: text("share_id")
|
||||
.notNull()
|
||||
.references(() => sessionShares.id, { onDelete: "cascade" }),
|
||||
|
||||
userId: text("user_id").references(() => users.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
guestLabel: text("guest_label"),
|
||||
|
||||
joinedAt: text("joined_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
leftAt: text("left_at"),
|
||||
},
|
||||
);
|
||||
|
||||
export const opksshTokens = sqliteTable(
|
||||
"opkssh_tokens",
|
||||
{
|
||||
@@ -1568,81 +1504,6 @@ export const aiProposals = sqliteTable(
|
||||
|
||||
// --- collab rooms ---
|
||||
|
||||
/**
|
||||
* A collaboration room: a group of users watching one "stage" - the live
|
||||
* session the current presenter is showing. The stage points at a
|
||||
* shareType="room" row in session_shares, so transport, gating, recording and
|
||||
* expiry all reuse the session-sharing machinery.
|
||||
*/
|
||||
export const collabRooms = sqliteTable(
|
||||
"collab_rooms",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
ownerUserId: text("owner_user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
|
||||
// Persistent rooms survive being emptied and can be re-used; one-off
|
||||
// rooms are ended explicitly and never listed again.
|
||||
persistent: integer("persistent", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
presenterUserId: text("presenter_user_id").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
stageProtocol: text("stage_protocol"),
|
||||
stageHostId: integer("stage_host_id").references(() => hosts.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
stageShareId: text("stage_share_id").references(() => sessionShares.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
|
||||
// Set = anonymous guests may watch the stage through this token.
|
||||
guestLinkToken: text("guest_link_token"),
|
||||
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
endedAt: text("ended_at"),
|
||||
},
|
||||
(table) => [
|
||||
index("idx_collab_rooms_owner").on(table.ownerUserId),
|
||||
uniqueIndex("idx_collab_rooms_guest_token").on(table.guestLinkToken),
|
||||
],
|
||||
);
|
||||
|
||||
export const collabRoomMembers = sqliteTable(
|
||||
"collab_room_members",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
roomId: text("room_id")
|
||||
.notNull()
|
||||
.references(() => collabRooms.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
|
||||
// "host" runs the room: invites, force-switches the presenter, ends it.
|
||||
roomRole: text("room_role").notNull().default("member"),
|
||||
addedBy: text("added_by").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("idx_collab_room_members_room_user").on(
|
||||
table.roomId,
|
||||
table.userId,
|
||||
),
|
||||
index("idx_collab_room_members_user").on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
// --- secret sources ---
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user