1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Neon from "@/Neon";
4import * as Provider from "@/Provider";
5import * as Test from "@/Test/Alchemy";
6import * as hyperdrive from "@distilled.cloud/cloudflare/hyperdrive";
7import { assert, expect } from "alchemy-test";
8import * as Data from "effect/Data";
9import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11import { MinimumLogLevel } from "effect/References";
12import * as Schedule from "effect/Schedule";
13
14const { test } = Test.make({
15 providers: Layer.merge(Cloudflare.providers(), Neon.providers()),
16});
17
18const logLevel = Effect.provideService(
19 MinimumLogLevel,
20 process.env.DEBUG ? "Debug" : "Info",
21);
22test.provider(
23 "create and delete hyperdrive with default props",
24 (stack) =>
25 Effect.gen(function* () {
26 const { accountId } = yield* yield* CloudflareEnvironment;
27
28 yield* stack.destroy();
29
30 const { db, hd } = yield* stack.deploy(
31 Effect.gen(function* () {
32 const db = yield* Neon.Project("DefaultProject");
33 const hd = yield* Cloudflare.Hyperdrive.Connection(
34 "DefaultHyperdrive",
35 {
36 origin: db.origin,
37 },
38 );
39 return { db, hd };
40 }),
41 );
42
43 expect(hd.hyperdriveId).toBeDefined();
44 expect(hd.name).toBeDefined();
45
46 const actual = yield* hyperdrive.getConfig({
47 accountId,
48 hyperdriveId: hd.hyperdriveId,
49 });
50 expect(actual.id).toEqual(hd.hyperdriveId);
51 assert("host" in actual.origin, "db.origin must have a host");
52 expect(actual.origin.host).toEqual(db.origin.host);
53
54 yield* stack.destroy();
55
56 yield* waitForConfigToBeDeleted(hd.hyperdriveId, accountId);
57 }).pipe(logLevel),
58 {
59 tags: [
60 "provider:cloudflare",
61 "provider:cloudflare:hyperdrive",
62 "provider:neon",
63 "provider:neon:project",
64 "live",
65 ],
66 },
67);
68
69test.provider(
70 "create, update, delete hyperdrive",
71 (stack) =>
72 Effect.gen(function* () {
73 const { accountId } = yield* yield* CloudflareEnvironment;
74
75 yield* stack.destroy();
76
77 const hd = yield* stack.deploy(
78 Effect.gen(function* () {
79 const project = yield* Neon.Project("CRUDProject");
80 return yield* Cloudflare.Hyperdrive.Connection("CRUDHyperdrive", {
81 origin: project.origin,
82 caching: { disabled: false, maxAge: 60 },
83 });
84 }),
85 );
86
87 const updated = yield* stack.deploy(
88 Effect.gen(function* () {
89 const project = yield* Neon.Project("CRUDProject");
90 return yield* Cloudflare.Hyperdrive.Connection("CRUDHyperdrive", {
91 origin: project.origin,
92 caching: { disabled: true },
93 });
94 }),
95 );
96
97 expect(updated.hyperdriveId).toEqual(hd.hyperdriveId);
98
99 const actual = yield* hyperdrive.getConfig({
100 accountId,
101 hyperdriveId: updated.hyperdriveId,
102 });
103 // After PUT with `disabled: true`, caching should reflect the change.
104 expect(actual.caching).toBeDefined();
105
106 yield* stack.destroy();
107
108 yield* waitForConfigToBeDeleted(hd.hyperdriveId, accountId);
109 }).pipe(logLevel),
110 {
111 tags: [
112 "provider:cloudflare",
113 "provider:cloudflare:hyperdrive",
114 "provider:neon",
115 "provider:neon:project",
116 "live",
117 ],
118 },
119);
120
121test.provider(
122 "list enumerates the deployed hyperdrive",
123 (stack) =>
124 Effect.gen(function* () {
125 yield* stack.destroy();
126
127 const hd = yield* stack.deploy(
128 Effect.gen(function* () {
129 const project = yield* Neon.Project("ListProject");
130 return yield* Cloudflare.Hyperdrive.Connection("ListHyperdrive", {
131 origin: project.origin,
132 });
133 }),
134 );
135
136 const provider = yield* Provider.findProvider(
137 Cloudflare.Hyperdrive.Connection,
138 );
139 const all = yield* provider.list();
140
141 expect(all.some((x) => x.hyperdriveId === hd.hyperdriveId)).toBe(true);
142
143 yield* stack.destroy();
144 }).pipe(logLevel),
145 {
146 tags: [
147 "provider:cloudflare",
148 "provider:cloudflare:hyperdrive",
149 "provider:neon",
150 "provider:neon:project",
151 "live",
152 ],
153 },
154);
155
156const waitForConfigToBeDeleted = Effect.fn(function* (
157 hyperdriveId: string,
158 accountId: string,
159) {
160 yield* hyperdrive.getConfig({ accountId, hyperdriveId }).pipe(
161 Effect.flatMap(() => Effect.fail(new ConfigStillExists())),
162 Effect.retry({
163 while: (e): e is ConfigStillExists => e instanceof ConfigStillExists,
164 schedule: Schedule.exponential(100),
165 }),
166 Effect.catchTag("HyperdriveConfigNotFound", () => Effect.void),
167 );
168});
169
170class ConfigStillExists extends Data.TaggedError("ConfigStillExists") {}