Submatter/packages/database/src/schema/chat.ts

334 lines
12 KiB
TypeScript
Raw Normal View History

import { sql } from "drizzle-orm";
import {
bigint,
check,
foreignKey,
index,
integer,
jsonb,
pgTable,
primaryKey,
text,
timestamp,
unique,
uniqueIndex,
uuid,
varchar,
} from "drizzle-orm/pg-core";
import { workspaceMembers } from "./objects";
import { workspaces } from "./workspaces";
export const chatChannels = pgTable(
"chat_channels",
{
id: uuid("id").primaryKey().defaultRandom(),
workspaceId: uuid("workspace_id")
.notNull()
.references(() => workspaces.id, { onDelete: "cascade" }),
kind: varchar("kind", { length: 16 })
.notNull()
.$type<"public" | "private" | "direct">(),
name: varchar("name", { length: 200 }),
ownerUserId: uuid("owner_user_id").notNull(),
directKey: text("direct_key"),
nextSequence: bigint("next_sequence", { mode: "bigint" })
.notNull()
.default(sql`1`),
/** Structural signal; transaction policy must reject writes when this is non-null. */
archivedAt: timestamp("archived_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(table) => ({
workspaceIdx: index("chat_channels_workspace_id_idx").on(table.workspaceId),
workspaceIdUnique: unique("chat_channels_workspace_id_id_unique").on(
table.workspaceId,
table.id,
),
ownerFk: foreignKey({
columns: [table.workspaceId, table.ownerUserId],
foreignColumns: [workspaceMembers.workspaceId, workspaceMembers.userId],
name: "chat_channels_owner_member_fk",
}),
directKeyUnique: uniqueIndex("chat_channels_workspace_id_direct_key_unique")
.on(table.workspaceId, table.directKey)
.where(sql`${table.directKey} is not null`),
kindCheck: check(
"chat_channels_kind_check",
sql`${table.kind} in ('public', 'private', 'direct')`,
),
directKeyCheck: check(
"chat_channels_direct_key_check",
sql`(${table.kind} = 'direct' and ${table.directKey} is not null and ${table.name} is null) or (${table.kind} <> 'direct' and ${table.directKey} is null)`,
),
sequenceCheck: check(
"chat_channels_next_sequence_check",
sql`${table.nextSequence} > 0`,
),
}),
);
export const chatChannelMembers = pgTable(
"chat_channel_members",
{
workspaceId: uuid("workspace_id")
.notNull()
.references(() => workspaces.id, { onDelete: "cascade" }),
channelId: uuid("channel_id").notNull(),
userId: uuid("user_id").notNull(),
joinedAt: timestamp("joined_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(table) => ({
pk: primaryKey({
columns: [table.workspaceId, table.channelId, table.userId],
}),
workspaceIdx: index("chat_channel_members_workspace_id_idx").on(
table.workspaceId,
),
userIdx: index("chat_channel_members_workspace_id_user_id_idx").on(
table.workspaceId,
table.userId,
),
channelFk: foreignKey({
columns: [table.workspaceId, table.channelId],
foreignColumns: [chatChannels.workspaceId, chatChannels.id],
name: "chat_channel_members_channel_fk",
}).onDelete("cascade"),
memberFk: foreignKey({
columns: [table.workspaceId, table.userId],
foreignColumns: [workspaceMembers.workspaceId, workspaceMembers.userId],
name: "chat_channel_members_workspace_member_fk",
}),
}),
);
export const chatMessages = pgTable(
"chat_messages",
{
id: uuid("id").primaryKey().defaultRandom(),
workspaceId: uuid("workspace_id")
.notNull()
.references(() => workspaces.id, { onDelete: "cascade" }),
channelId: uuid("channel_id").notNull(),
sequence: bigint("sequence", { mode: "bigint" }).notNull(),
parentMessageId: uuid("parent_message_id"),
authorType: varchar("author_type", { length: 16 })
.notNull()
.$type<"human" | "agent" | "system">(),
authorUserId: uuid("author_user_id"),
/** Opaque, pinned agent identity until the agent registry schema exists. */
authorAgentId: uuid("author_agent_id"),
body: text("body").notNull(),
status: varchar("status", { length: 16 })
.notNull()
.default("sent")
.$type<"sent" | "edited" | "deleted">(),
clientIdempotencyKey: varchar("client_idempotency_key", {
length: 200,
}).notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
editedAt: timestamp("edited_at", { withTimezone: true }),
deletedAt: timestamp("deleted_at", { withTimezone: true }),
},
(table) => ({
workspaceIdx: index("chat_messages_workspace_id_idx").on(table.workspaceId),
workspaceIdUnique: unique("chat_messages_workspace_id_id_unique").on(
table.workspaceId,
table.id,
),
workspaceChannelIdUnique: unique(
"chat_messages_workspace_channel_id_unique",
).on(table.workspaceId, table.channelId, table.id),
channelSequenceUnique: unique(
"chat_messages_workspace_channel_sequence_unique",
).on(table.workspaceId, table.channelId, table.sequence),
clientKeyUnique: unique(
"chat_messages_workspace_channel_client_key_unique",
).on(table.workspaceId, table.channelId, table.clientIdempotencyKey),
channelFk: foreignKey({
columns: [table.workspaceId, table.channelId],
foreignColumns: [chatChannels.workspaceId, chatChannels.id],
name: "chat_messages_channel_fk",
}).onDelete("cascade"),
parentFk: foreignKey({
columns: [table.workspaceId, table.channelId, table.parentMessageId],
foreignColumns: [table.workspaceId, table.channelId, table.id],
name: "chat_messages_parent_fk",
}),
humanAuthorFk: foreignKey({
columns: [table.workspaceId, table.authorUserId],
foreignColumns: [workspaceMembers.workspaceId, workspaceMembers.userId],
name: "chat_messages_human_author_member_fk",
}),
authorCheck: check(
"chat_messages_author_check",
sql`(${table.authorType} = 'human' and ${table.authorUserId} is not null and ${table.authorAgentId} is null) or (${table.authorType} = 'agent' and ${table.authorUserId} is null and ${table.authorAgentId} is not null) or (${table.authorType} = 'system' and ${table.authorUserId} is null and ${table.authorAgentId} is null)`,
),
statusCheck: check(
"chat_messages_status_check",
sql`${table.status} in ('sent', 'edited', 'deleted')`,
),
sequenceCheck: check(
"chat_messages_sequence_check",
sql`${table.sequence} >= 0`,
),
}),
);
export const chatMessageMentions = pgTable(
"chat_message_mentions",
{
workspaceId: uuid("workspace_id")
.notNull()
.references(() => workspaces.id, { onDelete: "cascade" }),
messageId: uuid("message_id").notNull(),
userId: uuid("user_id").notNull(),
},
(table) => ({
pk: primaryKey({
columns: [table.workspaceId, table.messageId, table.userId],
}),
workspaceIdx: index("chat_message_mentions_workspace_id_idx").on(
table.workspaceId,
),
messageFk: foreignKey({
columns: [table.workspaceId, table.messageId],
foreignColumns: [chatMessages.workspaceId, chatMessages.id],
name: "chat_message_mentions_message_fk",
}).onDelete("cascade"),
mentionedMemberFk: foreignKey({
columns: [table.workspaceId, table.userId],
foreignColumns: [workspaceMembers.workspaceId, workspaceMembers.userId],
name: "chat_message_mentions_workspace_member_fk",
}),
}),
);
export const chatReadCursors = pgTable(
"chat_read_cursors",
{
workspaceId: uuid("workspace_id")
.notNull()
.references(() => workspaces.id, { onDelete: "cascade" }),
channelId: uuid("channel_id").notNull(),
userId: uuid("user_id").notNull(),
sequence: bigint("sequence", { mode: "bigint" })
.notNull()
.default(sql`0`),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(table) => ({
pk: primaryKey({
columns: [table.workspaceId, table.channelId, table.userId],
}),
workspaceIdx: index("chat_read_cursors_workspace_id_idx").on(
table.workspaceId,
),
channelFk: foreignKey({
columns: [table.workspaceId, table.channelId],
foreignColumns: [chatChannels.workspaceId, chatChannels.id],
name: "chat_read_cursors_channel_fk",
}).onDelete("cascade"),
readerMemberFk: foreignKey({
columns: [table.workspaceId, table.userId],
foreignColumns: [workspaceMembers.workspaceId, workspaceMembers.userId],
name: "chat_read_cursors_workspace_member_fk",
}),
sequenceCheck: check(
"chat_read_cursors_sequence_check",
sql`${table.sequence} >= 0`,
),
}),
);
export const chatMutationReceipts = pgTable(
"chat_mutation_receipts",
{
workspaceId: uuid("workspace_id")
.notNull()
.references(() => workspaces.id, { onDelete: "cascade" }),
actorUserId: uuid("actor_user_id").notNull(),
clientMutationId: uuid("client_mutation_id").notNull(),
operation: varchar("operation", { length: 50 }).notNull(),
fingerprint: text("fingerprint").notNull(),
result: jsonb("result").$type<Record<string, unknown>>().notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(table) => ({
pk: primaryKey({
columns: [table.workspaceId, table.actorUserId, table.clientMutationId],
}),
workspaceIdx: index("chat_mutation_receipts_workspace_id_idx").on(
table.workspaceId,
),
actorFk: foreignKey({
columns: [table.workspaceId, table.actorUserId],
foreignColumns: [workspaceMembers.workspaceId, workspaceMembers.userId],
name: "chat_mutation_receipts_workspace_member_fk",
}).onDelete("cascade"),
}),
);
export const chatOutbox = pgTable(
"chat_outbox",
{
id: uuid("id").primaryKey().defaultRandom(),
workspaceId: uuid("workspace_id")
.notNull()
.references(() => workspaces.id, { onDelete: "cascade" }),
channelId: uuid("channel_id").notNull(),
messageId: uuid("message_id"),
eventVersion: integer("event_version").notNull().default(1),
eventType: varchar("event_type", { length: 100 }).notNull(),
dedupeKey: varchar("dedupe_key", { length: 250 }).notNull(),
payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
publishedAt: timestamp("published_at", { withTimezone: true }),
attempts: integer("attempts").notNull().default(0),
lastError: text("last_error"),
},
(table) => ({
workspaceIdx: index("chat_outbox_workspace_id_idx").on(table.workspaceId),
unpublishedIdx: index("chat_outbox_unpublished_idx")
.on(table.createdAt)
.where(sql`${table.publishedAt} is null`),
dedupeUnique: unique("chat_outbox_workspace_id_dedupe_key_unique").on(
table.workspaceId,
table.dedupeKey,
),
channelFk: foreignKey({
columns: [table.workspaceId, table.channelId],
foreignColumns: [chatChannels.workspaceId, chatChannels.id],
name: "chat_outbox_channel_fk",
}).onDelete("cascade"),
messageFk: foreignKey({
columns: [table.workspaceId, table.messageId],
foreignColumns: [chatMessages.workspaceId, chatMessages.id],
name: "chat_outbox_message_fk",
}).onDelete("cascade"),
versionCheck: check(
"chat_outbox_event_version_check",
sql`${table.eventVersion} > 0`,
),
attemptsCheck: check(
"chat_outbox_attempts_check",
sql`${table.attempts} >= 0`,
),
}),
);