/** * MessageDraftRepository - CRUD + upsert, delete_for_message for message_drafts table (§5) * * Implements Repository * per contracts §6. * * @module packages/runtime/src/storage/repositories/MessageDraftRepository */ import type { Repository, TransactionHandle, SessionID, MessageID, ISOTimeString, } from '@aircoding/contracts' import { DatabaseHandle } from '../MigrationRunner.js' import { assertEnumValues } from '../assertEnum.js' // ============================================================================= // Types - per db-schema §5 // ============================================================================= export interface MessageDraftRecord { message_id: MessageID session_id: SessionID role: 'user' | 'assistant' | 'system' | 'tool' canonical_format: 'anthropic' partial_content_json: string status: 'streaming' | 'interrupted' | 'error' created_at: ISOTimeString updated_at: ISOTimeString metadata_json?: string } export type MessageDraftInsert = Omit & { message_id?: MessageID } export type MessageDraftUpdate = Partial> // ============================================================================= // MessageDraftRepository // ============================================================================= export class MessageDraftRepository implements Repository { private db: DatabaseHandle constructor(db: DatabaseHandle) { this.db = db } /** * Get a draft by message ID. */ async get(message_id: MessageID, _tx?: TransactionHandle): Promise { const stmt = this.db.prepare('SELECT * FROM message_drafts WHERE message_id = ?') const row = stmt.get(message_id) as MessageDraftRecord | undefined return row } /** * Insert a new draft. */ async insert(record: MessageDraftInsert, _tx?: TransactionHandle): Promise { // Validate enum columns (only status is a closed enum for message_drafts) assertEnumValues('message_drafts', { status: record.status, }) const stmt = this.db.prepare(` INSERT INTO message_drafts ( message_id, session_id, role, canonical_format, partial_content_json, status, created_at, updated_at, metadata_json ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `) stmt.run( record.message_id, record.session_id, record.role, record.canonical_format, record.partial_content_json, record.status, record.created_at, record.updated_at, record.metadata_json ?? null, ) } /** * Update an existing draft. */ async update(message_id: MessageID, patch: MessageDraftUpdate, _tx?: TransactionHandle): Promise { // Validate enum columns if present (only status is a closed enum for message_drafts) if (patch.status !== undefined) { assertEnumValues('message_drafts', { status: patch.status }) } const fields: string[] = [] const values: unknown[] = [] if (patch.partial_content_json !== undefined) { fields.push('partial_content_json = ?') values.push(patch.partial_content_json) } if (patch.status !== undefined) { fields.push('status = ?') values.push(patch.status) } if (patch.updated_at !== undefined) { fields.push('updated_at = ?') values.push(patch.updated_at) } if (patch.metadata_json !== undefined) { fields.push('metadata_json = ?') values.push(patch.metadata_json) } if (fields.length === 0) { return // Nothing to update } values.push(message_id) const stmt = this.db.prepare(`UPDATE message_drafts SET ${fields.join(', ')} WHERE message_id = ?`) stmt.run(...values) } // ============================================================================= // Extra methods // ============================================================================= /** * Upsert a draft - insert or replace existing. */ async upsert(record: MessageDraftRecord, _tx?: TransactionHandle): Promise { // Validate enum columns (only status is a closed enum for message_drafts) assertEnumValues('message_drafts', { status: record.status, }) const stmt = this.db.prepare(` INSERT OR REPLACE INTO message_drafts ( message_id, session_id, role, canonical_format, partial_content_json, status, created_at, updated_at, metadata_json ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `) stmt.run( record.message_id, record.session_id, record.role, record.canonical_format, record.partial_content_json, record.status, record.created_at, record.updated_at, record.metadata_json ?? null, ) } /** * Delete draft for a specific message. */ async delete_for_message(message_id: MessageID, _tx?: TransactionHandle): Promise { const stmt = this.db.prepare('DELETE FROM message_drafts WHERE message_id = ?') stmt.run(message_id) } }