269 lines
6.8 KiB
TypeScript
269 lines
6.8 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<typeof DocumentVersionSchema>;
|
||
|
|
|
||
|
|
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<DocumentVersion> {
|
||
|
|
// 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<DocumentVersion>(
|
||
|
|
`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<DocumentVersion | null> {
|
||
|
|
const result = await query<DocumentVersion>(
|
||
|
|
`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<DocumentVersion[]> {
|
||
|
|
const result = await query<DocumentVersion>(
|
||
|
|
`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<DocumentVersion | null> {
|
||
|
|
const result = await query<DocumentVersion>(
|
||
|
|
`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<DocumentVersion | null> {
|
||
|
|
const result = await query<DocumentVersion>(
|
||
|
|
`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<VersionComparison | null> {
|
||
|
|
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<DocumentVersion> {
|
||
|
|
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<VersionHistoryEntry[]> {
|
||
|
|
const result = await query<VersionHistoryEntry>(
|
||
|
|
`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;
|
||
|
|
}
|
||
|
|
|