|
| 1 | +// SPDX-License-Identifier: Apache-2.0 |
| 2 | + |
| 3 | +package mcp |
| 4 | + |
| 5 | +import ( |
| 6 | + "fmt" |
| 7 | + "strings" |
| 8 | + |
| 9 | + "github.com/mendixlabs/mxcli/mdl/types" |
| 10 | + "github.com/mendixlabs/mxcli/model" |
| 11 | +) |
| 12 | + |
| 13 | +// Folders over MCP work the way PED allows: a document created with a folderPath |
| 14 | +// auto-creates the whole path, but PED can neither create an *empty* folder |
| 15 | +// (Projects$Folder is off the create whitelist) nor re-parent an existing document |
| 16 | +// (a document's folderPath is not a settable property). So: |
| 17 | +// |
| 18 | +// - CreateFolder records the folder as pending; it materializes on disk when the |
| 19 | +// first document is created into it (the executor places documents into folders |
| 20 | +// it asked us to create, by setting their container to the folder's ID). |
| 21 | +// - The create paths resolve a document's container — module or folder — to a |
| 22 | +// (moduleName, folderPath) pair and pass folderPath to ped_create_document. |
| 23 | +// - DeleteFolder and MoveFolder (and the per-document Move* family) are rejected: |
| 24 | +// PED exposes no folder delete and no document re-parent. |
| 25 | + |
| 26 | +// CreateFolder records a pending folder. PED can't create an empty one, but the |
| 27 | +// folder is realized when a document is created into it (see resolveDocContainer). |
| 28 | +// The executor has already assigned folder.ID, which it uses as the container of |
| 29 | +// documents placed in this folder. |
| 30 | +func (b *Backend) CreateFolder(folder *model.Folder) error { |
| 31 | + if folder.ID == "" { |
| 32 | + return fmt.Errorf("create folder %q: missing folder id", folder.Name) |
| 33 | + } |
| 34 | + b.sessionFolders = append(b.sessionFolders, &types.FolderInfo{ |
| 35 | + ID: folder.ID, |
| 36 | + ContainerID: folder.ContainerID, |
| 37 | + Name: folder.Name, |
| 38 | + }) |
| 39 | + return nil |
| 40 | +} |
| 41 | + |
| 42 | +// DeleteFolder is rejected: PED has no folder-delete (and an empty folder doesn't |
| 43 | +// exist on disk in the first place). |
| 44 | +func (b *Backend) DeleteFolder(id model.ID) error { |
| 45 | + return fmt.Errorf("DROP FOLDER is not supported by the MCP backend — PED cannot delete folders (a folder exists only while it holds documents); reorganize in Studio Pro or against a local .mpr") |
| 46 | +} |
| 47 | + |
| 48 | +// MoveFolder is rejected: PED cannot re-parent folders or documents. |
| 49 | +func (b *Backend) MoveFolder(id, newContainerID model.ID) error { |
| 50 | + return fmt.Errorf("MOVE FOLDER is not supported by the MCP backend — PED cannot re-parent folders or documents; reorganize in Studio Pro or against a local .mpr") |
| 51 | +} |
| 52 | + |
| 53 | +// ListFolders returns the local reader's folders merged with folders created this |
| 54 | +// session (so the executor's resolveFolder finds a just-created folder and builds |
| 55 | +// nested paths against it instead of recreating segments). |
| 56 | +func (b *Backend) ListFolders() ([]*types.FolderInfo, error) { |
| 57 | + local, err := b.reader.ListFolders() |
| 58 | + if err != nil { |
| 59 | + return nil, err |
| 60 | + } |
| 61 | + if len(b.sessionFolders) == 0 { |
| 62 | + return local, nil |
| 63 | + } |
| 64 | + return append(append([]*types.FolderInfo{}, local...), b.sessionFolders...), nil |
| 65 | +} |
| 66 | + |
| 67 | +// resolveDocContainer maps a document's container ID — a module or a folder — onto |
| 68 | +// the PED moduleName and folderPath. folderPath is "" for a module-root document; |
| 69 | +// for a foldered one it is the slash-joined folder path (e.g. "Processing/Archive"). |
| 70 | +func (b *Backend) resolveDocContainer(containerID model.ID) (moduleName, folderPath string, err error) { |
| 71 | + if mod, e := b.GetModule(containerID); e == nil { |
| 72 | + return mod.Name, "", nil |
| 73 | + } |
| 74 | + // Walk up the folder chain to the owning module, collecting names. |
| 75 | + var parts []string |
| 76 | + cur := containerID |
| 77 | + for range 64 { // depth guard |
| 78 | + fi := b.findFolder(cur) |
| 79 | + if fi == nil { |
| 80 | + return "", "", fmt.Errorf("container %s is neither a module nor a known folder", containerID) |
| 81 | + } |
| 82 | + parts = append([]string{fi.Name}, parts...) |
| 83 | + if mod, e := b.GetModule(fi.ContainerID); e == nil { |
| 84 | + return mod.Name, strings.Join(parts, "/"), nil |
| 85 | + } |
| 86 | + cur = fi.ContainerID |
| 87 | + } |
| 88 | + return "", "", fmt.Errorf("folder hierarchy for %s is too deep or cyclic", containerID) |
| 89 | +} |
| 90 | + |
| 91 | +// findFolder looks a folder up by ID, preferring session folders then the reader. |
| 92 | +func (b *Backend) findFolder(id model.ID) *types.FolderInfo { |
| 93 | + for _, f := range b.sessionFolders { |
| 94 | + if f.ID == id { |
| 95 | + return f |
| 96 | + } |
| 97 | + } |
| 98 | + if b.reader == nil { |
| 99 | + return nil |
| 100 | + } |
| 101 | + if all, err := b.reader.ListFolders(); err == nil { |
| 102 | + for _, f := range all { |
| 103 | + if f.ID == id { |
| 104 | + return f |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + return nil |
| 109 | +} |
0 commit comments