@rdlabo/workers-hono-kit 0.1.0 → 0.2.1

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 (97) hide show
  1. package/README.md +126 -11
  2. package/dist/ai/gateway.d.ts +54 -16
  3. package/dist/ai/gateway.js +37 -12
  4. package/dist/aws/cloudfront.d.ts +23 -5
  5. package/dist/aws/cloudfront.js +45 -6
  6. package/dist/aws/secrets-manager.d.ts +38 -4
  7. package/dist/aws/secrets-manager.js +48 -3
  8. package/dist/cache/kv-cache.d.ts +173 -10
  9. package/dist/cache/kv-cache.js +139 -7
  10. package/dist/db/connection.d.ts +56 -14
  11. package/dist/db/connection.js +39 -13
  12. package/dist/db/database.d.ts +160 -24
  13. package/dist/db/database.js +51 -7
  14. package/dist/db/index.d.ts +21 -10
  15. package/dist/db/index.js +17 -8
  16. package/dist/db/jst.d.ts +89 -6
  17. package/dist/db/jst.js +89 -23
  18. package/dist/db/orm-config.d.ts +61 -19
  19. package/dist/db/orm-config.js +43 -14
  20. package/dist/db/retry.d.ts +25 -3
  21. package/dist/db/retry.js +25 -3
  22. package/dist/db/write-result.d.ts +27 -4
  23. package/dist/db/write-result.js +22 -1
  24. package/dist/firebase/firebase-verifier.d.ts +53 -4
  25. package/dist/firebase/identity-toolkit.d.ts +54 -5
  26. package/dist/firebase/identity-toolkit.js +51 -0
  27. package/dist/firebase/jose-firebase-verifier.d.ts +81 -9
  28. package/dist/firebase/jose-firebase-verifier.js +68 -7
  29. package/dist/firebase/remote-verifier.d.ts +43 -5
  30. package/dist/firebase/remote-verifier.js +60 -11
  31. package/dist/http/app-env.d.ts +41 -8
  32. package/dist/http/app-env.js +38 -8
  33. package/dist/http/app-info.d.ts +25 -3
  34. package/dist/http/app-info.js +16 -2
  35. package/dist/http/http-status.d.ts +12 -3
  36. package/dist/http/http-status.js +12 -3
  37. package/dist/http/nest-error.d.ts +90 -29
  38. package/dist/http/nest-error.js +59 -18
  39. package/dist/http/user-protocol.d.ts +23 -3
  40. package/dist/http/user-protocol.js +14 -2
  41. package/dist/index.d.ts +41 -30
  42. package/dist/index.js +29 -21
  43. package/dist/middleware/auth.d.ts +75 -14
  44. package/dist/middleware/auth.js +31 -7
  45. package/dist/middleware/finalize-response.d.ts +30 -0
  46. package/dist/middleware/finalize-response.js +41 -12
  47. package/dist/middleware/validation.d.ts +83 -9
  48. package/dist/middleware/validation.js +52 -9
  49. package/dist/middleware/zod-coerce.d.ts +56 -2
  50. package/dist/middleware/zod-coerce.js +68 -9
  51. package/dist/stripe/client.d.ts +46 -9
  52. package/dist/stripe/client.js +41 -3
  53. package/dist/testing/auth.d.ts +60 -12
  54. package/dist/testing/auth.js +58 -10
  55. package/dist/testing/configurable-fake.d.ts +20 -9
  56. package/dist/testing/configurable-fake.js +23 -11
  57. package/dist/testing/db.d.ts +81 -12
  58. package/dist/testing/db.js +23 -1
  59. package/dist/testing/fakes.d.ts +79 -11
  60. package/dist/testing/fakes.js +70 -8
  61. package/dist/testing/index.d.ts +15 -8
  62. package/dist/testing/index.js +15 -10
  63. package/dist/testing/stripe-fixtures.d.ts +93 -3
  64. package/dist/testing/stripe-fixtures.js +93 -3
  65. package/package.json +19 -9
  66. package/src/ai/gateway.ts +66 -27
  67. package/src/aws/cloudfront.ts +46 -6
  68. package/src/aws/secrets-manager.ts +56 -7
  69. package/src/cache/kv-cache.ts +194 -12
  70. package/src/db/connection.ts +56 -14
  71. package/src/db/database.ts +163 -27
  72. package/src/db/index.ts +21 -12
  73. package/src/db/jst.ts +89 -23
  74. package/src/db/orm-config.ts +61 -19
  75. package/src/db/retry.ts +25 -3
  76. package/src/db/write-result.ts +27 -4
  77. package/src/firebase/firebase-verifier.ts +53 -4
  78. package/src/firebase/identity-toolkit.ts +57 -5
  79. package/src/firebase/jose-firebase-verifier.ts +81 -11
  80. package/src/firebase/remote-verifier.ts +61 -12
  81. package/src/http/app-env.ts +41 -8
  82. package/src/http/app-info.ts +25 -3
  83. package/src/http/http-status.ts +12 -3
  84. package/src/http/nest-error.ts +106 -37
  85. package/src/http/user-protocol.ts +23 -3
  86. package/src/index.ts +47 -33
  87. package/src/middleware/auth.ts +79 -17
  88. package/src/middleware/finalize-response.ts +41 -12
  89. package/src/middleware/validation.ts +89 -15
  90. package/src/middleware/zod-coerce.ts +68 -9
  91. package/src/stripe/client.ts +46 -9
  92. package/src/testing/auth.ts +60 -12
  93. package/src/testing/configurable-fake.ts +23 -11
  94. package/src/testing/db.ts +82 -13
  95. package/src/testing/fakes.ts +80 -12
  96. package/src/testing/index.ts +18 -13
  97. package/src/testing/stripe-fixtures.ts +93 -3
