@rdlabo/workers-mysql 0.1.0-beta.pr48.sha371f5792ce8a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/db-baseline.mjs +4 -0
- package/dist/baseline-cli.d.ts +2 -0
- package/dist/baseline-cli.js +47 -0
- package/dist/columns.d.ts +38 -0
- package/dist/columns.js +38 -0
- package/dist/connection.d.ts +76 -0
- package/dist/connection.js +67 -0
- package/dist/database.d.ts +249 -0
- package/dist/database.js +196 -0
- package/dist/drizzle.d.ts +6 -0
- package/dist/drizzle.js +5 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/jst.d.ts +43 -0
- package/dist/jst.js +59 -0
- package/dist/migrate.d.ts +51 -0
- package/dist/migrate.js +109 -0
- package/dist/migrations.d.ts +5 -0
- package/dist/migrations.js +3 -0
- package/dist/orm-config.d.ts +134 -0
- package/dist/orm-config.js +127 -0
- package/dist/retry.d.ts +28 -0
- package/dist/retry.js +56 -0
- package/dist/testing/db.d.ts +106 -0
- package/dist/testing/db.js +95 -0
- package/dist/testing/fakes.d.ts +13 -0
- package/dist/testing/fakes.js +25 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +2 -0
- package/dist/write-result.d.ts +39 -0
- package/dist/write-result.js +34 -0
- package/docs/api.md +72 -0
- package/docs/drizzle.md +41 -0
- package/docs/migration.md +28 -0
- package/docs/runtime.md +72 -0
- package/docs/tooling.md +51 -0
- package/package.json +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rdlabo-dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @rdlabo/workers-mysql
|
|
2
|
+
|
|
3
|
+
MySQL and Hyperdrive infrastructure for Cloudflare Workers without a Hono dependency. Compose
|
|
4
|
+
invocation-scoped primary/replica access, deadlock retries, optional Drizzle helpers, and Node.js
|
|
5
|
+
migration/testing tools while the application keeps its schemas and credentials.
|
|
6
|
+
|
|
7
|
+
The Worker must enable Node.js compatibility because `mysql2` uses Node.js networking APIs:
|
|
8
|
+
|
|
9
|
+
```toml
|
|
10
|
+
# wrangler.toml
|
|
11
|
+
compatibility_flags = ["nodejs_compat"]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Choose an entry point
|
|
15
|
+
|
|
16
|
+
| Import | Responsibility |
|
|
17
|
+
| ---------------------------------- | ------------------------------------------------------------------------------- |
|
|
18
|
+
| `@rdlabo/workers-mysql` | Workers MySQL and Hyperdrive runtime, retry, write-result, and JST wire helpers |
|
|
19
|
+
| `@rdlabo/workers-mysql/drizzle` | Drizzle configuration and JST column helpers |
|
|
20
|
+
| `@rdlabo/workers-mysql/migrations` | Node.js migration and brownfield baseline helpers |
|
|
21
|
+
| `@rdlabo/workers-mysql/testing` | Local MySQL/Drizzle test database and fakes |
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @rdlabo/workers-mysql
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`mysql2` is included as a direct dependency. Add `drizzle-orm` when using `/drizzle` or `/testing`:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install drizzle-orm
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Keeping Drizzle as a peer gives the application and its schemas one type identity.
|
|
36
|
+
|
|
37
|
+
The public connection types use Node.js declarations. `@types/node@>=20.19.43` is a required peer
|
|
38
|
+
(also when deploying to Workers). TypeScript applications should add it directly so its global
|
|
39
|
+
declarations are visible with strict package layouts, including pnpm:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm install -D @types/node@20
|
|
43
|
+
# pnpm users:
|
|
44
|
+
pnpm add -D @types/node@20
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Use the matching supported major for your tooling. Automatic peer installation alone may not expose
|
|
48
|
+
these global declarations to the application's TypeScript compiler under pnpm.
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
Create the database inside each Worker invocation. In this fragment, `env` contains the application's
|
|
53
|
+
Hyperdrive bindings and `schema` is its own Drizzle schema:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { createHyperdriveDatabase } from '@rdlabo/workers-mysql';
|
|
57
|
+
import { DRIZZLE_ORM_OPTIONS } from '@rdlabo/workers-mysql/drizzle';
|
|
58
|
+
import { drizzle } from 'drizzle-orm/mysql2';
|
|
59
|
+
|
|
60
|
+
const db = createHyperdriveDatabase({
|
|
61
|
+
primaryHyperdrive: env.PRIMARY,
|
|
62
|
+
replicaHyperdrive: env.REPLICA,
|
|
63
|
+
createOrm: (connection) => drizzle(connection, { schema, ...DRIZZLE_ORM_OPTIONS }),
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
With `nodejs_compat` enabled, the package root is Workers-runtime-safe and does not load Drizzle or
|
|
68
|
+
Node-only migration code.
|
|
69
|
+
|
|
70
|
+
Fixed `+09:00` storage helpers are a MySQL wire contract. They do not follow IANA display timezones
|
|
71
|
+
from [`@rdlabo/workers-timezone`](https://docs.rdlabo.dev/projects/workers-timezone/docs/readme).
|
|
72
|
+
|
|
73
|
+
## Hono integration
|
|
74
|
+
|
|
75
|
+
Hono middleware remains an adapter in `@rdlabo/workers-hono-kit/mysql`; the database package itself
|
|
76
|
+
does not depend on Hono.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { createContainerRuntime } from '@rdlabo/workers-hono-kit/mysql';
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This adapter is available from Hono kit `0.12.0`. Install both packages:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
npm install @rdlabo/workers-mysql @rdlabo/workers-hono-kit
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Documentation
|
|
89
|
+
|
|
90
|
+
- [Runtime](https://docs.rdlabo.dev/projects/workers-mysql/docs/runtime) — request lifetime, primary/replica reads, and retry safety.
|
|
91
|
+
- [Drizzle and dates](https://docs.rdlabo.dev/projects/workers-mysql/docs/drizzle) — schema ownership, optional peer, and fixed-JST storage.
|
|
92
|
+
- [Migrations and testing](https://docs.rdlabo.dev/projects/workers-mysql/docs/tooling) — Node.js tooling and destructive test helpers.
|
|
93
|
+
- [API](https://docs.rdlabo.dev/projects/workers-mysql/docs/api) — public exports by entry point.
|
|
94
|
+
- [Migration](https://docs.rdlabo.dev/projects/workers-mysql/docs/migration) — kit compatibility imports.
|
|
95
|
+
|
|
96
|
+
These guides describe this source revision. Use the matching release tag for an installed version.
|
|
97
|
+
|
|
98
|
+
## Migrating from workers-hono-kit
|
|
99
|
+
|
|
100
|
+
Kit `0.12.0` changes the import boundaries. Its old `/db` and DB-related `/testing` exports remain
|
|
101
|
+
available as maintained compatibility paths with `@deprecated` notices; there is no planned removal.
|
|
102
|
+
See [Migration](https://docs.rdlabo.dev/projects/workers-mysql/docs/migration) for the import map.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createConnection } from 'mysql2/promise';
|
|
2
|
+
import { baselineMigrations } from './migrate.js';
|
|
3
|
+
import { resolveDbSecret } from './orm-config.js';
|
|
4
|
+
function arg(name) {
|
|
5
|
+
const index = process.argv.indexOf(`--${name}`);
|
|
6
|
+
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
7
|
+
}
|
|
8
|
+
/** Run the brownfield migration-baseline command. */
|
|
9
|
+
export async function runBaselineCli() {
|
|
10
|
+
const migrationsFolder = arg('migrations') ?? process.env.MIGRATIONS_DIR ?? './drizzle';
|
|
11
|
+
const secret = resolveDbSecret();
|
|
12
|
+
const connectionOptions = secret
|
|
13
|
+
? {
|
|
14
|
+
host: secret.host,
|
|
15
|
+
port: secret.port,
|
|
16
|
+
user: secret.username,
|
|
17
|
+
password: secret.password,
|
|
18
|
+
database: secret.dbname,
|
|
19
|
+
}
|
|
20
|
+
: {
|
|
21
|
+
host: process.env.DB_HOST ?? '127.0.0.1',
|
|
22
|
+
port: Number(process.env.DB_PORT ?? '3306'),
|
|
23
|
+
user: process.env.DB_USER ?? 'root',
|
|
24
|
+
password: process.env.DB_PASSWORD ?? 'root',
|
|
25
|
+
database: process.env.DB_NAME,
|
|
26
|
+
};
|
|
27
|
+
if (!connectionOptions.database) {
|
|
28
|
+
console.error('[db:baseline] DB_NAME (or DB_SECRET) is required.');
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
console.log('[db:baseline] database target configured.');
|
|
33
|
+
const db = await createConnection(connectionOptions);
|
|
34
|
+
await baselineMigrations({ db, migrationsFolder })
|
|
35
|
+
.then((result) => {
|
|
36
|
+
if (result.status === 'already-baselined') {
|
|
37
|
+
console.log(`[db:baseline] already baselined (${result.tag}, created_at=${result.when}). no-op.`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.log(`[db:baseline] inserted baseline marker for ${result.tag} (created_at=${result.when}). ` +
|
|
41
|
+
'0000 is now recorded as applied; future migrations will run.');
|
|
42
|
+
}, (error) => {
|
|
43
|
+
console.error('[db:baseline] failed:', error instanceof Error ? error.message : error);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
})
|
|
46
|
+
.finally(() => db.end());
|
|
47
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL expression for `ON UPDATE CURRENT_TIMESTAMP` (the MySQL session clock).
|
|
3
|
+
* customType columns have no `.onUpdateNow()`, so pair this with `.$onUpdateFn(() => jstOnUpdateNow(fsp))`.
|
|
4
|
+
*
|
|
5
|
+
* @param fsp - optional fractional-seconds precision; when provided, emits `CURRENT_TIMESTAMP(fsp)`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const jstOnUpdateNow: (fsp?: number) => import("drizzle-orm").SQL<unknown>;
|
|
8
|
+
/** MySQL `timestamp` — pass-through. A `Date` is formatted as JST by mysql2 via the connection `timezone:'+09:00'`. */
|
|
9
|
+
export declare const jstTimestamp: (name: string, opts?: {
|
|
10
|
+
fsp?: number;
|
|
11
|
+
}) => import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
|
|
12
|
+
name: string;
|
|
13
|
+
dataType: "custom";
|
|
14
|
+
columnType: "MySqlCustomColumn";
|
|
15
|
+
data: string | Date;
|
|
16
|
+
driverParam: string | Date;
|
|
17
|
+
enumValues: undefined;
|
|
18
|
+
}>;
|
|
19
|
+
/** MySQL `datetime` — same pass-through policy as {@link jstTimestamp}. */
|
|
20
|
+
export declare const jstDatetime: (name: string, opts?: {
|
|
21
|
+
fsp?: number;
|
|
22
|
+
}) => import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
|
|
23
|
+
name: string;
|
|
24
|
+
dataType: "custom";
|
|
25
|
+
columnType: "MySqlCustomColumn";
|
|
26
|
+
data: string | Date;
|
|
27
|
+
driverParam: string | Date;
|
|
28
|
+
enumValues: undefined;
|
|
29
|
+
}>;
|
|
30
|
+
/** MySQL `date` — on INSERT/UPDATE, normalizes ISO / empty strings to `YYYY-MM-DD` (via `toDriver`). */
|
|
31
|
+
export declare const jstDate: (name: string) => import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
|
|
32
|
+
name: string;
|
|
33
|
+
dataType: "custom";
|
|
34
|
+
columnType: "MySqlCustomColumn";
|
|
35
|
+
data: string | null;
|
|
36
|
+
driverParam: string | null;
|
|
37
|
+
enumValues: undefined;
|
|
38
|
+
}>;
|
package/dist/columns.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Drizzle column helpers. Removes the need for a thin `custom-types.ts` / `columns.ts` wrapper
|
|
3
|
+
* in each repo.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* `drizzle-orm` is a **peer** (the consumer resolves a single copy); this package does not bundle it. The
|
|
7
|
+
* return types are the `customType` inference as-is (`MySqlCustomColumnBuilder<…>`) with no `any`, so
|
|
8
|
+
* the column's semantic type (`string | Date`, etc.) propagates to the consumer table's `$inferSelect`.
|
|
9
|
+
*
|
|
10
|
+
* **Precondition (a single drizzle copy)**: Drizzle's `SQL` is a **nominal** type carrying a private
|
|
11
|
+
* field `shouldInlineParams`, so if the package and consumer resolve different copies of drizzle,
|
|
12
|
+
* `jstTimestamp(…).default(sql\`…\`)` fails with `TS2345: separate declarations of a private property
|
|
13
|
+
* 'shouldInlineParams'`. A direct `file:` link can nest a second copy of drizzle under the package.
|
|
14
|
+
* Pin `drizzle-orm` to the consumer's **own single copy** with tsconfig
|
|
15
|
+
* `paths` (see the "Drizzle column helpers" section of the README). The published package (a single
|
|
16
|
+
* copy) is already unified.
|
|
17
|
+
*
|
|
18
|
+
* **DEFAULT / ON UPDATE CURRENT_TIMESTAMP** is a server-side default (an INSERT / UPDATE that omits the
|
|
19
|
+
* column). The connection's `timezone:'+09:00'` ({@link hyperdriveConnectionOptions}) only applies when
|
|
20
|
+
* the **app binds a `Date`**. Do not conflate the two (see the `datetime-wire` / `drizzle-smoke` JST
|
|
21
|
+
* tests).
|
|
22
|
+
*/
|
|
23
|
+
import { sql } from 'drizzle-orm';
|
|
24
|
+
import { customType } from 'drizzle-orm/mysql-core';
|
|
25
|
+
import { jstDateParams, jstDatetimeParams, jstTimestampParams } from './jst.js';
|
|
26
|
+
/**
|
|
27
|
+
* SQL expression for `ON UPDATE CURRENT_TIMESTAMP` (the MySQL session clock).
|
|
28
|
+
* customType columns have no `.onUpdateNow()`, so pair this with `.$onUpdateFn(() => jstOnUpdateNow(fsp))`.
|
|
29
|
+
*
|
|
30
|
+
* @param fsp - optional fractional-seconds precision; when provided, emits `CURRENT_TIMESTAMP(fsp)`.
|
|
31
|
+
*/
|
|
32
|
+
export const jstOnUpdateNow = (fsp) => fsp != null ? sql `(CURRENT_TIMESTAMP(${sql.raw(String(fsp))}))` : sql `(CURRENT_TIMESTAMP)`;
|
|
33
|
+
/** MySQL `timestamp` — pass-through. A `Date` is formatted as JST by mysql2 via the connection `timezone:'+09:00'`. */
|
|
34
|
+
export const jstTimestamp = (name, opts) => customType(jstTimestampParams(opts?.fsp))(name);
|
|
35
|
+
/** MySQL `datetime` — same pass-through policy as {@link jstTimestamp}. */
|
|
36
|
+
export const jstDatetime = (name, opts) => customType(jstDatetimeParams(opts?.fsp))(name);
|
|
37
|
+
/** MySQL `date` — on INSERT/UPDATE, normalizes ISO / empty strings to `YYYY-MM-DD` (via `toDriver`). */
|
|
38
|
+
export const jstDate = (name) => customType(jstDateParams())(name);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Connection } from 'mysql2/promise';
|
|
2
|
+
/** Minimal structural shape retained by the compatibility connection-lifecycle API. */
|
|
3
|
+
export interface ExecutionContextLike {
|
|
4
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Minimal structural shape of a Cloudflare Hyperdrive binding.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Declared structurally to avoid a dependency on `@cloudflare/workers-types`; any object with these
|
|
11
|
+
* connection fields satisfies it.
|
|
12
|
+
*/
|
|
13
|
+
export interface HyperdriveLike {
|
|
14
|
+
/** Database host to connect to. */
|
|
15
|
+
host: string;
|
|
16
|
+
/** Database user. */
|
|
17
|
+
user: string;
|
|
18
|
+
/** Database password. */
|
|
19
|
+
password: string;
|
|
20
|
+
/** Database name. */
|
|
21
|
+
database: string;
|
|
22
|
+
/** Database port. */
|
|
23
|
+
port: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build mysql2 `createConnection` options from a Hyperdrive binding, applying the package defaults.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* Three defaults are applied and can each be overridden via `extra`:
|
|
30
|
+
*
|
|
31
|
+
* - `disableEval: true` — `eval` is unavailable in the Workers runtime, so the driver's eval-based
|
|
32
|
+
* fast paths must be disabled.
|
|
33
|
+
* - `decimalNumbers: true` — return `DECIMAL`/`NEWDECIMAL` columns as JS `number` rather than
|
|
34
|
+
* strings, so raw-SQL reads and Drizzle's inferred types align on a single numeric domain type.
|
|
35
|
+
* This assumes no column's precision exceeds the JS safe-integer range.
|
|
36
|
+
* - `timezone: '+09:00'` — interpret and serialize JavaScript `Date` values as JST. This is a
|
|
37
|
+
* mysql2 client-side conversion option; it does not issue `SET time_zone` or change the MySQL
|
|
38
|
+
* session timezone. Non-JST deployments can override it via `extra: { timezone: '...' }`.
|
|
39
|
+
*
|
|
40
|
+
* @param hyperdrive - the Hyperdrive binding to derive connection fields from.
|
|
41
|
+
* @param extra - additional mysql2 options merged last, overriding the defaults above.
|
|
42
|
+
* @returns a plain options object to pass to mysql2 `createConnection`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function hyperdriveConnectionOptions(hyperdrive: HyperdriveLike, extra?: Record<string, unknown>): Record<string, unknown>;
|
|
45
|
+
/**
|
|
46
|
+
* Open primary and replica connections in parallel and run `fn` with them.
|
|
47
|
+
*
|
|
48
|
+
* Cloudflare Workers automatically cleans up connections created during an invocation. Calling
|
|
49
|
+
* `Connection.end()` is unnecessary and can race work registered with `waitUntil`. The `ctx`
|
|
50
|
+
* parameter remains for API compatibility and is intentionally not used for connection teardown.
|
|
51
|
+
*
|
|
52
|
+
* @typeParam T - resolved value produced by `fn`.
|
|
53
|
+
* @param hyperdrives - the primary and replica Hyperdrive bindings to connect to.
|
|
54
|
+
* @param ctx - the request execution context, retained for API compatibility.
|
|
55
|
+
* @param fn - callback invoked with the open `primary` and `replica` connections.
|
|
56
|
+
* @param connectionOptions - extra mysql2 options forwarded to {@link hyperdriveConnectionOptions}.
|
|
57
|
+
* @returns the value resolved by `fn`.
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const data = await withMysqlConnections(
|
|
61
|
+
* { primary: env.PRIMARY, replica: env.REPLICA },
|
|
62
|
+
* ctx,
|
|
63
|
+
* async ({ primary, replica }) => {
|
|
64
|
+
* const [rows] = await replica.query('SELECT 1');
|
|
65
|
+
* return rows;
|
|
66
|
+
* },
|
|
67
|
+
* );
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare function withMysqlConnections<T>(hyperdrives: {
|
|
71
|
+
primary: HyperdriveLike;
|
|
72
|
+
replica: HyperdriveLike;
|
|
73
|
+
}, ctx: ExecutionContextLike, fn: (connections: {
|
|
74
|
+
primary: Connection;
|
|
75
|
+
replica: Connection;
|
|
76
|
+
}) => Promise<T>, connectionOptions?: Record<string, unknown>): Promise<T>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createConnection } from 'mysql2/promise';
|
|
2
|
+
import { MYSQL_TIMEZONE } from './jst.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build mysql2 `createConnection` options from a Hyperdrive binding, applying the package defaults.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Three defaults are applied and can each be overridden via `extra`:
|
|
8
|
+
*
|
|
9
|
+
* - `disableEval: true` — `eval` is unavailable in the Workers runtime, so the driver's eval-based
|
|
10
|
+
* fast paths must be disabled.
|
|
11
|
+
* - `decimalNumbers: true` — return `DECIMAL`/`NEWDECIMAL` columns as JS `number` rather than
|
|
12
|
+
* strings, so raw-SQL reads and Drizzle's inferred types align on a single numeric domain type.
|
|
13
|
+
* This assumes no column's precision exceeds the JS safe-integer range.
|
|
14
|
+
* - `timezone: '+09:00'` — interpret and serialize JavaScript `Date` values as JST. This is a
|
|
15
|
+
* mysql2 client-side conversion option; it does not issue `SET time_zone` or change the MySQL
|
|
16
|
+
* session timezone. Non-JST deployments can override it via `extra: { timezone: '...' }`.
|
|
17
|
+
*
|
|
18
|
+
* @param hyperdrive - the Hyperdrive binding to derive connection fields from.
|
|
19
|
+
* @param extra - additional mysql2 options merged last, overriding the defaults above.
|
|
20
|
+
* @returns a plain options object to pass to mysql2 `createConnection`.
|
|
21
|
+
*/
|
|
22
|
+
export function hyperdriveConnectionOptions(hyperdrive, extra) {
|
|
23
|
+
return {
|
|
24
|
+
host: hyperdrive.host,
|
|
25
|
+
user: hyperdrive.user,
|
|
26
|
+
password: hyperdrive.password,
|
|
27
|
+
database: hyperdrive.database,
|
|
28
|
+
port: hyperdrive.port,
|
|
29
|
+
disableEval: true,
|
|
30
|
+
decimalNumbers: true,
|
|
31
|
+
timezone: MYSQL_TIMEZONE,
|
|
32
|
+
...extra,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Open primary and replica connections in parallel and run `fn` with them.
|
|
37
|
+
*
|
|
38
|
+
* Cloudflare Workers automatically cleans up connections created during an invocation. Calling
|
|
39
|
+
* `Connection.end()` is unnecessary and can race work registered with `waitUntil`. The `ctx`
|
|
40
|
+
* parameter remains for API compatibility and is intentionally not used for connection teardown.
|
|
41
|
+
*
|
|
42
|
+
* @typeParam T - resolved value produced by `fn`.
|
|
43
|
+
* @param hyperdrives - the primary and replica Hyperdrive bindings to connect to.
|
|
44
|
+
* @param ctx - the request execution context, retained for API compatibility.
|
|
45
|
+
* @param fn - callback invoked with the open `primary` and `replica` connections.
|
|
46
|
+
* @param connectionOptions - extra mysql2 options forwarded to {@link hyperdriveConnectionOptions}.
|
|
47
|
+
* @returns the value resolved by `fn`.
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* const data = await withMysqlConnections(
|
|
51
|
+
* { primary: env.PRIMARY, replica: env.REPLICA },
|
|
52
|
+
* ctx,
|
|
53
|
+
* async ({ primary, replica }) => {
|
|
54
|
+
* const [rows] = await replica.query('SELECT 1');
|
|
55
|
+
* return rows;
|
|
56
|
+
* },
|
|
57
|
+
* );
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export async function withMysqlConnections(hyperdrives, ctx, fn, connectionOptions) {
|
|
61
|
+
void ctx;
|
|
62
|
+
const [primary, replica] = await Promise.all([
|
|
63
|
+
createConnection(hyperdriveConnectionOptions(hyperdrives.primary, connectionOptions)),
|
|
64
|
+
createConnection(hyperdriveConnectionOptions(hyperdrives.replica, connectionOptions)),
|
|
65
|
+
]);
|
|
66
|
+
return fn({ primary, replica });
|
|
67
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { Connection, Pool } from 'mysql2/promise';
|
|
2
|
+
import type { HyperdriveLike } from './connection.js';
|
|
3
|
+
/**
|
|
4
|
+
* Dual-connection data layer that separates reads from writes.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* The two sides of the database are deliberately handled differently:
|
|
8
|
+
*
|
|
9
|
+
* - Reads go to the **replica** as raw SQL (`QueryRunner.query`) by default, returning plain rows.
|
|
10
|
+
* Hyperdrive-backed databases also expose `query()` for freshness-sensitive primary SELECTs.
|
|
11
|
+
* - Writes and transactions go to the **primary** through the Drizzle ORM for type safety, but only
|
|
12
|
+
* via `write(fn)` / `transaction(fn)` — the raw query builder is never exposed. The builder is
|
|
13
|
+
* awaited inside those methods, which removes a foot-gun: a Drizzle builder is a lazy thenable, so
|
|
14
|
+
* a bare `return builder` would silently become a no-op.
|
|
15
|
+
*
|
|
16
|
+
* Both sides retry on `ER_LOCK_DEADLOCK`.
|
|
17
|
+
*
|
|
18
|
+
* This package deliberately avoids depending on the type identity of `drizzle-orm`: the consumer creates
|
|
19
|
+
* the ORM instance with its own copy of `drizzle-orm` and passes it in, and {@link Database} is
|
|
20
|
+
* generic over that ORM type (`TDrizzle`). This keeps the ORM's `MySqlTable`/`SQL` brands from
|
|
21
|
+
* clashing even when the package and consumer resolve separate copies of `drizzle-orm`.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Minimal connection interface used for reads.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* A mysql2 `Connection` or `Pool` satisfies this structurally.
|
|
28
|
+
*/
|
|
29
|
+
export interface QueryRunner {
|
|
30
|
+
/**
|
|
31
|
+
* Run a parameterized SQL query.
|
|
32
|
+
*
|
|
33
|
+
* @param sql - the SQL text, with `?` placeholders for `params`.
|
|
34
|
+
* @param params - optional positional parameters.
|
|
35
|
+
* @returns the driver's raw result (typically `[rows, fields]`).
|
|
36
|
+
*/
|
|
37
|
+
query(sql: string, params?: unknown[]): Promise<unknown>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extract the transaction-handle type that a Drizzle instance passes to its `.transaction(cb)`
|
|
41
|
+
* callback.
|
|
42
|
+
*
|
|
43
|
+
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
44
|
+
*/
|
|
45
|
+
export type TxOf<TDrizzle> = TDrizzle extends {
|
|
46
|
+
transaction(cb: (tx: infer Tx) => Promise<unknown>): Promise<unknown>;
|
|
47
|
+
} ? Tx : unknown;
|
|
48
|
+
/**
|
|
49
|
+
* The read/write surface of the data layer.
|
|
50
|
+
*
|
|
51
|
+
* @typeParam TDrizzle - the consumer's Drizzle ORM type used for writes and transactions.
|
|
52
|
+
* @typeParam TTx - the transaction-handle type, inferred from `TDrizzle` by default.
|
|
53
|
+
*/
|
|
54
|
+
export interface Database<TDrizzle, TTx = TxOf<TDrizzle>> {
|
|
55
|
+
/**
|
|
56
|
+
* Run a raw SQL read against the replica. Hyperdrive-backed databases retry deadlocks and repeat
|
|
57
|
+
* the SELECT once on a fresh connection after mysql2 reports a fatal connection error.
|
|
58
|
+
*
|
|
59
|
+
* @typeParam T - the row shape.
|
|
60
|
+
* @param sql - the SQL text, with `?` placeholders for `params`.
|
|
61
|
+
* @param params - optional positional parameters.
|
|
62
|
+
* @returns the rows returned by the query.
|
|
63
|
+
*/
|
|
64
|
+
read<T>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
65
|
+
/**
|
|
66
|
+
* Run a single INSERT/UPDATE/DELETE against the primary, awaited with deadlock retry.
|
|
67
|
+
*
|
|
68
|
+
* @typeParam T - the value resolved by `fn`.
|
|
69
|
+
* @param fn - callback that receives the Drizzle ORM and returns the awaited write.
|
|
70
|
+
* @returns the value resolved by `fn`.
|
|
71
|
+
*/
|
|
72
|
+
write<T>(fn: (dz: TDrizzle) => Promise<T>): Promise<T>;
|
|
73
|
+
/**
|
|
74
|
+
* Run multiple writes inside a single transaction; the whole transaction is retried on deadlock.
|
|
75
|
+
*
|
|
76
|
+
* @typeParam T - the value resolved by `fn`.
|
|
77
|
+
* @param fn - callback that receives the transaction handle and returns the awaited work.
|
|
78
|
+
* @returns the value resolved by `fn`.
|
|
79
|
+
*/
|
|
80
|
+
transaction<T>(fn: (tx: TTx) => Promise<T>): Promise<T>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A {@link Database} that opens its own connections.
|
|
84
|
+
*
|
|
85
|
+
* @remarks
|
|
86
|
+
* Used by variants that open connections internally. Lifecycle behavior depends on the backing
|
|
87
|
+
* implementation: pool-backed databases close their pool, while Hyperdrive-backed databases leave
|
|
88
|
+
* invocation-scoped connection cleanup to the Workers runtime.
|
|
89
|
+
*
|
|
90
|
+
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
91
|
+
* @typeParam TTx - the transaction-handle type, inferred from `TDrizzle` by default.
|
|
92
|
+
*/
|
|
93
|
+
export interface DisposableDatabase<TDrizzle, TTx = TxOf<TDrizzle>> extends Database<TDrizzle, TTx> {
|
|
94
|
+
/**
|
|
95
|
+
* Release resources owned by the implementation. Hyperdrive-backed databases keep this method as
|
|
96
|
+
* a compatibility no-op; pool-backed databases use it to close their pool.
|
|
97
|
+
*
|
|
98
|
+
* @returns a promise that resolves after implementation-specific cleanup.
|
|
99
|
+
*/
|
|
100
|
+
dispose(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A Hyperdrive-backed database with an explicit primary query path.
|
|
104
|
+
*
|
|
105
|
+
* @remarks
|
|
106
|
+
* Use `query()` only for SELECTs that require read-after-write consistency or cannot use the
|
|
107
|
+
* configured replica. Fatal connection errors recreate the primary connection and repeat the
|
|
108
|
+
* SELECT at most once. Writes and transactions are never repeated for connection errors.
|
|
109
|
+
* Read-only transactions are serialized on a separately cached primary connection so their
|
|
110
|
+
* snapshot boundaries cannot mix with ordinary primary operations or with each other. Do not call
|
|
111
|
+
* `readTransaction()` recursively from its own callback.
|
|
112
|
+
*
|
|
113
|
+
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
114
|
+
* @typeParam TTx - the transaction-handle type, inferred from `TDrizzle` by default.
|
|
115
|
+
*/
|
|
116
|
+
export interface HyperdriveDatabase<TDrizzle, TTx = TxOf<TDrizzle>> extends DisposableDatabase<TDrizzle, TTx> {
|
|
117
|
+
/**
|
|
118
|
+
* Run a raw SQL SELECT against the primary database.
|
|
119
|
+
*
|
|
120
|
+
* @typeParam T - the complete rows result type (for example, `User[]`).
|
|
121
|
+
* @param sql - the SQL text, with `?` placeholders for `params`.
|
|
122
|
+
* @param params - optional positional parameters.
|
|
123
|
+
* @returns the rows returned by the query, typed as `T`.
|
|
124
|
+
*/
|
|
125
|
+
query<T = unknown>(sql: string, params?: unknown[]): Promise<T>;
|
|
126
|
+
/**
|
|
127
|
+
* Run a repeatable-read, consistent snapshot on the primary database.
|
|
128
|
+
*
|
|
129
|
+
* @remarks
|
|
130
|
+
* The callback is repeated at most once on a fresh connection after a fatal connection error.
|
|
131
|
+
* This is safe only because the transaction is declared read-only. Use {@link transaction} for
|
|
132
|
+
* writes; write transactions are never repeated after connection loss. Calls are serialized on
|
|
133
|
+
* one dedicated connection, so recursive `readTransaction()` calls are not supported.
|
|
134
|
+
*/
|
|
135
|
+
readTransaction<T>(fn: (reader: ReadTransaction<TTx>) => Promise<T>): Promise<T>;
|
|
136
|
+
}
|
|
137
|
+
/** Primary read-only transaction handles that share one consistent snapshot. */
|
|
138
|
+
export interface ReadTransaction<TTx> {
|
|
139
|
+
/**
|
|
140
|
+
* The consumer's Drizzle transaction handle.
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* Drizzle's type does not distinguish read-only transactions. MySQL enforces read-only mode at
|
|
144
|
+
* runtime; applications may expose a narrower SELECT-only facade when they need compile-time
|
|
145
|
+
* enforcement.
|
|
146
|
+
*/
|
|
147
|
+
orm: TTx;
|
|
148
|
+
/** Run raw SQL on the same primary connection and snapshot. */
|
|
149
|
+
query: <T = unknown>(sql: string, params?: unknown[]) => Promise<T>;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Options for {@link createMysqlDatabase}.
|
|
153
|
+
*
|
|
154
|
+
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
155
|
+
*/
|
|
156
|
+
export interface CreateMysqlDatabaseOptions<TDrizzle> {
|
|
157
|
+
/**
|
|
158
|
+
* The Drizzle ORM used for writes, created by the consumer with its own `drizzle-orm`
|
|
159
|
+
* (e.g. `drizzle(primary, { schema, ... })`).
|
|
160
|
+
*/
|
|
161
|
+
orm: TDrizzle;
|
|
162
|
+
/** The connection used for reads (raw SQL). */
|
|
163
|
+
replica: QueryRunner;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Assemble a {@link Database} from an already-connected ORM and replica.
|
|
167
|
+
*
|
|
168
|
+
* @remarks
|
|
169
|
+
* The caller (typically the worker entry point) owns creating the connections and the ORM, and is
|
|
170
|
+
* responsible for closing the connections; this variant does not manage their lifecycle.
|
|
171
|
+
*
|
|
172
|
+
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
173
|
+
* @param options - the write ORM and the read connection.
|
|
174
|
+
* @returns a {@link Database} backed by the supplied ORM and replica.
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* const db = createMysqlDatabase({
|
|
178
|
+
* orm: drizzle(primary, { schema, ...DRIZZLE_ORM_OPTIONS }),
|
|
179
|
+
* replica,
|
|
180
|
+
* });
|
|
181
|
+
* const rows = await db.read<User>('SELECT * FROM users WHERE id = ?', [id]);
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export declare function createMysqlDatabase<TDrizzle>(options: CreateMysqlDatabaseOptions<TDrizzle>): Database<TDrizzle>;
|
|
185
|
+
/**
|
|
186
|
+
* Options for {@link createHyperdriveDatabase}.
|
|
187
|
+
*
|
|
188
|
+
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
189
|
+
*/
|
|
190
|
+
export interface CreateHyperdriveDatabaseOptions<TDrizzle> {
|
|
191
|
+
/** The Hyperdrive binding for the primary (write) connection. */
|
|
192
|
+
primaryHyperdrive: HyperdriveLike;
|
|
193
|
+
/** The Hyperdrive binding for the replica (read) connection. */
|
|
194
|
+
replicaHyperdrive: HyperdriveLike;
|
|
195
|
+
/**
|
|
196
|
+
* Factory that builds the write ORM from the primary connection, using the consumer's
|
|
197
|
+
* `drizzle-orm`.
|
|
198
|
+
*/
|
|
199
|
+
createOrm: (primary: Connection) => TDrizzle;
|
|
200
|
+
/**
|
|
201
|
+
* Extra options forwarded to mysql2 `createConnection`, merged on top of the defaults applied by
|
|
202
|
+
* {@link hyperdriveConnectionOptions} (`disableEval: true`, `decimalNumbers: true`, and
|
|
203
|
+
* `timezone: '+09:00'`). Pass a field here to override any of those defaults.
|
|
204
|
+
*/
|
|
205
|
+
connectionOptions?: Record<string, unknown>;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Create a {@link HyperdriveDatabase} that lazily opens its connections from Hyperdrive bindings.
|
|
209
|
+
*
|
|
210
|
+
* @remarks
|
|
211
|
+
* Construct one per request. Connections and the ORM are created on first use and reused for the
|
|
212
|
+
* lifetime of the instance. `read()` uses the replica, while `query()` provides an explicit
|
|
213
|
+
* primary SELECT path. Workers automatically cleans up connections at the end of the invocation,
|
|
214
|
+
* so callers do not need to close them manually. Either SELECT path is repeated once on a fresh
|
|
215
|
+
* connection after a fatal mysql2 connection error. Writes and transactions are never repeated
|
|
216
|
+
* for connection errors because their commit state can be ambiguous.
|
|
217
|
+
*
|
|
218
|
+
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
219
|
+
* @param options - the primary/replica Hyperdrive bindings, the ORM factory, and connection options.
|
|
220
|
+
* @returns a {@link HyperdriveDatabase} whose compatibility `dispose()` method is a no-op; Workers
|
|
221
|
+
* cleans up its invocation-scoped connections automatically.
|
|
222
|
+
* @example
|
|
223
|
+
* ```ts
|
|
224
|
+
* const db = createHyperdriveDatabase({
|
|
225
|
+
* primaryHyperdrive: env.PRIMARY,
|
|
226
|
+
* replicaHyperdrive: env.REPLICA,
|
|
227
|
+
* createOrm: (primary) => drizzle(primary, { schema, ...DRIZZLE_ORM_OPTIONS }),
|
|
228
|
+
* });
|
|
229
|
+
* await db.write((dz) => dz.insert(users).values(user));
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
export declare function createHyperdriveDatabase<TDrizzle>(options: CreateHyperdriveDatabaseOptions<TDrizzle>): HyperdriveDatabase<TDrizzle>;
|
|
233
|
+
/**
|
|
234
|
+
* Internal helper that assembles a {@link Database} from an ORM and a replica connection.
|
|
235
|
+
*
|
|
236
|
+
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
237
|
+
* @param orm - the Drizzle ORM used for writes and transactions.
|
|
238
|
+
* @param replica - the connection used for reads.
|
|
239
|
+
* @returns a {@link Database} wiring reads to `replica` and writes to `orm`, both with deadlock retry.
|
|
240
|
+
* @internal
|
|
241
|
+
*/
|
|
242
|
+
export declare function databaseFrom<TDrizzle>(orm: TDrizzle, replica: QueryRunner): Database<TDrizzle>;
|
|
243
|
+
/**
|
|
244
|
+
* Re-export of the mysql2 `Connection` and `Pool` types.
|
|
245
|
+
*
|
|
246
|
+
* @remarks
|
|
247
|
+
* Both are structurally assignable to this package's {@link QueryRunner}.
|
|
248
|
+
*/
|
|
249
|
+
export type { Connection, Pool };
|