@rdlabo/workers-mysql 0.1.0

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.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Centralizes Drizzle column-name casing so it is fixed (standard: `snake_case`) in both the
3
+ * config and the runtime ORM.
4
+ *
5
+ * @remarks
6
+ * Casing is configured in two distinct places:
7
+ *
8
+ * 1. The top-level `casing` in `drizzle.config.ts` decides the column names that `db:generate`
9
+ * **creates** (see {@link workersDrizzleConfig}).
10
+ * 2. The `drizzle(conn, { …casing })` call decides the column names the **runtime write builder**
11
+ * resolves to (see {@link DRIZZLE_ORM_OPTIONS}).
12
+ *
13
+ * If these two disagree, a multi-word camelCase column without an explicit column name will be
14
+ * generated with one name but queried with another, producing a runtime `Unknown column` error —
15
+ * something neither the type-check nor the migration surface, so it is caught late. Sourcing both
16
+ * from here makes the mismatch structurally impossible. Casing is ignored for columns that declare
17
+ * an explicit name, so this is a pure safety net that does not change existing behavior.
18
+ *
19
+ * The runtime `drizzle()` call itself is made by the consuming app with its own `drizzle-orm`; the
20
+ * package only provides values, never the ORM instance, to avoid splitting `drizzle-orm` into two
21
+ * copies and breaking type identity.
22
+ */
23
+ /**
24
+ * Runtime ORM options shared by the consuming app's `drizzle()` call.
25
+ *
26
+ * Spread into the runtime ORM as `drizzle(conn, { schema, ...DRIZZLE_ORM_OPTIONS })` so the write
27
+ * builder resolves column names as `snake_case`, matching what `db:generate` creates.
28
+ *
29
+ * @remarks
30
+ * Fixes `mode: 'default'` and `casing: 'snake_case'`. See the module-level documentation for why
31
+ * the same casing must be used by both the config and the runtime ORM.
32
+ */
33
+ export const DRIZZLE_ORM_OPTIONS = { mode: 'default', casing: 'snake_case' };
34
+ /**
35
+ * Build a `drizzle.config.ts` configuration object with the Workers MySQL defaults.
36
+ *
37
+ * Fixes `casing: 'snake_case'`, the `schema`/`out` paths, and `dbCredentials` (with env-based
38
+ * defaults), while leaving `tablesFilter` and `introspect` opt-in.
39
+ *
40
+ * @remarks
41
+ * Returns a plain object rather than a typed drizzle-kit config so that `drizzle-kit` need not be a
42
+ * dependency of this package; the drizzle-kit CLI only reads the default export.
43
+ *
44
+ * @param options - configuration overrides; only `database` is required.
45
+ * @returns a plain configuration object suitable for `export default` in `drizzle.config.ts`.
46
+ * @example
47
+ * ```ts
48
+ * // drizzle.config.ts
49
+ * import { workersDrizzleConfig } from '@rdlabo/workers-mysql/drizzle';
50
+ *
51
+ * export default workersDrizzleConfig({ database: 'app' });
52
+ * ```
53
+ */
54
+ export function workersDrizzleConfig(options) {
55
+ const { database, host, port, user, password, schema = './src/db/schemes', out = './drizzle', tablesFilter, introspect, } = options;
56
+ // CI/production migrate absorbs the pattern of passing a whole AWS Secrets Manager RDS managed secret
57
+ // (keys host/port/dbname/username/password) via `DB_SECRET`. It is parsed with JSON.parse, so key-name
58
+ // differences (host ≠ DB_HOST) can be mapped and special characters in the password stay shell-safe.
59
+ // When `DB_SECRET` is set, it is treated as a complete secret and fully determines the connection
60
+ // (missing/invalid → throw). Only when it is unset do we fall back to the individual DB_* env vars
61
+ // and then the defaults (the local / db:generate path).
62
+ const secret = resolveDbSecret();
63
+ const dbCredentials = secret
64
+ ? {
65
+ host: secret.host,
66
+ port: secret.port,
67
+ user: secret.username,
68
+ password: secret.password,
69
+ database: secret.dbname,
70
+ }
71
+ : {
72
+ host: host ?? process.env.DB_HOST ?? '127.0.0.1',
73
+ port: port ?? Number(process.env.DB_PORT ?? 3306),
74
+ user: user ?? process.env.DB_USER ?? 'root',
75
+ password: password ?? process.env.DB_PASSWORD ?? 'root',
76
+ database,
77
+ };
78
+ return {
79
+ dialect: 'mysql',
80
+ schema,
81
+ out,
82
+ casing: 'snake_case',
83
+ ...(tablesFilter ? { tablesFilter } : {}),
84
+ ...(introspect ? { introspect } : {}),
85
+ dbCredentials,
86
+ };
87
+ }
88
+ /**
89
+ * @deprecated Use {@link workersDrizzleConfig}; retained for source compatibility with
90
+ * `@rdlabo/workers-hono-kit/db`.
91
+ */
92
+ export const honoDrizzleConfig = workersDrizzleConfig;
93
+ /**
94
+ * Resolve an AWS RDS managed secret (a JSON string placed in `DB_SECRET`).
95
+ *
96
+ * @remarks
97
+ * - `DB_SECRET` unset → `undefined` (the normal local / `db:generate` fallback).
98
+ * - When set, it must be complete connection info: **invalid JSON / a missing required key throws**
99
+ * (rather than silently falling back to localhost and causing an incident). A missing `port` alone
100
+ * defaults to 3306.
101
+ *
102
+ * Both `workersDrizzleConfig` (db:migrate) and the compatibility baseline command use this logic.
103
+ *
104
+ * @returns the resolved connection info, or `undefined` when `DB_SECRET` is unset.
105
+ * @throws Error when `DB_SECRET` is set but is not valid JSON or is missing a required key.
106
+ */
107
+ export function resolveDbSecret() {
108
+ const raw = process.env.DB_SECRET;
109
+ if (!raw) {
110
+ return undefined;
111
+ }
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(raw);
115
+ }
116
+ catch {
117
+ throw new Error('DB_SECRET is set but is not valid JSON (expected an AWS RDS managed secret string).');
118
+ }
119
+ const { host, dbname, username, password } = parsed;
120
+ if (typeof host !== 'string' ||
121
+ typeof dbname !== 'string' ||
122
+ typeof username !== 'string' ||
123
+ typeof password !== 'string') {
124
+ throw new Error('DB_SECRET must contain string host, dbname, username, password (AWS RDS managed secret shape).');
125
+ }
126
+ return { host, dbname, username, password, port: parsed.port === undefined ? 3306 : Number(parsed.port) };
127
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Run an async unit of work, retrying it on MySQL deadlock errors with exponential backoff.
3
+ *
4
+ * Retries are triggered only by the `ER_LOCK_DEADLOCK` error code. Each failed attempt waits
5
+ * `delay * attempt` milliseconds (linear growth of the base delay) before the next try, and any
6
+ * non-deadlock error is rethrown immediately without retrying.
7
+ *
8
+ * @remarks
9
+ * MySQL rolls back the entire transaction when it detects a deadlock, so re-running the same unit
10
+ * of work is safe. Pass a `fn` that represents one complete unit — a single statement or an entire
11
+ * transaction — because the whole `fn` is re-executed on each retry.
12
+ *
13
+ * @typeParam T - resolved value produced by `fn`.
14
+ * @param fn - the unit of work to execute; it is invoked again from scratch on each retry.
15
+ * @param retries - maximum number of attempts (default `3`).
16
+ * @param delay - base backoff in milliseconds; attempt N waits `delay * N` (default `100`).
17
+ * @returns the value resolved by the first successful call to `fn`.
18
+ * @throws the last error thrown by `fn` once retries are exhausted, or any non-deadlock error on
19
+ * the first occurrence.
20
+ * @example
21
+ * ```ts
22
+ * await retryWhenDeadlock(() => db.transaction(async (tx) => {
23
+ * await tx.insert(orders).values(order);
24
+ * await tx.update(stock).set({ qty: sql`qty - 1` }).where(eq(stock.id, order.itemId));
25
+ * }));
26
+ * ```
27
+ */
28
+ export declare function retryWhenDeadlock<T>(fn: () => Promise<T>, retries?: number, delay?: number): Promise<T>;
package/dist/retry.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Run an async unit of work, retrying it on MySQL deadlock errors with exponential backoff.
3
+ *
4
+ * Retries are triggered only by the `ER_LOCK_DEADLOCK` error code. Each failed attempt waits
5
+ * `delay * attempt` milliseconds (linear growth of the base delay) before the next try, and any
6
+ * non-deadlock error is rethrown immediately without retrying.
7
+ *
8
+ * @remarks
9
+ * MySQL rolls back the entire transaction when it detects a deadlock, so re-running the same unit
10
+ * of work is safe. Pass a `fn` that represents one complete unit — a single statement or an entire
11
+ * transaction — because the whole `fn` is re-executed on each retry.
12
+ *
13
+ * @typeParam T - resolved value produced by `fn`.
14
+ * @param fn - the unit of work to execute; it is invoked again from scratch on each retry.
15
+ * @param retries - maximum number of attempts (default `3`).
16
+ * @param delay - base backoff in milliseconds; attempt N waits `delay * N` (default `100`).
17
+ * @returns the value resolved by the first successful call to `fn`.
18
+ * @throws the last error thrown by `fn` once retries are exhausted, or any non-deadlock error on
19
+ * the first occurrence.
20
+ * @example
21
+ * ```ts
22
+ * await retryWhenDeadlock(() => db.transaction(async (tx) => {
23
+ * await tx.insert(orders).values(order);
24
+ * await tx.update(stock).set({ qty: sql`qty - 1` }).where(eq(stock.id, order.itemId));
25
+ * }));
26
+ * ```
27
+ */
28
+ export async function retryWhenDeadlock(fn, retries = 3, delay = 100) {
29
+ for (let attempt = 0; attempt < retries; attempt++) {
30
+ const invoke = async () => fn();
31
+ const outcome = await invoke().then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
32
+ if (outcome.ok) {
33
+ return outcome.value;
34
+ }
35
+ if (isDeadlock(outcome.error) && attempt < retries - 1) {
36
+ await new Promise((resolve) => setTimeout(resolve, delay * (attempt + 1)));
37
+ continue;
38
+ }
39
+ throw outcome.error;
40
+ }
41
+ // Unreachable: the loop returns on success and throws on the final failed attempt.
42
+ throw new Error('retryWhenDeadlock: exhausted retries');
43
+ }
44
+ function isDeadlock(error) {
45
+ let current = error;
46
+ const seen = new Set();
47
+ while (typeof current === 'object' && current !== null && !seen.has(current)) {
48
+ seen.add(current);
49
+ const value = current;
50
+ if (value.code === 'ER_LOCK_DEADLOCK') {
51
+ return true;
52
+ }
53
+ current = value.cause;
54
+ }
55
+ return false;
56
+ }
@@ -0,0 +1,106 @@
1
+ import type { Pool } from 'mysql2/promise';
2
+ /**
3
+ * Connection parameters for the test MySQL server.
4
+ *
5
+ * @see {@link CreateTestDbOptions.connection} for how defaults are resolved.
6
+ */
7
+ export interface TestDbConnection {
8
+ /** Server host. */
9
+ host: string;
10
+ /** Server port. */
11
+ port: number;
12
+ /** User name. */
13
+ user: string;
14
+ /** Password. */
15
+ password: string;
16
+ }
17
+ /**
18
+ * Options for {@link createTestDb}.
19
+ */
20
+ export interface CreateTestDbOptions {
21
+ /**
22
+ * Test database name (e.g. `'app_test'`). To isolate parallel runs per feature, resolve a per-run
23
+ * name on the caller side and pass it here.
24
+ */
25
+ dbName: string;
26
+ /**
27
+ * Absolute path to the Drizzle migrations folder. Resolve it on the caller side, e.g.
28
+ * `join(here, '..', 'drizzle')`.
29
+ */
30
+ migrationsFolder: string;
31
+ /**
32
+ * Connection overrides. Unspecified fields fall back to environment variables
33
+ * (`DB_HOST`/`DB_PORT`/`DB_USER`/`DB_PASSWORD`), then to `127.0.0.1`/`3306`/`root`/`root`.
34
+ */
35
+ connection?: Partial<TestDbConnection>;
36
+ }
37
+ /**
38
+ * Test database handle returned by {@link createTestDb}, bundling schema setup, pooling, and
39
+ * fixture helpers for a single test database.
40
+ */
41
+ export interface TestDb {
42
+ /** The resolved test database name. */
43
+ readonly dbName: string;
44
+ /** The resolved connection parameters. */
45
+ readonly connection: TestDbConnection;
46
+ /**
47
+ * Drop and recreate the database, then apply the committed Drizzle migrations to build the schema.
48
+ *
49
+ * @returns A promise that resolves once migrations have been applied.
50
+ */
51
+ resetSchema(): Promise<void>;
52
+ /**
53
+ * Create a mysql2 pool connected to the test database.
54
+ *
55
+ * @remarks Call `pool.end()` (e.g. in `afterAll`) to release connections.
56
+ * @returns A connection pool for the test database.
57
+ */
58
+ createTestPool(): Pool;
59
+ /**
60
+ * Truncate every base table in the database.
61
+ *
62
+ * @remarks Table names are discovered dynamically from `information_schema`; the
63
+ * `__drizzle_migrations` bookkeeping table is excluded. Foreign-key checks are disabled for the
64
+ * duration so truncation order does not matter.
65
+ * @param pool - Pool connected to the test database.
66
+ */
67
+ truncateAll(pool: Pool): Promise<void>;
68
+ /**
69
+ * Insert a single row, mapping column names to values — a generic fixture helper for specs.
70
+ *
71
+ * @param pool - Pool connected to the test database.
72
+ * @param table - Target table name.
73
+ * @param row - Column-name to value map. A no-op if empty.
74
+ */
75
+ seed(pool: Pool, table: string, row: Record<string, unknown>): Promise<void>;
76
+ /**
77
+ * Report whether the local MySQL server is reachable.
78
+ *
79
+ * @remarks Useful as a guard, e.g. `describe.skipIf(!(await mysqlReachable()))`.
80
+ * @returns `true` if a connection could be opened, otherwise `false`.
81
+ */
82
+ mysqlReachable(): Promise<boolean>;
83
+ }
84
+ /**
85
+ * Create a {@link TestDb} handle for a single test database.
86
+ *
87
+ * @remarks
88
+ * The test schema is built from the committed Drizzle migrations as the single source of truth
89
+ * (the `db:generate` output under `./drizzle`), rather than a hand-written `schema.sql`. This helper
90
+ * is Node-only test infrastructure (run under Vitest) and is unrelated to runtime behavior.
91
+ *
92
+ * @param options - Database name, migrations folder, and optional connection overrides. See
93
+ * {@link CreateTestDbOptions}.
94
+ * @returns A handle exposing schema setup, pooling, truncation, seeding, and a reachability probe.
95
+ * @example
96
+ * ```ts
97
+ * const testDb = createTestDb({ dbName: 'app_test', migrationsFolder: join(here, '..', 'drizzle') });
98
+ * beforeAll(async () => {
99
+ * await testDb.resetSchema();
100
+ * });
101
+ * const pool = testDb.createTestPool();
102
+ * beforeEach(() => testDb.truncateAll(pool));
103
+ * afterAll(() => pool.end());
104
+ * ```
105
+ */
106
+ export declare function createTestDb(options: CreateTestDbOptions): TestDb;
@@ -0,0 +1,95 @@
1
+ import { drizzle } from 'drizzle-orm/mysql2';
2
+ import { migrate } from 'drizzle-orm/mysql2/migrator';
3
+ import { createConnection, createPool } from 'mysql2/promise';
4
+ function resolveConnection(override) {
5
+ const env = globalThis.process?.env ?? {};
6
+ return {
7
+ host: override?.host ?? env.DB_HOST ?? '127.0.0.1',
8
+ port: override?.port ?? Number(env.DB_PORT ?? '3306'),
9
+ user: override?.user ?? env.DB_USER ?? 'root',
10
+ password: override?.password ?? env.DB_PASSWORD ?? 'root',
11
+ };
12
+ }
13
+ /**
14
+ * Create a {@link TestDb} handle for a single test database.
15
+ *
16
+ * @remarks
17
+ * The test schema is built from the committed Drizzle migrations as the single source of truth
18
+ * (the `db:generate` output under `./drizzle`), rather than a hand-written `schema.sql`. This helper
19
+ * is Node-only test infrastructure (run under Vitest) and is unrelated to runtime behavior.
20
+ *
21
+ * @param options - Database name, migrations folder, and optional connection overrides. See
22
+ * {@link CreateTestDbOptions}.
23
+ * @returns A handle exposing schema setup, pooling, truncation, seeding, and a reachability probe.
24
+ * @example
25
+ * ```ts
26
+ * const testDb = createTestDb({ dbName: 'app_test', migrationsFolder: join(here, '..', 'drizzle') });
27
+ * beforeAll(async () => {
28
+ * await testDb.resetSchema();
29
+ * });
30
+ * const pool = testDb.createTestPool();
31
+ * beforeEach(() => testDb.truncateAll(pool));
32
+ * afterAll(() => pool.end());
33
+ * ```
34
+ */
35
+ export function createTestDb(options) {
36
+ const { dbName, migrationsFolder } = options;
37
+ const connection = resolveConnection(options.connection);
38
+ return {
39
+ dbName,
40
+ connection,
41
+ async resetSchema() {
42
+ const admin = await createConnection({ ...connection, multipleStatements: true });
43
+ await admin.query(`DROP DATABASE IF EXISTS \`${dbName}\`; CREATE DATABASE \`${dbName}\` DEFAULT CHARACTER SET utf8mb4;`);
44
+ await admin.changeUser({ database: dbName });
45
+ await migrate(drizzle(admin), { migrationsFolder });
46
+ await admin.end();
47
+ },
48
+ createTestPool() {
49
+ // decimalNumbers / timezone mirror the runtime hyperdriveConnectionOptions so specs read
50
+ // DECIMAL columns as numbers and handle datetime in +09:00 (JST), matching production.
51
+ const pool = createPool({
52
+ ...connection,
53
+ database: dbName,
54
+ connectionLimit: 5,
55
+ decimalNumbers: true,
56
+ timezone: '+09:00',
57
+ });
58
+ // Pin ONLY_FULL_GROUP_BY on every pooled connection so GROUP BY violations surface in specs
59
+ // regardless of the server's my.cnf (the policy is centralized here, not left to each server). CONCAT keeps
60
+ // the server's other sql_mode flags and is harmless if ONLY_FULL_GROUP_BY is already present.
61
+ // mysql2 queues this SET ahead of the consumer's first query on each new physical connection.
62
+ pool.on('connection', (conn) => {
63
+ void conn.query("SET SESSION sql_mode = CONCAT(@@SESSION.sql_mode, ',ONLY_FULL_GROUP_BY')");
64
+ });
65
+ return pool;
66
+ },
67
+ async truncateAll(pool) {
68
+ const [rows] = await pool.query("SELECT table_name AS t FROM information_schema.tables WHERE table_schema = ? AND table_type='BASE TABLE' AND table_name <> '__drizzle_migrations'", [dbName]);
69
+ const tables = rows.map((r) => r.t);
70
+ await pool.query('SET FOREIGN_KEY_CHECKS=0');
71
+ for (const t of tables) {
72
+ await pool.query(`TRUNCATE TABLE \`${t}\``);
73
+ }
74
+ await pool.query('SET FOREIGN_KEY_CHECKS=1');
75
+ },
76
+ async seed(pool, table, row) {
77
+ const cols = Object.keys(row);
78
+ if (cols.length === 0) {
79
+ return;
80
+ }
81
+ const placeholders = cols.map(() => '?').join(', ');
82
+ const columnList = cols.map((c) => `\`${c}\``).join(', ');
83
+ await pool.query(`INSERT INTO \`${table}\` (${columnList}) VALUES (${placeholders})`, Object.values(row));
84
+ },
85
+ async mysqlReachable() {
86
+ const connect = async () => createConnection({ ...connection });
87
+ const c = await connect().catch(() => undefined);
88
+ if (!c) {
89
+ return false;
90
+ }
91
+ const close = async () => c.end();
92
+ return close().then(() => true, () => false);
93
+ },
94
+ };
95
+ }
@@ -0,0 +1,13 @@
1
+ import type { Pool } from 'mysql2/promise';
2
+ import type { DisposableDatabase } from '../database.js';
3
+ /** Options for {@link createPoolDatabase}. */
4
+ export interface CreatePoolDatabaseOptions<TDrizzle> {
5
+ /** Test pool used as both primary and replica. */
6
+ pool: Pool;
7
+ /** Drizzle instance built by the consumer with its own `drizzle-orm`. */
8
+ orm: TDrizzle;
9
+ }
10
+ /** Create a `Database` backed by one pool used as both primary and replica. */
11
+ export declare function createPoolDatabase<TDrizzle>(options: CreatePoolDatabaseOptions<TDrizzle>): DisposableDatabase<TDrizzle>;
12
+ /** Create a no-op database stub that fails on unexpected writes. */
13
+ export declare function createNoopDatabase<TDrizzle = unknown>(): DisposableDatabase<TDrizzle>;
@@ -0,0 +1,25 @@
1
+ import { databaseFrom } from '../database.js';
2
+ /** Create a `Database` backed by one pool used as both primary and replica. */
3
+ export function createPoolDatabase(options) {
4
+ const { pool, orm } = options;
5
+ const base = databaseFrom(orm, pool);
6
+ return {
7
+ ...base,
8
+ async dispose() {
9
+ await pool.end();
10
+ },
11
+ };
12
+ }
13
+ /** Create a no-op database stub that fails on unexpected writes. */
14
+ export function createNoopDatabase() {
15
+ return {
16
+ read: async () => [],
17
+ write: () => {
18
+ throw new Error('noopDatabase.write accessed unexpectedly');
19
+ },
20
+ transaction: () => {
21
+ throw new Error('noopDatabase.transaction accessed unexpectedly');
22
+ },
23
+ dispose: async () => { },
24
+ };
25
+ }
@@ -0,0 +1,5 @@
1
+ export { createTestDb } from './db.js';
2
+ export type { TestDb, CreateTestDbOptions, TestDbConnection } from './db.js';
3
+ export { createPoolDatabase, createNoopDatabase } from './fakes.js';
4
+ export type { CreatePoolDatabaseOptions } from './fakes.js';
5
+ export type { Database, DisposableDatabase, QueryRunner, TxOf } from '../database.js';
@@ -0,0 +1,2 @@
1
+ export { createTestDb } from './db.js';
2
+ export { createPoolDatabase, createNoopDatabase } from './fakes.js';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shape of a Drizzle (mysql2) write result, narrowed to the fields callers actually read.
3
+ *
4
+ * @remarks
5
+ * A mysql2 INSERT/UPDATE/DELETE result is the tuple `[ResultSetHeader, FieldPacket[]]`. Typing the
6
+ * result this way lets repositories extract the common values without exposing the raw query
7
+ * builder or the full `ResultSetHeader` to the rest of the codebase.
8
+ */
9
+ export type DzWriteResult = readonly [{
10
+ insertId: number;
11
+ affectedRows: number;
12
+ }, ...unknown[]];
13
+ /**
14
+ * Extract the auto-increment `insertId` from a write result.
15
+ *
16
+ * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
17
+ * @returns the `insertId` reported by mysql2 (the id of the first inserted row).
18
+ */
19
+ export declare function insertIdOf(result: DzWriteResult): number;
20
+ /**
21
+ * Extract the number of affected rows from a write result.
22
+ *
23
+ * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
24
+ * @returns the `affectedRows` count reported by mysql2.
25
+ */
26
+ export declare function affectedRowsOf(result: DzWriteResult): number;
27
+ /**
28
+ * Reconstruct the auto-increment ids assigned by a bulk INSERT.
29
+ *
30
+ * @remarks
31
+ * mysql2 reports only the first `insertId` for a multi-row INSERT, so the remaining ids are derived
32
+ * by assuming a contiguous sequence (`base`, `base + 1`, …). This holds for tables with a standard
33
+ * `AUTO_INCREMENT` column and the default `innodb_autoinc_lock_mode`.
34
+ *
35
+ * @param result - the result of a bulk INSERT.
36
+ * @param count - the number of rows that were inserted.
37
+ * @returns an array of the `count` auto-increment ids, starting at the reported `insertId`.
38
+ */
39
+ export declare function insertedIdsOf(result: DzWriteResult, count: number): number[];
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Extract the auto-increment `insertId` from a write result.
3
+ *
4
+ * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
5
+ * @returns the `insertId` reported by mysql2 (the id of the first inserted row).
6
+ */
7
+ export function insertIdOf(result) {
8
+ return result[0].insertId;
9
+ }
10
+ /**
11
+ * Extract the number of affected rows from a write result.
12
+ *
13
+ * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
14
+ * @returns the `affectedRows` count reported by mysql2.
15
+ */
16
+ export function affectedRowsOf(result) {
17
+ return result[0].affectedRows;
18
+ }
19
+ /**
20
+ * Reconstruct the auto-increment ids assigned by a bulk INSERT.
21
+ *
22
+ * @remarks
23
+ * mysql2 reports only the first `insertId` for a multi-row INSERT, so the remaining ids are derived
24
+ * by assuming a contiguous sequence (`base`, `base + 1`, …). This holds for tables with a standard
25
+ * `AUTO_INCREMENT` column and the default `innodb_autoinc_lock_mode`.
26
+ *
27
+ * @param result - the result of a bulk INSERT.
28
+ * @param count - the number of rows that were inserted.
29
+ * @returns an array of the `count` auto-increment ids, starting at the reported `insertId`.
30
+ */
31
+ export function insertedIdsOf(result, count) {
32
+ const base = result[0].insertId;
33
+ return Array.from({ length: count }, (_, i) => base + i);
34
+ }
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@rdlabo/workers-mysql",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public",
7
+ "registry": "https://registry.npmjs.org/"
8
+ },
9
+ "type": "module",
10
+ "description": "MySQL and Hyperdrive utilities for Cloudflare Workers",
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/rdlabo-dev/workers-hono-kit.git",
15
+ "directory": "packages/mysql"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "bin"
20
+ ],
21
+ "bin": {
22
+ "workers-mysql-db-baseline": "./bin/db-baseline.mjs"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./drizzle": {
31
+ "types": "./dist/drizzle.d.ts",
32
+ "import": "./dist/drizzle.js",
33
+ "default": "./dist/drizzle.js"
34
+ },
35
+ "./migrations": {
36
+ "types": "./dist/migrations.d.ts",
37
+ "import": "./dist/migrations.js",
38
+ "default": "./dist/migrations.js"
39
+ },
40
+ "./baseline-cli": {
41
+ "types": "./dist/baseline-cli.d.ts",
42
+ "import": "./dist/baseline-cli.js",
43
+ "default": "./dist/baseline-cli.js"
44
+ },
45
+ "./testing": {
46
+ "types": "./dist/testing/index.d.ts",
47
+ "import": "./dist/testing/index.js",
48
+ "default": "./dist/testing/index.js"
49
+ }
50
+ },
51
+ "scripts": {
52
+ "build": "tsc -p tsconfig.build.json",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "test:package": "node scripts/package-smoke.mjs",
56
+ "lint": "eslint \"src/**/*.ts\"",
57
+ "lint:fix": "eslint \"src/**/*.ts\" --fix",
58
+ "format": "prettier --write .",
59
+ "format:check": "prettier --check .",
60
+ "prepack": "npm run build"
61
+ },
62
+ "engines": {
63
+ "node": ">=20.0.0"
64
+ },
65
+ "dependencies": {
66
+ "mysql2": "^3.24.3"
67
+ },
68
+ "peerDependencies": {
69
+ "@types/node": ">=20.19.43",
70
+ "drizzle-orm": "^0.45.2"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "drizzle-orm": {
74
+ "optional": true
75
+ }
76
+ },
77
+ "devDependencies": {
78
+ "@hono/eslint-config": "^2.1.0",
79
+ "@rdlabo/workers-timezone": "^0.1.0",
80
+ "drizzle-orm": "^0.45.2",
81
+ "eslint": "^9.39.4",
82
+ "prettier": "^3.8.4",
83
+ "typescript": "~5.6.2",
84
+ "typescript-eslint": "^8.61.1",
85
+ "vitest": "^2.1.0"
86
+ }
87
+ }