@@ -1,43 +1,108 @@
1
1
  import { createConnection } from 'mysql2/promise';
2
2
  import type { Connection, Pool } from 'mysql2/promise';
3
- import { hyperdriveConnectionOptions } from './connection';
4
- import type { HyperdriveLike } from './connection';
5
- import { retryWhenDeadlock } from './retry';
3
+ import { hyperdriveConnectionOptions } from './connection.js';
4
+ import type { HyperdriveLike } from './connection.js';
5
+ import { retryWhenDeadlock } from './retry.js';
6
6
 
7
7
  /**
8
- * フリート共通のデータ層(NestJS/TypeORM master/slave + retryWhenDeadlock 置換)。
9
- * - reads → replica。透明性のため生 SQL を維持(旧 helper.slave().query 相当)。
10
- * - writes → primary。型安全のため Drizzle 経由。ただし `write(fn)`/`transaction(fn)` 経由のみで、
11
- * builder は公開しない。builder はここで await される(Drizzle builder lazy thenable のため
12
- * `return builder` が黙って no-op になるフットガンを排除)。両者とも ER_LOCK_DEADLOCK を retry。
13
- *
14
- * 重要: kit は `drizzle-orm` の **型同一性に依存しない**。各 repo が自分の drizzle-orm で作った orm を
15
- * 渡す(`Database` orm `TDrizzle` にジェネリック)。これにより kit repo drizzle-orm
16
- * コピーが分かれても(symlink 構成)`MySqlTable`/`SQL` のブランド衝突が起きない。
8
+ * Dual-connection data layer that separates reads from writes.
9
+ *
10
+ * @remarks
11
+ * The two sides of the database are deliberately handled differently:
12
+ *
13
+ * - Reads go to the **replica** as raw SQL (`QueryRunner.query`) for transparency, returning plain
14
+ * rows.
15
+ * - Writes and transactions go to the **primary** through the Drizzle ORM for type safety, but only
16
+ * via `write(fn)` / `transaction(fn)` — the raw query builder is never exposed. The builder is
17
+ * awaited inside those methods, which removes a foot-gun: a Drizzle builder is a lazy thenable, so
18
+ * a bare `return builder` would silently become a no-op.
19
+ *
20
+ * Both sides retry on `ER_LOCK_DEADLOCK`.
21
+ *
22
+ * The kit deliberately avoids depending on the type identity of `drizzle-orm`: the consumer creates
23
+ * the ORM instance with its own copy of `drizzle-orm` and passes it in, and {@link Database} is
24
+ * generic over that ORM type (`TDrizzle`). This keeps the ORM's `MySqlTable`/`SQL` brands from
25
+ * clashing even when the kit and the consumer resolve separate copies of `drizzle-orm`.
17
26
  */
18
27
 
19
- /** reads 用の最小接続インターフェース(mysql2 Connection / Pool が構造的に満たす)。 */
28
+ /**
29
+ * Minimal connection interface used for reads.
30
+ *
31
+ * @remarks
32
+ * A mysql2 `Connection` or `Pool` satisfies this structurally.
33
+ */
20
34
  export interface QueryRunner {
35
+ /**
36
+ * Run a parameterized SQL query.
37
+ *
38
+ * @param sql - the SQL text, with `?` placeholders for `params`.
39
+ * @param params - optional positional parameters.
40
+ * @returns the driver's raw result (typically `[rows, fields]`).
41
+ */
21
42
  query(sql: string, params?: unknown[]): Promise<unknown>;
22
43
  }
23
44
 
24
- /** drizzle インスタンスの `.transaction(cb)` が受け取る tx ハンドルの型を取り出す。 */
45
+ /**
46
+ * Extract the transaction-handle type that a Drizzle instance passes to its `.transaction(cb)`
47
+ * callback.
48
+ *
49
+ * @typeParam TDrizzle - the consumer's Drizzle ORM type.
50
+ */
25
51
  export type TxOf<TDrizzle> = TDrizzle extends {
26
52
  transaction(cb: (tx: infer Tx) => Promise<unknown>): Promise<unknown>;
27
53
  }
28
54
  ? Tx
29
55
  : unknown;
30
56
 
