Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 0d6be68

Browse files
authored
Add Hyperdrive Bindings (#718)
* Add Hyperdrive Binding support * Add tests for the hyperdrive binding
1 parent a37ede3 commit 0d6be68

File tree

3 files changed

+150
-1
lines changed

3 files changed

+150
-1
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { z } from "zod";
2+
import { Service, Worker_Binding } from "../../runtime";
3+
import { Plugin } from "../shared";
4+
5+
export const HYPERDRIVE_PLUGIN_NAME = "hyperdrive";
6+
7+
export const HyperdriveSchema = z
8+
.string()
9+
.url()
10+
.transform((urlString, ctx) => {
11+
const url = new URL(urlString);
12+
if (url.protocol === "") {
13+
ctx.addIssue({
14+
code: z.ZodIssueCode.custom,
15+
message: "You must specify the database protocol - e.g. 'postgresql'.",
16+
});
17+
} else if (
18+
url.protocol !== "postgresql:" &&
19+
url.protocol !== "postgres:" &&
20+
url.protocol !== ""
21+
) {
22+
ctx.addIssue({
23+
code: z.ZodIssueCode.custom,
24+
message:
25+
"Only PostgreSQL or PostgreSQL compatible databases are currently supported.",
26+
});
27+
}
28+
if (url.host === "") {
29+
ctx.addIssue({
30+
code: z.ZodIssueCode.custom,
31+
message:
32+
"You must provide a hostname or IP address in your connection string - e.g. 'user:[email protected]:5432/databasename",
33+
});
34+
}
35+
if (url.port === "") {
36+
ctx.addIssue({
37+
code: z.ZodIssueCode.custom,
38+
message:
39+
"You must provide a port number - e.g. 'user:[email protected]:port/databasename",
40+
});
41+
}
42+
if (url.pathname === "") {
43+
ctx.addIssue({
44+
code: z.ZodIssueCode.custom,
45+
message:
46+
"You must provide a database name as the path component - e.g. /postgres",
47+
});
48+
}
49+
if (url.username === "") {
50+
ctx.addIssue({
51+
code: z.ZodIssueCode.custom,
52+
message:
53+
"You must provide a username - e.g. 'user:[email protected]:port/databasename'",
54+
});
55+
}
56+
if (url.password === "") {
57+
ctx.addIssue({
58+
code: z.ZodIssueCode.custom,
59+
message:
60+
"You must provide a password - e.g. 'user:[email protected]:port/databasename' ",
61+
});
62+
}
63+
return {
64+
database: url.pathname.replace("/", ""),
65+
user: url.username,
66+
password: url.password,
67+
scheme: url.protocol.replace(":", ""),
68+
host: url.host,
69+
port: url.port,
70+
};
71+
});
72+
73+
export const HyperdriveInputOptionsSchema = z.object({
74+
hyperdrives: z.record(z.string(), HyperdriveSchema).optional(),
75+
});
76+
77+
export const HYPERDRIVE_PLUGIN: Plugin<typeof HyperdriveInputOptionsSchema> = {
78+
options: HyperdriveInputOptionsSchema,
79+
getBindings(options) {
80+
return Object.entries(options.hyperdrives ?? {}).map<Worker_Binding>(
81+
([name, config]) => ({
82+
name,
83+
hyperdrive: {
84+
designator: {
85+
name: `${HYPERDRIVE_PLUGIN_NAME}:${name}`,
86+
},
87+
database: config.database,
88+
user: config.user,
89+
password: config.password,
90+
scheme: config.scheme,
91+
},
92+
})
93+
);
94+
},
95+
getNodeBindings() {
96+
return {};
97+
},
98+
async getServices({ options }) {
99+
return Object.entries(options.hyperdrives ?? {}).map<Service>(
100+
([name, config]) => ({
101+
name: `${HYPERDRIVE_PLUGIN_NAME}:${name}`,
102+
external: {
103+
address: `${config.host}:${config.port}`,
104+
tcp: {},
105+
},
106+
})
107+
);
108+
},
109+
};

packages/miniflare/src/plugins/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CACHE_PLUGIN, CACHE_PLUGIN_NAME } from "./cache";
44
import { CORE_PLUGIN, CORE_PLUGIN_NAME } from "./core";
55
import { D1_PLUGIN, D1_PLUGIN_NAME } from "./d1";
66
import { DURABLE_OBJECTS_PLUGIN, DURABLE_OBJECTS_PLUGIN_NAME } from "./do";
7+
import { HYPERDRIVE_PLUGIN, HYPERDRIVE_PLUGIN_NAME } from "./hyperdrive";
78
import { KV_PLUGIN, KV_PLUGIN_NAME } from "./kv";
89
import { QUEUES_PLUGIN, QUEUES_PLUGIN_NAME } from "./queues";
910
import { R2_PLUGIN, R2_PLUGIN_NAME } from "./r2";
@@ -16,6 +17,7 @@ export const PLUGINS = {
1617
[KV_PLUGIN_NAME]: KV_PLUGIN,
1718
[QUEUES_PLUGIN_NAME]: QUEUES_PLUGIN,
1819
[R2_PLUGIN_NAME]: R2_PLUGIN,
20+
[HYPERDRIVE_PLUGIN_NAME]: HYPERDRIVE_PLUGIN,
1921
};
2022
export type Plugins = typeof PLUGINS;
2123

