taskService.ts•40.7 kB
import { int } from "neo4j-driver"; // Import 'int' for pagination
import { logger, requestContextService } from "../../utils/index.js"; // Updated import path
import { neo4jDriver } from "./driver.js";
import { generateId, buildListQuery } from "./helpers.js"; // Import buildListQuery
import {
Neo4jTask, // This type no longer has assignedTo
NodeLabels,
PaginatedResult,
RelationshipTypes,
TaskFilterOptions,
} from "./types.js";
import { Neo4jUtils } from "./utils.js";
/**
* Service for managing Task entities in Neo4j
*/
export class TaskService {
/**
* Create a new task and optionally assign it to a user.
* @param task Task data, including optional assignedTo for relationship creation
* @returns The created task
*/
static async createTask(
task: Omit<Neo4jTask, "id" | "createdAt" | "updatedAt"> & {
id?: string;
assignedTo?: string;
},
): Promise<Neo4jTask & { assignedToUserId?: string | null }> {
const session = await neo4jDriver.getSession();
try {
const projectExists = await Neo4jUtils.nodeExists(
NodeLabels.Project,
"id",
task.projectId,
);
if (!projectExists) {
throw new Error(`Project with ID ${task.projectId} not found`);
}
const taskId = task.id || `task_${generateId()}`;
const now = Neo4jUtils.getCurrentTimestamp();
const assignedToUserId = task.assignedTo; // Get assignee from input
// No longer check if user exists here, will use MERGE later
// Serialize urls to JSON string
const query = `
MATCH (p:${NodeLabels.Project} {id: $projectId})
CREATE (t:${NodeLabels.Task} {
id: $id,
projectId: $projectId,
title: $title,
description: $description,
priority: $priority,
status: $status,
// assignedTo removed
urls: $urls,
tags: $tags,
completionRequirements: $completionRequirements,
outputFormat: $outputFormat,
taskType: $taskType,
createdAt: $createdAt,
updatedAt: $updatedAt
})
CREATE (p)-[:${RelationshipTypes.CONTAINS_TASK}]->(t)
// Optionally create ASSIGNED_TO relationship using MERGE for the User node
WITH t
${assignedToUserId ? `MERGE (u:${NodeLabels.User} {id: $assignedToUserId}) ON CREATE SET u.createdAt = $createdAt CREATE (t)-[:${RelationshipTypes.ASSIGNED_TO}]->(u)` : ""}
// Return properties defined in Neo4jTask
WITH t // Ensure t is in scope before optional match
OPTIONAL MATCH (t)-[:${RelationshipTypes.ASSIGNED_TO}]->(assigned_user:${NodeLabels.User}) // Match to get assigned user's ID
RETURN t.id as id,
t.projectId as projectId,
t.title as title,
t.description as description,
t.priority as priority,
t.status as status,
assigned_user.id as assignedToUserId, // Add this
t.urls as urls,
t.tags as tags,
t.completionRequirements as completionRequirements,
t.outputFormat as outputFormat,
t.taskType as taskType,
t.createdAt as createdAt,
t.updatedAt as updatedAt
`;
// Serialize urls to JSON string
const params: Record<string, any> = {
id: taskId,
projectId: task.projectId,
title: task.title,
description: task.description,
priority: task.priority,
status: task.status,
// assignedTo removed from params
urls: JSON.stringify(task.urls || []), // Serialize urls
tags: task.tags || [],
completionRequirements: task.completionRequirements,
outputFormat: task.outputFormat,
taskType: task.taskType,
createdAt: now,
updatedAt: now,
};
if (assignedToUserId) {
params.assignedToUserId = assignedToUserId;
}
const result = await session.executeWrite(async (tx) => {
const result = await tx.run(query, params);
// Use .get() for each field
return result.records.length > 0 ? result.records[0] : null;
});
if (!result) {
throw new Error("Failed to create task or retrieve its properties");
}
// Construct the Neo4jTask object - deserialize urls
const createdTaskData: Neo4jTask & { assignedToUserId?: string | null } =
{
id: result.get("id"),
projectId: result.get("projectId"),
title: result.get("title"),
description: result.get("description"),
priority: result.get("priority"),
status: result.get("status"),
urls: JSON.parse(result.get("urls") || "[]"), // Deserialize urls
tags: result.get("tags") || [],
completionRequirements: result.get("completionRequirements"),
outputFormat: result.get("outputFormat"),
taskType: result.get("taskType"),
createdAt: result.get("createdAt"),
updatedAt: result.get("updatedAt"),
assignedToUserId: result.get("assignedToUserId") || null,
};
const reqContext_create = requestContextService.createRequestContext({
operation: "createTask",
taskId: createdTaskData.id,
projectId: task.projectId,
assignedToUserId,
});
logger.info("Task created successfully", reqContext_create);
return createdTaskData;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorContext = requestContextService.createRequestContext({
operation: "createTask.error",
taskInput: task,
});
logger.error("Error creating task", error as Error, {
...errorContext,
detail: errorMessage,
});
throw error;
} finally {
await session.close();
}
}
/**
* Link a Task to a Knowledge item with a specified relationship type.
* @param taskId ID of the source Task item
* @param knowledgeId ID of the target Knowledge item
* @param relationshipType The type of relationship to create (e.g., 'ADDRESSES', 'REFERENCES') - Validation needed
* @returns True if the link was created successfully, false otherwise
*/
static async linkTaskToKnowledge(
taskId: string,
knowledgeId: string,
relationshipType: string,
): Promise<boolean> {
// TODO: Validate relationshipType against allowed types or RelationshipTypes enum
const session = await neo4jDriver.getSession();
const reqContext_link = requestContextService.createRequestContext({
operation: "linkTaskToKnowledge",
taskId,
knowledgeId,
relationshipType,
});
logger.debug(
`Attempting to link task ${taskId} to knowledge ${knowledgeId} with type ${relationshipType}`,
reqContext_link,
);
try {
const taskExists = await Neo4jUtils.nodeExists(
NodeLabels.Task,
"id",
taskId,
);
const knowledgeExists = await Neo4jUtils.nodeExists(
NodeLabels.Knowledge,
"id",
knowledgeId,
);
if (!taskExists || !knowledgeExists) {
logger.warning(
`Cannot link: Task (${taskId} exists: ${taskExists}) or Knowledge (${knowledgeId} exists: ${knowledgeExists}) not found.`,
{ ...reqContext_link, taskExists, knowledgeExists },
);
return false;
}
const escapedType = `\`${relationshipType.replace(/`/g, "``")}\``;
const query = `
MATCH (task:${NodeLabels.Task} {id: $taskId})
MATCH (knowledge:${NodeLabels.Knowledge} {id: $knowledgeId})
MERGE (task)-[r:${escapedType}]->(knowledge)
RETURN r
`;
const result = await session.executeWrite(async (tx) => {
const runResult = await tx.run(query, { taskId, knowledgeId });
return runResult.records;
});
const linkCreated = result.length > 0;
if (linkCreated) {
logger.info(
`Successfully linked task ${taskId} to knowledge ${knowledgeId} with type ${relationshipType}`,
reqContext_link,
);
} else {
logger.warning(
`Failed to link task ${taskId} to knowledge ${knowledgeId} (MERGE returned no relationship)`,
reqContext_link,
);
}
return linkCreated;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
logger.error("Error linking task to knowledge item", error as Error, {
...reqContext_link,
detail: errorMessage,
});
throw error;
} finally {
await session.close();
}
}
/**
* Get a task by ID, including the assigned user ID via relationship.
* @param id Task ID
* @returns The task with assignedToUserId property, or null if not found.
*/
static async getTaskById(
id: string,
): Promise<(Neo4jTask & { assignedToUserId: string | null }) | null> {
const session = await neo4jDriver.getSession();
try {
// Retrieve urls as JSON string
const query = `
MATCH (t:${NodeLabels.Task} {id: $id})
OPTIONAL MATCH (t)-[:${RelationshipTypes.ASSIGNED_TO}]->(u:${NodeLabels.User})
RETURN t.id as id,
t.projectId as projectId,
t.title as title,
t.description as description,
t.priority as priority,
t.status as status,
u.id as assignedToUserId,
t.urls as urls,
t.tags as tags,
t.completionRequirements as completionRequirements,
t.outputFormat as outputFormat,
t.taskType as taskType,
t.createdAt as createdAt,
t.updatedAt as updatedAt
`;
const result = await session.executeRead(async (tx) => {
const result = await tx.run(query, { id });
return result.records;
});
if (result.length === 0) {
return null;
}
const record = result[0];
// Construct the base Neo4jTask object - deserialize urls
const taskData: Neo4jTask = {
id: record.get("id"),
projectId: record.get("projectId"),
title: record.get("title"),
description: record.get("description"),
priority: record.get("priority"),
status: record.get("status"),
urls: JSON.parse(record.get("urls") || "[]"), // Deserialize urls
tags: record.get("tags") || [],
completionRequirements: record.get("completionRequirements"),
outputFormat: record.get("outputFormat"),
taskType: record.get("taskType"),
createdAt: record.get("createdAt"),
updatedAt: record.get("updatedAt"),
};
const assignedToUserId = record.get("assignedToUserId");
return {
...taskData,
assignedToUserId: assignedToUserId,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorContext = requestContextService.createRequestContext({
operation: "getTaskById.error",
taskId: id,
});
logger.error("Error getting task by ID", error as Error, {
...errorContext,
detail: errorMessage,
});
throw error;
} finally {
await session.close();
}
}
/**
* Check if all dependencies of a task are completed
* @param taskId Task ID to check dependencies for
* @returns True if all dependencies are completed, false otherwise
*/
static async areAllDependenciesCompleted(taskId: string): Promise<boolean> {
const session = await neo4jDriver.getSession();
try {
const query = `
MATCH (t:${NodeLabels.Task} {id: $taskId})-[:${RelationshipTypes.DEPENDS_ON}]->(dep:${NodeLabels.Task})
WHERE dep.status <> 'completed'
RETURN count(dep) AS incompleteCount
`;
const result = await session.executeRead(async (tx) => {
const result = await tx.run(query, { taskId });
// Use standard number directly, Neo4j count() returns a number, not an Integer object
return result.records[0]?.get("incompleteCount") || 0;
});
return result === 0;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorContext = requestContextService.createRequestContext({
operation: "areAllDependenciesCompleted.error",
taskId,
});
logger.error(
"Error checking task dependencies completion status",
error as Error,
{ ...errorContext, detail: errorMessage },
);
throw error;
} finally {
await session.close();
}
}
/**
* Update a task's properties and handle assignment changes via relationships.
* @param id Task ID
* @param updates Task updates, including optional assignedTo for relationship changes
* @returns The updated task (without assignedTo property)
*/
static async updateTask(
id: string,
updates: Partial<
Omit<Neo4jTask, "id" | "projectId" | "createdAt" | "updatedAt">
> & { assignedTo?: string | null },
): Promise<Neo4jTask> {
const session = await neo4jDriver.getSession();
try {
const exists = await Neo4jUtils.nodeExists(NodeLabels.Task, "id", id);
if (!exists) {
throw new Error(`Task with ID ${id} not found`);
}
if (updates.status === "in-progress" || updates.status === "completed") {
const depsCompleted = await this.areAllDependenciesCompleted(id);
if (!depsCompleted) {
throw new Error(
`Cannot mark task as ${updates.status} because not all dependencies are completed`,
);
}
}
const updateParams: Record<string, any> = {
id,
updatedAt: Neo4jUtils.getCurrentTimestamp(),
};
let setClauses = ["t.updatedAt = $updatedAt"];
const allowedProperties: (keyof Neo4jTask)[] = [
"projectId",
"title",
"description",
"priority",
"status",
"urls",
"tags",
"completionRequirements",
"outputFormat",
"taskType",
];
// Handle property updates - serialize urls if present
for (const [key, value] of Object.entries(updates)) {
if (
value !== undefined &&
key !== "assignedTo" &&
allowedProperties.includes(key as keyof Neo4jTask)
) {
// Serialize urls array to JSON string if it's the key being updated
updateParams[key] =
key === "urls" ? JSON.stringify(value || []) : value;
setClauses.push(`t.${key} = $${key}`);
}
}
// Handle assignment change (logic remains the same)
let assignmentClause = "";
const newAssigneeId = updates.assignedTo;
if (newAssigneeId !== undefined) {
// Check if assignedTo is part of the update
if (newAssigneeId === null) {
// Unassign: Delete existing relationship
assignmentClause = `
WITH t
OPTIONAL MATCH (t)-[oldRel:${RelationshipTypes.ASSIGNED_TO}]->(:${NodeLabels.User})
DELETE oldRel
`;
} else {
// Assign/Reassign: Use MERGE for the user node
updateParams.newAssigneeId = newAssigneeId;
assignmentClause = `
WITH t
OPTIONAL MATCH (t)-[oldRel:${RelationshipTypes.ASSIGNED_TO}]->(:${NodeLabels.User})
DELETE oldRel
WITH t
MERGE (newUser:${NodeLabels.User} {id: $newAssigneeId})
ON CREATE SET newUser.createdAt = $updatedAt
CREATE (t)-[:${RelationshipTypes.ASSIGNED_TO}]->(newUser)
`;
}
}
// Retrieve urls as JSON string
const query = `
MATCH (t:${NodeLabels.Task} {id: $id})
${setClauses.length > 0 ? `SET ${setClauses.join(", ")}` : ""}
${assignmentClause}
// Return properties defined in Neo4jTask
RETURN t.id as id,
t.projectId as projectId,
t.title as title,
t.description as description,
t.priority as priority,
t.status as status,
t.urls as urls,
t.tags as tags,
t.completionRequirements as completionRequirements,
t.outputFormat as outputFormat,
t.taskType as taskType,
t.createdAt as createdAt,
t.updatedAt as updatedAt
`;
const result = await session.executeWrite(async (tx) => {
const result = await tx.run(query, updateParams);
// Use .get() for each field
return result.records.length > 0 ? result.records[0] : null;
});
if (!result) {
throw new Error("Failed to update task or retrieve its properties");
}
// Construct the Neo4jTask object - deserialize urls
const updatedTaskData: Neo4jTask = {
id: result.get("id"),
projectId: result.get("projectId"),
title: result.get("title"),
description: result.get("description"),
priority: result.get("priority"),
status: result.get("status"),
urls: JSON.parse(result.get("urls") || "[]"), // Deserialize urls
tags: result.get("tags") || [],
completionRequirements: result.get("completionRequirements"),
outputFormat: result.get("outputFormat"),
taskType: result.get("taskType"),
createdAt: result.get("createdAt"),
updatedAt: result.get("updatedAt"),
};
const reqContext_update = requestContextService.createRequestContext({
operation: "updateTask",
taskId: id,
});
logger.info("Task updated successfully", reqContext_update);
return updatedTaskData;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorContext = requestContextService.createRequestContext({
operation: "updateTask.error",
taskId: id,
updatesApplied: updates,
});
logger.error("Error updating task", error as Error, {
...errorContext,
detail: errorMessage,
});
throw error;
} finally {
await session.close();
}
}
/**
* Delete a task
* @param id Task ID
* @returns True if deleted, false if not found
*/
static async deleteTask(id: string): Promise<boolean> {
const session = await neo4jDriver.getSession();
try {
const exists = await Neo4jUtils.nodeExists(NodeLabels.Task, "id", id);
if (!exists) {
return false;
}
const query = `
MATCH (t:${NodeLabels.Task} {id: $id})
DETACH DELETE t
`;
await session.executeWrite(async (tx) => {
await tx.run(query, { id });
});
const reqContext_delete = requestContextService.createRequestContext({
operation: "deleteTask",
taskId: id,
});
logger.info("Task deleted successfully", reqContext_delete);
return true;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorContext = requestContextService.createRequestContext({
operation: "deleteTask.error",
taskId: id,
});
logger.error("Error deleting task", error as Error, {
...errorContext,
detail: errorMessage,
});
throw error;
} finally {
await session.close();
}
}
/**
* Get tasks for a project with optional filtering and server-side pagination.
* Includes assigned user ID via relationship.
* @param options Filter and pagination options
* @returns Paginated list of tasks including assignedToUserId
*/
static async getTasks(
options: TaskFilterOptions,
): Promise<PaginatedResult<Neo4jTask & { assignedToUserId: string | null }>> {
const session = await neo4jDriver.getSession();
try {
const nodeAlias = "t";
const userAlias = "u"; // Alias for the User node
// Define how to match the assigned user relationship
let assignmentMatchClause = `OPTIONAL MATCH (${nodeAlias})-[:${RelationshipTypes.ASSIGNED_TO}]->(${userAlias}:${NodeLabels.User})`;
if (options.assignedTo) {
// If filtering by assignee, make the MATCH non-optional and filter by user ID
assignmentMatchClause = `MATCH (${nodeAlias})-[:${RelationshipTypes.ASSIGNED_TO}]->(${userAlias}:${NodeLabels.User} {id: $assignedTo})`;
}
// Define the properties to return from the query
const returnProperties = [
`${nodeAlias}.id as id`,
`${nodeAlias}.projectId as projectId`,
`${nodeAlias}.title as title`,
`${nodeAlias}.description as description`,
`${nodeAlias}.priority as priority`,
`${nodeAlias}.status as status`,
`${userAlias}.id as assignedToUserId`, // Get user ID from the relationship
`${nodeAlias}.urls as urls`,
`${nodeAlias}.tags as tags`,
`${nodeAlias}.completionRequirements as completionRequirements`,
`${nodeAlias}.outputFormat as outputFormat`,
`${nodeAlias}.taskType as taskType`,
`${nodeAlias}.createdAt as createdAt`,
`${nodeAlias}.updatedAt as updatedAt`,
];
// Use the buildListQuery helper
const { countQuery, dataQuery, params } = buildListQuery(
NodeLabels.Task,
returnProperties,
{
// Filters
projectId: options.projectId, // Pass projectId filter
status: options.status,
priority: options.priority,
assignedTo: options.assignedTo, // Pass assignedTo for potential filtering in helper/match clause
tags: options.tags,
taskType: options.taskType,
},
{
// Pagination
sortBy: options.sortBy,
sortDirection: options.sortDirection,
page: options.page,
limit: options.limit,
},
nodeAlias, // Primary node alias
assignmentMatchClause, // Additional MATCH clause for assignment
);
const reqContext_list = requestContextService.createRequestContext({
operation: "getTasks",
filterOptions: options,
});
// Execute count query
const totalResult = await session.executeRead(async (tx) => {
// buildListQuery returns params including skip/limit, remove them for count
const countParams = { ...params };
delete countParams.skip;
delete countParams.limit;
logger.debug("Executing Task Count Query (using buildListQuery):", {
...reqContext_list,
query: countQuery,
params: countParams,
});
const result = await tx.run(countQuery, countParams);
return result.records[0]?.get("total") ?? 0;
});
const total = totalResult;
// Execute data query
const dataResult = await session.executeRead(async (tx) => {
logger.debug("Executing Task Data Query (using buildListQuery):", {
...reqContext_list,
query: dataQuery,
params: params,
});
const result = await tx.run(dataQuery, params);
return result.records;
});
// Map results - deserialize urls
const tasks = dataResult.map((record) => {
// Construct the base Neo4jTask object
const taskData: Neo4jTask = {
id: record.get("id"),
projectId: record.get("projectId"),
title: record.get("title"),
description: record.get("description"),
priority: record.get("priority"),
status: record.get("status"),
urls: JSON.parse(record.get("urls") || "[]"), // Deserialize urls
tags: record.get("tags") || [],
completionRequirements: record.get("completionRequirements"),
outputFormat: record.get("outputFormat"),
taskType: record.get("taskType"),
createdAt: record.get("createdAt"),
updatedAt: record.get("updatedAt"),
};
// Get the assigned user ID from the record
const assignedToUserId = record.get("assignedToUserId");
// Combine base task data with the user ID
return {
...taskData,
assignedToUserId: assignedToUserId,
};
});
const page = Math.max(options.page || 1, 1);
const limit = Math.min(Math.max(options.limit || 20, 1), 100);
const totalPages = Math.ceil(total / limit);
return {
data: tasks,
total,
page,
limit,
totalPages,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorContext = requestContextService.createRequestContext({
operation: "getTasks.error",
filterOptions: options,
});
logger.error("Error getting tasks", error as Error, {
...errorContext,
detail: errorMessage,
});
throw error;
} finally {
await session.close();
}
}
/**
* Add a dependency relationship between tasks
* @param sourceTaskId ID of the dependent task (source)
* @param targetTaskId ID of the dependency task (target)
* @returns The IDs of the two tasks and the relationship ID
*/
static async addTaskDependency(
sourceTaskId: string,
targetTaskId: string,
): Promise<{ id: string; sourceTaskId: string; targetTaskId: string }> {
const session = await neo4jDriver.getSession();
try {
// Logic remains the same
const sourceExists = await Neo4jUtils.nodeExists(
NodeLabels.Task,
"id",
sourceTaskId,
);
const targetExists = await Neo4jUtils.nodeExists(
NodeLabels.Task,
"id",
targetTaskId,
);
if (!sourceExists)
throw new Error(`Source task with ID ${sourceTaskId} not found`);
if (!targetExists)
throw new Error(`Target task with ID ${targetTaskId} not found`);
const dependencyExists = await Neo4jUtils.relationshipExists(
NodeLabels.Task,
"id",
sourceTaskId,
NodeLabels.Task,
"id",
targetTaskId,
RelationshipTypes.DEPENDS_ON,
);
if (dependencyExists) {
throw new Error(
`Dependency relationship already exists between tasks ${sourceTaskId} and ${targetTaskId}`,
);
}
const circularDependencyQuery = `
MATCH path = (target:${NodeLabels.Task} {id: $targetTaskId})-[:${RelationshipTypes.DEPENDS_ON}*]->(source:${NodeLabels.Task} {id: $sourceTaskId})
RETURN count(path) > 0 AS hasCycle
`;
const cycleCheckResult = await session.executeRead(async (tx) => {
const result = await tx.run(circularDependencyQuery, {
sourceTaskId,
targetTaskId,
});
return result.records[0]?.get("hasCycle");
});
if (cycleCheckResult) {
throw new Error(
"Adding this dependency would create a circular dependency chain",
);
}
const dependencyId = `tdep_${generateId()}`;
const query = `
MATCH (source:${NodeLabels.Task} {id: $sourceTaskId}),
(target:${NodeLabels.Task} {id: $targetTaskId})
CREATE (source)-[r:${RelationshipTypes.DEPENDS_ON} {
id: $dependencyId,
createdAt: $createdAt
}]->(target)
RETURN r.id as id, source.id as sourceTaskId, target.id as targetTaskId
`;
const params = {
sourceTaskId,
targetTaskId,
dependencyId,
createdAt: Neo4jUtils.getCurrentTimestamp(),
};
const result = await session.executeWrite(async (tx) => {
const result = await tx.run(query, params);
return result.records;
});
if (!result || result.length === 0) {
throw new Error("Failed to create task dependency relationship");
}
const record = result[0];
const dependency = {
id: record.get("id"),
sourceTaskId: record.get("sourceTaskId"),
targetTaskId: record.get("targetTaskId"),
};
const reqContext_addDep = requestContextService.createRequestContext({
operation: "addTaskDependency",
sourceTaskId,
targetTaskId,
});
logger.info("Task dependency added successfully", reqContext_addDep);
return dependency;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorContext = requestContextService.createRequestContext({
operation: "addTaskDependency.error",
sourceTaskId,
targetTaskId,
});
logger.error("Error adding task dependency", error as Error, {
...errorContext,
detail: errorMessage,
});
throw error;
} finally {
await session.close();
}
}
/**
* Remove a dependency relationship between tasks
* @param dependencyId The ID of the dependency relationship to remove
* @returns True if removed, false if not found
*/
static async removeTaskDependency(dependencyId: string): Promise<boolean> {
const session = await neo4jDriver.getSession();
try {
const query = `
MATCH (source:${NodeLabels.Task})-[r:${RelationshipTypes.DEPENDS_ON} {id: $dependencyId}]->(target:${NodeLabels.Task})
DELETE r
`;
const result = await session.executeWrite(async (tx) => {
const res = await tx.run(query, { dependencyId });
return res.summary.counters.updates().relationshipsDeleted > 0;
});
const reqContext_removeDep = requestContextService.createRequestContext({
operation: "removeTaskDependency",
dependencyId,
});
if (result) {
logger.info(
"Task dependency removed successfully",
reqContext_removeDep,
);
} else {
logger.warning(
"Task dependency not found or not removed",
reqContext_removeDep,
);
}
return result;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorContext = requestContextService.createRequestContext({
operation: "removeTaskDependency.error",
dependencyId,
});
logger.error("Error removing task dependency", error as Error, {
...errorContext,
detail: errorMessage,
});
throw error;
} finally {
await session.close();
}
}
/**
* Get task dependencies (both dependencies and dependents)
* @param taskId Task ID
* @returns Object containing dependencies and dependents
*/
static async getTaskDependencies(taskId: string): Promise<{
dependencies: {
id: string; // Relationship ID
taskId: string; // Target Task ID
title: string;
status: string;
priority: string;
}[];
dependents: {
id: string; // Relationship ID
taskId: string; // Source Task ID
title: string;
status: string;
priority: string;
}[];
}> {
const session = await neo4jDriver.getSession();
try {
// Logic remains the same
const exists = await Neo4jUtils.nodeExists(NodeLabels.Task, "id", taskId);
if (!exists) {
throw new Error(`Task with ID ${taskId} not found`);
}
const dependenciesQuery = `
MATCH (source:${NodeLabels.Task} {id: $taskId})-[r:${RelationshipTypes.DEPENDS_ON}]->(target:${NodeLabels.Task})
RETURN r.id as id,
target.id AS taskId,
target.title AS title,
target.status AS status,
target.priority AS priority
ORDER BY target.priority DESC, target.title
`;
const dependentsQuery = `
MATCH (source:${NodeLabels.Task})-[r:${RelationshipTypes.DEPENDS_ON}]->(target:${NodeLabels.Task} {id: $taskId})
RETURN r.id as id,
source.id AS taskId,
source.title AS title,
source.status AS status,
source.priority AS priority
ORDER BY source.priority DESC, source.title
`;
const [dependenciesResult, dependentsResult] = await Promise.all([
session.executeRead(
async (tx) => (await tx.run(dependenciesQuery, { taskId })).records,
),
session.executeRead(
async (tx) => (await tx.run(dependentsQuery, { taskId })).records,
),
]);
const dependencies = dependenciesResult.map((record) => ({
id: record.get("id"),
taskId: record.get("taskId"),
title: record.get("title"),
status: record.get("status"),
priority: record.get("priority"),
}));
const dependents = dependentsResult.map((record) => ({
id: record.get("id"),
taskId: record.get("taskId"),
title: record.get("title"),
status: record.get("status"),
priority: record.get("priority"),
}));
return { dependencies, dependents };
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorContext = requestContextService.createRequestContext({
operation: "getTaskDependencies.error",
taskId,
});
logger.error("Error getting task dependencies", error as Error, {
...errorContext,
detail: errorMessage,
});
throw error;
} finally {
await session.close();
}
}
/**
* Assign a task to a user by creating an ASSIGNED_TO relationship.
* @param taskId Task ID
* @param userId User ID
* @returns The updated task (without assignedTo property)
*/
static async assignTask(taskId: string, userId: string): Promise<Neo4jTask> {
const session = await neo4jDriver.getSession();
try {
// Logic remains the same
const taskExists = await Neo4jUtils.nodeExists(
NodeLabels.Task,
"id",
taskId,
);
if (!taskExists) throw new Error(`Task with ID ${taskId} not found`);
const userExists = await Neo4jUtils.nodeExists(
NodeLabels.User,
"id",
userId,
);
if (!userExists) throw new Error(`User with ID ${userId} not found`);
const query = `
MATCH (t:${NodeLabels.Task} {id: $taskId}), (u:${NodeLabels.User} {id: $userId})
OPTIONAL MATCH (t)-[r:${RelationshipTypes.ASSIGNED_TO}]->(:${NodeLabels.User})
DELETE r
CREATE (t)-[:${RelationshipTypes.ASSIGNED_TO}]->(u)
SET t.updatedAt = $updatedAt
// Return properties defined in Neo4jTask
RETURN t.id as id,
t.projectId as projectId,
t.title as title,
t.description as description,
t.priority as priority,
t.status as status,
// assignedTo removed
t.urls as urls,
t.tags as tags,
t.completionRequirements as completionRequirements,
t.outputFormat as outputFormat,
t.taskType as taskType,
t.createdAt as createdAt,
t.updatedAt as updatedAt
`;
const params = {
taskId,
userId,
updatedAt: Neo4jUtils.getCurrentTimestamp(),
};
const result = await session.executeWrite(async (tx) => {
const result = await tx.run(query, params);
// Use .get() for each field
return result.records.length > 0 ? result.records[0] : null;
});
if (!result) {
throw new Error("Failed to assign task or retrieve its properties");
}
// Construct the Neo4jTask object - deserialize urls
const updatedTaskData: Neo4jTask = {
id: result.get("id"),
projectId: result.get("projectId"),
title: result.get("title"),
description: result.get("description"),
priority: result.get("priority"),
status: result.get("status"),
urls: JSON.parse(result.get("urls") || "[]"), // Deserialize urls
tags: result.get("tags") || [],
completionRequirements: result.get("completionRequirements"),
outputFormat: result.get("outputFormat"),
taskType: result.get("taskType"),
createdAt: result.get("createdAt"),
updatedAt: result.get("updatedAt"),
};
const reqContext_assign = requestContextService.createRequestContext({
operation: "assignTask",
taskId,
userId,
});
logger.info("Task assigned successfully", reqContext_assign);
return updatedTaskData;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorContext = requestContextService.createRequestContext({
operation: "assignTask.error",
taskId,
userId,
});
logger.error("Error assigning task", error as Error, {
...errorContext,
detail: errorMessage,
});
throw error;
} finally {
await session.close();
}
}
/**
* Unassign a task by deleting the ASSIGNED_TO relationship.
* @param taskId Task ID
* @returns The updated task (without assignedTo property)
*/
static async unassignTask(taskId: string): Promise<Neo4jTask> {
const session = await neo4jDriver.getSession();
try {
// Logic remains the same
const taskExists = await Neo4jUtils.nodeExists(
NodeLabels.Task,
"id",
taskId,
);
if (!taskExists) throw new Error(`Task with ID ${taskId} not found`);
const query = `
MATCH (t:${NodeLabels.Task} {id: $taskId})
OPTIONAL MATCH (t)-[r:${RelationshipTypes.ASSIGNED_TO}]->(:${NodeLabels.User})
DELETE r
SET t.updatedAt = $updatedAt
// Return properties defined in Neo4jTask
RETURN t.id as id,
t.projectId as projectId,
t.title as title,
t.description as description,
t.priority as priority,
t.status as status,
// assignedTo removed
t.urls as urls,
t.tags as tags,
t.completionRequirements as completionRequirements,
t.outputFormat as outputFormat,
t.taskType as taskType,
t.createdAt as createdAt,
t.updatedAt as updatedAt
`;
const params = {
taskId,
updatedAt: Neo4jUtils.getCurrentTimestamp(),
};
const result = await session.executeWrite(async (tx) => {
const result = await tx.run(query, params);
// Use .get() for each field
return result.records.length > 0 ? result.records[0] : null;
});
if (!result) {
throw new Error("Failed to unassign task or retrieve its properties");
}
// Construct the Neo4jTask object - deserialize urls
const updatedTaskData: Neo4jTask = {
id: result.get("id"),
projectId: result.get("projectId"),
title: result.get("title"),
description: result.get("description"),
priority: result.get("priority"),
status: result.get("status"),
urls: JSON.parse(result.get("urls") || "[]"), // Deserialize urls
tags: result.get("tags") || [],
completionRequirements: result.get("completionRequirements"),
outputFormat: result.get("outputFormat"),
taskType: result.get("taskType"),
createdAt: result.get("createdAt"),
updatedAt: result.get("updatedAt"),
};
const reqContext_unassign = requestContextService.createRequestContext({
operation: "unassignTask",
taskId,
});
logger.info("Task unassigned successfully", reqContext_unassign);
return updatedTaskData;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorContext = requestContextService.createRequestContext({
operation: "unassignTask.error",
taskId,
});
logger.error("Error unassigning task", error as Error, {
...errorContext,
detail: errorMessage,
});
throw error;
} finally {
await session.close();
}
}
}