| 1 | /** |
| 2 | * The `repos` group: repo CRUD, fork, import, and compaction |
| 3 | * (DESIGN.md §5). Every route is an Effect `HttpApiEndpoint`. Who may |
| 4 | * call it is decided by the middleware applied to its route layer. |
| 5 | */ |
| 6 | import * as HttpApiEndpoint from "effect/http-api/HttpApiEndpoint"; |
| 7 | import * as Schema from "effect/Schema"; |
| 8 | import * as HttpApiGroup from "effect/http-api/HttpApiGroup"; |
| 9 | import * as HttpApiSchema from "effect/http-api/HttpApiSchema"; |
| 10 | import { |
| 11 | ImportFailed, |
| 12 | OwnerName, |
| 13 | Paginated, |
| 14 | RefNotFound, |
| 15 | Repo, |
| 16 | RepoAlreadyExists, |
| 17 | RepoCreated, |
| 18 | RepoName, |
| 19 | RepoNotFound, |
| 20 | RepoNotReady, |
| 21 | RepoPath, |
| 22 | ValidationError, |
| 23 | } from "./Schema.ts"; |
| 24 | |
| 25 | /** Creates a repo owned by `owner`. */ |
| 26 | export const CreateRepo = HttpApiEndpoint.post("create", "/repos", { |
| 27 | payload: Schema.Struct({ |
| 28 | owner: OwnerName, |
| 29 | name: RepoName, |
| 30 | /** Default branch short name. @default "main" */ |
| 31 | defaultBranch: Schema.optional(Schema.String), |
| 32 | description: Schema.optional(Schema.String), |
| 33 | /** |
| 34 | * Anyone can read/clone without a credential when `true`. |
| 35 | * @default false |
| 36 | */ |
| 37 | public: Schema.optional(Schema.Boolean), |
| 38 | /** @default false */ |
| 39 | readOnly: Schema.optional(Schema.Boolean), |
| 40 | }), |
| 41 | success: RepoCreated, |
| 42 | error: [RepoAlreadyExists, ValidationError], |
| 43 | }); |
| 44 | |
| 45 | /** Reads one repo (poll `status` for async fork/import/delete progress). */ |
| 46 | export const GetRepo = HttpApiEndpoint.get("get", "/repos/:owner/:repo", { |
| 47 | params: RepoPath, |
| 48 | success: Repo, |
| 49 | error: [RepoNotFound], |
| 50 | }); |
| 51 | |
| 52 | /** Patches description / default branch / readOnly / public. */ |
| 53 | export const UpdateRepo = HttpApiEndpoint.patch( |
| 54 | "update", |
| 55 | "/repos/:owner/:repo", |
| 56 | { |
| 57 | params: RepoPath, |
| 58 | payload: Schema.Struct({ |
| 59 | description: Schema.optional(Schema.NullOr(Schema.String)), |
| 60 | /** Must resolve to an existing branch. */ |
| 61 | defaultBranch: Schema.optional(Schema.String), |
| 62 | readOnly: Schema.optional(Schema.Boolean), |
| 63 | /** Anyone can read/clone without a credential when `true`. */ |
| 64 | public: Schema.optional(Schema.Boolean), |
| 65 | }), |
| 66 | success: Repo, |
| 67 | error: [RepoNotFound, RefNotFound], |
| 68 | }, |
| 69 | ); |
| 70 | |
| 71 | /** |
| 72 | * Lists repos, optionally filtered by owner. Lists everything the |
| 73 | * Registry holds; `public: true` narrows it to public repositories. |
| 74 | */ |
| 75 | export const ListRepos = HttpApiEndpoint.get("list", "/repos", { |
| 76 | query: Schema.Struct({ |
| 77 | owner: Schema.optional(OwnerName), |
| 78 | /** Only public repositories when `true`. */ |
| 79 | public: Schema.optional(Schema.Boolean), |
| 80 | cursor: Schema.optional(Schema.String), |
| 81 | limit: Schema.optional( |
| 82 | Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 })), |
| 83 | ), |
| 84 | }), |
| 85 | success: Paginated(Repo), |
| 86 | }); |
| 87 | |
| 88 | /** |
| 89 | * Deletes a repo. Async purge: responds 204 immediately, repo `status` |
| 90 | * flips to `'deleting'`, the name frees after the purge alarm completes |
| 91 | * (then 404). |
| 92 | */ |
| 93 | export const DeleteRepo = HttpApiEndpoint.delete( |
| 94 | "delete", |
| 95 | "/repos/:owner/:repo", |
| 96 | { |
| 97 | params: RepoPath, |
| 98 | success: HttpApiSchema.NoContent, |
| 99 | error: [RepoNotFound], |
| 100 | }, |
| 101 | ); |
| 102 | |
| 103 | /** |
| 104 | * Forks a repo. The fork starts in `status: 'forking'`; poll |
| 105 | * `GET /repos/:owner/:repo` until `'ready'`. |
| 106 | */ |
| 107 | export const ForkRepo = HttpApiEndpoint.post( |
| 108 | "fork", |
| 109 | "/repos/:owner/:repo/fork", |
| 110 | { |
| 111 | params: RepoPath, |
| 112 | payload: Schema.Struct({ |
| 113 | targetOwner: OwnerName, |
| 114 | targetName: RepoName, |
| 115 | }), |
| 116 | success: RepoCreated, |
| 117 | error: [RepoNotFound, RepoAlreadyExists, RepoNotReady], |
| 118 | }, |
| 119 | ); |
| 120 | |
| 121 | /** |
| 122 | * Imports from an external smart-HTTP source. The repo starts in |
| 123 | * `status: 'importing'`; poll `GET /repos/:owner/:repo` until `'ready'`. |
| 124 | */ |
| 125 | export const ImportRepo = HttpApiEndpoint.post("import", "/repos/import", { |
| 126 | payload: Schema.Struct({ |
| 127 | owner: OwnerName, |
| 128 | name: RepoName, |
| 129 | source: Schema.Struct({ |
| 130 | /** Smart-HTTP URL of the source repository. */ |
| 131 | url: Schema.String, |
| 132 | /** Restrict the import to a single ref. */ |
| 133 | ref: Schema.optional(Schema.String), |
| 134 | /** Depth-limit the imported history. */ |
| 135 | depth: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))), |
| 136 | }), |
| 137 | }), |
| 138 | success: RepoCreated, |
| 139 | error: [RepoAlreadyExists, ImportFailed], |
| 140 | }); |
| 141 | |
| 142 | /** |
| 143 | * Forces a compaction run now (`Maintain`): moves loose object bytes into |
| 144 | * an immutable pack. Normally armed automatically by size thresholds after |
| 145 | * a push — this is the operator/benchmark handle. Returns immediately; poll |
| 146 | * `GET /repos/:owner/:repo` and watch `objects.loose` fall to zero. |
| 147 | */ |
| 148 | export const CompactRepo = HttpApiEndpoint.post( |
| 149 | "compact", |
| 150 | "/repos/:owner/:repo/compact", |
| 151 | { |
| 152 | params: RepoPath, |
| 153 | success: HttpApiSchema.NoContent, |
| 154 | error: [RepoNotFound], |
| 155 | }, |
| 156 | ); |
| 157 | |
| 158 | /** The `repos` group, mounted at `/api/v1`. */ |
| 159 | export class Repos extends HttpApiGroup.make("repos") |
| 160 | .add( |
| 161 | CreateRepo, |
| 162 | GetRepo, |
| 163 | UpdateRepo, |
| 164 | ListRepos, |
| 165 | DeleteRepo, |
| 166 | ForkRepo, |
| 167 | ImportRepo, |
| 168 | CompactRepo, |
| 169 | ) |
| 170 | .prefix("/api/v1") {} |