1import * as Prisma from "@/Prisma";
2import * as Test from "@/Test/Alchemy";
3import { expect } from "alchemy-test";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as Redacted from "effect/Redacted";
8import * as Schedule from "effect/Schedule";
9import * as HttpServerResponse from "effect/http/HttpServerResponse";
10
11const { test } = Test.make({ providers: Prisma.providers(), dev: true });
12
13test.provider(
14 "dev mode returns a local Compute without a token",
15 (stack) =>
16 Effect.gen(function* () {
17 yield* stack.destroy();
18
19 const app = yield* stack.deploy(
20 Effect.gen(function* () {
21 return yield* Prisma.Compute("App", {
22 project: "project-dev",
23 appName: "api",
24 port: 8787,
25 dev: {
26 url: "http://localhost:8787",
27 },
28 });
29 }),
30 );
31
32 expect(app.local).toBe(true);
33 expect(app.url).toBe("http://localhost:8787");
34 expect(app.appId).toContain("dev:");
35
36 yield* stack.destroy();
37 }),
38 { tags: ["provider:prisma", "provider:prisma:compute", "local"] },
39);
40
41test.provider(
42 "dev mode supports the same stack shape with Project",
43 (stack) =>
44 Effect.gen(function* () {
45 yield* stack.destroy();
46
47 const output = yield* stack.deploy(
48 Effect.gen(function* () {
49 const project = yield* Prisma.Project("Project", {
50 name: "local-project",
51 createDatabase: false,
52 });
53 const app = yield* Prisma.Compute("AppWithProject", {
54 project,
55 appName: "api",
56 port: 8787,
57 dev: {
58 url: "http://localhost:8787",
59 },
60 });
61 return { project, app };
62 }),
63 );
64
65 expect(output.project.projectId).toBe("dev:project:Project");
66 expect(output.app.projectId).toBe(output.project.projectId);
67 expect(output.app.local).toBe(true);
68 expect(output.app.url).toBe("http://localhost:8787");
69
70 yield* stack.destroy();
71 }),
72 {
73 tags: [
74 "provider:prisma",
75 "provider:prisma:compute",
76 "provider:prisma:project",
77 "local",
78 ],
79 },
80);
81
82test.provider(
83 "dev mode records effect-native Connection.bind env on Compute",
84 (stack) =>
85 Effect.gen(function* () {
86 yield* stack.destroy();
87
88 const output = yield* stack.deploy(
89 Effect.gen(function* () {
90 const project = yield* Prisma.Project("Project", {
91 name: "local-project",
92 });
93 const database = yield* Prisma.Database("Database", {
94 project,
95 name: "main",
96 dev: {
97 name: "alchemy-compute-dev-bind-env",
98 },
99 });
100 const connection = yield* Prisma.Connection("Connection", {
101 database,
102 name: "api",
103 });
104 const keys = Prisma.connectEnvKeys(connection);
105 const app = yield* Prisma.Compute(
106 "App",
107 {
108 project,
109 appName: "api",
110 main: import.meta.filename,
111 dev: {
112 url: "http://localhost:8787",
113 },
114 },
115 Effect.gen(function* () {
116 const db = yield* Prisma.Connect(connection);
117
118 return {
119 fetch: Effect.gen(function* () {
120 const connectionId = yield* db.connectionId;
121 return HttpServerResponse.text(connectionId);
122 }),
123 };
124 }).pipe(Effect.provide(Prisma.ConnectBinding)),
125 );
126
127 return { app, keys };
128 }),
129 );
130
131 expect(output.app.local).toBe(true);
132 expect(output.app.environmentKeys).toEqual(
133 expect.arrayContaining([
134 output.keys.connectionId,
135 output.keys.databaseId,
136 output.keys.directConnectionString,
137 output.keys.pooledConnectionString,
138 output.keys.accelerateConnectionString,
139 output.keys.host,
140 output.keys.user,
141 output.keys.password,
142 ]),
143 );
144
145 yield* stack.destroy();
146 }),
147 {
148 tags: [
149 "provider:prisma",
150 "provider:prisma:compute",
151 "provider:prisma:connect",
152 "provider:prisma:connection",
153 "provider:prisma:database",
154 "provider:prisma:project",
155 "local",
156 ],
157 },
158);
159
160test.provider(
161 "dev mode resolves all owned Prisma resource shapes without a token",
162 (stack) =>
163 Effect.gen(function* () {
164 yield* stack.destroy();
165
166 const output = yield* stack.deploy(
167 Effect.gen(function* () {
168 const project = yield* Prisma.Project("Project", {
169 name: "local-project",
170 });
171 const database = yield* Prisma.Database("Database", {
172 project,
173 name: "main",
174 dev: {
175 name: "alchemy-compute-dev-owned-shapes",
176 },
177 });
178 const connection = yield* Prisma.Connection("Connection", {
179 database,
180 name: "api",
181 });
182 const branch = yield* Prisma.Branch("Branch", {
183 project,
184 gitName: "main",
185 isDefault: true,
186 });
187 const appResource = yield* Prisma.App("App", {
188 project,
189 displayName: "api",
190 branchId: branch.branchId,
191 });
192 const deployment = yield* Prisma.Deployment("Deployment", {
193 app: appResource,
194 portMapping: { http: 3000 },
195 });
196 const env = yield* Prisma.EnvironmentVariable("Environment", {
197 project,
198 class: "production",
199 key: "TOKEN",
200 value: Redacted.make("secret"),
201 });
202 const repo = yield* Prisma.SourceRepository("SourceRepository", {
203 project,
204 providerRepositoryId: 123,
205 });
206
207 return {
208 project,
209 database,
210 connection,
211 branch,
212 appResource,
213 deployment,
214 env,
215 repo,
216 };
217 }),
218 );
219
220 expect(output.project.projectId).toBe("dev:project:Project");
221 expect(output.database.projectId).toBe(output.project.projectId);
222 expect(output.database.databaseId).toBe("dev:database:Database");
223 expect(output.connection.databaseId).toBe(output.database.databaseId);
224 expect(output.branch.projectId).toBe(output.project.projectId);
225 expect(output.branch.isDefault).toBe(true);
226 expect(output.appResource.projectId).toBe(output.project.projectId);
227 expect(output.appResource.branchId).toBe(output.branch.branchId);
228 expect(output.deployment.appId).toBe(output.appResource.appId);
229 expect(output.env.projectId).toBe(output.project.projectId);
230 expect(output.env.key).toBe("TOKEN");
231 expect(Redacted.value(output.env.value)).toBe("secret");
232 expect(output.repo.projectId).toBe(output.project.projectId);
233 expect(output.repo.repoId).toBe(123);
234
235 yield* stack.destroy();
236 }),
237 {
238 tags: [
239 "provider:prisma",
240 "provider:prisma:app",
241 "provider:prisma:branch",
242 "provider:prisma:compute",
243 "provider:prisma:connection",
244 "provider:prisma:database",
245 "provider:prisma:deployment",
246 "provider:prisma:environmentvariable",
247 "provider:prisma:project",
248 "provider:prisma:sourcerepository",
249 "local",
250 ],
251 exclusive: true,
252 },
253);
254
255test.provider(
256 "dev mode starts a local command and passes env",
257 (stack) =>
258 Effect.gen(function* () {
259 yield* stack.destroy();
260
261 const fs = yield* FileSystem.FileSystem;
262 const path = yield* Path.Path;
263 const root = yield* fs.makeTempDirectory({
264 prefix: "alchemy-prisma-dev-",
265 });
266 const scriptPath = path.join(root, "dev-server.sh");
267 const outputPath = path.join(root, "dev-output.json");
268 const stoppedPath = path.join(root, "dev-stopped.txt");
269
270 yield* fs.writeFileString(
271 scriptPath,
272 [
273 "trap 'printf stopped > dev-stopped.txt; exit 0' TERM INT",
274 'printf \'{"port":"%s","greeting":"%s","nodeEnv":"%s"}\' "$PORT" "$GREETING" "$NODE_ENV" > dev-output.json',
275 "while true; do sleep 1; done",
276 "",
277 ].join("\n"),
278 );
279
280 const app = yield* stack.deploy(
281 Effect.gen(function* () {
282 return yield* Prisma.Compute("AppWithCommand", {
283 project: "project-dev",
284 appName: "api",
285 path: root,
286 port: 8788,
287 env: {
288 GREETING: "hello-dev",
289 },
290 dev: {
291 command: "sh dev-server.sh",
292 cwd: root,
293 port: 8789,
294 },
295 });
296 }),
297 );
298
299 expect(app.local).toBe(true);
300 expect(app.url).toBe("http://localhost:8789");
301
302 const output = yield* fs.readFileString(outputPath).pipe(
303 Effect.retry({
304 schedule: Schedule.max([
305 Schedule.fixed("50 millis"),
306 Schedule.recurs(40),
307 ]),
308 }),
309 );
310
311 expect(JSON.parse(output)).toEqual({
312 port: "8789",
313 greeting: "hello-dev",
314 nodeEnv: "development",
315 });
316
317 yield* stack.destroy();
318
319 const stopped = yield* fs.readFileString(stoppedPath).pipe(
320 Effect.retry({
321 schedule: Schedule.max([
322 Schedule.fixed("50 millis"),
323 Schedule.recurs(40),
324 ]),
325 }),
326 );
327
328 expect(stopped).toBe("stopped");
329 }),
330 {
331 tags: ["provider:prisma", "provider:prisma:compute", "local"],
332 timeout: 10_000,
333 },
334);