Merge branch 'Fix/Backend/Posix-Hierarchy-Fix'

This commit is contained in:
MangoPig
2026-06-22 11:14:08 +01:00
5 changed files with 154 additions and 24 deletions
+9
View File
@@ -978,6 +978,7 @@ func (service *Service) ensureBootstrapPOSIXSkeleton(
departmentPath, departmentPath,
teamPath, teamPath,
projectPath, projectPath,
filepath.Join(projectPath, "children"),
filepath.Join(projectPath, "tree"), filepath.Join(projectPath, "tree"),
filepath.Join(usersPath, "personals"), filepath.Join(usersPath, "personals"),
} { } {
@@ -1041,6 +1042,14 @@ func (service *Service) ensureBootstrapPOSIXSkeleton(
return fmt.Errorf("write project home.json: %w", err) return fmt.Errorf("write project home.json: %w", err)
} }
if err := writeJSONFile(filepath.Join(projectPath, "acl.json"), map[string]any{
"version": 1,
"inherits": true,
"rules": []any{},
}); err != nil {
return fmt.Errorf("write project acl.json: %w", err)
}
if err := writeJSONFile(filepath.Join(usersPath, "settings.json"), map[string]any{ if err := writeJSONFile(filepath.Join(usersPath, "settings.json"), map[string]any{
"primaryAdminId": admin.ID, "primaryAdminId": admin.ID,
}); err != nil { }); err != nil {
@@ -52,6 +52,8 @@ func TestEnsureBootstrapPOSIXSkeletonInitializesEmptyRoot(t *testing.T) {
filepath.Join(rootPath, "departments", "department-primary-department", "teams", "team-primary-team", "users.json"), filepath.Join(rootPath, "departments", "department-primary-department", "teams", "team-primary-team", "users.json"),
filepath.Join(rootPath, "projects", "project-primary-project", "settings.json"), filepath.Join(rootPath, "projects", "project-primary-project", "settings.json"),
filepath.Join(rootPath, "projects", "project-primary-project", "home.json"), filepath.Join(rootPath, "projects", "project-primary-project", "home.json"),
filepath.Join(rootPath, "projects", "project-primary-project", "acl.json"),
filepath.Join(rootPath, "projects", "project-primary-project", "children"),
filepath.Join(rootPath, "projects", "project-primary-project", "tree"), filepath.Join(rootPath, "projects", "project-primary-project", "tree"),
filepath.Join(rootPath, "users", "settings.json"), filepath.Join(rootPath, "users", "settings.json"),
filepath.Join(rootPath, "users", "data.json"), filepath.Join(rootPath, "users", "data.json"),
@@ -90,6 +92,11 @@ func TestEnsureBootstrapPOSIXSkeletonInitializesEmptyRoot(t *testing.T) {
t.Fatalf("expected project settings type project, got %#v", projectSettings["type"]) t.Fatalf("expected project settings type project, got %#v", projectSettings["type"])
} }
projectACL := readJSONFileForTest[map[string]any](t, filepath.Join(rootPath, "projects", "project-primary-project", "acl.json"))
if projectACL["inherits"] != true {
t.Fatalf("expected project acl to inherit by default, got %#v", projectACL["inherits"])
}
usersSettings := readJSONFileForTest[map[string]any](t, filepath.Join(rootPath, "users", "settings.json")) usersSettings := readJSONFileForTest[map[string]any](t, filepath.Join(rootPath, "users", "settings.json"))
if usersSettings["primaryAdminId"] != "admin-1" { if usersSettings["primaryAdminId"] != "admin-1" {
t.Fatalf("expected primary admin id admin-1, got %#v", usersSettings["primaryAdminId"]) t.Fatalf("expected primary admin id admin-1, got %#v", usersSettings["primaryAdminId"])
+46 -24
View File
@@ -321,18 +321,17 @@ func buildNode(rootPath, relPath string, entry fs.DirEntry, rootScope Scope) (No
func deriveScope(relPath string, rootScope Scope) Scope { func deriveScope(relPath string, rootScope Scope) Scope {
scope := rootScope scope := rootScope
parts := strings.Split(relPath, "/") parts := strings.Split(relPath, "/")
if len(parts) >= 2 && parts[0] == "departments" && strings.HasPrefix(parts[1], "department-") { for _, part := range parts {
scope.DepartmentSlug = strings.TrimPrefix(parts[1], "department-") switch {
} case strings.HasPrefix(part, "department-"):
if len(parts) >= 4 && parts[0] == "departments" && parts[2] == "teams" && strings.HasPrefix(parts[3], "team-") { scope.DepartmentSlug = strings.TrimPrefix(part, "department-")
scope.DepartmentSlug = strings.TrimPrefix(parts[1], "department-") case strings.HasPrefix(part, "team-"):
scope.TeamSlug = strings.TrimPrefix(parts[3], "team-") scope.TeamSlug = strings.TrimPrefix(part, "team-")
} case strings.HasPrefix(part, "project-"):
if len(parts) >= 2 && parts[0] == "projects" && strings.HasPrefix(parts[1], "project-") { scope.ProjectSlug = strings.TrimPrefix(part, "project-")
scope.ProjectSlug = strings.TrimPrefix(parts[1], "project-") case strings.HasPrefix(part, "personal-"):
} scope.PersonalSlug = strings.TrimPrefix(part, "personal-")
if len(parts) >= 3 && parts[0] == "users" && parts[1] == "personals" && strings.HasPrefix(parts[2], "personal-") { }
scope.PersonalSlug = strings.TrimPrefix(parts[2], "personal-")
} }
return scope return scope
} }
@@ -344,6 +343,13 @@ func classifyPath(relPath string, isDir bool) (logicalType, fileRole string) {
fileRole = strings.TrimSuffix(name, filepath.Ext(name)) fileRole = strings.TrimSuffix(name, filepath.Ext(name))
} }
hasChildrenAncestor := pathContainsSegment(parts, "children")
hasTreeAncestor := pathContainsSegment(parts, "tree")
parentName := ""
if len(parts) >= 2 {
parentName = parts[len(parts)-2]
}
switch { switch {
case relPath == "settings.json": case relPath == "settings.json":
return "tenant", "settings" return "tenant", "settings"
@@ -392,26 +398,33 @@ func classifyPath(relPath string, isDir bool) (logicalType, fileRole string) {
return "department", fileRole return "department", fileRole
case len(parts) >= 2 && parts[0] == "projects" && strings.HasPrefix(parts[1], "project-"): case len(parts) >= 2 && parts[0] == "projects" && strings.HasPrefix(parts[1], "project-"):
if isDir { if isDir {
if len(parts) == 2 { if strings.HasPrefix(name, "project-") {
return "project", "" return "project", ""
} }
if len(parts) == 3 && parts[2] == "tree" { if name == "children" {
return "project_children", ""
}
if name == "tree" {
return "project_tree", "" return "project_tree", ""
} }
if strings.Contains(relPath, "/tree/") || strings.HasSuffix(relPath, "/tree") { if hasChildrenAncestor && strings.HasPrefix(name, "folder-") {
if strings.HasPrefix(name, "folder-") { return "hierarchy_folder", ""
return "folder", "" }
} if hasTreeAncestor && strings.HasPrefix(name, "folder-") {
if strings.HasPrefix(name, "item-") { return "folder", ""
return "item", "" }
} if hasTreeAncestor && strings.HasPrefix(name, "item-") {
return "item", ""
} }
} }
if strings.Contains(relPath, "/tree/") { if hasChildrenAncestor && strings.HasPrefix(parentName, "folder-") {
if strings.HasPrefix(parts[len(parts)-2], "item-") { return "hierarchy_folder", fileRole
}
if hasTreeAncestor {
if strings.HasPrefix(parentName, "item-") {
return "item", fileRole return "item", fileRole
} }
if strings.HasPrefix(parts[len(parts)-2], "folder-") { if strings.HasPrefix(parentName, "folder-") {
return "folder", fileRole return "folder", fileRole
} }
} }
@@ -456,6 +469,15 @@ func classifyPath(relPath string, isDir bool) (logicalType, fileRole string) {
} }
} }
func pathContainsSegment(parts []string, target string) bool {
for _, part := range parts {
if part == target {
return true
}
}
return false
}
func projectionParentPath(relPath string) *string { func projectionParentPath(relPath string) *string {
if relPath == "" || relPath == rootProjectionPath { if relPath == "" || relPath == rootProjectionPath {
return nil return nil
@@ -13,7 +13,10 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
mustMkdirAll(t, filepath.Join(root, "catalog", "packs")) mustMkdirAll(t, filepath.Join(root, "catalog", "packs"))
mustMkdirAll(t, filepath.Join(root, "catalog", "standalone")) mustMkdirAll(t, filepath.Join(root, "catalog", "standalone"))
mustMkdirAll(t, filepath.Join(root, "departments", "department-primary-department", "teams", "team-primary-team")) mustMkdirAll(t, filepath.Join(root, "departments", "department-primary-department", "teams", "team-primary-team"))
mustMkdirAll(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "children"))
mustMkdirAll(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "tree"))
mustMkdirAll(t, filepath.Join(root, "projects", "project-primary-project", "tree")) mustMkdirAll(t, filepath.Join(root, "projects", "project-primary-project", "tree"))
mustMkdirAll(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap"))
mustMkdirAll(t, filepath.Join(root, "users", "personals")) mustMkdirAll(t, filepath.Join(root, "users", "personals"))
mustWriteJSON(t, filepath.Join(root, "settings.json"), map[string]any{ mustWriteJSON(t, filepath.Join(root, "settings.json"), map[string]any{
@@ -57,6 +60,48 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
"type": "project-home", "type": "project-home",
"project": "primary-project", "project": "primary-project",
}) })
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "acl.json"), map[string]any{
"inherits": true,
"rules": []any{},
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "folder.json"), map[string]any{
"name": "Design",
"slug": "design",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "acl.json"), map[string]any{
"inherits": true,
"rules": []any{},
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "settings.json"), map[string]any{
"id": "project-2",
"name": "Web Project",
"slug": "web",
"type": "project",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "home.json"), map[string]any{
"type": "project-home",
"project": "web",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "acl.json"), map[string]any{
"inherits": true,
"rules": []any{},
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "folder.json"), map[string]any{
"name": "Docs",
"slug": "docs",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", "item.json"), map[string]any{
"id": "item-1",
"name": "Roadmap",
"slug": "roadmap",
"type": "board",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", "schema.json"), map[string]any{
"type": "object",
})
mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", "data.json"), map[string]any{
"title": "Roadmap",
})
mustWriteJSON(t, filepath.Join(root, "users", "settings.json"), map[string]any{ mustWriteJSON(t, filepath.Join(root, "users", "settings.json"), map[string]any{
"primaryAdminId": "admin-1", "primaryAdminId": "admin-1",
}) })
@@ -119,6 +164,39 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) {
t.Fatalf("unexpected project tree node: %#v", projectTree) t.Fatalf("unexpected project tree node: %#v", projectTree)
} }
projectChildren := index["projects/project-primary-project/children"]
if projectChildren.LogicalType != "project_children" || projectChildren.NodeKind != NodeKindDirectory {
t.Fatalf("unexpected project children node: %#v", projectChildren)
}
hierarchyFolder := index["projects/project-primary-project/children/folder-design"]
if hierarchyFolder.LogicalType != "hierarchy_folder" || hierarchyFolder.ProjectSlug != "primary-project" {
t.Fatalf("unexpected hierarchy folder node: %#v", hierarchyFolder)
}
hierarchyFolderACL := index["projects/project-primary-project/children/folder-design/acl.json"]
if hierarchyFolderACL.LogicalType != "hierarchy_folder" || hierarchyFolderACL.FileRole != "acl" {
t.Fatalf("unexpected hierarchy folder acl classification: %#v", hierarchyFolderACL)
}
childProjectSettings := index["projects/project-primary-project/children/folder-design/children/project-web/settings.json"]
if childProjectSettings.LogicalType != "project" || childProjectSettings.ProjectSlug != "web" {
t.Fatalf("unexpected child project classification: %#v", childProjectSettings)
}
treeFolder := index["projects/project-primary-project/tree/folder-docs"]
if treeFolder.LogicalType != "folder" || treeFolder.ProjectSlug != "primary-project" {
t.Fatalf("unexpected tree folder node: %#v", treeFolder)
}
treeItem := index["projects/project-primary-project/tree/folder-docs/item-roadmap/item.json"]
if treeItem.LogicalType != "item" || treeItem.FileRole != "item" {
t.Fatalf("unexpected tree item classification: %#v", treeItem)
}
if treeItem.ResourceSlug != "roadmap" {
t.Fatalf("expected tree item resource slug roadmap, got %#v", treeItem.ResourceSlug)
}
usersData := index["users/data.json"] usersData := index["users/data.json"]
if usersData.LogicalType != "users" || usersData.FileRole != "data" { if usersData.LogicalType != "users" || usersData.FileRole != "data" {
t.Fatalf("unexpected users data classification: %#v", usersData) t.Fatalf("unexpected users data classification: %#v", usersData)
+14
View File
@@ -1,5 +1,7 @@
# POSIX Structure # POSIX Structure
[Filetree Link](https://tree.nathanfriend.com/?s=(%27optiUs!(%27fancy!Yue~fullPath!fbq~YailingSlash!Yue~rootDot!fbq)~R(%27R%27PJ%20or%20OrganizatiU%20%7Bqrver%7DM*46layout6cNlog7packs7*pack37A2*enYies75W5A_standbUe7WA6HwH30LZ2teamw5teamGL5Z6FwF30LKTC058T5C7XFG5LXKXTXC7XI7I05QG5BN2*8QG5BN6Z04_dN_pJw*pJGlayout2L*KI70%27)~vEiU!%271%27)*%20%200M52_*3-%3Cslug%3E4qttings5**69M*7%2F08VG*V259.jsUA5manifestBQ25*schema25*dCchildrenEersFprojectG305HdepartmentI*YeeJEUbKhome2L*42M%5CnNataQitemRsource!Tacl2UonVfolderW*app37X55YtrZusE_90balqsews0%01wqb_ZYXWVUTRQNMLKJIHGFECBA987654320*)
``` markdown ``` markdown
Personal or Organization (server)/ Personal or Organization (server)/
├── settings.json ├── settings.json
@@ -26,6 +28,18 @@ Personal or Organization (server)/
│ └── project-<slug>/ │ └── project-<slug>/
│ ├── settings.json │ ├── settings.json
│ ├── home.json │ ├── home.json
│ ├── acl.json
│ ├── children/
│ │ └── folder-<slug>/
│ │ ├── folder.json
│ │ ├── acl.json
│ │ └── children/
│ │ └── project-<slug>/
│ │ ├── settings.json
│ │ ├── home.json
│ │ ├── acl.json
│ │ ├── children/
│ │ └── tree/
│ └── tree/ │ └── tree/
│ ├── item-<slug>/ │ ├── item-<slug>/
│ │ ├── item.json │ │ ├── item.json