import { ok, err, type CommandContext } from "@tailor-platform/erp-kit/core"; import type { Transaction } from "../generated/kysely-tailordb"; import { CircularReferenceError, MaxDepthExceededError, NodeNotFoundError, ParentNodeNotFoundError, } from "../lib/errors.generated"; export interface MoveTaxonomyNodeInput { id: string; newParentId: string | null; } /** * Function: moveTaxonomyNode * * Reparents a taxonomy node. Moving to null promotes to root. * Validates circular references and depth limits. */ export async function run( db: Transaction, input: MoveTaxonomyNodeInput, _ctx: CommandContext, config: { maxDepth: number }, ) { const { maxDepth } = config; const { id, newParentId } = input; // 1. Check node exists (read-then-update — lock) const node = await db .selectFrom("TaxonomyNode") .selectAll() .where("id", "=", id) .forUpdate() .executeTakeFirst(); if (!node) { return err(new NodeNotFoundError(id)); } // 2. If promoting to root, just update if (newParentId === null) { const movedNode = await db .updateTable("TaxonomyNode") .set({ parentId: null }) .where("id", "=", id) .returningAll() .executeTakeFirstOrThrow(); return ok({ taxonomyNode: movedNode }); } // 3. Check new parent exists (validation only — no lock) const newParent = await db .selectFrom("TaxonomyNode") .selectAll() .where("id", "=", newParentId) .executeTakeFirst(); if (!newParent) { return err(new ParentNodeNotFoundError(newParentId)); } // 4. Check circular reference — walk up from new parent to root if (newParentId === id) { return err(new CircularReferenceError(`${id} -> ${newParentId}`)); } let ancestorId: string | null = newParentId; while (ancestorId) { const ancestor = await db .selectFrom("TaxonomyNode") .selectAll() .where("id", "=", ancestorId) .executeTakeFirst(); if (!ancestor) break; ancestorId = ancestor.parentId; if (ancestorId === id) { return err(new CircularReferenceError(`${id} -> ${newParentId}`)); } } // 5. Check depth limit: calculate new parent depth let newParentDepth = 1; let currentId: string | null = newParentId; while (currentId) { const n = await db .selectFrom("TaxonomyNode") .selectAll() .where("id", "=", currentId) .executeTakeFirst(); if (!n?.parentId) break; newParentDepth++; currentId = n.parentId; } // Calculate subtree depth of moved node async function getSubtreeDepth(nodeId: string): Promise { const children = await db .selectFrom("TaxonomyNode") .selectAll() .where("parentId", "=", nodeId) .execute(); if (!children || children.length === 0) return 1; let maxChildDepth = 0; for (const child of children) { const childDepth = await getSubtreeDepth(child.id); if (childDepth > maxChildDepth) maxChildDepth = childDepth; } return 1 + maxChildDepth; } const subtreeDepth = await getSubtreeDepth(id); const totalDepth = newParentDepth + subtreeDepth; if (totalDepth > maxDepth) { return err(new MaxDepthExceededError(`${totalDepth} exceeds ${maxDepth}`)); } // 6. Update parent const movedNode = await db .updateTable("TaxonomyNode") .set({ parentId: newParentId }) .where("id", "=", id) .returningAll() .executeTakeFirstOrThrow(); return ok({ taxonomyNode: movedNode }); }