@rdlabo/workers-hono-kit 0.11.1 → 0.12.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.
Files changed (56) hide show
  1. package/README.md +73 -37
  2. package/dist/aws/sts.d.ts +2 -2
  3. package/dist/aws/sts.js +4 -4
  4. package/dist/business-time/index.d.ts +68 -122
  5. package/dist/business-time/index.js +55 -222
  6. package/dist/container/middleware.d.ts +1 -2
  7. package/dist/container/middleware.js +1 -1
  8. package/dist/db/index.d.ts +95 -20
  9. package/dist/db/index.js +56 -15
  10. package/dist/db/payment-failed.d.ts +1 -1
  11. package/dist/db/payment-failed.js +1 -1
  12. package/dist/index.d.ts +0 -3
  13. package/dist/index.js +0 -3
  14. package/dist/mysql/index.d.ts +9 -0
  15. package/dist/mysql/index.js +8 -0
  16. package/dist/testing/auth.d.ts +1 -1
  17. package/dist/testing/db.d.ts +19 -106
  18. package/dist/testing/db.js +3 -95
  19. package/dist/testing/fakes.d.ts +15 -97
  20. package/dist/testing/fakes.js +12 -104
  21. package/dist/testing/index.d.ts +8 -3
  22. package/dist/testing/index.js +8 -2
  23. package/docs/api-business-time.md +47 -30
  24. package/docs/api-db.md +48 -29
  25. package/docs/api-offline.md +11 -15
  26. package/docs/api-root.md +64 -58
  27. package/docs/api-testing.md +41 -13
  28. package/docs/api.md +35 -12
  29. package/docs/cli.md +11 -7
  30. package/docs/data-layer.md +78 -6
  31. package/docs/development.md +125 -5
  32. package/docs/http-auth.md +15 -0
  33. package/docs/realtime-offline.md +15 -0
  34. package/docs/role-policies.md +4 -4
  35. package/docs/testing-operations.md +25 -2
  36. package/package.json +24 -18
  37. package/scripts/db-baseline.mjs +3 -68
  38. package/scripts/workspace-package-smoke.mjs +206 -0
  39. package/dist/business-time/types.d.ts +0 -9
  40. package/dist/business-time/types.js +0 -5
  41. package/dist/db/columns.d.ts +0 -38
  42. package/dist/db/columns.js +0 -38
  43. package/dist/db/connection.d.ts +0 -74
  44. package/dist/db/connection.js +0 -67
  45. package/dist/db/database.d.ts +0 -222
  46. package/dist/db/database.js +0 -146
  47. package/dist/db/jst.d.ts +0 -45
  48. package/dist/db/jst.js +0 -47
  49. package/dist/db/migrate.d.ts +0 -51
  50. package/dist/db/migrate.js +0 -109
  51. package/dist/db/orm-config.d.ts +0 -127
  52. package/dist/db/orm-config.js +0 -122
  53. package/dist/db/retry.d.ts +0 -28
  54. package/dist/db/retry.js +0 -44
  55. package/dist/db/write-result.d.ts +0 -39
  56. package/dist/db/write-result.js +0 -34
