1import * as Cloudflare from "@/Cloudflare";
2import * as Drizzle from "@/Drizzle";
3import * as Test from "@/Test/Alchemy";
4import { expect } from "alchemy-test";
5import * as Data from "effect/Data";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Schedule from "effect/Schedule";
9import * as HttpClient from "effect/http/HttpClient";
10import * as HttpClientRequest from "effect/http/HttpClientRequest";
11import type * as HttpClientResponse from "effect/http/HttpClientResponse";
12import Stack from "./fixtures/drizzle-stack.ts";
13
14const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
15 providers: Layer.mergeAll(Cloudflare.providers(), Drizzle.providers()),
16 state: Cloudflare.state(),
17});
18
19const HOOK_TIMEOUT = 300_000;
20const TEST_TIMEOUT = 120_000;
21
22class WorkerNotReady extends Data.TaggedError("WorkerNotReady")<{
23 status: number;
24 body: string;
25}> {}
26
27// Bounded spaced schedule — rides out fresh-workers.dev cold-start
28// propagation without blowing past the test timeout.
29const ready = Schedule.max([Schedule.spaced("2 seconds"), Schedule.recurs(45)]);
30
31/** Retry an HTTP call until it returns 200. */
32const untilOk = <E, R>(
33 eff: Effect.Effect<HttpClientResponse.HttpClientResponse, E, R>,
34) =>
35 eff.pipe(
36 Effect.flatMap((res) =>
37 res.status === 200
38 ? Effect.succeed(res)
39 : res.text.pipe(
40 Effect.flatMap((body) =>
41 Effect.fail(new WorkerNotReady({ status: res.status, body })),
42 ),
43 ),
44 ),
45 Effect.retry({
46 while: (e): e is WorkerNotReady => e instanceof WorkerNotReady,
47 schedule: ready,
48 }),
49 );
50
51/**
52 * End-to-end test of the Drizzle-on-D1 surface against a real Cloudflare
53 * Worker + D1 database:
54 *
55 * - deploy applies the checked-in migration (generated by drizzle-kit from
56 * `fixtures/drizzle-schema.ts`) via `migrationsDir` into the
57 * `drizzle_migrations` table;
58 * - `Drizzle.D1(d1, { relations })` drives insert/select/relational queries
59 * through the drizzle-orm `effect-d1` driver;
60 * - `SQL.D1(d1)` drives raw tagged-template SQL through `@effect/sql-d1`.
61 */
62const stack = beforeAll(deploy(Stack), { timeout: HOOK_TIMEOUT });
63afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack), {
64 timeout: HOOK_TIMEOUT,
65});
66
67test(
68 "Drizzle.D1 + SQL.D1 exercise the deployed database",
69 Effect.gen(function* () {
70 const { url } = yield* stack;
71
72 // drizzle insert ... returning
73 const userRes = yield* untilOk(
74 HttpClient.execute(
75 HttpClientRequest.post(`${url}/users`).pipe(
76 HttpClientRequest.bodyJsonUnsafe({
77 name: "alice",
78 email: "alice@example.com",
79 }),
80 ),
81 ),
82 );
83 const { user } = (yield* userRes.json) as {
84 user: { id: number; name: string; email: string };
85 };
86 expect(user.name).toBe("alice");
87 expect(user.email).toBe("alice@example.com");
88 expect(typeof user.id).toBe("number");
89
90 const postRes = yield* untilOk(
91 HttpClient.execute(
92 HttpClientRequest.post(`${url}/posts`).pipe(
93 HttpClientRequest.bodyJsonUnsafe({
94 userId: user.id,
95 title: "hello world",
96 }),
97 ),
98 ),
99 );
100 const { post } = (yield* postRes.json) as {
101 post: { id: number; userId: number; title: string };
102 };
103 expect(post.userId).toBe(user.id);
104 expect(post.title).toBe("hello world");
105
106 // drizzle select
107 const listRes = yield* untilOk(HttpClient.get(`${url}/users`));
108 const { users } = (yield* listRes.json) as {
109 users: Array<{ id: number; name: string }>;
110 };
111 expect(users.map((u) => u.name)).toContain("alice");
112
113 // drizzle relational query (RQB v2): user with posts
114 const relRes = yield* untilOk(HttpClient.get(`${url}/users/${user.id}`));
115 const rel = (yield* relRes.json) as {
116 user: { id: number; posts: Array<{ title: string }> };
117 };
118 expect(rel.user.id).toBe(user.id);
119 expect(rel.user.posts.map((p) => p.title)).toContain("hello world");
120
121 // raw @effect/sql-d1 tagged-template client
122 const sqlRes = yield* untilOk(HttpClient.get(`${url}/sql/users`));
123 const { rows } = (yield* sqlRes.json) as {
124 rows: Array<{ id: number; name: string; email: string }>;
125 };
126 expect(rows.map((r) => r.email)).toContain("alice@example.com");
127 }),
128 {
129 tags: [
130 "provider:cloudflare",
131 "provider:cloudflare:d1",
132 "provider:cloudflare:worker",
133 "live",
134 ],
135 timeout: TEST_TIMEOUT,
136 },
137);