1import { listProjectBranches } from "@distilled.cloud/neon";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import type { Input } from "../Input.ts";
5
6/** Select an explicit branch or the project's current default branch, never both. */
7export type BranchScope =
8 | {
9 /** Branch resource or explicit branch identity. */
10 branch: { projectId: string; branchId: string };
11 project?: never;
12 }
13 | {
14 /** Project whose default branch is selected at reconciliation time. */
15 project: { projectId: string };
16 branch?: never;
17 };
18
19export interface ResolvedBranchScope {
20 /** Neon project ID. */
21 projectId: string;
22 /** Neon branch ID. */
23 branchId: string;
24}
25
26export class InvalidBranchScope extends Data.TaggedError("InvalidBranchScope")<{
27 message: string;
28}> {}
29
30/** Resolve lifecycle props after Alchemy has resolved resource references. */
31export const resolveBranchScope = Effect.fn(function* (
32 scope: Input.Resolve<BranchScope>,
33) {
34 if ((scope.branch !== undefined) === (scope.project !== undefined)) {
35 return yield* new InvalidBranchScope({
36 message: "Specify exactly one of branch or project",
37 });
38 }
39 if (scope.branch !== undefined) {
40 const { projectId, branchId } = scope.branch;
41 if (!projectId || !branchId) {
42 return yield* new InvalidBranchScope({
43 message: "Branch requires projectId and branchId",
44 });
45 }
46 return { projectId, branchId };
47 }
48 const projectId = scope.project?.projectId;
49 if (!projectId) {
50 return yield* new InvalidBranchScope({
51 message: "Project requires projectId",
52 });
53 }
54 let cursor: string | undefined;
55 const seen = new Set<string>();
56 do {
57 const page = yield* listProjectBranches({ project_id: projectId, cursor });
58 const branch = page.branches.find((branch) => branch.default);
59 if (branch) return { projectId, branchId: branch.id };
60 cursor = page.pagination?.next;
61 if (cursor && seen.has(cursor)) {
62 return yield* new InvalidBranchScope({
63 message: "Branch pagination repeated a cursor",
64 });
65 }
66 if (cursor) seen.add(cursor);
67 } while (cursor);
68 return yield* new InvalidBranchScope({
69 message: `Project ${projectId} has no default branch`,
70 });
71});