- Add Cloud for Sovereignty landing zone architecture and deployment - Implement complete legal document management system - Reorganize documentation with improved navigation - Add infrastructure improvements (Dockerfiles, K8s, monitoring) - Add operational improvements (graceful shutdown, rate limiting, caching) - Create comprehensive project structure documentation - Add Azure deployment automation scripts - Improve repository navigation and organization
219 lines
5.3 KiB
TypeScript
219 lines
5.3 KiB
TypeScript
/**
|
|
* 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<typeof DocumentCheckoutSchema>;
|
|
|
|
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<DocumentCheckout> {
|
|
// 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<DocumentCheckout>(
|
|
`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<boolean> {
|
|
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<DocumentCheckout | null> {
|
|
// Clean up expired checkouts first
|
|
await cleanupExpiredCheckouts();
|
|
|
|
const result = await query<DocumentCheckout>(
|
|
`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<DocumentCheckout> {
|
|
const result = await query<DocumentCheckout>(
|
|
`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<boolean> {
|
|
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<number> {
|
|
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<DocumentCheckout[]> {
|
|
await cleanupExpiredCheckouts();
|
|
|
|
const result = await query<DocumentCheckout>(
|
|
`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<DocumentCheckout[]> {
|
|
await cleanupExpiredCheckouts();
|
|
|
|
const result = await query<DocumentCheckout>(
|
|
`SELECT * FROM document_checkouts
|
|
WHERE expires_at > NOW()
|
|
ORDER BY expires_at ASC`
|
|
);
|
|
return result.rows;
|
|
}
|
|
|