@@ -1,146 +0,0 @@
1
- import { createConnection } from 'mysql2/promise';
2
- import { hyperdriveConnectionOptions } from './connection.js';
3
- import { retryWhenDeadlock } from './retry.js';
4
- /**
5
- * Assemble a {@link Database} from an already-connected ORM and replica.
6
- *
7
- * @remarks
8
- * The caller (typically the worker entry point) owns creating the connections and the ORM, and is
9
- * responsible for closing the connections; this variant does not manage their lifecycle.
10
- *
11
- * @typeParam TDrizzle - the consumer's Drizzle ORM type.
12
- * @param options - the write ORM and the read connection.
13
- * @returns a {@link Database} backed by the supplied ORM and replica.
14
- * @example
15
- * ```ts
16
- * const db = createMysqlDatabase({
17
- * orm: drizzle(primary, { schema, ...DRIZZLE_ORM_OPTIONS }),
18
- * replica,
19
- * });
20
- * const rows = await db.read<User>('SELECT * FROM users WHERE id = ?', [id]);
21
- * ```
22
- */
23
- export function createMysqlDatabase(options) {
24
- return databaseFrom(options.orm, options.replica);
25
- }
26
- /**
27
- * Create a {@link HyperdriveDatabase} that lazily opens its connections from Hyperdrive bindings.
28
- *
29
- * @remarks
30
- * Construct one per request. Connections and the ORM are created on first use and reused for the
31
- * lifetime of the instance. `read()` uses the replica, while `query()` provides an explicit
32
- * primary SELECT path. Workers automatically cleans up connections at the end of the invocation,
33
- * so callers do not need to close them manually. Either SELECT path is repeated once on a fresh
34
- * connection after a fatal mysql2 connection error. Writes and transactions are never repeated
35
- * for connection errors because their commit state can be ambiguous.
36
- *
37
- * @typeParam TDrizzle - the consumer's Drizzle ORM type.
38
- * @param options - the primary/replica Hyperdrive bindings, the ORM factory, and connection options.
39
- * @returns a {@link HyperdriveDatabase} whose compatibility `dispose()` method is a no-op; Workers
40
- * cleans up its invocation-scoped connections automatically.
41
- * @example
42
- * ```ts
43
- * const db = createHyperdriveDatabase({
44
- * primaryHyperdrive: env.PRIMARY,
45
- * replicaHyperdrive: env.REPLICA,
46
- * createOrm: (primary) => drizzle(primary, { schema, ...DRIZZLE_ORM_OPTIONS }),
47
- * });
48
- * await db.write((dz) => dz.insert(users).values(user));
49
- * ```
50
- */
51
- export function createHyperdriveDatabase(options) {
52
- const { primaryHyperdrive, replicaHyperdrive, createOrm, connectionOptions } = options;
53
- let primaryConn;
54
- let replicaConn;
55
- let orm;
56
- const primary = () => (primaryConn ??= connect(primaryHyperdrive, connectionOptions));
57
- const replica = () => (replicaConn ??= connect(replicaHyperdrive, connectionOptions));
58
- const ormFor = async () => (orm ??= createOrm(await primary()));
59
- const readFrom = (connection, sql, params) => retryWhenDeadlock(async () => {
60
- const [rows] = (await (await connection).query(sql, params));
61
- return rows;
62
- });
63
- const readWithRecovery = async (connectionFor, reset, sql, params) => {
64
- const connection = connectionFor();
65
- const outcome = await readFrom(connection, sql, params).then((rows) => ({ ok: true, rows }), (error) => ({ ok: false, error }));
66
- if (outcome.ok) {
67
- return outcome.rows;
68
- }
69
- if (!isFatalConnectionError(outcome.error)) {
70
- throw outcome.error;
71
- }
72
- reset(connection);
73
- return readFrom(connectionFor(), sql, params);
74
- };
75
- return {
76
- read(sql, params = []) {
77
- return readWithRecovery(replica, (failedConnection) => {
78
- if (replicaConn === failedConnection) {
79
- replicaConn = undefined;
80
- }
81
- }, sql, params);
82
- },
83
- query(sql, params = []) {
84
- return readWithRecovery(primary, (failedConnection) => {
85
- if (primaryConn === failedConnection) {
86
- primaryConn = undefined;
87
- orm = undefined;
88
- }
89
- }, sql, params);
90
- },
91
- async write(fn) {
92
- const dz = await ormFor();
93
- return retryWhenDeadlock(() => fn(dz));
94
- },
95
- async transaction(fn) {
96
- const dz = (await ormFor());
97
- return retryWhenDeadlock(() => dz.transaction(fn));
98
- },
99
- /** @deprecated Workers cleans up invocation-scoped connections automatically. */
100
- async dispose() {
101
- return;
102
- },
103
- };
104
- }
105
- /**
106
- * Internal helper that assembles a {@link Database} from an ORM and a replica connection.
107
- *
108
- * @typeParam TDrizzle - the consumer's Drizzle ORM type.
109
- * @param orm - the Drizzle ORM used for writes and transactions.
110
- * @param replica - the connection used for reads.
111
- * @returns a {@link Database} wiring reads to `replica` and writes to `orm`, both with deadlock retry.
112
- * @internal
113
- */
114
- export function databaseFrom(orm, replica) {
115
- const drizzleLike = orm;
116
- return {
117
- read(sql, params = []) {
118
- return retryWhenDeadlock(async () => {
119
- const [rows] = (await replica.query(sql, params));
120
- return rows;
121
- });
122
- },
123
- write(fn) {
124
- return retryWhenDeadlock(() => fn(orm));
125
- },
126
- transaction(fn) {
127
- return retryWhenDeadlock(() => drizzleLike.transaction(fn));
128
- },
129
- };
130
- }
131
- function connect(hyperdrive, extra) {
132
- return createConnection(hyperdriveConnectionOptions(hyperdrive, extra));
133
- }
134
- function isFatalConnectionError(error) {
135
- let current = error;
136
- const seen = new Set();
137
- while (typeof current === 'object' && current !== null && !seen.has(current)) {
138
- seen.add(current);
139
- const value = current;
140
- if (value.fatal === true) {
141
- return true;
142
- }
143
- current = value.cause;
144
- }
145
- return false;
146
- }
package/dist/db/jst.d.ts DELETED
@@ -1,45 +0,0 @@
1
- /**
2
- * JST wire conversion and DATE-column normalization for MySQL / Drizzle.
3
- *
4
- * @remarks
5
- * Business-time semantics are consolidated in {@link ../business-time/index.js | business-time}. This
6
- * module only owns the MySQL connection default, the DATE column's `toDriver`, and the column
7
- * `customType` params.
8
- */
9
- import type { BusinessDate } from '../business-time/index.js';
10
- /** Default mysql2 connection `timezone` (for the existing JST DB deployment). */
11
- export declare const MYSQL_TIMEZONE = "+09:00";
12
- /**
13
- * Normalize a client input to `YYYY-MM-DD` (a JST business calendar date) for a MySQL `DATE` column.
14
- * Accepts ISO 8601 / `YYYY-MM-DD` / empty strings. A `YYYY-MM-DD` value is passed through without
15
- * constructing a `Date`.
16
- *
17
- * @param value - the string or nullish input to normalize.
18
- * @returns the business date as `YYYY-MM-DD`, or `null` when the input cannot be resolved.
19
- */
20
- export declare function toJstDate(value: string | null | undefined): BusinessDate | null;
21
- /**
22
- * Build the params for a `customType` backing a MySQL `timestamp` column with `Date` pass-through.
23
- *
24
- * @param fsp - optional fractional-seconds precision; when provided, emits `timestamp(fsp)`.
25
- */
26
- export declare const jstTimestampParams: (fsp?: number) => {
27
- dataType: () => string;
28
- };
29
- /**
30
- * Build the params for a `customType` backing a MySQL `datetime` column with `Date` pass-through.
31
- *
32
- * @param fsp - optional fractional-seconds precision; when provided, emits `datetime(fsp)`.
33
- */
34
- export declare const jstDatetimeParams: (fsp?: number) => {
35
- dataType: () => string;
36
- };
37
- /**
38
- * Build the params for a `customType` backing a MySQL `date` column with JST normalization.
39
- *
40
- * @returns params with `toDriver` running {@link toJstDate}.
41
- */
42
- export declare const jstDateParams: () => {
43
- dataType: () => string;
44
- toDriver: (value: string | null) => string | null;
45
- };
package/dist/db/jst.js DELETED
@@ -1,47 +0,0 @@
1
- /**
2
- * JST wire conversion and DATE-column normalization for MySQL / Drizzle.
3
- *
4
- * @remarks
5
- * Business-time semantics are consolidated in {@link ../business-time/index.js | business-time}. This
6
- * module only owns the MySQL connection default, the DATE column's `toDriver`, and the column
7
- * `customType` params.
8
- */
9
- import { normalizeBusinessDate } from '../business-time/index.js';
10
- /** Default mysql2 connection `timezone` (for the existing JST DB deployment). */
11
- export const MYSQL_TIMEZONE = '+09:00';
12
- /**
13
- * Normalize a client input to `YYYY-MM-DD` (a JST business calendar date) for a MySQL `DATE` column.
14
- * Accepts ISO 8601 / `YYYY-MM-DD` / empty strings. A `YYYY-MM-DD` value is passed through without
15
- * constructing a `Date`.
16
- *
17
- * @param value - the string or nullish input to normalize.
18
- * @returns the business date as `YYYY-MM-DD`, or `null` when the input cannot be resolved.
19
- */
20
- export function toJstDate(value) {
21
- return normalizeBusinessDate(value ?? null);
22
- }
23
- /**
24
- * Build the params for a `customType` backing a MySQL `timestamp` column with `Date` pass-through.
25
- *
26
- * @param fsp - optional fractional-seconds precision; when provided, emits `timestamp(fsp)`.
27
- */
28
- export const jstTimestampParams = (fsp) => ({
29
- dataType: () => (fsp != null ? `timestamp(${fsp})` : 'timestamp'),
30
- });
31
- /**
32
- * Build the params for a `customType` backing a MySQL `datetime` column with `Date` pass-through.
33
- *
34
- * @param fsp - optional fractional-seconds precision; when provided, emits `datetime(fsp)`.
35
- */
36
- export const jstDatetimeParams = (fsp) => ({
37
- dataType: () => (fsp != null ? `datetime(${fsp})` : 'datetime'),
38
- });
39
- /**
40
- * Build the params for a `customType` backing a MySQL `date` column with JST normalization.
41
- *
42
- * @returns params with `toDriver` running {@link toJstDate}.
43
- */
44
- export const jstDateParams = () => ({
45
- dataType: () => 'date',
46
- toDriver: (value) => toJstDate(value),
47
- });
@@ -1,51 +0,0 @@
1
- import type { QueryRunner } from './database.js';
2
- /** Identifying info for the baseline (i.e. first) migration. */
3
- export interface BaselineEntry {
4
- /** The migration tag (e.g. `0000_melted_weapon_omega`). */
5
- tag: string;
6
- /** The `when` from `_journal.json` (= drizzle's `created_at` / `folderMillis`). */
7
- when: number;
8
- /** The sha256 of the raw `<tag>.sql` contents (the same algorithm as drizzle). */
9
- hash: string;
10
- }
11
- /**
12
- * Read the baseline (first) entry from `migrationsFolder` (drizzle's `out`, e.g. `./drizzle`).
13
- *
14
- * @param migrationsFolder - the folder containing `meta/_journal.json` and `<tag>.sql`.
15
- * @returns the baseline entry (tag/when/hash).
16
- * @throws Error when the journal is missing, the entries are empty, or `<tag>.sql` is missing.
17
- */
18
- export declare function readBaselineEntry(migrationsFolder: string): BaselineEntry;
19
- /** Options for {@link baselineMigrations}. */
20
- export interface BaselineMigrationsOptions {
21
- /** A QueryRunner for raw SQL (a mysql2 `Connection`/`Pool` is assignable). Must already be connected to the target DB. */
22
- db: QueryRunner;
23
- /** Drizzle's `out` folder (defaults to `./drizzle`). */
24
- migrationsFolder?: string;
25
- }
26
- /** The result of {@link baselineMigrations}. */
27
- export type BaselineResult = {
28
- status: 'inserted';
29
- tag: string;
30
- when: number;
31
- hash: string;
32
- } | {
33
- status: 'already-baselined';
34
- tag: string;
35
- when: number;
36
- };
37
- /**
38
- * Record the baseline (0000) as "applied" on an existing DB. Idempotent, with safety guards.
39
- *
40
- * @remarks
41
- * Guards:
42
- * - If a baseline marker (`created_at = when`) already exists → **no-op** (`already-baselined`).
43
- * - If there is no marker but `__drizzle_migrations` has other rows → **abort** (unexpected state).
44
- * - If the target DB has no base tables (an empty DB) → **abort** (skipping 0000 on an empty DB would
45
- * never create the tables; use `db:migrate` for a fresh DB).
46
- *
47
- * @param options - the connection and migrations folder; see {@link BaselineMigrationsOptions}.
48
- * @returns whether a marker was inserted or the DB was already baselined.
49
- * @throws Error when one of the guards above trips.
50
- */
51
- export declare function baselineMigrations(options: BaselineMigrationsOptions): Promise<BaselineResult>;
@@ -1,109 +0,0 @@
1
- /**
2
- * Brownfield baseline for Drizzle MySQL migrations.
3
- *
4
- * An existing (in-production) DB already has its schema, so running the committed baseline migration
5
- * (`drizzle/0000_*.sql` = the CREATE TABLE statements introspected from the current schema) via
6
- * `db:migrate` fails as every table collides. Instead, 0000 is **recorded as "applied" without being
7
- * executed**.
8
- *
9
- * How "applied" is decided (drizzle-orm/mysql-core dialect.migrate): it determines the pending set from
10
- * **only the maximum `created_at`** in `__drizzle_migrations(id, hash, created_at)`, running just the
11
- * migrations where `max(created_at) < entry.when`. The hash is stored but not used for the decision. So
12
- * inserting one row as the 0000 marker — `(hash, created_at = that entry's when)` — makes subsequent
13
- * `db:migrate` runs apply only the later 0001+ (larger `when`) and skip 0000. A fresh / test DB has no
14
- * marker, so the full chain runs (behavior unchanged).
15
- *
16
- * This function does not depend on `drizzle-orm` (it reads the journal/SQL itself and hashes with the
17
- * same sha256 as drizzle). It runs raw SQL against a QueryRunner (a mysql2 `Connection`/`Pool` is
18
- * structurally assignable).
19
- *
20
- * @packageDocumentation
21
- */
22
- import { createHash } from 'node:crypto';
23
- import { existsSync, readFileSync } from 'node:fs';
24
- import { join } from 'node:path';
25
- /** The default migration-tracking table name used by drizzle. */
26
- const MIGRATIONS_TABLE = '__drizzle_migrations';
27
- /**
28
- * Read the baseline (first) entry from `migrationsFolder` (drizzle's `out`, e.g. `./drizzle`).
29
- *
30
- * @param migrationsFolder - the folder containing `meta/_journal.json` and `<tag>.sql`.
31
- * @returns the baseline entry (tag/when/hash).
32
- * @throws Error when the journal is missing, the entries are empty, or `<tag>.sql` is missing.
33
- */
34
- export function readBaselineEntry(migrationsFolder) {
35
- const journalPath = join(migrationsFolder, 'meta', '_journal.json');
36
- if (!existsSync(journalPath)) {
37
- throw new Error(`Can't find meta/_journal.json under ${migrationsFolder}. Run \`drizzle-kit generate\` first.`);
38
- }
39
- const journal = JSON.parse(readFileSync(journalPath, 'utf8'));
40
- const entries = journal.entries ?? [];
41
- if (entries.length === 0) {
42
- throw new Error(`No migration entries in ${journalPath}.`);
43
- }
44
- // The origin is always the first entry (0000). Later 0001+ are "new changes" that should run even on
45
- // an existing DB.
46
- const first = entries[0];
47
- const sqlPath = join(migrationsFolder, `${first.tag}.sql`);
48
- if (!existsSync(sqlPath)) {
49
- throw new Error(`Can't find ${first.tag}.sql under ${migrationsFolder}.`);
50
- }
51
- const sql = readFileSync(sqlPath, 'utf8');
52
- return { tag: first.tag, when: first.when, hash: createHash('sha256').update(sql).digest('hex') };
53
- }
54
- async function rowsOf(db, sql, params) {
55
- const result = (await db.query(sql, params));
56
- return result[0] ?? [];
57
- }
58
- /**
59
- * Record the baseline (0000) as "applied" on an existing DB. Idempotent, with safety guards.
60
- *
61
- * @remarks
62
- * Guards:
63
- * - If a baseline marker (`created_at = when`) already exists → **no-op** (`already-baselined`).
64
- * - If there is no marker but `__drizzle_migrations` has other rows → **abort** (unexpected state).
65
- * - If the target DB has no base tables (an empty DB) → **abort** (skipping 0000 on an empty DB would
66
- * never create the tables; use `db:migrate` for a fresh DB).
67
- *
68
- * @param options - the connection and migrations folder; see {@link BaselineMigrationsOptions}.
69
- * @returns whether a marker was inserted or the DB was already baselined.
70
- * @throws Error when one of the guards above trips.
71
- */
72
- export async function baselineMigrations(options) {
73
- const { db, migrationsFolder = './drizzle' } = options;
74
- const baseline = readBaselineEntry(migrationsFolder);
75
- // Same DDL as the migrator (a no-op if it already exists).
76
- await db.query(`create table if not exists \`${MIGRATIONS_TABLE}\` (
77
- id serial primary key,
78
- hash text not null,
79
- created_at bigint
80
- )`);
81
- // If a baseline marker already exists, this is an idempotent no-op.
82
- const existing = await rowsOf(db, `select id from \`${MIGRATIONS_TABLE}\` where created_at = ? limit 1`, [
83
- baseline.when,
84
- ]);
85
- if (existing.length > 0) {
86
- return { status: 'already-baselined', tag: baseline.tag, when: baseline.when };
87
- }
88
- // No marker but rows exist = already in some other state. Abort to avoid misfiring.
89
- const countRows = await rowsOf(db, `select count(*) as n from \`${MIGRATIONS_TABLE}\``);
90
- const rowCount = Number(countRows[0]?.n ?? 0);
91
- if (rowCount > 0) {
92
- throw new Error(`${MIGRATIONS_TABLE} already has ${rowCount} row(s) but no baseline marker (created_at=${baseline.when}). ` +
93
- `Migration state is unexpected — refusing to insert. Inspect \`${MIGRATIONS_TABLE}\` manually.`);
94
- }
95
- // Baselining an empty DB is dangerous (treating 0000 as skipped would never create the tables).
96
- // Confirm this is a brownfield DB.
97
- const tableRows = await rowsOf(db, `select count(*) as n from information_schema.tables
98
- where table_schema = DATABASE() and table_type = 'BASE TABLE' and table_name <> ?`, [MIGRATIONS_TABLE]);
99
- const baseTableCount = Number(tableRows[0]?.n ?? 0);
100
- if (baseTableCount === 0) {
101
- throw new Error(`Target DB has no base tables. baseline records 0000 as applied WITHOUT creating tables — this is only ` +
102
- `for existing (brownfield) DBs. For a fresh/empty DB run \`drizzle-kit migrate\` instead.`);
103
- }
104
- await db.query(`insert into \`${MIGRATIONS_TABLE}\` (\`hash\`, \`created_at\`) values (?, ?)`, [
105
- baseline.hash,
106
- baseline.when,
107
- ]);
108
- return { status: 'inserted', tag: baseline.tag, when: baseline.when, hash: baseline.hash };
109
- }
@@ -1,127 +0,0 @@
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 honoDrizzleConfig}).
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
- * kit only ever 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 declare const DRIZZLE_ORM_OPTIONS: {
34
- readonly mode: "default";
35
- readonly casing: "snake_case";
36
- };
37
- /**
38
- * Options for {@link honoDrizzleConfig}.
39
- */
40
- export interface HonoDrizzleConfigOptions {
41
- /** drizzle-kit `dbCredentials.database` — the database name to connect to. */
42
- database: string;
43
- /** Database host; defaults to `process.env.DB_HOST` then `127.0.0.1`. */
44
- host?: string;
45
- /** Database port; defaults to `process.env.DB_PORT` then `3306`. */
46
- port?: number;
47
- /** Database user; defaults to `process.env.DB_USER` then `root`. */
48
- user?: string;
49
- /** Database password; defaults to `process.env.DB_PASSWORD` then `root`. */
50
- password?: string;
51
- /** Path to the schema directory; defaults to `'./src/db/schemes'`. */
52
- schema?: string;
53
- /** Output directory for generated migrations; defaults to `'./drizzle'`. */
54
- out?: string;
55
- /**
56
- * Optional table allow-list. Use this to restrict drizzle-kit to the schema's own tables when the
57
- * database is shared with another application.
58
- */
59
- tablesFilter?: string[];
60
- /**
61
- * Optional `db:introspect` (DB → JS) casing. This is an independent axis from the generation-side
62
- * `casing: 'snake_case'` and only affects introspection output.
63
- */
64
- introspect?: {
65
- casing: 'camel' | 'preserve';
66
- };
67
- }
68
- /**
69
- * Build a `drizzle.config.ts` configuration object with the kit's standard defaults.
70
- *
71
- * Fixes `casing: 'snake_case'`, the `schema`/`out` paths, and `dbCredentials` (with env-based
72
- * defaults), while leaving `tablesFilter` and `introspect` opt-in.
73
- *
74
- * @remarks
75
- * Returns a plain object rather than a typed drizzle-kit config so that `drizzle-kit` need not be a
76
- * dependency of the kit; the drizzle-kit CLI only reads the default export.
77
- *
78
- * @param options - configuration overrides; only `database` is required.
79
- * @returns a plain configuration object suitable for `export default` in `drizzle.config.ts`.
80
- * @example
81
- * ```ts
82
- * // drizzle.config.ts
83
- * import { honoDrizzleConfig } from '@rdlabo/workers-hono-kit/db';
84
- *
85
- * export default honoDrizzleConfig({ database: 'app' });
86
- * ```
87
- */
88
- export declare function honoDrizzleConfig(options: HonoDrizzleConfigOptions): {
89
- dbCredentials: {
90
- host: string;
91
- port: number;
92
- user: string;
93
- password: string;
94
- database: string;
95
- };
96
- introspect?: {
97
- casing: "camel" | "preserve";
98
- } | undefined;
99
- tablesFilter?: string[] | undefined;
100
- dialect: "mysql";
101
- schema: string;
102
- out: string;
103
- casing: "snake_case";
104
- };
105
- /** The return value of {@link resolveDbSecret} (normalized connection info). */
106
- export interface ResolvedDbSecret {
107
- host: string;
108
- port: number;
109
- dbname: string;
110
- username: string;
111
- password: string;
112
- }
113
- /**
114
- * Resolve an AWS RDS managed secret (a JSON string placed in `DB_SECRET`).
115
- *
116
- * @remarks
117
- * - `DB_SECRET` unset → `undefined` (the normal local / `db:generate` fallback).
118
- * - When set, it must be complete connection info: **invalid JSON / a missing required key throws**
119
- * (rather than silently falling back to localhost and causing an incident). A missing `port` alone
120
- * defaults to 3306.
121
- *
122
- * Both `honoDrizzleConfig` (db:migrate) and the `workers-hono-kit-db-baseline` bin use this same logic.
123
- *
124
- * @returns the resolved connection info, or `undefined` when `DB_SECRET` is unset.
125
- * @throws Error when `DB_SECRET` is set but is not valid JSON or is missing a required key.
126
- */
127
- export declare function resolveDbSecret(): ResolvedDbSecret | undefined;
@@ -1,122 +0,0 @@
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 honoDrizzleConfig}).
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
- * kit only ever 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 kit's standard 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 the kit; 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 { honoDrizzleConfig } from '@rdlabo/workers-hono-kit/db';
50
- *
51
- * export default honoDrizzleConfig({ database: 'app' });
52
- * ```
53
- */
54
- export function honoDrizzleConfig(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
- * Resolve an AWS RDS managed secret (a JSON string placed in `DB_SECRET`).
90
- *
91
- * @remarks
92
- * - `DB_SECRET` unset → `undefined` (the normal local / `db:generate` fallback).
93
- * - When set, it must be complete connection info: **invalid JSON / a missing required key throws**
94
- * (rather than silently falling back to localhost and causing an incident). A missing `port` alone
95
- * defaults to 3306.
96
- *
97
- * Both `honoDrizzleConfig` (db:migrate) and the `workers-hono-kit-db-baseline` bin use this same logic.
98
- *
99
- * @returns the resolved connection info, or `undefined` when `DB_SECRET` is unset.
100
- * @throws Error when `DB_SECRET` is set but is not valid JSON or is missing a required key.
101
- */
102
- export function resolveDbSecret() {
103
- const raw = process.env.DB_SECRET;
104
- if (!raw) {
105
- return undefined;
106
- }
107
- let parsed;
108
- try {
109
- parsed = JSON.parse(raw);
110
- }
111
- catch {
112
- throw new Error('DB_SECRET is set but is not valid JSON (expected an AWS RDS managed secret string).');
113
- }
114
- const { host, dbname, username, password } = parsed;
115
- if (typeof host !== 'string' ||
116
- typeof dbname !== 'string' ||
117
- typeof username !== 'string' ||
118
- typeof password !== 'string') {
119
- throw new Error('DB_SECRET must contain string host, dbname, username, password (AWS RDS managed secret shape).');
120
- }
121
- return { host, dbname, username, password, port: parsed.port === undefined ? 3306 : Number(parsed.port) };
122
- }
@@ -1,28 +0,0 @@
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>;