@rdlabo/workers-hono-kit 0.2.0 → 0.3.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.
Files changed (104) 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 +159 -23
  13. package/dist/db/database.js +49 -5
  14. package/dist/db/index.d.ts +11 -0
  15. package/dist/db/index.js +11 -2
  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 +79 -7
  28. package/dist/firebase/jose-firebase-verifier.js +68 -7
  29. package/dist/firebase/remote-verifier.d.ts +42 -4
  30. package/dist/firebase/remote-verifier.js +58 -9
  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 +15 -0
  42. package/dist/index.js +14 -3
  43. package/dist/middleware/auth.d.ts +74 -13
  44. package/dist/middleware/auth.js +30 -6
  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/queue/consumer.d.ts +112 -0
  52. package/dist/queue/consumer.js +80 -0
  53. package/dist/queue/send.d.ts +90 -0
  54. package/dist/queue/send.js +85 -0
  55. package/dist/stripe/client.d.ts +46 -9
  56. package/dist/stripe/client.js +41 -3
  57. package/dist/testing/auth.d.ts +58 -10
  58. package/dist/testing/auth.js +58 -10
  59. package/dist/testing/configurable-fake.d.ts +20 -9
  60. package/dist/testing/configurable-fake.js +23 -11
  61. package/dist/testing/db.d.ts +81 -12
  62. package/dist/testing/db.js +23 -1
  63. package/dist/testing/fakes.d.ts +77 -9
  64. package/dist/testing/fakes.js +69 -7
  65. package/dist/testing/index.d.ts +7 -0
  66. package/dist/testing/index.js +10 -5
  67. package/dist/testing/stripe-fixtures.d.ts +93 -3
  68. package/dist/testing/stripe-fixtures.js +93 -3
  69. package/package.json +3 -2
  70. package/scripts/check-subrequest-fanout.mjs +86 -0
  71. package/src/ai/gateway.ts +66 -27
  72. package/src/aws/cloudfront.ts +46 -6
  73. package/src/aws/secrets-manager.ts +56 -7
  74. package/src/cache/kv-cache.ts +194 -12
  75. package/src/db/connection.ts +56 -14
  76. package/src/db/database.ts +160 -24
  77. package/src/db/index.ts +11 -2
  78. package/src/db/jst.ts +89 -23
  79. package/src/db/orm-config.ts +61 -19
  80. package/src/db/retry.ts +25 -3
  81. package/src/db/write-result.ts +27 -4
  82. package/src/firebase/firebase-verifier.ts +53 -4
  83. package/src/firebase/identity-toolkit.ts +57 -5
  84. package/src/firebase/jose-firebase-verifier.ts +79 -9
  85. package/src/firebase/remote-verifier.ts +58 -9
  86. package/src/http/app-env.ts +41 -8
  87. package/src/http/app-info.ts +25 -3
  88. package/src/http/http-status.ts +12 -3
  89. package/src/http/nest-error.ts +106 -37
  90. package/src/http/user-protocol.ts +23 -3
  91. package/src/index.ts +17 -3
  92. package/src/middleware/auth.ts +77 -15
  93. package/src/middleware/finalize-response.ts +41 -12
  94. package/src/middleware/validation.ts +89 -15
  95. package/src/middleware/zod-coerce.ts +68 -9
  96. package/src/queue/consumer.ts +146 -0
  97. package/src/queue/send.ts +129 -0
  98. package/src/stripe/client.ts +46 -9
  99. package/src/testing/auth.ts +58 -10
  100. package/src/testing/configurable-fake.ts +23 -11
  101. package/src/testing/db.ts +82 -13
  102. package/src/testing/fakes.ts +77 -9
  103. package/src/testing/index.ts +10 -5
  104. package/src/testing/stripe-fixtures.ts +93 -3