57
+ /**
58
+ * The read/write surface of the data layer.
59
+ *
60
+ * @typeParam TDrizzle - the consumer's Drizzle ORM type used for writes and transactions.
61
+ * @typeParam TTx - the transaction-handle type, inferred from `TDrizzle` by default.
62
+ */
31
63
  export interface Database<TDrizzle, TTx = TxOf<TDrizzle>> {
64
+ /**
65
+ * Run a raw SQL read against the replica, with deadlock retry.
66
+ *
67
+ * @typeParam T - the row shape.
68
+ * @param sql - the SQL text, with `?` placeholders for `params`.
69
+ * @param params - optional positional parameters.
70
+ * @returns the rows returned by the query.
71
+ */
32
72
  read<T>(sql: string, params?: unknown[]): Promise<T[]>;
33
- /** 単一 INSERT/UPDATE/DELETE。primary で await + deadlock retry。 */
73
+ /**
74
+ * Run a single INSERT/UPDATE/DELETE against the primary, awaited with deadlock retry.
75
+ *
76
+ * @typeParam T - the value resolved by `fn`.
77
+ * @param fn - callback that receives the Drizzle ORM and returns the awaited write.
78
+ * @returns the value resolved by `fn`.
79
+ */
34
80
  write<T>(fn: (dz: TDrizzle) => Promise<T>): Promise<T>;
35
- /** 複数 write を 1 トランザクションで。deadlock 時は全体を retry。 */
81
+ /**
82
+ * Run multiple writes inside a single transaction; the whole transaction is retried on deadlock.
83
+ *
84
+ * @typeParam T - the value resolved by `fn`.
85
+ * @param fn - callback that receives the transaction handle and returns the awaited work.
86
+ * @returns the value resolved by `fn`.
87
+ */
36
88
  transaction<T>(fn: (tx: TTx) => Promise<T>): Promise<T>;
37
89
  }
38
90
 
39
- /** dispose() を持つ Database(接続をモジュール内で開く版=Hyperdrive / Pool 背面)。 */
91
+ /**
92
+ * A {@link Database} that owns its connections and must be disposed.
93
+ *
94
+ * @remarks
95
+ * Used by the variants that open connections internally (Hyperdrive- or Pool-backed).
96
+ *
97
+ * @typeParam TDrizzle - the consumer's Drizzle ORM type.
98
+ * @typeParam TTx - the transaction-handle type, inferred from `TDrizzle` by default.
99
+ */
40
100
  export interface DisposableDatabase<TDrizzle, TTx = TxOf<TDrizzle>> extends Database<TDrizzle, TTx> {
101
+ /**
102
+ * Close the connections opened by this database.
103
+ *
104
+ * @returns a promise that settles once both connections are closed.
105
+ */
41
106
  dispose(): Promise<void>;
42
107
  }
43
108
 
@@ -45,33 +110,91 @@ interface DrizzleLike<TTx> {
45
110
  transaction<T>(cb: (tx: TTx) => Promise<T>): Promise<T>;
46
111
  }
47
112
 
113
+ /**
114
+ * Options for {@link createMysqlDatabase}.
115
+ *
116
+ * @typeParam TDrizzle - the consumer's Drizzle ORM type.
117
+ */
48
118
  export interface CreateMysqlDatabaseOptions<TDrizzle> {
49
- /** 消費側が自分の drizzle-orm で `drizzle(primary, { schema, ... })` を作って渡す(writes 用)。 */
119
+ /**
120
+ * The Drizzle ORM used for writes, created by the consumer with its own `drizzle-orm`
121
+ * (e.g. `drizzle(primary, { schema, ... })`).
122
+ */
50
123
  orm: TDrizzle;
51
- /** reads(生 SQL)用の接続。 */
124
+ /** The connection used for reads (raw SQL). */
52
125
  replica: QueryRunner;
53
126
  }
54
127
 
55
128
  /**
56
- * 接続済みの orm/replica を受け取り Database を組み立てる(receptray/tipsys MysqlDatabase 相当)。
57
- * 接続・orm の生成と接続の破棄は呼び出し側(worker entry)が担う。
129
+ * Assemble a {@link Database} from an already-connected ORM and replica.
130
+ *
131
+ * @remarks
132
+ * The caller (typically the worker entry point) owns creating the connections and the ORM, and is
133
+ * responsible for closing the connections; this variant does not manage their lifecycle.
134
+ *
135
+ * @typeParam TDrizzle - the consumer's Drizzle ORM type.
136
+ * @param options - the write ORM and the read connection.
137
+ * @returns a {@link Database} backed by the supplied ORM and replica.
138
+ * @example
139
+ * ```ts
140
+ * const db = createMysqlDatabase({
141
+ * orm: drizzle(primary, { schema, ...DRIZZLE_ORM_OPTIONS }),
142
+ * replica,
143
+ * });
144
+ * const rows = await db.read<User>('SELECT * FROM users WHERE id = ?', [id]);
145
+ * ```
58
146
  */
59
147
  export function createMysqlDatabase<TDrizzle>(options: CreateMysqlDatabaseOptions<TDrizzle>): Database<TDrizzle> {
60
148
  return databaseFrom(options.orm, options.replica);
61
149
  }
62
150
 
