1import * as AWS from "@/AWS";
2import * as Cloudflare from "@/Cloudflare";
3import type { Branch } from "@/Neon/Branch";
4import { providers } from "@/Neon/Providers";
5import { runSql } from "@/Neon/Migrations";
6import * as Redacted from "effect/Redacted";
7import * as Test from "@/Test/Alchemy";
8import { getConnectionURI, getProjectBranch } from "@distilled.cloud/neon";
9import { expect } from "alchemy-test";
10import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";
12import * as Schedule from "effect/Schedule";
13import * as HttpClient from "effect/http/HttpClient";
14import { ConnectBranch } from "./fixtures/connect-database.ts";
15import ConnectLambda from "./fixtures/connect-lambda.ts";
16import ConnectFunction from "./fixtures/connect-function.ts";
17import ConnectWorker from "./fixtures/connect-worker.ts";
18import ConnectProjectFunction from "./fixtures/connect-project-function.ts";
19
20const { test: workerTest } = Test.make({
21 providers: Layer.mergeAll(providers(), Cloudflare.providers()),
22});
23const { test: lambdaTest } = Test.make({
24 providers: Layer.mergeAll(providers(), AWS.providers()),
25});
26
27const { test: functionTest } = Test.make({ providers: providers() });
28
29const verify = Effect.fn(function* (
30 url: string,
31 branch: Branch["Attributes"],
32 injected = false,
33 parentInjected = false,
34) {
35 const parent = yield* getConnectionURI({
36 project_id: branch.projectId,
37 branch_id: branch.parentBranchId,
38 database_name: branch.databaseName,
39 role_name: branch.roleName,
40 pooled: false,
41 });
42 yield* runSql(
43 Redacted.make(parent.uri),
44 "CREATE TABLE IF NOT EXISTS alchemy_connect_marker (value text); TRUNCATE alchemy_connect_marker; INSERT INTO alchemy_connect_marker VALUES ('parent');",
45 );
46 yield* runSql(
47 Redacted.make(branch.connectionUri),
48 "CREATE TABLE IF NOT EXISTS alchemy_connect_marker (value text); TRUNCATE alchemy_connect_marker; INSERT INTO alchemy_connect_marker VALUES ('branch');",
49 );
50 const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient);
51 yield* Effect.gen(function* () {
52 const response = yield* client.get(url);
53 expect(response.status).toBe(200);
54 expect(yield* response.json).toEqual({
55 database: branch.databaseName,
56 directDatabase: branch.databaseName,
57 parentDatabase: "neondb",
58 branchMarker: "branch",
59 directMarker: "branch",
60 parentMarker: "parent",
61 hasAccountKey: false,
62 injected,
63 parentInjected,
64 });
65 }).pipe(Effect.retry({ schedule: Schedule.spaced("2 seconds"), times: 8 }));
66 const observed = yield* getProjectBranch({
67 project_id: branch.projectId,
68 branch_id: branch.branchId,
69 });
70 expect(observed.branch.id).toBe(branch.branchId);
71 const direct = yield* getConnectionURI({
72 project_id: branch.projectId,
73 branch_id: branch.branchId,
74 database_name: branch.databaseName,
75 role_name: branch.roleName,
76 pooled: false,
77 });
78 expect(branch.connectionUri === direct.uri).toBe(true);
79});
80
81functionTest.provider(
82 "Connect uses injected same-branch and namespaced cross-branch Function environments",
83 (stack) =>
84 Effect.gen(function* () {
85 yield* stack.destroy();
86 const { fn, branch } = yield* stack.deploy(
87 Effect.gen(function* () {
88 const fn = yield* ConnectFunction;
89 const branch = yield* ConnectBranch;
90 return { fn, branch };
91 }),
92 );
93 yield* verify(fn.url, branch, true);
94 yield* verify(fn.url, branch, true);
95 yield* stack.destroy();
96 expect(
97 yield* getProjectBranch({
98 project_id: branch.projectId,
99 branch_id: branch.branchId,
100 }).pipe(
101 Effect.as(false),
102 Effect.catchTag("NotFound", () => Effect.succeed(true)),
103 ),
104 ).toBe(true);
105 }),
106 {
107 tags: [
108 "provider:neon",
109 "provider:neon:branch",
110 "provider:neon:function",
111 "provider:neon:project",
112 "live",
113 ],
114 timeout: 120_000,
115 },
116);
117
118functionTest.provider(
119 "Connect resolves injection for a raw default-project scope",
120 (stack) =>
121 Effect.gen(function* () {
122 yield* stack.destroy();
123 const { fn, branch } = yield* stack.deploy(
124 Effect.gen(function* () {
125 const fn = yield* ConnectProjectFunction;
126 const branch = yield* ConnectBranch;
127 return { fn, branch };
128 }),
129 );
130 yield* verify(fn.url, branch, false, true);
131 yield* verify(fn.url, branch, false, true);
132 yield* stack.destroy();
133 expect(
134 yield* getProjectBranch({
135 project_id: branch.projectId,
136 branch_id: branch.branchId,
137 }).pipe(
138 Effect.as(false),
139 Effect.catchTag("NotFound", () => Effect.succeed(true)),
140 ),
141 ).toBe(true);
142 }),
143 {
144 tags: [
145 "provider:neon",
146 "provider:neon:branch",
147 "provider:neon:function",
148 "provider:neon:project",
149 "live",
150 ],
151 timeout: 120_000,
152 },
153);
154
155workerTest.provider(
156 "Connect queries pooled and direct Postgres from a Worker",
157 (stack) =>
158 Effect.gen(function* () {
159 yield* stack.destroy();
160 const { worker, branch } = yield* stack.deploy(
161 Effect.gen(function* () {
162 const worker = yield* ConnectWorker;
163 const branch = yield* ConnectBranch;
164 return { worker, branch };
165 }),
166 );
167 expect(worker.url).toBeDefined();
168 yield* verify(worker.url!, branch);
169 yield* verify(worker.url!, branch);
170 yield* stack.destroy();
171 expect(
172 yield* getProjectBranch({
173 project_id: branch.projectId,
174 branch_id: branch.branchId,
175 }).pipe(
176 Effect.as(false),
177 Effect.catchTag("NotFound", () => Effect.succeed(true)),
178 ),
179 ).toBe(true);
180 }),
181 {
182 tags: [
183 "provider:cloudflare",
184 "provider:cloudflare:worker",
185 "provider:neon",
186 "provider:neon:branch",
187 "provider:neon:project",
188 "live",
189 ],
190 timeout: 120_000,
191 },
192);
193
194lambdaTest.provider(
195 "Connect queries pooled and direct Postgres from Lambda",
196 (stack) =>
197 Effect.gen(function* () {
198 yield* stack.destroy();
199 const { fn, branch } = yield* stack.deploy(
200 Effect.gen(function* () {
201 const fn = yield* ConnectLambda;
202 const branch = yield* ConnectBranch;
203 return { fn, branch };
204 }),
205 );
206 expect(fn.functionUrl).toBeDefined();
207 yield* verify(fn.functionUrl!, branch);
208 yield* verify(fn.functionUrl!, branch);
209 yield* stack.destroy();
210 expect(
211 yield* getProjectBranch({
212 project_id: branch.projectId,
213 branch_id: branch.branchId,
214 }).pipe(
215 Effect.as(false),
216 Effect.catchTag("NotFound", () => Effect.succeed(true)),
217 ),
218 ).toBe(true);
219 }),
220 {
221 tags: [
222 "provider:aws",
223 "provider:aws:lambda",
224 "provider:neon",
225 "provider:neon:branch",
226 "provider:neon:project",
227 "live",
228 ],
229 timeout: 240_000,
230 },
231);