@@ -60,7 +62,8 @@ export type WorkerOptions = z.infer<typeof CORE_PLUGIN.options> &
6062
z.infer<typeof DURABLE_OBJECTS_PLUGIN.options> &
6163
z.infer<typeof KV_PLUGIN.options> &
6264
z.infer<typeof QUEUES_PLUGIN.options> &
63-
z.infer<typeof R2_PLUGIN.options>;
65+
z.infer<typeof R2_PLUGIN.options> &
66+
z.input<typeof HYPERDRIVE_PLUGIN.options>;
6467
export type SharedOptions = z.infer<typeof CORE_PLUGIN.sharedOptions> &
6568
z.infer<typeof CACHE_PLUGIN.sharedOptions> &
6669
z.infer<typeof D1_PLUGIN.sharedOptions> &
@@ -104,3 +107,4 @@ export * from "./do";
104107
export * from "./kv";
105108
export * from "./queues";
106109
export * from "./r2";
110+
export * from "./hyperdrive";
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Hyperdrive } from "@cloudflare/workers-types/experimental";
2+
import { MiniflareOptions } from "miniflare";
3+
import { MiniflareTestContext, miniflareTest } from "../../test-shared";
4+
5+
const TEST_CONN_STRING = `postgresql://user:password@localhost:5432/database`;
6+
7+
const opts: Partial<MiniflareOptions> = {
8+
hyperdrives: {
9+
hyperdrive: TEST_CONN_STRING,
10+
},
11+
};
12+
13+
const test = miniflareTest<{ hyperdrive: Hyperdrive }, MiniflareTestContext>(
14+
opts,
15+
async (global, _, env) => {
16+
return global.Response.json({
17+
connectionString: env.hyperdrive.connectionString,
18+
user: env.hyperdrive.user,
19+
password: env.hyperdrive.password,
20+
database: env.hyperdrive.database,
21+
host: env.hyperdrive.host,
22+
});
23+
}
24+
);
25+
26+
test("configuration: fields match expected", async (t) => {
27+
const hyperdriveResp = await t.context.mf.dispatchFetch("http://localhost/");
28+
const hyperdrive: any = await hyperdriveResp.json();
29+
// Since the host is random, this connectionString should be different
30+
t.not(hyperdrive.connectionString, TEST_CONN_STRING);
31+
t.is(hyperdrive.user, "user");
32+
t.is(hyperdrive.password, "password");
33+
t.is(hyperdrive.database, "database");
34+
// Random host should not be the same as the original
35+
t.not(hyperdrive.host, "localhost");
36+
});

0 commit comments

Comments
 (0)