151
+ /**
152
+ * Options for {@link createHyperdriveDatabase}.
153
+ *
154
+ * @typeParam TDrizzle - the consumer's Drizzle ORM type.
155
+ */
63
156
  export interface CreateHyperdriveDatabaseOptions<TDrizzle> {
157
+ /** The Hyperdrive binding for the primary (write) connection. */
64
158
  primaryHyperdrive: HyperdriveLike;
159
+ /** The Hyperdrive binding for the replica (read) connection. */
65
160
  replicaHyperdrive: HyperdriveLike;
66
- /** 消費側の drizzle-orm で primary 接続から orm を作る factory(writes 用)。 */
161
+ /**
162
+ * Factory that builds the write ORM from the primary connection, using the consumer's
163
+ * `drizzle-orm`.
164
+ */
67
165
  createOrm: (primary: Connection) => TDrizzle;
68
- /** createConnection に渡す追加オプション(timezone など)。disableEval:true は既定で付与。 */
166
+ /**
167
+ * Extra options forwarded to mysql2 `createConnection`, merged on top of the defaults applied by
168
+ * {@link hyperdriveConnectionOptions} (`disableEval: true`, `decimalNumbers: true`, and
169
+ * `timezone: '+09:00'`). Pass a field here to override any of those defaults.
170
+ */
69
171
  connectionOptions?: Record<string, unknown>;
70
172
  }
71
173
 
72
174
  /**
73
- * Hyperdrive バインディングから接続を遅延生成する Database(foodlabel MysqlDatabase 相当)。
74
- * リクエスト毎に new し、レスポンス後 `dispose()` で接続を閉じる。read/write/transaction の面は同一。
175
+ * Create a {@link DisposableDatabase} that lazily opens its connections from Hyperdrive bindings.
176
+ *
177
+ * @remarks
178
+ * Construct one per request and call `dispose()` after the response to close the connections.
179
+ * Connections and the ORM are created on first use and reused for the lifetime of the instance; the
180
+ * read/write/transaction surface is identical to {@link createMysqlDatabase}.
181
+ *
182
+ * @typeParam TDrizzle - the consumer's Drizzle ORM type.
183
+ * @param options - the primary/replica Hyperdrive bindings, the ORM factory, and connection options.
184
+ * @returns a {@link DisposableDatabase} that must be disposed when done.
185
+ * @example
186
+ * ```ts
187
+ * const db = createHyperdriveDatabase({
188
+ * primaryHyperdrive: env.PRIMARY,
189
+ * replicaHyperdrive: env.REPLICA,
190
+ * createOrm: (primary) => drizzle(primary, { schema, ...DRIZZLE_ORM_OPTIONS }),
191
+ * });
192
+ * try {
193
+ * await db.write((dz) => dz.insert(users).values(user));
194
+ * } finally {
195
+ * await db.dispose();
196
+ * }
197
+ * ```
75
198
  */
76
199
  export function createHyperdriveDatabase<TDrizzle>(
77
200
  options: CreateHyperdriveDatabaseOptions<TDrizzle>,
@@ -106,7 +229,15 @@ export function createHyperdriveDatabase<TDrizzle>(
106
229
  };
107
230
  }
108
231
 
109
- /** orm と replica 接続から Database を組み立てる内部ヘルパ。 */
232
+ /**
233
+ * Internal helper that assembles a {@link Database} from an ORM and a replica connection.
234
+ *
235
+ * @typeParam TDrizzle - the consumer's Drizzle ORM type.
236
+ * @param orm - the Drizzle ORM used for writes and transactions.
237
+ * @param replica - the connection used for reads.
238
+ * @returns a {@link Database} wiring reads to `replica` and writes to `orm`, both with deadlock retry.
239
+ * @internal
240
+ */
110
241
  export function databaseFrom<TDrizzle>(orm: TDrizzle, replica: QueryRunner): Database<TDrizzle> {
111
242
  const drizzleLike = orm as DrizzleLike<TxOf<TDrizzle>>;
112
243
  return {
@@ -125,7 +256,12 @@ export function databaseFrom<TDrizzle>(orm: TDrizzle, replica: QueryRunner): Dat
125
256
  };
126
257
  }
127
258
 
128
- /** Pool/Connection は mysql2 の型。kit の QueryRunner には構造的に代入可能。 */
259
+ /**
260
+ * Re-export of the mysql2 `Connection` and `Pool` types.
261
+ *
262
+ * @remarks
263
+ * Both are structurally assignable to the kit's {@link QueryRunner}.
264
+ */
129
265
  export type { Connection, Pool };
130
266
 
