/** * ChannelStore — CRUD for `chat_channels`. * * All authorization is handled at a higher layer (`ChatV2Service`); * this store only enforces DB-level invariants (FKs, CHECKs, primary key). * * @module services/chat-v2/sqlite/channel.store */ import { type ChatChannelRow, type ChatChannelType } from '../types.js'; import type { ChatDatabase } from './chat-db.js'; /** Payload for `ChannelStore.create`. */ export interface ChannelCreateInput { /** * Wire-level session binding. For `type='dm'` (default), this is the * agent's session ID; an agent may be bound to N active channels (no * unique index on this column). For `type='channel'`, callers should * pass the empty string `''`. */ agentSession: string; ownerUserId: string; name: string; purpose?: string | null; /** * Phase A (SEALED §3.1) — channel type. Defaults to `'dm'` to preserve * the Phase 1 / Week 2 contract for callers that omit this field. */ type?: ChatChannelType; /** Phase A — required when `type='channel'`; null/empty for DMs. */ teamId?: string | null; /** Phase A — optional project link for project-scoped channels. */ projectId?: string | null; /** Phase A — for `type='dm'`, the resolved member-ID being DM'd. */ targetMemberId?: string | null; /** Override the clock (tests). */ nowMs?: number; /** Override the generated id (tests / deterministic callers). */ id?: string; } /** SQLite-backed store for chat channels. */ export declare class ChannelStore { private readonly db; constructor(db: ChatDatabase); /** * Create a new channel. * * An agent may hold any number of active channels — the former 1:1 * agent<->channel unique index was dropped (unified-chat-message-store * spec, Option B; see chat-db.ts), so no binding check happens here. * * @param input - The channel creation payload * @returns The inserted channel row * @throws The underlying SQLite error, unchanged, when the insert violates * a DB constraint (e.g. `SQLITE_CONSTRAINT_PRIMARYKEY` for a duplicate * `input.id`, or a CHECK violation). * @throws {ChatError} `internal_error` (500) if the row cannot be read back * immediately after a successful insert. */ create(input: ChannelCreateInput): ChatChannelRow; /** * Look up a channel by its id (active or archived). * * @param id - The channel id * @returns The row, or null if not found */ getById(id: string): ChatChannelRow | null; /** * Find an active DM channel bound to an agent session, if any. * Agents may hold several active channels (the former unique index was * dropped), so this returns an arbitrary first match; use * `findActiveDmByOwnerAndAgent` when the owner matters. * * @param agentSession - The agent session id * @returns The active channel row, or null */ findActiveByAgentSession(agentSession: string): ChatChannelRow | null; /** * Find the most-recent active DM channel owned by a specific user and * bound to a specific agent session. Used by the `ensureDmChannel` * helper backing the /agents page — the owner-scoped variant is needed * because the `uq_channel_agent_dm_active` unique index was dropped * (see chat-db.ts:380-386), so multiple DM rows for the same agent * can coexist (e.g. created by legacy bridge code with the synthetic * `'system'` owner). * * Ordering matches `listByOwner`: most-recent activity first, so the * caller always lands on the DM the user was last using. * * @param ownerUserId - The user_id that must own the channel * @param agentSession - The agent session id * @returns The most-recent active DM channel row, or null */ findActiveDmByOwnerAndAgent(ownerUserId: string, agentSession: string): ChatChannelRow | null; /** * Find the oldest active `type='channel'` row for a team, if any. * * Team channels are not owner-scoped — a team channel belongs to the * whole team — so this lookup is keyed on `team_id` alone. Ordering by * `created_at ASC` returns the team's original (e.g. `#general`) channel * stably, which is what `ensureTeamChannel` treats as the canonical * "the team already has a channel" marker. * * @param teamId - The team id whose channel to find * @returns The oldest active team channel row, or null when none exists */ findActiveChannelByTeam(teamId: string): ChatChannelRow | null; /** * List channels owned by a user. * * Phase C — extended with `type` + `teamId` filter options so the * `GET /api/chat/channels` endpoint can serve the channel-rail's * grouped/workspace-scoped views without shipping every row to the * client. Filters compose with AND; passing `undefined` for either is * a no-op (the existing all-channels behavior). * * @param ownerUserId - The user_id whose channels to return * @param options - Listing options * @param options.includeArchived - When false (default), filter out archived rows * @param options.limit - Max rows to return (capped at 100) * @param options.type - Phase C: when set, filter rows to this channel type * (`'dm'` or `'channel'`). Useful for the channel-rail's "DMs only" / * "Channels only" views. * @param options.teamId - Phase C: when set, filter rows whose `team_id` * matches. Used by the workspace-scoped Channels group; empty / null * `team_id` rows are excluded by this filter on purpose (DMs and * workspace-less rows belong to no team). * @returns Channel rows sorted by `last_message_at DESC, created_at DESC` */ listByOwner(ownerUserId: string, options?: { includeArchived?: boolean; limit?: number; type?: ChatChannelType; teamId?: string; }): ChatChannelRow[]; /** * List active "bridged" channels — inbound conversations from external * surfaces (currently Slack), persisted by the bridge under the synthetic * `'system'` owner and keyed by a `slack-…` id. These belong to no single * Crewly user, so they are surfaced in the consolidated chat list across * the (single-user OSS) account rather than via `listByOwner`. * * @param options.includeArchived - Include archived rows (default false). * @param options.limit - Max rows (capped at 200). * @returns Bridged channel rows, most-recent first. */ listBridged(options?: { includeArchived?: boolean; limit?: number; }): ChatChannelRow[]; /** * Mark a channel as archived (soft-delete). No-op if already archived. * * @param id - The channel id * @param nowMs - Optional override for the archive timestamp * @returns True if a row was updated (was active), false otherwise */ archive(id: string, nowMs?: number): boolean; /** * Phase 6.0b — clear an archived flag. Inverse of {@link archive}; * required by the legacy `unarchiveConversation` controller route. * * @param id - The channel id * @returns True if a row was updated (was archived), false otherwise */ unarchive(id: string): boolean; /** * Phase 6.0b — rename a channel. Used by the legacy * `updateConversationTitle` controller route. * * @param id - The channel id * @param name - The new name * @returns True if a row was updated, false if the channel doesn't exist */ rename(id: string, name: string): boolean; /** * Phase 6.0b — hard-delete a channel and all its messages. Note: this * is distinct from {@link archive} (soft delete via archived_at). * Used by the legacy `deleteConversation` controller route. * * FK ON DELETE CASCADE on `chat_messages.channel_id` cleans up * messages atomically with the channel row. * * @param id - The channel id to remove * @returns True if a row was removed, false if no such channel */ hardDelete(id: string): boolean; /** * Bump `last_message_at` to a provided timestamp if it is greater than * the current value. Safe to call many times. * * @param id - The channel id * @param timestampMs - The timestamp to record */ touchLastMessageAt(id: string, timestampMs: number): void; } //# sourceMappingURL=channel.store.d.ts.map