2025-07-13 22:07:07 -05:00

330 lines
9.0 KiB
TypeScript

import { z } from "zod";
import type {
Project,
Task,
Documentation,
Tag,
TaskTag,
TaskDependency,
AuditLog,
TaskWithRelations,
ProjectOverview,
} from "../types/taskmaster.js";
// Database row type converters
export function convertProjectRow(row: any): Project {
return {
id: row.id,
name: row.name,
description: row.description || undefined,
goals: row.goals || undefined,
target_users: row.target_users || undefined,
why_statement: row.why_statement || undefined,
created_by: row.created_by,
created_at: new Date(row.created_at),
updated_at: new Date(row.updated_at),
};
}
export function convertTaskRow(row: any): Task {
return {
id: row.id,
project_id: row.project_id,
title: row.title,
description: row.description || undefined,
status: row.status,
priority: row.priority,
assigned_to: row.assigned_to || undefined,
parent_task_id: row.parent_task_id || undefined,
estimated_hours: row.estimated_hours || undefined,
actual_hours: row.actual_hours || undefined,
due_date: row.due_date ? new Date(row.due_date) : undefined,
acceptance_criteria: row.acceptance_criteria || undefined,
created_by: row.created_by,
created_at: new Date(row.created_at),
updated_at: new Date(row.updated_at),
};
}
export function convertDocumentationRow(row: any): Documentation {
return {
id: row.id,
project_id: row.project_id,
type: row.type,
title: row.title,
content: row.content,
version: row.version,
created_by: row.created_by,
created_at: new Date(row.created_at),
updated_at: new Date(row.updated_at),
};
}
export function convertTagRow(row: any): Tag {
return {
id: row.id,
name: row.name,
color: row.color || undefined,
description: row.description || undefined,
created_by: row.created_by,
created_at: new Date(row.created_at),
};
}
export function convertTaskDependencyRow(row: any): TaskDependency {
return {
task_id: row.task_id,
depends_on_task_id: row.depends_on_task_id,
dependency_type: row.dependency_type,
};
}
export function convertAuditLogRow(row: any): AuditLog {
return {
id: row.id,
table_name: row.table_name,
record_id: row.record_id,
action: row.action,
old_values: row.old_values || undefined,
new_values: row.new_values || undefined,
changed_by: row.changed_by,
changed_at: new Date(row.changed_at),
};
}
// Database operation helpers
export interface TaskListFilters {
project_id?: string;
status?: Task['status'];
priority?: Task['priority'];
assigned_to?: string;
tag?: string;
parent_task_id?: string;
has_dependencies?: boolean;
due_before?: Date;
due_after?: Date;
created_by?: string;
limit?: number;
offset?: number;
}
export interface DocumentationListFilters {
project_id?: string;
type?: Documentation['type'];
created_by?: string;
limit?: number;
offset?: number;
}
export interface ProjectListFilters {
created_by?: string;
search_term?: string;
limit?: number;
offset?: number;
}
// Validation schemas for database operations
export const DatabaseTaskSchema = z.object({
project_id: z.string().uuid(),
title: z.string().min(1).max(500),
description: z.string().optional(),
status: z.enum(['pending', 'in_progress', 'completed', 'blocked']).default('pending'),
priority: z.enum(['low', 'medium', 'high', 'urgent']).default('medium'),
assigned_to: z.string().optional(),
parent_task_id: z.string().uuid().optional(),
estimated_hours: z.number().int().positive().optional(),
actual_hours: z.number().int().min(0).optional(),
due_date: z.date().optional(),
acceptance_criteria: z.array(z.string()).optional(),
created_by: z.string().min(1),
});
export const DatabaseProjectSchema = z.object({
name: z.string().min(1).max(255),
description: z.string().optional(),
goals: z.string().optional(),
target_users: z.string().optional(),
why_statement: z.string().optional(),
created_by: z.string().min(1),
});
export const DatabaseDocumentationSchema = z.object({
project_id: z.string().uuid(),
type: z.enum(['goals', 'why', 'target_users', 'specifications', 'notes']),
title: z.string().min(1).max(255),
content: z.string().min(1),
version: z.number().int().positive().default(1),
created_by: z.string().min(1),
});
export const DatabaseTagSchema = z.object({
name: z.string().min(1).max(100),
color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
description: z.string().optional(),
created_by: z.string().min(1),
});
// Query builders for complex operations
export interface TaskQueryBuilder {
withTags?: boolean;
withDependencies?: boolean;
withProject?: boolean;
withParentTask?: boolean;
withSubtasks?: boolean;
}
export interface ProjectStatsQuery {
include_task_counts?: boolean;
include_recent_activity?: boolean;
include_upcoming_deadlines?: boolean;
recent_activity_limit?: number;
upcoming_deadline_days?: number;
}
// Audit logging helpers
export interface AuditLogEntry {
table_name: string;
record_id: string;
action: 'insert' | 'update' | 'delete';
old_values?: Record<string, any>;
new_values?: Record<string, any>;
changed_by: string;
}
export function createAuditLogEntry(
tableName: string,
recordId: string,
action: AuditLogEntry['action'],
changedBy: string,
oldValues?: Record<string, any>,
newValues?: Record<string, any>
): AuditLogEntry {
return {
table_name: tableName,
record_id: recordId,
action,
old_values: oldValues,
new_values: newValues,
changed_by: changedBy,
};
}
// SQL query templates
export const SQL_QUERIES = {
// Project queries
SELECT_PROJECT_BY_ID: `
SELECT * FROM projects WHERE id = $1
`,
SELECT_PROJECT_BY_NAME: `
SELECT * FROM projects WHERE name = $1
`,
LIST_PROJECTS: `
SELECT * FROM projects
WHERE ($1::text IS NULL OR created_by = $1)
AND ($2::text IS NULL OR name ILIKE '%' || $2 || '%' OR description ILIKE '%' || $2 || '%')
ORDER BY created_at DESC
LIMIT $3 OFFSET $4
`,
// Task queries
SELECT_TASK_BY_ID: `
SELECT t.*, p.name as project_name
FROM tasks t
LEFT JOIN projects p ON t.project_id = p.id
WHERE t.id = $1
`,
LIST_TASKS_WITH_FILTERS: `
SELECT DISTINCT t.*, p.name as project_name
FROM tasks t
LEFT JOIN projects p ON t.project_id = p.id
LEFT JOIN task_tags tt ON t.id = tt.task_id
LEFT JOIN tags tag ON tt.tag_id = tag.id
WHERE ($1::uuid IS NULL OR t.project_id = $1)
AND ($2::text IS NULL OR t.status = $2)
AND ($3::text IS NULL OR t.priority = $3)
AND ($4::text IS NULL OR t.assigned_to = $4)
AND ($5::text IS NULL OR tag.name = $5)
AND ($6::uuid IS NULL OR t.parent_task_id = $6)
AND ($7::text IS NULL OR t.created_by = $7)
ORDER BY t.created_at DESC
LIMIT $8 OFFSET $9
`,
SELECT_TASK_TAGS: `
SELECT tag.* FROM tags tag
JOIN task_tags tt ON tag.id = tt.tag_id
WHERE tt.task_id = $1
`,
SELECT_TASK_DEPENDENCIES: `
SELECT td.*, t.title as depends_on_title
FROM task_dependencies td
JOIN tasks t ON td.depends_on_task_id = t.id
WHERE td.task_id = $1
`,
// Documentation queries
LIST_DOCUMENTATION: `
SELECT * FROM documentation
WHERE ($1::uuid IS NULL OR project_id = $1)
AND ($2::text IS NULL OR type = $2)
AND ($3::text IS NULL OR created_by = $3)
ORDER BY created_at DESC
LIMIT $4 OFFSET $5
`,
// Tag queries
SELECT_TAG_BY_NAME: `
SELECT * FROM tags WHERE name = $1
`,
LIST_TAGS: `
SELECT * FROM tags ORDER BY name
`,
// Project overview queries
PROJECT_TASK_STATISTICS: `
SELECT
COUNT(*) as total_tasks,
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_tasks,
COUNT(CASE WHEN status = 'in_progress' THEN 1 END) as in_progress_tasks,
COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending_tasks,
COUNT(CASE WHEN status = 'blocked' THEN 1 END) as blocked_tasks,
CASE
WHEN COUNT(*) > 0 THEN
ROUND((COUNT(CASE WHEN status = 'completed' THEN 1 END) * 100.0 / COUNT(*)), 2)
ELSE 0
END as completion_percentage
FROM tasks
WHERE project_id = $1
`,
RECENT_TASKS: `
SELECT * FROM tasks
WHERE project_id = $1
ORDER BY updated_at DESC
LIMIT $2
`,
UPCOMING_DEADLINES: `
SELECT * FROM tasks
WHERE project_id = $1
AND due_date IS NOT NULL
AND due_date >= CURRENT_DATE
AND due_date <= CURRENT_DATE + INTERVAL '$2 days'
AND status != 'completed'
ORDER BY due_date ASC
`,
} as const;
// Type-safe query parameter helpers
export type QueryParams<T extends keyof typeof SQL_QUERIES> =
T extends 'LIST_PROJECTS' ? [string | null, string | null, number, number] :
T extends 'LIST_TASKS_WITH_FILTERS' ? [string | null, string | null, string | null, string | null, string | null, string | null, string | null, number, number] :
T extends 'PROJECT_TASK_STATISTICS' ? [string] :
T extends 'RECENT_TASKS' ? [string, number] :
T extends 'UPCOMING_DEADLINES' ? [string, number] :
any[];