feat: comprehensive project structure improvements and Cloud for Sovereignty landing zone
- 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
This commit is contained in:
265
packages/database/src/document-comments.ts
Normal file
265
packages/database/src/document-comments.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Document Comments and Annotations
|
||||
* Handles comments, annotations, and collaborative review features
|
||||
*/
|
||||
|
||||
import { query } from './client';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const DocumentCommentSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
document_id: z.string().uuid(),
|
||||
version_id: z.string().uuid().optional(),
|
||||
parent_comment_id: z.string().uuid().optional(),
|
||||
comment_text: z.string(),
|
||||
comment_type: z.enum(['comment', 'suggestion', 'question', 'resolution']),
|
||||
status: z.enum(['open', 'resolved', 'dismissed']),
|
||||
page_number: z.number().int().optional(),
|
||||
x_position: z.number().optional(),
|
||||
y_position: z.number().optional(),
|
||||
highlight_text: z.string().optional(),
|
||||
author_id: z.string().uuid(),
|
||||
resolved_by: z.string().uuid().optional(),
|
||||
resolved_at: z.date().optional(),
|
||||
created_at: z.date(),
|
||||
updated_at: z.date(),
|
||||
});
|
||||
|
||||
export type DocumentComment = z.infer<typeof DocumentCommentSchema>;
|
||||
|
||||
export interface CreateDocumentCommentInput {
|
||||
document_id: string;
|
||||
version_id?: string;
|
||||
parent_comment_id?: string;
|
||||
comment_text: string;
|
||||
comment_type?: 'comment' | 'suggestion' | 'question' | 'resolution';
|
||||
status?: 'open' | 'resolved' | 'dismissed';
|
||||
page_number?: number;
|
||||
x_position?: number;
|
||||
y_position?: number;
|
||||
highlight_text?: string;
|
||||
author_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document comment
|
||||
*/
|
||||
export async function createDocumentComment(
|
||||
input: CreateDocumentCommentInput
|
||||
): Promise<DocumentComment> {
|
||||
const result = await query<DocumentComment>(
|
||||
`INSERT INTO document_comments
|
||||
(document_id, version_id, parent_comment_id, comment_text, comment_type,
|
||||
status, page_number, x_position, y_position, highlight_text, author_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING *`,
|
||||
[
|
||||
input.document_id,
|
||||
input.version_id || null,
|
||||
input.parent_comment_id || null,
|
||||
input.comment_text,
|
||||
input.comment_type || 'comment',
|
||||
input.status || 'open',
|
||||
input.page_number || null,
|
||||
input.x_position || null,
|
||||
input.y_position || null,
|
||||
input.highlight_text || null,
|
||||
input.author_id,
|
||||
]
|
||||
);
|
||||
|
||||
return result.rows[0]!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comment by ID
|
||||
*/
|
||||
export async function getDocumentComment(id: string): Promise<DocumentComment | null> {
|
||||
const result = await query<DocumentComment>(
|
||||
`SELECT * FROM document_comments WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all comments for a document
|
||||
*/
|
||||
export async function getDocumentComments(
|
||||
document_id: string,
|
||||
version_id?: string,
|
||||
include_resolved = false
|
||||
): Promise<DocumentComment[]> {
|
||||
const conditions = ['document_id = $1'];
|
||||
const params: unknown[] = [document_id];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (version_id) {
|
||||
conditions.push(`version_id = $${paramIndex++}`);
|
||||
params.push(version_id);
|
||||
}
|
||||
|
||||
if (!include_resolved) {
|
||||
conditions.push(`status != 'resolved'`);
|
||||
}
|
||||
|
||||
const result = await query<DocumentComment>(
|
||||
`SELECT * FROM document_comments
|
||||
WHERE ${conditions.join(' AND ')}
|
||||
ORDER BY created_at ASC`,
|
||||
params
|
||||
);
|
||||
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get threaded comments (with replies)
|
||||
*/
|
||||
export interface ThreadedComment extends DocumentComment {
|
||||
replies?: ThreadedComment[];
|
||||
}
|
||||
|
||||
export async function getThreadedDocumentComments(
|
||||
document_id: string,
|
||||
version_id?: string
|
||||
): Promise<ThreadedComment[]> {
|
||||
const allComments = await getDocumentComments(document_id, version_id, true);
|
||||
|
||||
// Build tree structure
|
||||
const commentMap = new Map<string, ThreadedComment>();
|
||||
const rootComments: ThreadedComment[] = [];
|
||||
|
||||
// First pass: create map
|
||||
for (const comment of allComments) {
|
||||
commentMap.set(comment.id, { ...comment, replies: [] });
|
||||
}
|
||||
|
||||
// Second pass: build tree
|
||||
for (const comment of allComments) {
|
||||
const threaded = commentMap.get(comment.id)!;
|
||||
if (comment.parent_comment_id) {
|
||||
const parent = commentMap.get(comment.parent_comment_id);
|
||||
if (parent) {
|
||||
parent.replies!.push(threaded);
|
||||
}
|
||||
} else {
|
||||
rootComments.push(threaded);
|
||||
}
|
||||
}
|
||||
|
||||
return rootComments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update comment
|
||||
*/
|
||||
export async function updateDocumentComment(
|
||||
id: string,
|
||||
updates: Partial<Pick<DocumentComment, 'comment_text' | 'status'>>
|
||||
): Promise<DocumentComment | null> {
|
||||
const fields: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (updates.comment_text !== undefined) {
|
||||
fields.push(`comment_text = $${paramIndex++}`);
|
||||
values.push(updates.comment_text);
|
||||
}
|
||||
|
||||
if (updates.status !== undefined) {
|
||||
fields.push(`status = $${paramIndex++}`);
|
||||
values.push(updates.status);
|
||||
|
||||
if (updates.status === 'resolved') {
|
||||
// Get current user from context - for now, we'll need to pass it
|
||||
// This should be handled by the service layer
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return getDocumentComment(id);
|
||||
}
|
||||
|
||||
fields.push(`updated_at = NOW()`);
|
||||
values.push(id);
|
||||
|
||||
const result = await query<DocumentComment>(
|
||||
`UPDATE document_comments
|
||||
SET ${fields.join(', ')}
|
||||
WHERE id = $${paramIndex}
|
||||
RETURNING *`,
|
||||
values
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve comment
|
||||
*/
|
||||
export async function resolveDocumentComment(
|
||||
id: string,
|
||||
resolved_by: string
|
||||
): Promise<DocumentComment | null> {
|
||||
const result = await query<DocumentComment>(
|
||||
`UPDATE document_comments
|
||||
SET status = 'resolved', resolved_by = $1, resolved_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $2
|
||||
RETURNING *`,
|
||||
[resolved_by, id]
|
||||
);
|
||||
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comment statistics for a document
|
||||
*/
|
||||
export interface CommentStatistics {
|
||||
total: number;
|
||||
open: number;
|
||||
resolved: number;
|
||||
dismissed: number;
|
||||
by_type: Record<string, number>;
|
||||
}
|
||||
|
||||
export async function getDocumentCommentStatistics(
|
||||
document_id: string
|
||||
): Promise<CommentStatistics> {
|
||||
const result = await query<{
|
||||
total: string;
|
||||
open: string;
|
||||
resolved: string;
|
||||
dismissed: string;
|
||||
comment_type: string;
|
||||
count: string;
|
||||
}>(
|
||||
`SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(*) FILTER (WHERE status = 'open') as open,
|
||||
COUNT(*) FILTER (WHERE status = 'resolved') as resolved,
|
||||
COUNT(*) FILTER (WHERE status = 'dismissed') as dismissed,
|
||||
comment_type,
|
||||
COUNT(*) as count
|
||||
FROM document_comments
|
||||
WHERE document_id = $1
|
||||
GROUP BY comment_type`,
|
||||
[document_id]
|
||||
);
|
||||
|
||||
const stats: CommentStatistics = {
|
||||
total: parseInt(result.rows[0]?.total || '0', 10),
|
||||
open: parseInt(result.rows[0]?.open || '0', 10),
|
||||
resolved: parseInt(result.rows[0]?.resolved || '0', 10),
|
||||
dismissed: parseInt(result.rows[0]?.dismissed || '0', 10),
|
||||
by_type: {},
|
||||
};
|
||||
|
||||
for (const row of result.rows) {
|
||||
stats.by_type[row.comment_type] = parseInt(row.count, 10);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user