Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ MYSQL_URL=mysql://test:test@localhost:3306/db0
PLANETSCALE_HOST=aws.connect.psdb.cloud
PLANETSCALE_USERNAME=username
PLANETSCALE_PASSWORD=password

# Cloudflare Hyperdrive
WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_POSTGRESQL=postgresql://test:test@localhost:5432/db0
WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_MYSQL=mysql://test:test@localhost:3306/db0
115 changes: 110 additions & 5 deletions docs/2.connectors/cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
icon: devicon-plain:cloudflareworkers
---

# Cloudflare D1
# Cloudflare

> Connect DB0 to Cloudflare D1
> Connect DB0 to Cloudflare D1 or PostgreSQL/MySQL using Cloudflare Hyperdrive


## Cloudflare D1

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

> [!NOTE]
> This connector works within cloudflare workers with D1 enabled.

## Usage
### Usage

Use `cloudflare-d1` connector:

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

## Options
### Options

### `bindingName`
#### `bindingName`

Assigned binding name.

---

## Hyperdrive PostgreSQL

:read-more{to="https://developers.cloudflare.com/hyperdrive"}

> [!NOTE]
> This connector works within Cloudflare Workers with Hyperdrive enabled.

### Usage

For this connector, you need to install [`pg`](https://www.npmjs.com/package/pg) dependency:

:pm-install{name="pg @types/pg"}

Use `cloudflare-hyperdrive-postgresql` connector:

```js
import { createDatabase } from "db0";
import cloudflareHyperdrivePostgresql from "db0/connectors/cloudflare-hyperdrive-postgresql";

const db = createDatabase(
cloudflareHyperdrivePostgresql({
bindingName: "POSTGRESQL",
}),
);
```

### Options

#### `bindingName`

Assigned binding name for your Hyperdrive instance.

#### Additional Options

You can also pass PostgreSQL client configuration options (except for `user`, `database`, `password`, `port`, `host`, and `connectionString` which are managed by Hyperdrive):

```js
const db = createDatabase(
cloudflareHyperdrivePostgresql({
bindingName: "HYPERDRIVE",
// Additional PostgreSQL options
statement_timeout: 5000,
query_timeout: 10000,
}),
);
```

:read-more{title="node-postgres documentation" to="https://node-postgres.com/apis/client#new-client"}

---

## Hyperdrive MySQL

:read-more{to="https://developers.cloudflare.com/hyperdrive"}

> [!NOTE]
> This connector works within Cloudflare Workers with Hyperdrive enabled.

### Usage

For this connector, you need to install [`mysql2`](https://www.npmjs.com/package/mysql2) dependency:

:pm-install{name="mysql2"}

Use `cloudflare-hyperdrive-mysql` connector:

```js
import { createDatabase } from "db0";
import cloudflareHyperdriveMysql from "db0/connectors/cloudflare-hyperdrive-mysql";

const db = createDatabase(
cloudflareHyperdriveMysql({
bindingName: "MYSQL",
}),
);
```

### Options

#### `bindingName`

Assigned binding name for your Hyperdrive instance.

### Additional Options

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):

```js
const db = createDatabase(
cloudflareHyperdriveMysql({
bindingName: "HYPERDRIVE",
// Additional MySQL options
connectTimeout: 10000,
queryTimeout: 5000,
}),
);
```

:read-more{to="https://github.com/sidorares/node-mysql2/blob/master/typings/mysql/lib/Connection.d.ts#L82-L329"}
8 changes: 7 additions & 1 deletion src/_connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import type { ConnectorOptions as BetterSQLite3Options } from "db0/connectors/better-sqlite3";
import type { ConnectorOptions as BunSQLiteOptions } from "db0/connectors/bun-sqlite";
import type { ConnectorOptions as CloudflareD1Options } from "db0/connectors/cloudflare-d1";
import type { ConnectorOptions as CloudflareHyperdriveMySQLOptions } from "db0/connectors/cloudflare-hyperdrive-mysql";
import type { ConnectorOptions as CloudflareHyperdrivePostgreSQLOptions } from "db0/connectors/cloudflare-hyperdrive-postgresql";
import type { ConnectorOptions as LibSQLCoreOptions } from "db0/connectors/libsql/core";
import type { ConnectorOptions as LibSQLHttpOptions } from "db0/connectors/libsql/http";
import type { ConnectorOptions as LibSQLNodeOptions } from "db0/connectors/libsql/node";
Expand All @@ -14,14 +16,16 @@ import type { ConnectorOptions as PlanetscaleOptions } from "db0/connectors/plan
import type { ConnectorOptions as PostgreSQLOptions } from "db0/connectors/postgresql";
import type { ConnectorOptions as SQLite3Options } from "db0/connectors/sqlite3";

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";
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";

export type ConnectorOptions = {
"better-sqlite3": BetterSQLite3Options;
"bun-sqlite": BunSQLiteOptions;
/** alias of bun-sqlite */
"bun": BunSQLiteOptions;
"cloudflare-d1": CloudflareD1Options;
"cloudflare-hyperdrive-mysql": CloudflareHyperdriveMySQLOptions;
"cloudflare-hyperdrive-postgresql": CloudflareHyperdrivePostgreSQLOptions;
"libsql-core": LibSQLCoreOptions;
"libsql-http": LibSQLHttpOptions;
"libsql-node": LibSQLNodeOptions;
Expand All @@ -44,6 +48,8 @@ export const connectors: Record<ConnectorName, string> = Object.freeze({
/** alias of bun-sqlite */
"bun": "db0/connectors/bun-sqlite",
"cloudflare-d1": "db0/connectors/cloudflare-d1",
"cloudflare-hyperdrive-mysql": "db0/connectors/cloudflare-hyperdrive-mysql",
"cloudflare-hyperdrive-postgresql": "db0/connectors/cloudflare-hyperdrive-postgresql",
"libsql-core": "db0/connectors/libsql/core",
"libsql-http": "db0/connectors/libsql/http",
"libsql-node": "db0/connectors/libsql/node",
Expand Down
17 changes: 17 additions & 0 deletions src/connectors/_internal/cloudflare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Hyperdrive } from "@cloudflare/workers-types";

function getCloudflareEnv() {
return (
(globalThis as any).__env__ ||
import("cloudflare:workers" as any).then((mod) => mod.env)
);
}

export async function getHyperdrive(bindingName: string): Promise<Hyperdrive> {
const env = await getCloudflareEnv();
const binding: Hyperdrive = env[bindingName];
if (!binding) {
throw new Error(`[db0] [hyperdrive] binding \`${bindingName}\` not found`);
}
return binding;
}
105 changes: 105 additions & 0 deletions src/connectors/cloudflare-hyperdrive-mysql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import mysql from "mysql2/promise";
import type { Connector, Primitive } from "db0";
import { BoundableStatement } from "./_internal/statement.ts";
import { getHyperdrive } from "./_internal/cloudflare.ts";

type OmitMysqlConfig = Omit<
mysql.ConnectionOptions,
| "user"
| "database"
| "password"
| "password1"
| "password2"
| "password3"
| "port"
| "host"
| "uri"
| "localAddress"
| "socketPath"
| "insecureAuth"
| "passwordSha1"
| "disableEval"
>;

export type ConnectorOptions = {
bindingName: string;
} & OmitMysqlConfig;

type InternalQuery = (
sql: string,
params?: unknown[],
) => Promise<mysql.QueryResult>;

export default function cloudflareHyperdriveMysqlConnector(
opts: ConnectorOptions,
): Connector<mysql.Connection> {
let _connection: mysql.Connection | undefined;

const getConnection = async () => {
if (_connection) {
return _connection;
}

const hyperdrive = await getHyperdrive(opts.bindingName);
_connection = await mysql.createConnection({
...opts,
host: hyperdrive.host,
user: hyperdrive.user,
password: hyperdrive.password,
database: hyperdrive.database,
port: hyperdrive.port,
// The following line is needed for mysql2 compatibility with Workers
// mysql2 uses eval() to optimize result parsing for rows with > 100 columns
// Configure mysql2 to use static parsing instead of eval() parsing with disableEval
disableEval: true,
});

return _connection;
};

const query: InternalQuery = (sql, params) =>
getConnection()
.then((c) => c.query(sql, params))
.then((res) => res[0]);

return {
name: "cloudflare-hyperdrive-mysql",
dialect: "mysql",
getInstance: () => getConnection(),
exec: (sql) => query(sql),
prepare: (sql) => new StatementWrapper(sql, query),
dispose: async () => {
await _connection?.end?.();
_connection = undefined;
},
};
}

class StatementWrapper extends BoundableStatement<void> {
#query: InternalQuery;
#sql: string;

constructor(sql: string, query: InternalQuery) {
super();
this.#sql = sql;
this.#query = query;
}

async all(...params: Primitive[]) {
const res = (await this.#query(this.#sql, params)) as mysql.RowDataPacket[];
return res;
}

async run(...params: Primitive[]) {
const res = (await this.#query(this.#sql, params)) as mysql.RowDataPacket[];
return {
success: true,
...res,
};
}

async get(...params: Primitive[]) {
const res = (await this.#query(this.#sql, params)) as mysql.RowDataPacket[];
return res[0];
}
}
92 changes: 92 additions & 0 deletions src/connectors/cloudflare-hyperdrive-postgresql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import pg from "pg";

import type { Connector, Primitive } from "db0";

import { BoundableStatement } from "./_internal/statement.ts";
import { getHyperdrive } from "./_internal/cloudflare.ts";

type OmitPgConfig = Omit<
pg.ClientConfig,
"user" | "database" | "password" | "port" | "host" | "connectionString"
>;
export type ConnectorOptions = {
bindingName: string;
} & OmitPgConfig;

type InternalQuery = (
sql: string,
params?: Primitive[],
) => Promise<pg.QueryResult>;

export default function cloudflareHyperdrivePostgresqlConnector(
opts: ConnectorOptions,
): Connector<pg.Client> {
let _client: undefined | pg.Client | Promise<pg.Client>;
async function getClient() {
if (_client) {
return _client;
}
const hyperdrive = await getHyperdrive(opts.bindingName);
const client = new pg.Client({
...opts,
connectionString: hyperdrive.connectionString,
});
_client = client.connect().then(() => {
_client = client;
return _client;
});
return _client;
}

const query: InternalQuery = async (sql, params) => {
const client = await getClient();
return client.query(normalizeParams(sql), params);
};

return {
name: "cloudflare-hyperdrive-postgresql",
dialect: "postgresql",
getInstance: () => getClient(),
exec: (sql) => query(sql),
prepare: (sql) => new StatementWrapper(sql, query),
dispose: async () => {
await (await _client)?.end?.();
_client = undefined;
},
};
}

// https://www.postgresql.org/docs/9.3/sql-prepare.html
function normalizeParams(sql: string) {
let i = 0;
return sql.replace(/\?/g, () => `$${++i}`);
}

class StatementWrapper extends BoundableStatement<void> {
#query: InternalQuery;
#sql: string;

constructor(sql: string, query: InternalQuery) {
super();
this.#sql = sql;
this.#query = query;
}

async all(...params: Primitive[]) {
const res = await this.#query(this.#sql, params);
return res.rows;
}

async run(...params: Primitive[]) {
const res = await this.#query(this.#sql, params);
return {
success: true,
...res,
};
}

async get(...params: Primitive[]) {
const res = await this.#query(this.#sql, params);
return res.rows[0];
}
}
Loading