@@ -1,30 +1,93 @@
1
1
  /**
2
- * Workers KV を使った cache-aside キャッシュ(フリート共通 = winecode/tipsys hono CacheService)。
3
- * 同一 DB を参照する透過キャッシュなので `/api` とレスポンスは一致する(perf 層であり parity に影響しない)。
2
+ * Cache-aside helper backed by Cloudflare Workers KV.
4
3
  *
5
- * キー構成は各 repo `/api`(旧 Valkey)と一致させる:
6
- * `${appName}${version}${table}_${type}_${column}` (column: id string なら sha256hex、number はそのまま)
7
- * KV expirationTtl 60s 下限のため lifetime 60 でクランプする。
4
+ * Provides a thin, JSON-serializing wrapper around a {@link KVNamespace} for the common
5
+ * "look in cache, fall back to the source of truth" pattern. Reads and writes are best-effort:
6
+ * any KV error, serialization failure, or oversized key is swallowed so callers transparently
7
+ * fall through to their backing store instead of throwing.
8
+ *
9
+ * Cache keys are namespaced as `<appName><version><table>_<type>_<id>`, where a string `id` is
10
+ * hashed with SHA-256 (hex) and a numeric `id` is used verbatim.
11
+ *
12
+ * @remarks
13
+ * Workers KV enforces a 60-second minimum on `expirationTtl`, so every write clamps its lifetime
14
+ * up to at least {@link KVCacheOptions.minTtlSeconds} (60 by default). Keys whose UTF-8 byte length
15
+ * exceeds the KV 1024-byte limit are skipped entirely, leaving the value uncached.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const cache = new KVCache(env.KV, { appName: 'myapp' });
20
+ * const cached = await cache.get<User>('users', 'profile', userId);
21
+ * if (!cached) {
22
+ * const user = await db.loadUser(userId);
23
+ * await cache.set('users', 'profile', userId, user);
24
+ * }
25
+ * ```
8
26
  */
9
27
 
10
- /** @cloudflare/workers-types の KVNamespace 最小サブセット(get/put/delete のみ使用)。 */
28
+ /**
29
+ * Minimal subset of `@cloudflare/workers-types`' `KVNamespace` used by {@link KVCache}.
30
+ *
31
+ * Declared locally so consumers are not forced to depend on `@cloudflare/workers-types`. Only the
32
+ * three operations the cache actually needs are modeled.
33
+ */
11
34
  export interface KVNamespace {
35
+ /**
36
+ * Read the string value stored under `key`.
37
+ *
38
+ * @param key - Fully namespaced cache key.
39
+ * @returns The stored value, or `null` when the key is absent or expired.
40
+ */
12
41
  get(key: string): Promise<string | null>;
42
+ /**
43
+ * Write `value` under `key`, optionally with a time-to-live.
44
+ *
45
+ * @param key - Fully namespaced cache key.
46
+ * @param value - String payload to store.
47
+ * @param options - Optional write options; `expirationTtl` is the lifetime in seconds.
48
+ * @returns A promise that resolves once the write is accepted.
49
+ */
13
50
  put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;
51
+ /**
52
+ * Remove the entry stored under `key`.
53
+ *
54
+ * @param key - Fully namespaced cache key.
55
+ * @returns A promise that resolves once the delete is accepted.
56
+ */
14
57
  delete(key: string): Promise<void>;
15
58
  }
16
59
 
60
+ /**
61
+ * Configuration for a {@link KVCache} instance.
62
+ */
17
63
  export interface KVCacheOptions {
18
- /** キー前置(repo 名)。例 `'winecode'` / `'tipsys'`。 */
64
+ /**
65
+ * Application-level key prefix used to isolate this app's entries within a shared namespace.
66
+ * For example `'myapp'`.
67
+ */
19
68
  appName: string;
20
- /** バージョン前置。既定 `'v8_'`。 */
69
+ /**
70
+ * Schema/version prefix applied after {@link appName}, letting you invalidate every key at once
71
+ * by bumping it. Defaults to `'v8_'`.
72
+ */
21
73
  version?: string;
22
- /** lifetime の下限秒(KV の最小 TTL)。既定 `60`。 */
74
+ /**
75
+ * Lower bound, in seconds, applied to every write's TTL. Matches the Workers KV 60-second
76
+ * minimum and defaults to `60`.
77
+ */
23
78
  minTtlSeconds?: number;
24
- /** lifetime 未指定時の既定秒。既定 `600`。 */
79
+ /**
80
+ * Default TTL, in seconds, used by {@link KVCache.set} when no per-call `lifetime` is supplied.
81
+ * Defaults to `600`.
82
+ */
25
83
  defaultLifetime?: number;
26
84
  }
