/** * Document Checkout/Lock Management * Prevents concurrent edits and manages document locks */ import { query } from './client'; import { z } from 'zod'; export const DocumentCheckoutSchema = z.object({ id: z.string().uuid(), document_id: z.string().uuid(), checked_out_by: z.string().uuid(), checked_out_at: z.date(), expires_at: z.date(), lock_type: z.enum(['exclusive', 'shared_read']), notes: z.string().optional(), }); export type DocumentCheckout = z.infer; export interface CreateCheckoutInput { document_id: string; checked_out_by: string; duration_hours?: number; lock_type?: 'exclusive' | 'shared_read'; notes?: string; } /** * Check out a document (lock it for editing) */ export async function checkoutDocument( input: CreateCheckoutInput ): Promise { // Check if document is already checked out const existing = await getDocumentCheckout(input.document_id); if (existing) { if (existing.checked_out_by !== input.checked_out_by) { throw new Error('Document is already checked out by another user'); } // Same user - extend checkout return extendCheckout(input.document_id, input.duration_hours || 24); } const duration_hours = input.duration_hours || 24; const expires_at = new Date(); expires_at.setHours(expires_at.getHours() + duration_hours); const result = await query( `INSERT INTO document_checkouts (document_id, checked_out_by, expires_at, lock_type, notes) VALUES ($1, $2, $3, $4, $5) RETURNING *`, [ input.document_id, input.checked_out_by, expires_at, input.lock_type || 'exclusive', input.notes || null, ] ); // Update document table await query( `UPDATE documents SET is_checked_out = TRUE, checked_out_by = $1, checked_out_at = NOW() WHERE id = $2`, [input.checked_out_by, input.document_id] ); return result.rows[0]!; } /** * Check in a document (release the lock) */ export async function checkinDocument( document_id: string, checked_out_by: string ): Promise { const checkout = await getDocumentCheckout(document_id); if (!checkout) { return false; // Not checked out } if (checkout.checked_out_by !== checked_out_by) { throw new Error('Document is checked out by another user'); } await query( `DELETE FROM document_checkouts WHERE document_id = $1`, [document_id] ); // Update document table await query( `UPDATE documents SET is_checked_out = FALSE, checked_out_by = NULL, checked_out_at = NULL WHERE id = $1`, [document_id] ); return true; } /** * Get checkout status for a document */ export async function getDocumentCheckout( document_id: string ): Promise { // Clean up expired checkouts first await cleanupExpiredCheckouts(); const result = await query( `SELECT * FROM document_checkouts WHERE document_id = $1 AND expires_at > NOW()`, [document_id] ); return result.rows[0] || null; } /** * Extend checkout duration */ export async function extendCheckout( document_id: string, additional_hours: number ): Promise { const result = await query( `UPDATE document_checkouts SET expires_at = expires_at + INTERVAL '${additional_hours} hours' WHERE document_id = $1 RETURNING *`, [document_id] ); if (result.rows.length === 0) { throw new Error('Document is not checked out'); } return result.rows[0]!; } /** * Force release checkout (admin function) */ export async function forceReleaseCheckout(document_id: string): Promise { await query(`DELETE FROM document_checkouts WHERE document_id = $1`, [document_id]); await query( `UPDATE documents SET is_checked_out = FALSE, checked_out_by = NULL, checked_out_at = NULL WHERE id = $1`, [document_id] ); return true; } /** * Clean up expired checkouts */ export async function cleanupExpiredCheckouts(): Promise { const result = await query<{ count: string }>( `SELECT COUNT(*) as count FROM document_checkouts WHERE expires_at <= NOW()` ); const expiredCount = parseInt(result.rows[0]?.count || '0', 10); if (expiredCount > 0) { await query( `DELETE FROM document_checkouts WHERE expires_at <= NOW()` ); // Update documents table await query( `UPDATE documents SET is_checked_out = FALSE, checked_out_by = NULL, checked_out_at = NULL WHERE id IN ( SELECT document_id FROM document_checkouts WHERE expires_at <= NOW() )` ); } return expiredCount; } /** * Get all checkouts for a user */ export async function getUserCheckouts(user_id: string): Promise { await cleanupExpiredCheckouts(); const result = await query( `SELECT * FROM document_checkouts WHERE checked_out_by = $1 AND expires_at > NOW() ORDER BY expires_at ASC`, [user_id] ); return result.rows; } /** * Get all active checkouts */ export async function getAllActiveCheckouts(): Promise { await cleanupExpiredCheckouts(); const result = await query( `SELECT * FROM document_checkouts WHERE expires_at > NOW() ORDER BY expires_at ASC` ); return result.rows; }