334 lines
12 KiB
TypeScript
334 lines
12 KiB
TypeScript
|
|
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`,
|
||
|
|
),
|
||
|
|
}),
|
||
|
|
);
|