131
267
  function connect(hyperdrive: HyperdriveLike, extra?: Record<string, unknown>): Promise<Connection> {
package/src/db/index.ts CHANGED
@@ -1,9 +1,18 @@
1
- // @rdlabo/workers-hono-kit/db — mysql2 依存のデータ層ヘルパ(ルート `.` は web 標準のみのため別サブパス)。
2
- // drizzle-orm の型同一性には依存しない(orm は消費側が渡す)。
1
+ /**
2
+ * Data-layer helpers that depend on `mysql2` (exposed under the `/db` subpath because the package
3
+ * root is reserved for web-standard-only code).
4
+ *
5
+ * @remarks
6
+ * This module never depends on the type identity of `drizzle-orm`: the ORM instance is always
7
+ * supplied by the consumer. That keeps the kit safe to use even when the kit and the consuming app
8
+ * resolve separate copies of `drizzle-orm`.
9
+ *
10
+ * @packageDocumentation
11
+ */
3
12
 
4
- export { retryWhenDeadlock } from './retry';
13
+ export { retryWhenDeadlock } from './retry.js';
5
14
 
6
- export { createMysqlDatabase, createHyperdriveDatabase, databaseFrom } from './database';
15
+ export { createMysqlDatabase, createHyperdriveDatabase, databaseFrom } from './database.js';
7
16
  export type {
8
17
  Database,
9
18
  DisposableDatabase,
@@ -13,15 +22,15 @@ export type {
13
22
  CreateHyperdriveDatabaseOptions,
14
23
  Connection,
15
24
  Pool,
16
- } from './database';
25
+ } from './database.js';
17
26
 
18
- export { insertIdOf, affectedRowsOf, insertedIdsOf } from './write-result';
19
- export type { DzWriteResult } from './write-result';
27
+ export { insertIdOf, affectedRowsOf, insertedIdsOf } from './write-result.js';
28
+ export type { DzWriteResult } from './write-result.js';
20
29
 
21
- export { hyperdriveConnectionOptions, withMysqlConnections } from './connection';
22
- export type { HyperdriveLike, ExecutionContextLike } from './connection';
30
+ export { hyperdriveConnectionOptions, withMysqlConnections } from './connection.js';
31
+ export type { HyperdriveLike, ExecutionContextLike } from './connection.js';
23
32
 
24
- export { toJstDate, jstTimestampParams, jstDatetimeParams, jstDateParams } from './jst';
33
+ export { toJstDate, jstTimestampParams, jstDatetimeParams, jstDateParams } from './jst.js';
25
34
 
26
- export { DRIZZLE_ORM_OPTIONS, honoDrizzleConfig } from './orm-config';
27
- export type { HonoDrizzleConfigOptions } from './orm-config';
35
+ export { DRIZZLE_ORM_OPTIONS, honoDrizzleConfig } from './orm-config.js';
36
+ export type { HonoDrizzleConfigOptions } from './orm-config.js';
package/src/db/jst.ts CHANGED
@@ -1,27 +1,45 @@
1
- // JST 日時の整形を「DB 書き込みの副作用」として列の境界に寄せるための共有部品。
2
- // drizzle-orm の値・型は import しない(kit の脱・型同一性方針)。kit で完成した customType 列を
3
- // export すると、消費側と drizzle-orm のコピーが分かれた場合に `.default(sql\`…\`)` 等で `SQL` の
4
- // private プロパティ(shouldInlineParams)が nominal 不一致になり型衝突する(実測で確認)。そのため
5
- // 列生成は消費側の `customType` に委ね、kit params とヘルパだけ供給する:
6
- //
7
- // import { customType } from 'drizzle-orm/mysql-core';
8
- // import { jstTimestampParams, jstDateParams } from '@rdlabo/workers-hono-kit/db';
9
- // export const jstTimestamp = (name: string, opts?: { fsp?: number }) =>
10
- // customType<{ data: string | Date; driverData: string | Date }>(jstTimestampParams(opts?.fsp))(name);
11
- // export const jstDate = (name: string) =>
12
- // customType<{ data: string | null; driverData: string | null }>(jstDateParams())(name);
13
- //
14
- // timestamp/datetime toDriver を置かず Date を素通し 接続の `timezone:'+09:00'`(hyperdrive
15
- // 既定)で mysql2 が JST 整形、整形済み文字列も素通し。Drizzle ネイティブ `mode:'date'` は Date を
16
- // tz 層より前に UTC 文字列化して −9h で壊れるため使わない(customType pass-through が唯一クリーン)。
17
- // date 列はクライアントの ISO/空文字を MySQL DATE が弾く+JST 日跨ぎ正規化が要るので toJstDate を残す。
1
+ /**
2
+ * Shared building blocks for normalizing JST (Asia/Tokyo) date/time values.
3
+ *
4
+ * @remarks
5
+ * JST normalization is applied at the column boundary as a write-time side effect, and neither
6
+ * values nor types from `drizzle-orm` are imported here on purpose. Exporting a fully-built
7
+ * `customType` column from the kit would cause type collisions when the kit and the consumer
8
+ * resolve separate copies of `drizzle-orm` (the private `SQL` brand stops being nominally
9
+ * compatible). Instead the kit ships only the params and helpers, and the consumer builds the
10
+ * column with its own `customType`:
11
+ *
12
+ * ```ts
13
+ * import { customType } from 'drizzle-orm/mysql-core';
14
+ * import { jstTimestampParams, jstDateParams } from '@rdlabo/workers-hono-kit/db';
15
+ *
16
+ * export const jstTimestamp = (name: string, opts?: { fsp?: number }) =>
17
+ * customType<{ data: string | Date; driverData: string | Date }>(jstTimestampParams(opts?.fsp))(name);
18
+ * export const jstDate = (name: string) =>
19
+ * customType<{ data: string | null; driverData: string | null }>(jstDateParams())(name);
20
+ * ```
21
+ *
22
+ * `timestamp`/`datetime` columns omit `toDriver` and pass `Date` values straight through, so the
23
+ * connection's `timezone: '+09:00'` default makes mysql2 format them as JST; pre-formatted strings
24
+ * also pass through. Drizzle's native `mode: 'date'` is avoided because it stringifies `Date` to
25
+ * UTC before the timezone layer, shifting values by -9h. `date` columns keep `toJstDate` because
26
+ * MySQL `DATE` rejects ISO/empty strings and a JST day-boundary normalization is required.
27
+ */
18
28
 
19
29
  const JST_OFFSET_MS = 9 * 60 * 60 * 1000;
20
30
 
21
31
  /**
22
- * クライアント送出の日付(ISO 8601 `...Z` / `YYYY-MM-DD` / 空文字)を MySQL DATE 用の
23
- * `YYYY-MM-DD`(JST)へ正規化。nullish/空/解釈不能は null。MySQL DATE は ISO を弾く
24
- * (ER_TRUNCATED_WRONG_VALUE)ため driver では代替できず、列の toDriver に必要。
32
+ * Normalize a client-supplied date to the `YYYY-MM-DD` (JST) form accepted by a MySQL `DATE` column.
33
+ *
34
+ * Accepts ISO 8601 (`...Z`), `YYYY-MM-DD`, or an empty string. Nullish, empty, or unparseable input
35
+ * resolves to `null`.
36
+ *
37
+ * @remarks
38
+ * MySQL `DATE` rejects ISO strings with `ER_TRUNCATED_WRONG_VALUE`, so this cannot be handled by the
39
+ * driver alone; it is needed as the `toDriver` transform for a `date` column.
40
+ *
41
+ * @param value - the raw date string from the client (ISO 8601, `YYYY-MM-DD`, or empty), or nullish.
42
+ * @returns the JST calendar date as `YYYY-MM-DD`, or `null` when the input is empty or unparseable.
25
43
  */
26
44
  export function toJstDate(value: string | null | undefined): string | null {
27
45
  if (!value) {
@@ -36,17 +54,65 @@ export function toJstDate(value: string | null | undefined): string | null {
36
54
  return `${jst.getUTCFullYear()}-${p(jst.getUTCMonth() + 1)}-${p(jst.getUTCDate())}`;
37
55
  }
38
56
 
39
- /** `customType` に渡す params: `timestamp(fsp)` 列(created_at 群)。Date 素通し(pass-through)。 */
57
+ /**
58
+ * Build the params for a `customType` backing a MySQL `timestamp` column with `Date` pass-through.
59
+ *
60
+ * The column omits `toDriver`, so `Date` values flow straight to mysql2 and are formatted as JST by
61
+ * the connection's `timezone: '+09:00'` default.
62
+ *
63
+ * @param fsp - optional fractional-seconds precision; when provided, emits `timestamp(fsp)`.
64
+ * @returns the `customType` params object exposing the column's `dataType`.
65
+ * @example
66
+ * ```ts
67
+ * import { customType } from 'drizzle-orm/mysql-core';
68
+ * import { jstTimestampParams } from '@rdlabo/workers-hono-kit/db';
69
+ *
70
+ * const jstTimestamp = (name: string) =>
71
+ * customType<{ data: string | Date; driverData: string | Date }>(jstTimestampParams())(name);
72
+ * ```
73
+ */
40
74
  export const jstTimestampParams = (fsp?: number): { dataType: () => string } => ({
41
75
  dataType: () => (fsp != null ? `timestamp(${fsp})` : 'timestamp'),
42
76
  });
43
77
 
44
- /** `customType` に渡す params: `datetime` 列(payment.limit_at 等)。Date 素通し(pass-through)。 */
78
+ /**
79
+ * Build the params for a `customType` backing a MySQL `datetime` column with `Date` pass-through.
80
+ *
81
+ * Behaves like {@link jstTimestampParams} but emits a `datetime` data type; `Date` values pass
82
+ * through and are formatted as JST by the connection's `timezone: '+09:00'` default.
83
+ *
84
+ * @param fsp - optional fractional-seconds precision; when provided, emits `datetime(fsp)`.
85
+ * @returns the `customType` params object exposing the column's `dataType`.
86
+ * @example
87
+ * ```ts
88
+ * import { customType } from 'drizzle-orm/mysql-core';
89
+ * import { jstDatetimeParams } from '@rdlabo/workers-hono-kit/db';
90
+ *
91
+ * const jstDatetime = (name: string) =>
92
+ * customType<{ data: string | Date; driverData: string | Date }>(jstDatetimeParams())(name);
93
+ * ```
94
+ */
45
95
  export const jstDatetimeParams = (fsp?: number): { dataType: () => string } => ({
46
96
  dataType: () => (fsp != null ? `datetime(${fsp})` : 'datetime'),
47
97
  });
48
98
 
49
- /** `customType` に渡す params: `date` 列(expiry_date 等)。toJstDate で文字列を正規化。 */
99
+ /**
100
+ * Build the params for a `customType` backing a MySQL `date` column with JST normalization.
101
+ *
102
+ * Unlike the timestamp/datetime params, this defines a `toDriver` transform that runs
103
+ * {@link toJstDate} so client-supplied ISO/empty strings are normalized to a JST `YYYY-MM-DD` value
104
+ * the column accepts.
105
+ *
106
+ * @returns the `customType` params object exposing the column's `dataType` and `toDriver`.
107
+ * @example
108
+ * ```ts
109
+ * import { customType } from 'drizzle-orm/mysql-core';
110
+ * import { jstDateParams } from '@rdlabo/workers-hono-kit/db';
111
+ *
112
+ * const jstDate = (name: string) =>
113
+ * customType<{ data: string | null; driverData: string | null }>(jstDateParams())(name);
114
+ * ```
115
+ */
50
116
  export const jstDateParams = (): {
51
117
  dataType: () => string;
52
118
  toDriver: (value: string | null) => string | null;
@@ -1,45 +1,87 @@
1
1
  /**
2
- * Drizzle の列名 casing をフリートで一元管理する(標準: snake_case を「config」と「runtime」の両方で固定)。
2
+ * Centralizes Drizzle column-name casing so it is fixed (standard: `snake_case`) in both the
3
+ * config and the runtime ORM.
3
4
  *
4
- * casing は2か所にあり別物:
5
- * drizzle.config.ts top-level `casing` `db:generate` が **作る列名** を決める(→ honoDrizzleConfig)
6
- * ② database.ts の `drizzle(conn, { …casing })` … **実行時の書き込みビルダが参照する列名** を決める(→ DRIZZLE_ORM_OPTIONS)
5
+ * @remarks
6
+ * Casing is configured in two distinct places:
7
7
  *
8
- * この2つが食い違うと、明示列名を書き忘れた camelCase 複数単語列で generate と実行時がズレて実行時
9
- * `Unknown column` になる(typecheck もマイグレーションも正常に見えるので発覚が遅い)。両方ここから取れば
10
- * ズレが構造的に起きない。明示列名がある列では casing は無視されるので、既存挙動は変えない純粋な安全網。
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}).
11
12
  *
12
- * 注: runtime `drizzle()` 呼び出し自体は **消費側 repo が自分の drizzle-orm で行う**(kit drizzle()
13
- * 呼ぶと kit repo drizzle-orm が別コピーになり型同一性が壊れるため)。kit は「値」だけを提供する。
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.
14
22
  */
15
23
 
16
24
  /**
17
- * runtime 用。消費側 repo database.ts `drizzle(conn, { schema, ...DRIZZLE_ORM_OPTIONS })` と spread して使う。
18
- * mode/casing を kit が固定し、書き込みビルダの列名解決を snake_case に揃える。
25
+ * Runtime ORM options shared by the consuming app's `drizzle()` call.
26
+ *
27
+ * Spread into the runtime ORM as `drizzle(conn, { schema, ...DRIZZLE_ORM_OPTIONS })` so the write
28
+ * builder resolves column names as `snake_case`, matching what `db:generate` creates.
29
+ *
30
+ * @remarks
31
+ * Fixes `mode: 'default'` and `casing: 'snake_case'`. See the module-level documentation for why
32
+ * the same casing must be used by both the config and the runtime ORM.
19
33
  */
20
34
  export const DRIZZLE_ORM_OPTIONS = { mode: 'default', casing: 'snake_case' } as const;
21
35
 
36
+ /**
37
+ * Options for {@link honoDrizzleConfig}.
38
+ */
22
39
  export interface HonoDrizzleConfigOptions {
23
- /** drizzle-kit dbCredentials.database(localConnectionString とは別)。 */
40
+ /** drizzle-kit `dbCredentials.database` the database name to connect to. */
24
41
  database: string;
42
+ /** Database host; defaults to `process.env.DB_HOST` then `127.0.0.1`. */
25
43
  host?: string;
44
+ /** Database port; defaults to `process.env.DB_PORT` then `3306`. */
26
45
  port?: number;
46
+ /** Database user; defaults to `process.env.DB_USER` then `root`. */
27
47
  user?: string;
48
+ /** Database password; defaults to `process.env.DB_PASSWORD` then `root`. */
28
49
  password?: string;
29
- /** 既定 './src/db/schemes' */
50
+ /** Path to the schema directory; defaults to `'./src/db/schemes'`. */
30
51
  schema?: string;
31
- /** 既定 './drizzle' */
52
+ /** Output directory for generated migrations; defaults to `'./drizzle'`. */
32
53
  out?: string;
33
- /** /api と DB を共有する repo は schema 由来テーブルに限定する(省略可)。 */
54
+ /**
55
+ * Optional table allow-list. Use this to restrict drizzle-kit to the schema's own tables when the
56
+ * database is shared with another application.
57
+ */
34
58
  tablesFilter?: string[];
35
- /** db:introspect(DB→JS)の casing。生成方向の `casing:'snake_case'` とは別軸(省略可)。 */
59
+ /**
60
+ * Optional `db:introspect` (DB → JS) casing. This is an independent axis from the generation-side
61
+ * `casing: 'snake_case'` and only affects introspection output.
62
+ */
36
63
  introspect?: { casing: 'camel' | 'preserve' };
37
64
  }
38
65
 
39
66
  /**
40
- * drizzle.config.ts 用ファクトリ。`export default honoDrizzleConfig({ database })` で使う。
41
- * casing:'snake_case'・schema/out・dbCredentials(env 既定)を kit が owner として固定する。
42
- * drizzle-kit kit の依存にしないため plain object を返す(drizzle-kit CLI default export を読むだけ)。
67
+ * Build a `drizzle.config.ts` configuration object with the kit's standard defaults.
68
+ *
69
+ * Fixes `casing: 'snake_case'`, the `schema`/`out` paths, and `dbCredentials` (with env-based
70
+ * defaults), while leaving `tablesFilter` and `introspect` opt-in.
71
+ *
72
+ * @remarks
73
+ * Returns a plain object rather than a typed drizzle-kit config so that `drizzle-kit` need not be a
74
+ * dependency of the kit; the drizzle-kit CLI only reads the default export.
75
+ *
76
+ * @param options - configuration overrides; only `database` is required.
77
+ * @returns a plain configuration object suitable for `export default` in `drizzle.config.ts`.
78
+ * @example
79
+ * ```ts
80
+ * // drizzle.config.ts
81
+ * import { honoDrizzleConfig } from '@rdlabo/workers-hono-kit/db';
82
+ *
83
+ * export default honoDrizzleConfig({ database: 'app' });
84
+ * ```
43
85
  */
44
86
  export function honoDrizzleConfig(options: HonoDrizzleConfigOptions) {
45
87
  const {
package/src/db/retry.ts CHANGED
@@ -1,7 +1,29 @@
1
1
  /**
2
- * ER_LOCK_DEADLOCK を指数バックオフで retry する(NestJS/TypeORM retryWhenDeadlock 相当)。
3
- * MySQL はデッドロック時にトランザクション全体をロールバックするため、同じ作業単位の再実行は安全。
4
- * `fn` は単一文(write)またはトランザクション全体(transaction)であること retry `fn` 全体を再実行する。
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
+ * ```
5
27
  */
6
28
  export async function retryWhenDeadlock<T>(fn: () => Promise<T>, retries = 3, delay = 100): Promise<T> {
7
29
  for (let attempt = 0; attempt < retries; attempt++) {
@@ -1,21 +1,44 @@
1
1
  /**
2
- * Drizzle(mysql2) write 結果から insertId / affectedRows を型安全に取り出すヘルパ。
3
- * 生の builder/ResultSetHeader を repo 側に晒さずに、よく使う値だけを取り出す。
2
+ * Shape of a Drizzle (mysql2) write result, narrowed to the fields callers actually read.
4
3
  *
5
- * mysql2 の INSERT/UPDATE/DELETE 結果は `[ResultSetHeader, FieldPacket[]]` 形。
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.
6
8
  */
7
9
  export type DzWriteResult = readonly [{ insertId: number; affectedRows: number }, ...unknown[]];
8
10
 
11
+ /**
12
+ * Extract the auto-increment `insertId` from a write result.
13
+ *
14
+ * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
15
+ * @returns the `insertId` reported by mysql2 (the id of the first inserted row).
16
+ */
9
17
  export function insertIdOf(result: DzWriteResult): number {
10
18
  return result[0].insertId;
11
19
  }
12
20
 
21
+ /**
22
+ * Extract the number of affected rows from a write result.
23
+ *
24
+ * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
25
+ * @returns the `affectedRows` count reported by mysql2.
26
+ */
13
27
  export function affectedRowsOf(result: DzWriteResult): number {
14
28
  return result[0].affectedRows;
15
29
  }
16
30
 
17
31
  /**
18
- * 一括 INSERT で連番採番された行の id 群を返す(mysql2 は先頭 insertId のみ返すため count 分を生成)。
32
+ * Reconstruct the auto-increment ids assigned by a bulk INSERT.
33
+ *
34
+ * @remarks
35
+ * mysql2 reports only the first `insertId` for a multi-row INSERT, so the remaining ids are derived
36
+ * by assuming a contiguous sequence (`base`, `base + 1`, …). This holds for tables with a standard
37
+ * `AUTO_INCREMENT` column and the default `innodb_autoinc_lock_mode`.
38
+ *
39
+ * @param result - the result of a bulk INSERT.
40
+ * @param count - the number of rows that were inserted.
41
+ * @returns an array of the `count` auto-increment ids, starting at the reported `insertId`.
19
42
  */
20
43
  export function insertedIdsOf(result: DzWriteResult, count: number): number[] {
21
44
  const base = result[0].insertId;