Skip to content

Commit fccca93

Browse files
RihanArfanpi0
andauthored
feat: cloudflare hyperdrive (#164)
Co-authored-by: Pooya Parsa <[email protected]>
1 parent 9a1f861 commit fccca93

File tree

12 files changed

+417
-11
lines changed

12 files changed

+417
-11
lines changed

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ MYSQL_URL=mysql://test:test@localhost:3306/db0
55
PLANETSCALE_HOST=aws.connect.psdb.cloud
66
PLANETSCALE_USERNAME=username
77
PLANETSCALE_PASSWORD=password
8+
9+
# Cloudflare Hyperdrive
10+
WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_POSTGRESQL=postgresql://test:test@localhost:5432/db0
11+
WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_MYSQL=mysql://test:test@localhost:3306/db0

docs/2.connectors/cloudflare.md

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
icon: devicon-plain:cloudflareworkers
33
---
44

5-
# Cloudflare D1
5+
# Cloudflare
66

7-
> Connect DB0 to Cloudflare D1
7+
> Connect DB0 to Cloudflare D1 or PostgreSQL/MySQL using Cloudflare Hyperdrive
8+
9+
10+
## Cloudflare D1
811

912
:read-more{to="https://developers.cloudflare.com/d1"}
1013

1114
> [!NOTE]
1215
> This connector works within cloudflare workers with D1 enabled.
1316
14-
## Usage
17+
### Usage
1518

1619
Use `cloudflare-d1` connector:
1720

@@ -31,8 +34,110 @@ const db = createDatabase(
3134
>
3235
> If you are using [Nitro](https://nitro.unjs.io/) you don't need to do any extra steps.
3336
34-
## Options
37+
### Options
3538

36-
### `bindingName`
39+
#### `bindingName`
3740

3841
Assigned binding name.
42+
43+
---
44+
45+
## Hyperdrive PostgreSQL
46+
47+
:read-more{to="https://developers.cloudflare.com/hyperdrive"}
48+
49+
> [!NOTE]
50+
> This connector works within Cloudflare Workers with Hyperdrive enabled.
51+
52+
### Usage
53+
54+
For this connector, you need to install [`pg`](https://www.npmjs.com/package/pg) dependency:
55+
56+
:pm-install{name="pg @types/pg"}
57+
58+
Use `cloudflare-hyperdrive-postgresql` connector:
59+
60+
```js
61+
import { createDatabase } from "db0";
62+
import cloudflareHyperdrivePostgresql from "db0/connectors/cloudflare-hyperdrive-postgresql";
63+
64+
const db = createDatabase(
65+
cloudflareHyperdrivePostgresql({
66+
bindingName: "POSTGRESQL",
67+
}),
68+
);
69+
```
70+
71+
### Options
72+
73+
#### `bindingName`
74+
75+
Assigned binding name for your Hyperdrive instance.
76+
77+
#### Additional Options
78+
79+
You can also pass PostgreSQL client configuration options (except for `user`, `database`, `password`, `port`, `host`, and `connectionString` which are managed by Hyperdrive):
80+
81+
```js
82+
const db = createDatabase(
83+
cloudflareHyperdrivePostgresql({
84+
bindingName: "HYPERDRIVE",
85+
// Additional PostgreSQL options
86+
statement_timeout: 5000,
87+
query_timeout: 10000,
88+
}),
89+
);
90+
```
91+
92+
:read-more{title="node-postgres documentation" to="https://node-postgres.com/apis/client#new-client"}
93+
94+
---
95+
96+
## Hyperdrive MySQL
97+
98+
:read-more{to="https://developers.cloudflare.com/hyperdrive"}
99+
100+
> [!NOTE]
101+
> This connector works within Cloudflare Workers with Hyperdrive enabled.
102+
103+
### Usage
104+
105+
For this connector, you need to install [`mysql2`](https://www.npmjs.com/package/mysql2) dependency:
106+
107+
:pm-install{name="mysql2"}
108+
109+
Use `cloudflare-hyperdrive-mysql` connector:
110+
111+
```js
112+
import { createDatabase } from "db0";
113+
import cloudflareHyperdriveMysql from "db0/connectors/cloudflare-hyperdrive-mysql";
114+
115+
const db = createDatabase(
116+
cloudflareHyperdriveMysql({
117+
bindingName: "MYSQL",
118+
}),
119+
);
120+
```
121+
122+
### Options
123+
124+
#### `bindingName`
125+
126+
Assigned binding name for your Hyperdrive instance.
127+
128+
### Additional Options
129+
130+
You can also pass MySQL client configuration options (except for connection/authentication options which are managed by Hyperdrive, and `disableEval` which is incompatible in Cloudflare Workers):
131+
132+
```js
133+
const db = createDatabase(
134+
cloudflareHyperdriveMysql({
135+
bindingName: "HYPERDRIVE",
136+
// Additional MySQL options
137+
connectTimeout: 10000,
138+
queryTimeout: 5000,
139+
}),
140+
);
141+
```
142+
143+
:read-more{to="https://github.com/sidorares/node-mysql2/blob/master/typings/mysql/lib/Connection.d.ts#L82-L329"}

src/_connectors.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import type { ConnectorOptions as BetterSQLite3Options } from "db0/connectors/better-sqlite3";
44
import type { ConnectorOptions as BunSQLiteOptions } from "db0/connectors/bun-sqlite";
55
import type { ConnectorOptions as CloudflareD1Options } from "db0/connectors/cloudflare-d1";
6+
import type { ConnectorOptions as CloudflareHyperdriveMySQLOptions } from "db0/connectors/cloudflare-hyperdrive-mysql";
7+
import type { ConnectorOptions as CloudflareHyperdrivePostgreSQLOptions } from "db0/connectors/cloudflare-hyperdrive-postgresql";
68
import type { ConnectorOptions as LibSQLCoreOptions } from "db0/connectors/libsql/core";
79
import type { ConnectorOptions as LibSQLHttpOptions } from "db0/connectors/libsql/http";
810
import type { ConnectorOptions as LibSQLNodeOptions } from "db0/connectors/libsql/node";
@@ -14,14 +16,16 @@ import type { ConnectorOptions as PlanetscaleOptions } from "db0/connectors/plan
1416
import type { ConnectorOptions as PostgreSQLOptions } from "db0/connectors/postgresql";
1517
import type { ConnectorOptions as SQLite3Options } from "db0/connectors/sqlite3";
1618

17-
export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mysql2" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3";
19+
export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "cloudflare-hyperdrive-mysql" | "cloudflare-hyperdrive-postgresql" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mysql2" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3";
1820

1921
export type ConnectorOptions = {
2022
"better-sqlite3": BetterSQLite3Options;
2123
"bun-sqlite": BunSQLiteOptions;
2224
/** alias of bun-sqlite */
2325
"bun": BunSQLiteOptions;
2426
"cloudflare-d1": CloudflareD1Options;
27+
"cloudflare-hyperdrive-mysql": CloudflareHyperdriveMySQLOptions;
28+
"cloudflare-hyperdrive-postgresql": CloudflareHyperdrivePostgreSQLOptions;
2529
"libsql-core": LibSQLCoreOptions;
2630
"libsql-http": LibSQLHttpOptions;
2731
"libsql-node": LibSQLNodeOptions;
@@ -44,6 +48,8 @@ export const connectors: Record<ConnectorName, string> = Object.freeze({
4448
/** alias of bun-sqlite */
4549
"bun": "db0/connectors/bun-sqlite",
4650
"cloudflare-d1": "db0/connectors/cloudflare-d1",
51+
"cloudflare-hyperdrive-mysql": "db0/connectors/cloudflare-hyperdrive-mysql",
52+
"cloudflare-hyperdrive-postgresql": "db0/connectors/cloudflare-hyperdrive-postgresql",
4753
"libsql-core": "db0/connectors/libsql/core",
4854
"libsql-http": "db0/connectors/libsql/http",
4955
"libsql-node": "db0/connectors/libsql/node",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Hyperdrive } from "@cloudflare/workers-types";
2+
3+
function getCloudflareEnv() {
4+
return (
5+
(globalThis as any).__env__ ||
6+
import("cloudflare:workers" as any).then((mod) => mod.env)
7+
);
8+
}
9+
10+
export async function getHyperdrive(bindingName: string): Promise<Hyperdrive> {
11+
const env = await getCloudflareEnv();
12+
const binding: Hyperdrive = env[bindingName];
13+
if (!binding) {
14+
throw new Error(`[db0] [hyperdrive] binding \`${bindingName}\` not found`);
15+
}
16+
return binding;
17+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import mysql from "mysql2/promise";
2+
import type { Connector, Primitive } from "db0";
3+
import { BoundableStatement } from "./_internal/statement.ts";
4+
import { getHyperdrive } from "./_internal/cloudflare.ts";
5+
6+
type OmitMysqlConfig = Omit<
7+
mysql.ConnectionOptions,
8+
| "user"
9+
| "database"
10+
| "password"
11+
| "password1"
12+
| "password2"
13+
| "password3"
14+
| "port"
15+
| "host"
16+
| "uri"
17+
| "localAddress"
18+
| "socketPath"
19+
| "insecureAuth"
20+
| "passwordSha1"
21+
| "disableEval"
22+
>;
23+
24+
export type ConnectorOptions = {
25+
bindingName: string;
26+
} & OmitMysqlConfig;
27+
28+
type InternalQuery = (
29+
sql: string,
30+
params?: unknown[],
31+
) => Promise<mysql.QueryResult>;
32+
33+
export default function cloudflareHyperdriveMysqlConnector(
34+
opts: ConnectorOptions,
35+
): Connector<mysql.Connection> {
36+
let _connection: mysql.Connection | undefined;
37+
38+
const getConnection = async () => {
39+
if (_connection) {
40+
return _connection;
41+
}
42+
43+
const hyperdrive = await getHyperdrive(opts.bindingName);
44+
_connection = await mysql.createConnection({
45+
...opts,
46+
host: hyperdrive.host,
47+
user: hyperdrive.user,
48+
password: hyperdrive.password,
49+
database: hyperdrive.database,
50+
port: hyperdrive.port,
51+
// The following line is needed for mysql2 compatibility with Workers
52+
// mysql2 uses eval() to optimize result parsing for rows with > 100 columns
53+
// Configure mysql2 to use static parsing instead of eval() parsing with disableEval
54+
disableEval: true,
55+
});
56+
57+
return _connection;
58+
};
59+
60+
const query: InternalQuery = (sql, params) =>
61+
getConnection()
62+
.then((c) => c.query(sql, params))
63+
.then((res) => res[0]);
64+
65+
return {
66+
name: "cloudflare-hyperdrive-mysql",
67+
dialect: "mysql",
68+
getInstance: () => getConnection(),
69+
exec: (sql) => query(sql),
70+
prepare: (sql) => new StatementWrapper(sql, query),
71+
dispose: async () => {
72+
await _connection?.end?.();
73+
_connection = undefined;
74+
},
75+
};
76+
}
77+
78+
class StatementWrapper extends BoundableStatement<void> {
79+
#query: InternalQuery;
80+
#sql: string;
81+
82+
constructor(sql: string, query: InternalQuery) {
83+
super();
84+
this.#sql = sql;
85+
this.#query = query;
86+
}
87+
88+
async all(...params: Primitive[]) {
89+
const res = (await this.#query(this.#sql, params)) as mysql.RowDataPacket[];
90+
return res;
91+
}
92+
93+
async run(...params: Primitive[]) {
94+
const res = (await this.#query(this.#sql, params)) as mysql.RowDataPacket[];
95+
return {
96+
success: true,
97+
...res,
98+
};
99+
}
100+
101+
async get(...params: Primitive[]) {
102+
const res = (await this.#query(this.#sql, params)) as mysql.RowDataPacket[];
103+
return res[0];
104+
}
105+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import pg from "pg";
2+
3+
import type { Connector, Primitive } from "db0";
4+
5+
import { BoundableStatement } from "./_internal/statement.ts";
6+
import { getHyperdrive } from "./_internal/cloudflare.ts";
7+
8+
type OmitPgConfig = Omit<
9+
pg.ClientConfig,
10+
"user" | "database" | "password" | "port" | "host" | "connectionString"
11+
>;
12+
export type ConnectorOptions = {
13+
bindingName: string;
14+
} & OmitPgConfig;
15+
16+
type InternalQuery = (
17+
sql: string,
18+
params?: Primitive[],
19+
) => Promise<pg.QueryResult>;
20+
21+
export default function cloudflareHyperdrivePostgresqlConnector(
22+
opts: ConnectorOptions,
23+
): Connector<pg.Client> {
24+
let _client: undefined | pg.Client | Promise<pg.Client>;
25+
async function getClient() {
26+
if (_client) {
27+
return _client;
28+
}
29+
const hyperdrive = await getHyperdrive(opts.bindingName);
30+
const client = new pg.Client({
31+
...opts,
32+
connectionString: hyperdrive.connectionString,
33+
});
34+
_client = client.connect().then(() => {
35+
_client = client;
36+
return _client;
37+
});
38+
return _client;
39+
}
40+
41+
const query: InternalQuery = async (sql, params) => {
42+
const client = await getClient();
43+
return client.query(normalizeParams(sql), params);
44+
};
45+
46+
return {
47+
name: "cloudflare-hyperdrive-postgresql",
48+
dialect: "postgresql",
49+
getInstance: () => getClient(),
50+
exec: (sql) => query(sql),
51+
prepare: (sql) => new StatementWrapper(sql, query),
52+
dispose: async () => {
53+
await (await _client)?.end?.();
54+
_client = undefined;
55+
},
56+
};
57+
}
58+
59+
// https://www.postgresql.org/docs/9.3/sql-prepare.html
60+
function normalizeParams(sql: string) {
61+
let i = 0;
62+
return sql.replace(/\?/g, () => `$${++i}`);
63+
}
64+
65+
class StatementWrapper extends BoundableStatement<void> {
66+
#query: InternalQuery;
67+
#sql: string;
68+
69+
constructor(sql: string, query: InternalQuery) {
70+
super();
71+
this.#sql = sql;
72+
this.#query = query;
73+
}
74+
75+
async all(...params: Primitive[]) {
76+
const res = await this.#query(this.#sql, params);
77+
return res.rows;
78+
}
79+
80+
async run(...params: Primitive[]) {
81+
const res = await this.#query(this.#sql, params);
82+
return {
83+
success: true,
84+
...res,
85+
};
86+
}
87+
88+
async get(...params: Primitive[]) {
89+
const res = await this.#query(this.#sql, params);
90+
return res.rows[0];
91+
}
92+
}

0 commit comments

Comments
 (0)