27
85
 
86
+ /**
87
+ * A single entry to store via {@link KVCache.setMany}.
88
+ *
89
+ * @internal
90
+ */
28
91
  interface CacheSetItem {
29
92
  table: string;
30
93
  type: string | number;
@@ -33,6 +96,11 @@ interface CacheSetItem {
33
96
  lifetime?: number;
34
97
  }
35
98
 
99
+ /**
100
+ * Key coordinates identifying a single entry for {@link KVCache.getMany}.
101
+ *
102
+ * @internal
103
+ */
36
104
  interface CacheKeyItem {
37
105
  table: string;
38
106
  type: string | number;
@@ -41,11 +109,32 @@ interface CacheKeyItem {
41
109
 
42
110
  const encoder = new TextEncoder();
43
111
 
112
+ /**
113
+ * Compute the lowercase hex SHA-256 digest of a UTF-8 string.
114
+ *
115
+ * @param input - String to hash.
116
+ * @returns The 64-character hex-encoded digest.
117
+ * @internal
118
+ */
44
119
  async function sha256Hex(input: string): Promise<string> {
45
120
  const digest = await crypto.subtle.digest('SHA-256', encoder.encode(input));
46
121
  return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
47
122
  }
48
123
 
124
+ /**
125
+ * Cache-aside wrapper over a Workers {@link KVNamespace}.
126
+ *
127
+ * Serializes values to JSON, namespaces keys, and clamps TTLs to the KV minimum. All operations are
128
+ * fail-soft: errors are swallowed so a cache miss or backend failure degrades to a source-of-truth
129
+ * lookup rather than propagating.
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * const cache = new KVCache(env.KV, { appName: 'myapp', defaultLifetime: 300 });
134
+ * await cache.set('users', 'profile', 42, { name: 'Ada' });
135
+ * const user = await cache.get<{ name: string }>('users', 'profile', 42);
136
+ * ```
137
+ */
49
138
  export class KVCache {
50
139
  readonly #kv: KVNamespace;
51
140
  readonly #appName: string;
@@ -53,6 +142,12 @@ export class KVCache {
53
142
  readonly #minTtl: number;
54
143
  readonly #defaultLifetime: number;
55
144
 
145
+ /**
146
+ * Create a cache bound to a specific KV namespace.
147
+ *
148
+ * @param kv - The Workers KV namespace that backs this cache.
149
+ * @param options - Key-prefix and TTL configuration; see {@link KVCacheOptions}.
150
+ */
56
151
  constructor(kv: KVNamespace, options: KVCacheOptions) {
57
152
  this.#kv = kv;
58
153
  this.#appName = options.appName;
@@ -61,16 +156,41 @@ export class KVCache {
61
156
  this.#defaultLifetime = options.defaultLifetime ?? 600;
62
157
  }
63
158
 
159
+ /**
160
+ * Build the fully namespaced KV key for the given coordinates.
161
+ *
162
+ * A string `id` is hashed with SHA-256 (hex); a numeric `id` is used as-is.
163
+ *
164
+ * @param table - Logical table or entity name.
165
+ * @param type - Sub-key discriminator (e.g. lookup variant).
166
+ * @param id - Entity identifier; strings are hashed, numbers used verbatim.
167
+ * @returns The key, or `undefined` when it would exceed the KV 1024-byte limit.
168
+ * @internal
169
+ */
64
170
  async #buildKey(table: string, type: string | number, id: string | number): Promise<string | undefined> {
65
171
  const column = typeof id === 'string' ? await sha256Hex(id) : id;
66
172
  const key = `${this.#appName}${this.#version}${table}_${type}_${column}`;
67
- // KV のキーは 512〜1024 バイト上限。超えるものはキャッシュ対象外(cache-aside なので DB 直読みに落ちる)。
173
+ // KV keys are capped at 1024 bytes. Oversized keys are left uncached; cache-aside callers fall
174
+ // back to reading directly from their source of truth.
68
175
  if (encoder.encode(key).byteLength > 1024) {
69
176
  return undefined;
70
177
  }
71
178
  return key;
72
179
  }
73
180
 
181
+ /**
182
+ * Read and JSON-parse a cached value.
183
+ *
184
+ * @typeParam T - Expected shape of the cached value.
185
+ * @param table - Logical table or entity name.
186
+ * @param type - Sub-key discriminator.
187
+ * @param id - Entity identifier.
188
+ * @returns The parsed value, or `undefined` on a miss, oversized key, or any read/parse error.
189
+ * @example
190
+ * ```ts
191
+ * const user = await cache.get<User>('users', 'profile', userId);
192
+ * ```
193
+ */
74
194
  async get<T>(table: string, type: string | number, id: string | number): Promise<T | undefined> {
75
195
  const key = await this.#buildKey(table, type, id);
76
196
  if (!key) {
@@ -87,6 +207,24 @@ export class KVCache {
87
207
  }
88
208
  }
89
209
 
210
+ /**
211
+ * JSON-serialize and store a value.
212
+ *
213
+ * Falsy `data` is ignored. The effective TTL is `max(minTtlSeconds, lifetime ?? defaultLifetime)`,
214
+ * honoring the KV 60-second floor. Oversized keys and serialization/write failures are silently
215
+ * skipped.
216
+ *
217
+ * @param table - Logical table or entity name.
218
+ * @param type - Sub-key discriminator.
219
+ * @param id - Entity identifier.
220
+ * @param data - Value to cache; serialized with `JSON.stringify`.
221
+ * @param lifetime - Optional TTL in seconds; defaults to {@link KVCacheOptions.defaultLifetime}.
222
+ * @returns A promise that resolves once the write attempt completes.
223
+ * @example
224
+ * ```ts
225
+ * await cache.set('users', 'profile', userId, user, 300);
226
+ * ```
227
+ */
90
228
  async set(
91
229
  table: string,
92
230
  type: string | number,
@@ -111,6 +249,22 @@ export class KVCache {
111
249
  await this.#kv.put(key, payload, { expirationTtl: ttl }).catch(() => undefined);
112
250
  }
113
251
 
252
+ /**
253
+ * Store many values concurrently.
254
+ *
255
+ * Each item is written via {@link KVCache.set}, so the same fail-soft and TTL rules apply per item.
256
+ * An empty array is a no-op.
257
+ *
258
+ * @param items - Entries to store; see {@link CacheSetItem}.
259
+ * @returns A promise that resolves once every write attempt completes.
260
+ * @example
261
+ * ```ts
262
+ * await cache.setMany([
263
+ * { table: 'users', type: 'profile', id: 1, data: userA },
264
+ * { table: 'users', type: 'profile', id: 2, data: userB, lifetime: 120 },
265
+ * ]);
266
+ * ```
267
+ */
114
268
  async setMany(items: CacheSetItem[]): Promise<void> {
115
269
  if (items.length === 0) {
116
270
  return;
@@ -118,12 +272,40 @@ export class KVCache {
118
272
  await Promise.all(items.map((i) => this.set(i.table, i.type, i.id, i.data, i.lifetime)));
119
273
  }
120
274
 
121
- // T は呼び出し側が指定する戻り値型(get<T> と同じく ergonomics 目的)。
275
+ /**
276
+ * Read many values concurrently.
277
+ *
278
+ * @typeParam T - Expected shape of each cached value.
279
+ * @param items - Key coordinates to look up; see {@link CacheKeyItem}.
280
+ * @returns One `{ id, value }` pair per input item, preserving order; `value` is `undefined` on miss.
281
+ * @example
282
+ * ```ts
283
+ * const rows = await cache.getMany<User>([
284
+ * { table: 'users', type: 'profile', id: 1 },
285
+ * { table: 'users', type: 'profile', id: 2 },
286
+ * ]);
287
+ * ```
288
+ */
289
+ // The generic `T` is the caller-specified return type, mirroring `get<T>` for ergonomics.
122
290
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
123
291
  async getMany<T>(items: CacheKeyItem[]): Promise<{ id: string | number; value: T | undefined }[]> {
124
292
  return Promise.all(items.map(async (i) => ({ id: i.id, value: await this.get<T>(i.table, i.type, i.id) })));
125
293
  }
126
294
 
295
+ /**
296
+ * Remove a cached entry.
297
+ *
298
+ * Oversized keys and delete failures are silently ignored.
299
+ *
300
+ * @param table - Logical table or entity name.
301
+ * @param type - Sub-key discriminator.
302
+ * @param id - Entity identifier.
303
+ * @returns A promise that resolves once the delete attempt completes.
304
+ * @example
305
+ * ```ts
306
+ * await cache.delete('users', 'profile', userId);
307
+ * ```
308
+ */
127
309
  async delete(table: string, type: string | number, id: string | number): Promise<void> {
128
310
  const key = await this.#buildKey(table, type, id);
129
311
  if (!key) {
@@ -2,30 +2,45 @@ import { createConnection } from 'mysql2/promise';
2
2
  import type { Connection } from 'mysql2/promise';
3
3
 
4
4
  /**
5
- * Hyperdrive バインディングの最小形(@cloudflare/workers-types への依存を避けるための構造型)。
5
+ * Minimal structural shape of a Cloudflare Hyperdrive binding.
6
+ *
7
+ * @remarks
8
+ * Declared structurally to avoid a dependency on `@cloudflare/workers-types`; any object with these
9
+ * connection fields satisfies it.
6
10
  */
7
11
  export interface HyperdriveLike {
12
+ /** Database host to connect to. */
8
13
  host: string;
14
+ /** Database user. */
9
15
  user: string;
16
+ /** Database password. */
10
17
  password: string;
18
+ /** Database name. */
11
19
  database: string;
20
+ /** Database port. */
12
21
  port: number;
13
22
  }
14
23
 
15
24
  /**
16
- * Hyperdrive バインディングから mysql2 createConnection 用オプションを作る。
17
- * `disableEval: true`(Workers で eval 不可)は既定で付与。`extra` で timezone 等を上書き/追加。
25
+ * Build mysql2 `createConnection` options from a Hyperdrive binding, applying the kit's defaults.
26
+ *
27
+ * @remarks
28
+ * Three defaults are applied and can each be overridden via `extra`:
18
29
  *
19
- * `decimalNumbers: true`: DECIMAL/NEWDECIMAL を文字列でなく JS number で返す。Drizzle
20
- * `$inferSelect`(decimal→string)と生 SQL reads の戻り値を、各 repo の数値ドメイン型
21
- * (nutrition number 等)に揃えるため既定で有効化。precision/scale JS の安全整数域
22
- * (decimal(15,2) 程度まで)を超える列が無いことが前提。
30
+ * - `disableEval: true` `eval` is unavailable in the Workers runtime, so the driver's eval-based
31
+ * fast paths must be disabled.
32
+ * - `decimalNumbers: true` return `DECIMAL`/`NEWDECIMAL` columns as JS `number` rather than
33
+ * strings, so raw-SQL reads and Drizzle's inferred types align on a single numeric domain type.
34
+ * This assumes no column's precision exceeds the JS safe-integer range.
35
+ * - `timezone: '+09:00'` — set the driver's session timezone to JST. mysql2 defaults to `'local'`,
36
+ * which is UTC in the Workers runtime; pinning the driver timezone keeps `datetime`/`timestamp`
37
+ * round-trips independent of the database's session timezone (only the internally stored UTC
38
+ * value differs, which is invisible to the application). Non-JST deployments can override this
39
+ * via `extra: { timezone: '...' }`.
23
40
  *
24
- * `timezone: '+09:00'`: フリートの接続先 RDB session time_zone=Asia/Tokyo。mysql2 driver
25
- * `timezone` 既定は `'local'`=Workers では UTC で、揃わないと `datetime/timestamp` の生 Date 読みが
26
- * +9h・生 Date 書きが −9h ズレる(NestJS JST 実行で一致=移植で顕在化する潜在バグ)。driver
27
- * 固定すれば round-trip の観測値は DB の session tz に非依存(内部格納 UTC 値だけ変わるが app 不可視)。
28
- * 非 JST repo は `extra: { timezone: '...' }` で上書き可。
41
+ * @param hyperdrive - the Hyperdrive binding to derive connection fields from.
42
+ * @param extra - additional mysql2 options merged last, overriding the defaults above.
43
+ * @returns a plain options object to pass to mysql2 `createConnection`.
29
44
  */
30
45
  export function hyperdriveConnectionOptions(
31
46
  hyperdrive: HyperdriveLike,
@@ -44,13 +59,40 @@ export function hyperdriveConnectionOptions(
44
59
  };
45
60
  }
46
61
 
62
+ /**
63
+ * Minimal structural shape of a Workers `ExecutionContext`, limited to `waitUntil`.
64
+ *
65
+ * @remarks
66
+ * Declared structurally to avoid a dependency on `@cloudflare/workers-types`.
67
+ */
47
68
  export interface ExecutionContextLike {
69
+ /** Extend the request's lifetime until `promise` settles (used to close connections after the response). */
48
70
  waitUntil(promise: Promise<unknown>): void;
49
71
  }
50
72
 
51
73
  /**
52
- * primary/replica の接続を開いて `fn` を実行し、finally `ctx.waitUntil` 越しに閉じる
53
- * (receptray/tipsys の worker entry の接続ライフサイクル相当)。
74
+ * Open primary and replica connections, run `fn` with them, and close both afterwards.
75
+ *
76
+ * The connections are always closed in a `finally` block; closing is scheduled through
77
+ * `ctx.waitUntil` so it can complete after the response has been returned, without blocking it.
78
+ *
79
+ * @typeParam T - resolved value produced by `fn`.
80
+ * @param hyperdrives - the primary and replica Hyperdrive bindings to connect to.
81
+ * @param ctx - the execution context whose `waitUntil` defers connection teardown past the response.
82
+ * @param fn - callback invoked with the open `primary` and `replica` connections.
83
+ * @param connectionOptions - extra mysql2 options forwarded to {@link hyperdriveConnectionOptions}.
84
+ * @returns the value resolved by `fn`.
85
+ * @example
86
+ * ```ts
87
+ * const data = await withMysqlConnections(
88
+ * { primary: env.PRIMARY, replica: env.REPLICA },
89
+ * ctx,
90
+ * async ({ primary, replica }) => {
91
+ * const [rows] = await replica.query('SELECT 1');
92
+ * return rows;
93
+ * },
94
+ * );
95
+ * ```
54
96
  */
55
97
  export async function withMysqlConnections<T>(
56
98
  hyperdrives: { primary: HyperdriveLike; replica: HyperdriveLike },
@@ -5,39 +5,104 @@ import type { HyperdriveLike } from './connection.js';
5
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,5 +1,14 @@
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
13
  export { retryWhenDeadlock } from './retry.js';
5
14