| 1 | using AgentForge; |
| 2 | using AgentForge.Abstractions; |
| 3 | using AgentForge.Plugin.Sdk; |
| 4 | using AgentForge.Contracts; |
| 5 | using AgentForge.Data; |
| 6 | using AgentForge.Data.Entities; |
| 7 | using AgentForge.Models; |
| 8 | using AgentForge.Services; |
| 9 | |
| 10 | namespace AgentForge.Plugin.Org.Web.Endpoints; |
| 11 | |
| 12 | internal static class ForgeOrgRepoGroupEndpoints |
| 13 | { |
| 14 | internal static RouteGroupBuilder MapOrgRepoGroupEndpoints(this RouteGroupBuilder api) |
| 15 | { |
| 16 | api.MapGet("/orgs/{orgSlug}/repo-groups", ListGroups); |
| 17 | api.MapPost("/orgs/{orgSlug}/repo-groups", CreateGroup); |
| 18 | api.MapGet($"/orgs/{{orgSlug}}/repo-groups/{ForgeRepoGroupRouting.Pattern}", GetGroup); |
| 19 | api.MapGet($"/orgs/{{orgSlug}}/repo-groups/{ForgeRepoGroupRouting.Pattern}/repos", ListGroupRepos); |
| 20 | return api; |
| 21 | } |
| 22 | |
| 23 | private static IResult ListGroups(string orgSlug, ForgeRepository repository) |
| 24 | { |
| 25 | var org = repository.GetOrgBySlug(orgSlug) |
| 26 | ?? throw new ForgeApiException(404, $"Organization '{orgSlug}' not found."); |
| 27 | var groups = repository.ListRepoGroups(org.Id); |
| 28 | var paths = repository.BuildRepoGroupPathLookup(org.Id); |
| 29 | var items = groups |
| 30 | .Select(g => ToResponse(g, org.Slug, repository.CountReposInGroup(g.Id), paths)) |
| 31 | .ToList(); |
| 32 | return Results.Ok(new RepoGroupListResponse(items.Count, items)); |
| 33 | } |
| 34 | |
| 35 | private static IResult CreateGroup( |
| 36 | string orgSlug, |
| 37 | CreateRepoGroupRequest request, |
| 38 | HttpContext context, |
| 39 | ForgeRepository repository, |
| 40 | ForgeRepoAccessService access) |
| 41 | { |
| 42 | var org = repository.GetOrgBySlug(orgSlug) |
| 43 | ?? throw new ForgeApiException(404, $"Organization '{orgSlug}' not found."); |
| 44 | access.EnsureOrgCatalogAdmin(org.Id, access.Resolve(context)); |
| 45 | if (!ForgeSlug.IsValid(request.Slug)) |
| 46 | throw new ForgeApiException(400, "Invalid group slug. Use lowercase slug: a-z, 0-9, hyphens."); |
| 47 | var slug = ForgeSlug.Normalize(request.Slug); |
| 48 | |
| 49 | string? parentGroupId = null; |
| 50 | if (!string.IsNullOrWhiteSpace(request.Parent)) |
| 51 | { |
| 52 | var parentSegments = ForgeRepoGroupPath.ParseSegments(request.Parent); |
| 53 | if (parentSegments.Count == 0 || parentSegments.Any(s => !ForgeSlug.IsValid(s))) |
| 54 | throw new ForgeApiException(400, "Invalid parent group path."); |
| 55 | var parent = repository.GetRepoGroupByOrgAndPath(org.Id, request.Parent) |
| 56 | ?? throw new ForgeApiException(404, $"Parent repository group '{request.Parent}' not found."); |
| 57 | parentGroupId = parent.Id; |
| 58 | } |
| 59 | |
| 60 | if (repository.RepoGroupSlugExistsInParent(org.Id, slug, parentGroupId)) |
| 61 | throw new ForgeApiException(409, $"Repository group '{slug}' already exists under this parent."); |
| 62 | |
| 63 | RepoVisibility? defaultVisibility = null; |
| 64 | if (!string.IsNullOrWhiteSpace(request.DefaultVisibility)) |
| 65 | { |
| 66 | if (!RepoVisibilityExtensions.TryParseSlug(request.DefaultVisibility, out var parsed)) |
| 67 | throw new ForgeApiException(400, "defaultVisibility must be public or private."); |
| 68 | defaultVisibility = parsed; |
| 69 | } |
| 70 | |
| 71 | var entity = repository.InsertRepoGroup( |
| 72 | org.Id, |
| 73 | slug, |
| 74 | request.DisplayName?.Trim() ?? slug, |
| 75 | request.Description?.Trim() ?? string.Empty, |
| 76 | defaultVisibility, |
| 77 | parentGroupId); |
| 78 | var paths = repository.BuildRepoGroupPathLookup(org.Id); |
| 79 | var path = paths[entity.Id]; |
| 80 | return Results.Created( |
| 81 | $"/api/v1/orgs/{orgSlug}/repo-groups/{ForgeRepoGroupPath.ToUrlPath(path)}", |
| 82 | ToResponse(entity, org.Slug, 0, paths)); |
| 83 | } |
| 84 | |
| 85 | private static IResult GetGroup(string orgSlug, string groupPath, ForgeRepository repository) |
| 86 | { |
| 87 | var org = repository.GetOrgBySlug(orgSlug) |
| 88 | ?? throw new ForgeApiException(404, $"Organization '{orgSlug}' not found."); |
| 89 | var normalizedPath = ForgeRepoGroupPath.NormalizeRoute(groupPath); |
| 90 | var group = repository.GetRepoGroupByOrgAndPath(org.Id, normalizedPath) |
| 91 | ?? throw new ForgeApiException(404, $"Repository group '{normalizedPath}' not found."); |
| 92 | var paths = repository.BuildRepoGroupPathLookup(org.Id); |
| 93 | return Results.Ok(ToResponse(group, org.Slug, repository.CountReposInGroup(group.Id), paths)); |
| 94 | } |
| 95 | |
| 96 | private static IResult ListGroupRepos( |
| 97 | string orgSlug, |
| 98 | string groupPath, |
| 99 | ForgeRepository repository, |
| 100 | GitRepoService git, |
| 101 | ForgeRepoAccessService access, |
| 102 | HttpContext http) |
| 103 | { |
| 104 | var org = repository.GetOrgBySlug(orgSlug) |
| 105 | ?? throw new ForgeApiException(404, $"Organization '{orgSlug}' not found."); |
| 106 | var normalizedPath = ForgeRepoGroupPath.NormalizeRoute(groupPath); |
| 107 | var group = repository.GetRepoGroupByOrgAndPath(org.Id, normalizedPath) |
| 108 | ?? throw new ForgeApiException(404, $"Repository group '{normalizedPath}' not found."); |
| 109 | var principal = access.Resolve(http); |
| 110 | var repos = access.FilterReadable(repository.ListReposInGroup(group.Id), principal) |
| 111 | .Select(repo => ForgePluginResponses.ToRepoResponse(repo, git, repository)) |
| 112 | .ToList(); |
| 113 | return Results.Ok(new RepoListResponse(repos.Count, repos)); |
| 114 | } |
| 115 | |
| 116 | private static RepoGroupResponse ToResponse( |
| 117 | ForgeRepoGroupEntity group, |
| 118 | string orgSlug, |
| 119 | int repoCount, |
| 120 | IReadOnlyDictionary<string, string> paths) |
| 121 | { |
| 122 | paths.TryGetValue(group.Id, out var path); |
| 123 | path ??= group.Slug; |
| 124 | string? parentPath = null; |
| 125 | if (group.ParentGroupId is not null && paths.TryGetValue(group.ParentGroupId, out var resolvedParent)) |
| 126 | parentPath = resolvedParent; |
| 127 | |
| 128 | return new RepoGroupResponse( |
| 129 | group.Slug, |
| 130 | group.DisplayName, |
| 131 | group.Description, |
| 132 | repoCount, |
| 133 | group.CreatedAt, |
| 134 | orgSlug, |
| 135 | group.DefaultVisibility?.ToSlug(), |
| 136 | path, |
| 137 | parentPath); |
| 138 | } |
| 139 | } |
| 140 | |