@rdlabo/workers-hono-kit 0.3.7 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -1
- package/dist/business-time/index.d.ts +49 -0
- package/dist/business-time/index.js +149 -0
- package/dist/business-time/types.d.ts +9 -0
- package/dist/business-time/types.js +5 -0
- package/dist/db/columns.d.ts +46 -0
- package/dist/db/columns.js +36 -0
- package/dist/db/connection.js +2 -1
- package/dist/db/decimal.d.ts +27 -0
- package/dist/db/decimal.js +50 -0
- package/dist/db/index.d.ts +4 -1
- package/dist/db/index.js +3 -1
- package/dist/db/jst.d.ts +10 -72
- package/dist/db/jst.js +10 -82
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/workers-bindings.d.ts +49 -0
- package/dist/testing/workers-bindings.js +62 -0
- package/package.json +7 -3
- package/scripts/db-baseline.mjs +0 -0
- package/src/ai/gateway.ts +0 -120
- package/src/aws/cloudfront.ts +0 -105
- package/src/aws/secrets-manager.ts +0 -112
- package/src/cache/kv-cache.ts +0 -316
- package/src/db/connection.ts +0 -107
- package/src/db/database.ts +0 -269
- package/src/db/index.ts +0 -39
- package/src/db/jst.ts +0 -122
- package/src/db/migrate.ts +0 -155
- package/src/db/orm-config.ts +0 -171
- package/src/db/retry.ts +0 -43
- package/src/db/write-result.ts +0 -46
- package/src/firebase/firebase-verifier.ts +0 -76
- package/src/firebase/identity-toolkit.ts +0 -179
- package/src/firebase/jose-firebase-verifier.ts +0 -159
- package/src/firebase/remote-verifier.ts +0 -98
- package/src/http/app-env.ts +0 -53
- package/src/http/app-info.ts +0 -38
- package/src/http/execution-context.ts +0 -11
- package/src/http/http-status.ts +0 -71
- package/src/http/nest-error.ts +0 -207
- package/src/http/trailing-slash.ts +0 -28
- package/src/http/user-protocol.ts +0 -36
- package/src/index.ts +0 -77
- package/src/middleware/auth.ts +0 -129
- package/src/middleware/finalize-response.ts +0 -90
- package/src/middleware/validation.ts +0 -158
- package/src/middleware/zod-coerce.ts +0 -124
- package/src/queue/consumer.ts +0 -146
- package/src/queue/send.ts +0 -129
- package/src/stripe/client.ts +0 -85
- package/src/testing/auth.ts +0 -110
- package/src/testing/configurable-fake.ts +0 -45
- package/src/testing/db.ts +0 -194
- package/src/testing/fakes.ts +0 -153
- package/src/testing/index.ts +0 -31
- package/src/testing/stripe-fixtures.ts +0 -175
package/src/cache/kv-cache.ts
DELETED
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cache-aside helper backed by Cloudflare Workers KV.
|
|
3
|
-
*
|
|
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
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
57
|
-
delete(key: string): Promise<void>;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Configuration for a {@link KVCache} instance.
|
|
62
|
-
*/
|
|
63
|
-
export interface KVCacheOptions {
|
|
64
|
-
/**
|
|
65
|
-
* Application-level key prefix used to isolate this app's entries within a shared namespace.
|
|
66
|
-
* For example `'myapp'`.
|
|
67
|
-
*/
|
|
68
|
-
appName: string;
|
|
69
|
-
/**
|
|
70
|
-
* Schema/version prefix applied after {@link appName}, letting you invalidate every key at once
|
|
71
|
-
* by bumping it. Defaults to `'v8_'`.
|
|
72
|
-
*/
|
|
73
|
-
version?: string;
|
|
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
|
-
*/
|
|
78
|
-
minTtlSeconds?: number;
|
|
79
|
-
/**
|
|
80
|
-
* Default TTL, in seconds, used by {@link KVCache.set} when no per-call `lifetime` is supplied.
|
|
81
|
-
* Defaults to `600`.
|
|
82
|
-
*/
|
|
83
|
-
defaultLifetime?: number;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* A single entry to store via {@link KVCache.setMany}.
|
|
88
|
-
*
|
|
89
|
-
* @internal
|
|
90
|
-
*/
|
|
91
|
-
interface CacheSetItem {
|
|
92
|
-
table: string;
|
|
93
|
-
type: string | number;
|
|
94
|
-
id: string | number;
|
|
95
|
-
data: unknown;
|
|
96
|
-
lifetime?: number;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Key coordinates identifying a single entry for {@link KVCache.getMany}.
|
|
101
|
-
*
|
|
102
|
-
* @internal
|
|
103
|
-
*/
|
|
104
|
-
interface CacheKeyItem {
|
|
105
|
-
table: string;
|
|
106
|
-
type: string | number;
|
|
107
|
-
id: string | number;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const encoder = new TextEncoder();
|
|
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
|
-
*/
|
|
119
|
-
async function sha256Hex(input: string): Promise<string> {
|
|
120
|
-
const digest = await crypto.subtle.digest('SHA-256', encoder.encode(input));
|
|
121
|
-
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
122
|
-
}
|
|
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
|
-
*/
|
|
138
|
-
export class KVCache {
|
|
139
|
-
readonly #kv: KVNamespace;
|
|
140
|
-
readonly #appName: string;
|
|
141
|
-
readonly #version: string;
|
|
142
|
-
readonly #minTtl: number;
|
|
143
|
-
readonly #defaultLifetime: number;
|
|
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
|
-
*/
|
|
151
|
-
constructor(kv: KVNamespace, options: KVCacheOptions) {
|
|
152
|
-
this.#kv = kv;
|
|
153
|
-
this.#appName = options.appName;
|
|
154
|
-
this.#version = options.version ?? 'v8_';
|
|
155
|
-
this.#minTtl = options.minTtlSeconds ?? 60;
|
|
156
|
-
this.#defaultLifetime = options.defaultLifetime ?? 600;
|
|
157
|
-
}
|
|
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
|
-
*/
|
|
170
|
-
async #buildKey(table: string, type: string | number, id: string | number): Promise<string | undefined> {
|
|
171
|
-
const column = typeof id === 'string' ? await sha256Hex(id) : id;
|
|
172
|
-
const key = `${this.#appName}${this.#version}${table}_${type}_${column}`;
|
|
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.
|
|
175
|
-
if (encoder.encode(key).byteLength > 1024) {
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
return key;
|
|
179
|
-
}
|
|
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
|
-
*/
|
|
194
|
-
async get<T>(table: string, type: string | number, id: string | number): Promise<T | undefined> {
|
|
195
|
-
const key = await this.#buildKey(table, type, id);
|
|
196
|
-
if (!key) {
|
|
197
|
-
return undefined;
|
|
198
|
-
}
|
|
199
|
-
try {
|
|
200
|
-
const data = await this.#kv.get(key);
|
|
201
|
-
if (!data) {
|
|
202
|
-
return undefined;
|
|
203
|
-
}
|
|
204
|
-
return JSON.parse(data) as T;
|
|
205
|
-
} catch {
|
|
206
|
-
return undefined;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
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
|
-
*/
|
|
228
|
-
async set(
|
|
229
|
-
table: string,
|
|
230
|
-
type: string | number,
|
|
231
|
-
id: string | number,
|
|
232
|
-
data: unknown,
|
|
233
|
-
lifetime?: number,
|
|
234
|
-
): Promise<void> {
|
|
235
|
-
if (!data) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
const key = await this.#buildKey(table, type, id);
|
|
239
|
-
if (!key) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
let payload: string;
|
|
243
|
-
try {
|
|
244
|
-
payload = JSON.stringify(data);
|
|
245
|
-
} catch {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const ttl = Math.max(this.#minTtl, lifetime ?? this.#defaultLifetime);
|
|
249
|
-
await this.#kv.put(key, payload, { expirationTtl: ttl }).catch(() => undefined);
|
|
250
|
-
}
|
|
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
|
-
*/
|
|
268
|
-
async setMany(items: CacheSetItem[]): Promise<void> {
|
|
269
|
-
if (items.length === 0) {
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
await Promise.all(items.map((i) => this.set(i.table, i.type, i.id, i.data, i.lifetime)));
|
|
273
|
-
}
|
|
274
|
-
|
|
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.
|
|
290
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
291
|
-
async getMany<T>(items: CacheKeyItem[]): Promise<{ id: string | number; value: T | undefined }[]> {
|
|
292
|
-
return Promise.all(items.map(async (i) => ({ id: i.id, value: await this.get<T>(i.table, i.type, i.id) })));
|
|
293
|
-
}
|
|
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
|
-
*/
|
|
309
|
-
async delete(table: string, type: string | number, id: string | number): Promise<void> {
|
|
310
|
-
const key = await this.#buildKey(table, type, id);
|
|
311
|
-
if (!key) {
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
await this.#kv.delete(key).catch(() => undefined);
|
|
315
|
-
}
|
|
316
|
-
}
|
package/src/db/connection.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { createConnection } from 'mysql2/promise';
|
|
2
|
-
import type { Connection } from 'mysql2/promise';
|
|
3
|
-
import type { ExecutionContextLike } from '../http/execution-context.js';
|
|
4
|
-
|
|
5
|
-
export type { ExecutionContextLike } from '../http/execution-context.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Minimal structural shape of a Cloudflare Hyperdrive binding.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* Declared structurally to avoid a dependency on `@cloudflare/workers-types`; any object with these
|
|
12
|
-
* connection fields satisfies it.
|
|
13
|
-
*/
|
|
14
|
-
export interface HyperdriveLike {
|
|
15
|
-
/** Database host to connect to. */
|
|
16
|
-
host: string;
|
|
17
|
-
/** Database user. */
|
|
18
|
-
user: string;
|
|
19
|
-
/** Database password. */
|
|
20
|
-
password: string;
|
|
21
|
-
/** Database name. */
|
|
22
|
-
database: string;
|
|
23
|
-
/** Database port. */
|
|
24
|
-
port: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Build mysql2 `createConnection` options from a Hyperdrive binding, applying the kit's defaults.
|
|
29
|
-
*
|
|
30
|
-
* @remarks
|
|
31
|
-
* Three defaults are applied and can each be overridden via `extra`:
|
|
32
|
-
*
|
|
33
|
-
* - `disableEval: true` — `eval` is unavailable in the Workers runtime, so the driver's eval-based
|
|
34
|
-
* fast paths must be disabled.
|
|
35
|
-
* - `decimalNumbers: true` — return `DECIMAL`/`NEWDECIMAL` columns as JS `number` rather than
|
|
36
|
-
* strings, so raw-SQL reads and Drizzle's inferred types align on a single numeric domain type.
|
|
37
|
-
* This assumes no column's precision exceeds the JS safe-integer range.
|
|
38
|
-
* - `timezone: '+09:00'` — set the driver's session timezone to JST. mysql2 defaults to `'local'`,
|
|
39
|
-
* which is UTC in the Workers runtime; pinning the driver timezone keeps `datetime`/`timestamp`
|
|
40
|
-
* round-trips independent of the database's session timezone (only the internally stored UTC
|
|
41
|
-
* value differs, which is invisible to the application). Non-JST deployments can override this
|
|
42
|
-
* via `extra: { timezone: '...' }`.
|
|
43
|
-
*
|
|
44
|
-
* @param hyperdrive - the Hyperdrive binding to derive connection fields from.
|
|
45
|
-
* @param extra - additional mysql2 options merged last, overriding the defaults above.
|
|
46
|
-
* @returns a plain options object to pass to mysql2 `createConnection`.
|
|
47
|
-
*/
|
|
48
|
-
export function hyperdriveConnectionOptions(
|
|
49
|
-
hyperdrive: HyperdriveLike,
|
|
50
|
-
extra?: Record<string, unknown>,
|
|
51
|
-
): Record<string, unknown> {
|
|
52
|
-
return {
|
|
53
|
-
host: hyperdrive.host,
|
|
54
|
-
user: hyperdrive.user,
|
|
55
|
-
password: hyperdrive.password,
|
|
56
|
-
database: hyperdrive.database,
|
|
57
|
-
port: hyperdrive.port,
|
|
58
|
-
disableEval: true,
|
|
59
|
-
decimalNumbers: true,
|
|
60
|
-
timezone: '+09:00',
|
|
61
|
-
...extra,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Open primary and replica connections, run `fn` with them, and close both afterwards.
|
|
67
|
-
*
|
|
68
|
-
* The connections are always closed in a `finally` block; closing is scheduled through
|
|
69
|
-
* `ctx.waitUntil` so it can complete after the response has been returned, without blocking it.
|
|
70
|
-
*
|
|
71
|
-
* @typeParam T - resolved value produced by `fn`.
|
|
72
|
-
* @param hyperdrives - the primary and replica Hyperdrive bindings to connect to.
|
|
73
|
-
* @param ctx - the execution context whose `waitUntil` defers connection teardown past the response.
|
|
74
|
-
* @param fn - callback invoked with the open `primary` and `replica` connections.
|
|
75
|
-
* @param connectionOptions - extra mysql2 options forwarded to {@link hyperdriveConnectionOptions}.
|
|
76
|
-
* @returns the value resolved by `fn`.
|
|
77
|
-
* @example
|
|
78
|
-
* ```ts
|
|
79
|
-
* const data = await withMysqlConnections(
|
|
80
|
-
* { primary: env.PRIMARY, replica: env.REPLICA },
|
|
81
|
-
* ctx,
|
|
82
|
-
* async ({ primary, replica }) => {
|
|
83
|
-
* const [rows] = await replica.query('SELECT 1');
|
|
84
|
-
* return rows;
|
|
85
|
-
* },
|
|
86
|
-
* );
|
|
87
|
-
* ```
|
|
88
|
-
*/
|
|
89
|
-
export async function withMysqlConnections<T>(
|
|
90
|
-
hyperdrives: { primary: HyperdriveLike; replica: HyperdriveLike },
|
|
91
|
-
ctx: ExecutionContextLike,
|
|
92
|
-
fn: (connections: { primary: Connection; replica: Connection }) => Promise<T>,
|
|
93
|
-
connectionOptions?: Record<string, unknown>,
|
|
94
|
-
): Promise<T> {
|
|
95
|
-
let primary: Connection | undefined;
|
|
96
|
-
let replica: Connection | undefined;
|
|
97
|
-
try {
|
|
98
|
-
primary = await createConnection(hyperdriveConnectionOptions(hyperdrives.primary, connectionOptions));
|
|
99
|
-
replica = await createConnection(hyperdriveConnectionOptions(hyperdrives.replica, connectionOptions));
|
|
100
|
-
return await fn({ primary, replica });
|
|
101
|
-
} finally {
|
|
102
|
-
const closing = [primary, replica].filter((c): c is Connection => c !== undefined).map((c) => c.end());
|
|
103
|
-
if (closing.length > 0) {
|
|
104
|
-
ctx.waitUntil(Promise.allSettled(closing));
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
package/src/db/database.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { createConnection } from 'mysql2/promise';
|
|
2
|
-
import type { Connection, Pool } from 'mysql2/promise';
|
|
3
|
-
import { hyperdriveConnectionOptions } from './connection.js';
|
|
4
|
-
import type { HyperdriveLike } from './connection.js';
|
|
5
|
-
import { retryWhenDeadlock } from './retry.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
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`.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Minimal connection interface used for reads.
|
|
30
|
-
*
|
|
31
|
-
* @remarks
|
|
32
|
-
* A mysql2 `Connection` or `Pool` satisfies this structurally.
|
|
33
|
-
*/
|
|
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
|
-
*/
|
|
42
|
-
query(sql: string, params?: unknown[]): Promise<unknown>;
|
|
43
|
-
}
|
|
44
|
-
|
|
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
|
-
*/
|
|
51
|
-
export type TxOf<TDrizzle> = TDrizzle extends {
|
|
52
|
-
transaction(cb: (tx: infer Tx) => Promise<unknown>): Promise<unknown>;
|
|
53
|
-
}
|
|
54
|
-
? Tx
|
|
55
|
-
: unknown;
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
72
|
-
read<T>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
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
|
-
*/
|
|
80
|
-
write<T>(fn: (dz: TDrizzle) => Promise<T>): Promise<T>;
|
|
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
|
-
*/
|
|
88
|
-
transaction<T>(fn: (tx: TTx) => Promise<T>): Promise<T>;
|
|
89
|
-
}
|
|
90
|
-
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
106
|
-
dispose(): Promise<void>;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
interface DrizzleLike<TTx> {
|
|
110
|
-
transaction<T>(cb: (tx: TTx) => Promise<T>): Promise<T>;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Options for {@link createMysqlDatabase}.
|
|
115
|
-
*
|
|
116
|
-
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
117
|
-
*/
|
|
118
|
-
export interface CreateMysqlDatabaseOptions<TDrizzle> {
|
|
119
|
-
/**
|
|
120
|
-
* The Drizzle ORM used for writes, created by the consumer with its own `drizzle-orm`
|
|
121
|
-
* (e.g. `drizzle(primary, { schema, ... })`).
|
|
122
|
-
*/
|
|
123
|
-
orm: TDrizzle;
|
|
124
|
-
/** The connection used for reads (raw SQL). */
|
|
125
|
-
replica: QueryRunner;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
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
|
-
* ```
|
|
146
|
-
*/
|
|
147
|
-
export function createMysqlDatabase<TDrizzle>(options: CreateMysqlDatabaseOptions<TDrizzle>): Database<TDrizzle> {
|
|
148
|
-
return databaseFrom(options.orm, options.replica);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Options for {@link createHyperdriveDatabase}.
|
|
153
|
-
*
|
|
154
|
-
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
155
|
-
*/
|
|
156
|
-
export interface CreateHyperdriveDatabaseOptions<TDrizzle> {
|
|
157
|
-
/** The Hyperdrive binding for the primary (write) connection. */
|
|
158
|
-
primaryHyperdrive: HyperdriveLike;
|
|
159
|
-
/** The Hyperdrive binding for the replica (read) connection. */
|
|
160
|
-
replicaHyperdrive: HyperdriveLike;
|
|
161
|
-
/**
|
|
162
|
-
* Factory that builds the write ORM from the primary connection, using the consumer's
|
|
163
|
-
* `drizzle-orm`.
|
|
164
|
-
*/
|
|
165
|
-
createOrm: (primary: Connection) => TDrizzle;
|
|
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
|
-
*/
|
|
171
|
-
connectionOptions?: Record<string, unknown>;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
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
|
-
* ```
|
|
198
|
-
*/
|
|
199
|
-
export function createHyperdriveDatabase<TDrizzle>(
|
|
200
|
-
options: CreateHyperdriveDatabaseOptions<TDrizzle>,
|
|
201
|
-
): DisposableDatabase<TDrizzle> {
|
|
202
|
-
const { primaryHyperdrive, replicaHyperdrive, createOrm, connectionOptions } = options;
|
|
203
|
-
let primaryConn: Promise<Connection> | undefined;
|
|
204
|
-
let replicaConn: Promise<Connection> | undefined;
|
|
205
|
-
let orm: TDrizzle | undefined;
|
|
206
|
-
|
|
207
|
-
const primary = (): Promise<Connection> => (primaryConn ??= connect(primaryHyperdrive, connectionOptions));
|
|
208
|
-
const replica = (): Promise<Connection> => (replicaConn ??= connect(replicaHyperdrive, connectionOptions));
|
|
209
|
-
const ormFor = async (): Promise<TDrizzle> => (orm ??= createOrm(await primary()));
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
read<T>(sql: string, params: unknown[] = []): Promise<T[]> {
|
|
213
|
-
return retryWhenDeadlock(async () => {
|
|
214
|
-
const [rows] = (await (await replica()).query(sql, params)) as [unknown, unknown];
|
|
215
|
-
return rows as T[];
|
|
216
|
-
});
|
|
217
|
-
},
|
|
218
|
-
async write<T>(fn: (dz: TDrizzle) => Promise<T>): Promise<T> {
|
|
219
|
-
const dz = await ormFor();
|
|
220
|
-
return retryWhenDeadlock(() => fn(dz));
|
|
221
|
-
},
|
|
222
|
-
async transaction<T>(fn: (tx: TxOf<TDrizzle>) => Promise<T>): Promise<T> {
|
|
223
|
-
const dz = (await ormFor()) as DrizzleLike<TxOf<TDrizzle>>;
|
|
224
|
-
return retryWhenDeadlock(() => dz.transaction(fn));
|
|
225
|
-
},
|
|
226
|
-
async dispose(): Promise<void> {
|
|
227
|
-
await Promise.all([primaryConn?.then((c) => c.end()), replicaConn?.then((c) => c.end())]);
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
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
|
-
*/
|
|
241
|
-
export function databaseFrom<TDrizzle>(orm: TDrizzle, replica: QueryRunner): Database<TDrizzle> {
|
|
242
|
-
const drizzleLike = orm as DrizzleLike<TxOf<TDrizzle>>;
|
|
243
|
-
return {
|
|
244
|
-
read<T>(sql: string, params: unknown[] = []): Promise<T[]> {
|
|
245
|
-
return retryWhenDeadlock(async () => {
|
|
246
|
-
const [rows] = (await replica.query(sql, params)) as [unknown, unknown];
|
|
247
|
-
return rows as T[];
|
|
248
|
-
});
|
|
249
|
-
},
|
|
250
|
-
write<T>(fn: (dz: TDrizzle) => Promise<T>): Promise<T> {
|
|
251
|
-
return retryWhenDeadlock(() => fn(orm));
|
|
252
|
-
},
|
|
253
|
-
transaction<T>(fn: (tx: TxOf<TDrizzle>) => Promise<T>): Promise<T> {
|
|
254
|
-
return retryWhenDeadlock(() => drizzleLike.transaction(fn));
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
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
|
-
*/
|
|
265
|
-
export type { Connection, Pool };
|
|
266
|
-
|
|
267
|
-
function connect(hyperdrive: HyperdriveLike, extra?: Record<string, unknown>): Promise<Connection> {
|
|
268
|
-
return createConnection(hyperdriveConnectionOptions(hyperdrive, extra));
|
|
269
|
-
}
|