// Path: Backend/internal/bootstrap/project_mutations.go package bootstrap import ( "context" "fmt" "strings" ) func (service *Service) CreateProjectFolder(ctx context.Context, input CreateProjectFolderInput) (CreateProjectFolderResult, error) { return service.createProjectHierarchyFolder(ctx, input, projectHierarchyRootPath, service.createProjectHierarchyFolderOnDisk) } func (service *Service) CreateProjectTreeFolder(ctx context.Context, input CreateProjectFolderInput) (CreateProjectFolderResult, error) { return service.createProjectHierarchyFolder(ctx, input, projectTreeRootPath, service.createProjectTreeFolderOnDisk) } func (service *Service) DeleteProjectFolder(ctx context.Context, input DeleteProjectFolderInput) (DeleteProjectFolderResult, error) { return service.deleteProjectHierarchyFolder(ctx, input, projectHierarchyRootPath, service.deleteProjectHierarchyFolderOnDisk) } func (service *Service) DeleteProjectTreeFolder(ctx context.Context, input DeleteProjectFolderInput) (DeleteProjectFolderResult, error) { return service.deleteProjectHierarchyFolder(ctx, input, projectTreeRootPath, service.deleteProjectTreeFolderOnDisk) } func (service *Service) RenameProjectFolder(ctx context.Context, input RenameProjectFolderInput) (RenameProjectFolderResult, error) { return service.renameProjectHierarchyFolder(ctx, input, projectHierarchyRootPath, service.renameProjectHierarchyFolderOnDisk) } func (service *Service) RenameProjectTreeFolder(ctx context.Context, input RenameProjectFolderInput) (RenameProjectFolderResult, error) { return service.renameProjectHierarchyFolder(ctx, input, projectTreeRootPath, service.renameProjectTreeFolderOnDisk) } func (service *Service) MoveProjectFolder(ctx context.Context, input MoveProjectFolderInput) (MoveProjectFolderResult, error) { return service.moveProjectHierarchyFolder(ctx, input, projectHierarchyRootPath, service.moveProjectHierarchyFolderOnDisk) } func (service *Service) MoveProjectTreeFolder(ctx context.Context, input MoveProjectFolderInput) (MoveProjectFolderResult, error) { return service.moveProjectHierarchyFolder(ctx, input, projectTreeRootPath, service.moveProjectTreeFolderOnDisk) } func (service *Service) CreateProjectTreeItem(ctx context.Context, input CreateProjectItemInput) (CreateProjectItemResult, error) { return service.createProjectTreeItem(ctx, input, projectTreeRootPath) } func (service *Service) DeleteProjectTreeItem(ctx context.Context, input DeleteProjectItemInput) (DeleteProjectItemResult, error) { return service.deleteProjectTreeItem(ctx, input, projectTreeRootPath) } func (service *Service) MoveProjectTreeItem(ctx context.Context, input MoveProjectItemInput) (MoveProjectItemResult, error) { return service.moveProjectTreeItem(ctx, input, projectTreeRootPath) } func (service *Service) createProjectHierarchyFolder( ctx context.Context, input CreateProjectFolderInput, rootPath func(projectSlug string) string, createOnDisk func(projectSlug, parentFolderPath, name string) (string, string, error), ) (CreateProjectFolderResult, error) { project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID)) if err != nil { return CreateProjectFolderResult{}, err } currentFolders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath) if err != nil { return CreateProjectFolderResult{}, err } parentOrderID := "" trimmedParentFolderPath := strings.TrimSpace(input.ParentFolderPath) if trimmedParentFolderPath != "" { parentFolder, found := findProjectHierarchyFolderByPath(currentFolders, trimmedParentFolderPath) if !found { return CreateProjectFolderResult{}, ErrProjectFolderNotFound } parentOrderID = parentFolder.ID } createdPath, _, err := createOnDisk(project.Slug, strings.TrimSpace(input.ParentFolderPath), input.Name) if err != nil { return CreateProjectFolderResult{}, err } if err := service.rebuildProjection(ctx); err != nil { return CreateProjectFolderResult{}, err } folders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath) if err != nil { return CreateProjectFolderResult{}, err } createdFolder, ok := findProjectHierarchyFolderByPath(folders, createdPath) if !ok { return CreateProjectFolderResult{}, fmt.Errorf("created project folder missing from projection") } folderOrder := service.readProjectFolderOrder(project.Slug, rootPath(project.Slug)) seedFolderOrderParent(folderOrder, currentFolders, parentOrderID) insertFolderOrder(folderOrder, parentOrderID, createdFolder.ID, len(folderOrderChildren(folderOrder, parentOrderID))) if err := service.writeProjectFolderOrder(project.Slug, rootPath(project.Slug), folderOrder); err != nil { return CreateProjectFolderResult{}, err } folders, err = service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath) if err != nil { return CreateProjectFolderResult{}, err } createdFolder, ok = findProjectHierarchyFolderByPath(folders, createdPath) if !ok { return CreateProjectFolderResult{}, fmt.Errorf("created project folder missing from ordered projection") } return CreateProjectFolderResult{ProjectID: project.ID, CreatedFolder: createdFolder, Folders: folders}, nil } func (service *Service) deleteProjectHierarchyFolder( ctx context.Context, input DeleteProjectFolderInput, rootPath func(projectSlug string) string, deleteOnDisk func(projectSlug, folderPath string) (string, error), ) (DeleteProjectFolderResult, error) { project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID)) if err != nil { return DeleteProjectFolderResult{}, err } currentFolders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath) if err != nil { return DeleteProjectFolderResult{}, err } deletedFolder, found := findProjectHierarchyFolderByPath(currentFolders, strings.TrimSpace(input.FolderPath)) if !found { return DeleteProjectFolderResult{}, ErrProjectFolderNotFound } deletedFolderPath, err := deleteOnDisk(project.Slug, input.FolderPath) if err != nil { return DeleteProjectFolderResult{}, err } if err := service.rebuildProjection(ctx); err != nil { return DeleteProjectFolderResult{}, err } folders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath) if err != nil { return DeleteProjectFolderResult{}, err } if _, found := findProjectHierarchyFolderByPath(folders, deletedFolderPath); found { return DeleteProjectFolderResult{}, fmt.Errorf("deleted project folder still present in projection") } folderOrder := service.readProjectFolderOrder(project.Slug, rootPath(project.Slug)) removeFolderOrder(folderOrder, deletedFolder.ID) if err := service.writeProjectFolderOrder(project.Slug, rootPath(project.Slug), folderOrder); err != nil { return DeleteProjectFolderResult{}, err } folders, err = service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath) if err != nil { return DeleteProjectFolderResult{}, err } return DeleteProjectFolderResult{ProjectID: project.ID, DeletedFolderStableID: deletedFolder.ID, DeletedFolderPath: deletedFolderPath, Folders: folders}, nil } func (service *Service) renameProjectHierarchyFolder( ctx context.Context, input RenameProjectFolderInput, rootPath func(projectSlug string) string, renameOnDisk func(projectSlug, folderPath, name string) (string, string, error), ) (RenameProjectFolderResult, error) { project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID)) if err != nil { return RenameProjectFolderResult{}, err } previousFolderPath, renamedFolderPath, err := renameOnDisk(project.Slug, input.FolderPath, input.Name) if err != nil { return RenameProjectFolderResult{}, err } if err := service.rebuildProjection(ctx); err != nil { return RenameProjectFolderResult{}, err } folders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath) if err != nil { return RenameProjectFolderResult{}, err } renamedFolder, found := findProjectHierarchyFolderByPath(folders, renamedFolderPath) if !found { return RenameProjectFolderResult{}, fmt.Errorf("renamed project folder missing from projection") } if _, found := findProjectHierarchyFolderByPath(folders, previousFolderPath); found { return RenameProjectFolderResult{}, fmt.Errorf("previous project folder path still present in projection") } return RenameProjectFolderResult{ProjectID: project.ID, PreviousFolderStableID: renamedFolder.ID, PreviousFolderPath: previousFolderPath, RenamedFolder: renamedFolder, Folders: folders}, nil } func (service *Service) moveProjectHierarchyFolder( ctx context.Context, input MoveProjectFolderInput, rootPath func(projectSlug string) string, moveOnDisk func(projectSlug, folderPath, parentFolderPath string) (string, string, error), ) (MoveProjectFolderResult, error) { project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID)) if err != nil { return MoveProjectFolderResult{}, err } currentFolders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath) if err != nil { return MoveProjectFolderResult{}, err } currentFolder, found := findProjectHierarchyFolderByPath(currentFolders, strings.TrimSpace(input.FolderPath)) if !found { return MoveProjectFolderResult{}, ErrProjectFolderNotFound } movedFolderStableID := currentFolder.ID providedFolderStableID := strings.TrimSpace(input.FolderStableID) if providedFolderStableID != "" && providedFolderStableID != movedFolderStableID { return MoveProjectFolderResult{}, ErrInvalidProjectFolderMove } parentOrderID := "" trimmedParentFolderPath := strings.TrimSpace(input.ParentFolderPath) providedParentStableID := strings.TrimSpace(input.ParentStableID) if trimmedParentFolderPath != "" { parentFolder, found := findProjectHierarchyFolderByPath(currentFolders, trimmedParentFolderPath) if !found { return MoveProjectFolderResult{}, ErrProjectFolderNotFound } parentOrderID = parentFolder.ID if providedParentStableID != "" && providedParentStableID != parentOrderID { return MoveProjectFolderResult{}, ErrInvalidProjectFolderMove } } else if providedParentStableID != "" { return MoveProjectFolderResult{}, ErrInvalidProjectFolderMove } previousFolderPath, movedFolderPath, err := moveOnDisk(project.Slug, input.FolderPath, input.ParentFolderPath) if err != nil { return MoveProjectFolderResult{}, err } if err := service.rebuildProjection(ctx); err != nil { return MoveProjectFolderResult{}, err } folders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath) if err != nil { return MoveProjectFolderResult{}, err } movedFolder, found := findProjectHierarchyFolderByPath(folders, movedFolderPath) if !found { return MoveProjectFolderResult{}, fmt.Errorf("moved project folder missing from projection") } if previousFolderPath != movedFolderPath { if _, found := findProjectHierarchyFolderByPath(folders, previousFolderPath); found { return MoveProjectFolderResult{}, fmt.Errorf("previous project folder path still present in projection") } } folderOrder := service.readProjectFolderOrder(project.Slug, rootPath(project.Slug)) seedFolderOrderParent(folderOrder, currentFolders, parentOrderID) removeFolderOrderReference(folderOrder, movedFolderStableID) removeFolderOrderReference(folderOrder, movedFolder.ID) insertFolderOrder(folderOrder, parentOrderID, movedFolder.ID, input.TargetIndex) if err := service.writeProjectFolderOrder(project.Slug, rootPath(project.Slug), folderOrder); err != nil { return MoveProjectFolderResult{}, err } folders, err = service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath) if err != nil { return MoveProjectFolderResult{}, err } movedFolder, found = findProjectHierarchyFolderByPath(folders, movedFolderPath) if !found { return MoveProjectFolderResult{}, fmt.Errorf("moved project folder missing from ordered projection") } return MoveProjectFolderResult{ProjectID: project.ID, PreviousFolderStableID: movedFolder.ID, PreviousFolderPath: previousFolderPath, MovedFolder: movedFolder, Folders: folders}, nil } func (service *Service) createProjectTreeItem(ctx context.Context, input CreateProjectItemInput, rootPath func(projectSlug string) string) (CreateProjectItemResult, error) { project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID)) if err != nil { return CreateProjectItemResult{}, err } currentNodes, err := service.getProjectTreeNodesByRootPath(ctx, project.ID, rootPath) if err != nil { return CreateProjectItemResult{}, err } parentOrderID := "" trimmedParentFolderPath := strings.TrimSpace(input.ParentFolderPath) if trimmedParentFolderPath != "" { parentFolder, found := findProjectTreeFolderByPath(currentNodes, trimmedParentFolderPath) if !found { return CreateProjectItemResult{}, ErrProjectFolderNotFound } parentOrderID = parentFolder.ID } createdPath, err := service.createProjectTreeItemOnDisk(project.Slug, trimmedParentFolderPath, input.Name, input.ItemType) if err != nil { return CreateProjectItemResult{}, err } if err := service.rebuildProjection(ctx); err != nil { return CreateProjectItemResult{}, err } nodes, err := service.getProjectTreeNodesByRootPath(ctx, project.ID, rootPath) if err != nil { return CreateProjectItemResult{}, err } createdItem, ok := findProjectTreeNodeByPath(nodes, createdPath) if !ok || createdItem.Kind != "item" { return CreateProjectItemResult{}, fmt.Errorf("created project item missing from projection") } folderOrder := service.readProjectFolderOrder(project.Slug, rootPath(project.Slug)) seedProjectTreeOrderParent(folderOrder, currentNodes, parentOrderID) insertFolderOrder(folderOrder, parentOrderID, createdItem.ID, len(folderOrderChildren(folderOrder, parentOrderID))) if err := service.writeProjectFolderOrder(project.Slug, rootPath(project.Slug), folderOrder); err != nil { return CreateProjectItemResult{}, err } nodes, err = service.getProjectTreeNodesByRootPath(ctx, project.ID, rootPath) if err != nil { return CreateProjectItemResult{}, err } createdItem, ok = findProjectTreeNodeByPath(nodes, createdPath) if !ok || createdItem.Kind != "item" { return CreateProjectItemResult{}, fmt.Errorf("created project item missing from ordered projection") } return CreateProjectItemResult{ProjectID: project.ID, CreatedItem: createdItem, Nodes: nodes}, nil } func (service *Service) deleteProjectTreeItem(ctx context.Context, input DeleteProjectItemInput, rootPath func(projectSlug string) string) (DeleteProjectItemResult, error) { project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID)) if err != nil { return DeleteProjectItemResult{}, err } currentNodes, err := service.getProjectTreeNodesByRootPath(ctx, project.ID, rootPath) if err != nil { return DeleteProjectItemResult{}, err } deletedItem, found := findProjectTreeNodeByPath(currentNodes, strings.TrimSpace(input.ItemPath)) if !found || deletedItem.Kind != "item" { return DeleteProjectItemResult{}, ErrProjectItemNotFound } deletedItemPath, err := service.deleteProjectTreeItemOnDisk(project.Slug, input.ItemPath) if err != nil { return DeleteProjectItemResult{}, err } if err := service.rebuildProjection(ctx); err != nil { return DeleteProjectItemResult{}, err } nodes, err := service.getProjectTreeNodesByRootPath(ctx, project.ID, rootPath) if err != nil { return DeleteProjectItemResult{}, err } if _, found := findProjectTreeNodeByPath(nodes, deletedItemPath); found { return DeleteProjectItemResult{}, fmt.Errorf("deleted project item still present in projection") } folderOrder := service.readProjectFolderOrder(project.Slug, rootPath(project.Slug)) removeFolderOrderReference(folderOrder, deletedItem.ID) if err := service.writeProjectFolderOrder(project.Slug, rootPath(project.Slug), folderOrder); err != nil { return DeleteProjectItemResult{}, err } nodes, err = service.getProjectTreeNodesByRootPath(ctx, project.ID, rootPath) if err != nil { return DeleteProjectItemResult{}, err } return DeleteProjectItemResult{ProjectID: project.ID, DeletedItemStableID: deletedItem.ID, DeletedItemPath: deletedItemPath, Nodes: nodes}, nil } func (service *Service) moveProjectTreeItem(ctx context.Context, input MoveProjectItemInput, rootPath func(projectSlug string) string) (MoveProjectItemResult, error) { project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID)) if err != nil { return MoveProjectItemResult{}, err } currentNodes, err := service.getProjectTreeNodesByRootPath(ctx, project.ID, rootPath) if err != nil { return MoveProjectItemResult{}, err } currentItem, found := findProjectTreeNodeByPath(currentNodes, strings.TrimSpace(input.ItemPath)) if !found || currentItem.Kind != "item" { return MoveProjectItemResult{}, ErrProjectItemNotFound } movedItemStableID := currentItem.ID providedItemStableID := strings.TrimSpace(input.ItemStableID) if providedItemStableID != "" && providedItemStableID != movedItemStableID { return MoveProjectItemResult{}, ErrInvalidProjectItemMove } parentOrderID := "" trimmedParentFolderPath := strings.TrimSpace(input.ParentFolderPath) providedParentStableID := strings.TrimSpace(input.ParentStableID) if trimmedParentFolderPath != "" { parentFolder, found := findProjectTreeFolderByPath(currentNodes, trimmedParentFolderPath) if !found { return MoveProjectItemResult{}, ErrProjectFolderNotFound } parentOrderID = parentFolder.ID if providedParentStableID != "" && providedParentStableID != parentOrderID { return MoveProjectItemResult{}, ErrInvalidProjectItemMove } } else if providedParentStableID != "" { return MoveProjectItemResult{}, ErrInvalidProjectItemMove } previousItemPath, movedItemPath, err := service.moveProjectTreeItemOnDisk(project.Slug, input.ItemPath, input.ParentFolderPath) if err != nil { return MoveProjectItemResult{}, err } if err := service.rebuildProjection(ctx); err != nil { return MoveProjectItemResult{}, err } nodes, err := service.getProjectTreeNodesByRootPath(ctx, project.ID, rootPath) if err != nil { return MoveProjectItemResult{}, err } movedItem, found := findProjectTreeNodeByPath(nodes, movedItemPath) if !found || movedItem.Kind != "item" { return MoveProjectItemResult{}, fmt.Errorf("moved project item missing from projection") } if previousItemPath != movedItemPath { if _, found := findProjectTreeNodeByPath(nodes, previousItemPath); found { return MoveProjectItemResult{}, fmt.Errorf("previous project item path still present in projection") } } folderOrder := service.readProjectFolderOrder(project.Slug, rootPath(project.Slug)) seedProjectTreeOrderParent(folderOrder, currentNodes, parentOrderID) removeFolderOrderReference(folderOrder, movedItemStableID) removeFolderOrderReference(folderOrder, movedItem.ID) insertFolderOrder(folderOrder, parentOrderID, movedItem.ID, input.TargetIndex) if err := service.writeProjectFolderOrder(project.Slug, rootPath(project.Slug), folderOrder); err != nil { return MoveProjectItemResult{}, err } nodes, err = service.getProjectTreeNodesByRootPath(ctx, project.ID, rootPath) if err != nil { return MoveProjectItemResult{}, err } movedItem, found = findProjectTreeNodeByPath(nodes, movedItemPath) if !found || movedItem.Kind != "item" { return MoveProjectItemResult{}, fmt.Errorf("moved project item missing from ordered projection") } return MoveProjectItemResult{ProjectID: project.ID, PreviousItemStableID: movedItem.ID, PreviousItemPath: previousItemPath, MovedItem: movedItem, Nodes: nodes}, nil }