/** * Document Version Management * Handles document versioning, revision history, and version control */ import { query } from './client'; import { z } from 'zod'; export const DocumentVersionSchema = z.object({ id: z.string().uuid(), document_id: z.string().uuid(), version_number: z.number().int().positive(), version_label: z.string().optional(), content: z.string().optional(), file_url: z.string().url().optional(), file_hash: z.string().optional(), file_size: z.number().int().nonnegative().optional(), mime_type: z.string().optional(), change_summary: z.string().optional(), change_type: z.enum(['created', 'modified', 'restored', 'merged']), created_by: z.string().uuid().optional(), created_at: z.date(), }); export type DocumentVersion = z.infer; export interface CreateDocumentVersionInput { document_id: string; version_number?: number; version_label?: string; content?: string; file_url?: string; file_hash?: string; file_size?: number; mime_type?: string; change_summary?: string; change_type?: 'created' | 'modified' | 'restored' | 'merged'; created_by?: string; } /** * Create a new document version */ export async function createDocumentVersion( input: CreateDocumentVersionInput ): Promise { // Get next version number if not provided let version_number = input.version_number; if (!version_number) { const maxVersion = await query<{ max_version: number | null }>( `SELECT MAX(version_number) as max_version FROM document_versions WHERE document_id = $1`, [input.document_id] ); version_number = (maxVersion.rows[0]?.max_version || 0) + 1; } const result = await query( `INSERT INTO document_versions (document_id, version_number, version_label, content, file_url, file_hash, file_size, mime_type, change_summary, change_type, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`, [ input.document_id, version_number, input.version_label || null, input.content || null, input.file_url || null, input.file_hash || null, input.file_size || null, input.mime_type || null, input.change_summary || null, input.change_type || 'modified', input.created_by || null, ] ); const version = result.rows[0]!; // Update document's current version await query( `UPDATE documents SET current_version = $1, latest_version_id = $2, updated_at = NOW() WHERE id = $3`, [version_number, version.id, input.document_id] ); return version; } /** * Get document version by ID */ export async function getDocumentVersion(id: string): Promise { const result = await query( `SELECT * FROM document_versions WHERE id = $1`, [id] ); return result.rows[0] || null; } /** * Get all versions for a document */ export async function getDocumentVersions( document_id: string, limit = 100, offset = 0 ): Promise { const result = await query( `SELECT * FROM document_versions WHERE document_id = $1 ORDER BY version_number DESC LIMIT $2 OFFSET $3`, [document_id, limit, offset] ); return result.rows; } /** * Get specific version by number */ export async function getDocumentVersionByNumber( document_id: string, version_number: number ): Promise { const result = await query( `SELECT * FROM document_versions WHERE document_id = $1 AND version_number = $2`, [document_id, version_number] ); return result.rows[0] || null; } /** * Get latest version of a document */ export async function getLatestDocumentVersion( document_id: string ): Promise { const result = await query( `SELECT * FROM document_versions WHERE document_id = $1 ORDER BY version_number DESC LIMIT 1`, [document_id] ); return result.rows[0] || null; } /** * Compare two document versions */ export interface VersionComparison { version1: DocumentVersion; version2: DocumentVersion; differences: { field: string; old_value: unknown; new_value: unknown; }[]; } export async function compareDocumentVersions( version1_id: string, version2_id: string ): Promise { const v1 = await getDocumentVersion(version1_id); const v2 = await getDocumentVersion(version2_id); if (!v1 || !v2) { return null; } const differences: VersionComparison['differences'] = []; if (v1.content !== v2.content) { differences.push({ field: 'content', old_value: v1.content, new_value: v2.content, }); } if (v1.file_url !== v2.file_url) { differences.push({ field: 'file_url', old_value: v1.file_url, new_value: v2.file_url, }); } if (v1.file_hash !== v2.file_hash) { differences.push({ field: 'file_hash', old_value: v1.file_hash, new_value: v2.file_hash, }); } if (v1.change_summary !== v2.change_summary) { differences.push({ field: 'change_summary', old_value: v1.change_summary, new_value: v2.change_summary, }); } return { version1: v1, version2: v2, differences, }; } /** * Restore a document to a previous version */ export async function restoreDocumentVersion( document_id: string, version_id: string, restored_by: string, change_summary?: string ): Promise { const targetVersion = await getDocumentVersion(version_id); if (!targetVersion || targetVersion.document_id !== document_id) { throw new Error('Version not found or does not belong to document'); } // Create new version from the restored one return createDocumentVersion({ document_id, version_label: `Restored from v${targetVersion.version_number}`, content: targetVersion.content, file_url: targetVersion.file_url, file_hash: targetVersion.file_hash, file_size: targetVersion.file_size, mime_type: targetVersion.mime_type, change_summary: change_summary || `Restored from version ${targetVersion.version_number}`, change_type: 'restored', created_by: restored_by, }); } /** * Get version history with change summaries */ export interface VersionHistoryEntry extends DocumentVersion { created_by_name?: string; created_by_email?: string; } export async function getDocumentVersionHistory( document_id: string ): Promise { const result = await query( `SELECT dv.*, u.name as created_by_name, u.email as created_by_email FROM document_versions dv LEFT JOIN users u ON dv.created_by = u.id WHERE dv.document_id = $1 ORDER BY dv.version_number DESC`, [document_id] ); return result.rows; }