@rdlabo/workers-hono-kit 0.10.6 → 0.11.1-beta.pr46.sha8ccc8da384f0
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/LICENSE +1 -1
- package/README.md +40 -764
- package/dist/db/columns.d.ts +0 -10
- package/dist/db/columns.js +1 -5
- package/dist/db/database.d.ts +61 -9
- package/dist/db/database.js +98 -8
- package/dist/db/index.d.ts +2 -4
- package/dist/db/index.js +1 -2
- package/dist/db/retry.js +14 -2
- package/docs/api-business-time.md +33 -0
- package/docs/api-db.md +49 -0
- package/docs/api-offline.md +67 -0
- package/docs/api-root.md +71 -0
- package/docs/api-testing.md +16 -0
- package/docs/api.md +12 -0
- package/docs/cli.md +33 -0
- package/docs/data-layer.md +54 -0
- package/docs/development.md +32 -0
- package/docs/http-auth.md +36 -0
- package/docs/realtime-offline.md +27 -0
- package/docs/role-policies.md +48 -0
- package/docs/testing-operations.md +25 -0
- package/package.json +5 -4
- package/scripts/check-realtime-bundle.mjs +0 -0
- package/scripts/query-realtime-do-metrics.mjs +0 -0
- package/dist/db/decimal.d.ts +0 -35
- package/dist/db/decimal.js +0 -58
package/dist/db/columns.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { DecimalNumberConfig } from './decimal.js';
|
|
2
1
|
/**
|
|
3
2
|
* SQL expression for `ON UPDATE CURRENT_TIMESTAMP` (the MySQL session clock).
|
|
4
3
|
* customType columns have no `.onUpdateNow()`, so pair this with `.$onUpdateFn(() => jstOnUpdateNow(fsp))`.
|
|
@@ -37,12 +36,3 @@ export declare const jstDate: (name: string) => import("drizzle-orm/mysql-core")
|
|
|
37
36
|
driverParam: string | null;
|
|
38
37
|
enumValues: undefined;
|
|
39
38
|
}>;
|
|
40
|
-
/** MySQL `decimal` — SELECT coerces string→number via `fromDriver`; writes bind the number as-is. */
|
|
41
|
-
export declare const decimalNumber: (name: string, config: DecimalNumberConfig) => import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
|
|
42
|
-
name: string;
|
|
43
|
-
dataType: "custom";
|
|
44
|
-
columnType: "MySqlCustomColumn";
|
|
45
|
-
data: number | null;
|
|
46
|
-
driverParam: string | number | null;
|
|
47
|
-
enumValues: undefined;
|
|
48
|
-
}>;
|
package/dist/db/columns.js
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* @remarks
|
|
6
6
|
* `drizzle-orm` is a **peer** (the consumer resolves a single copy); the kit does not bundle it. The
|
|
7
7
|
* return types are the `customType` inference as-is (`MySqlCustomColumnBuilder<…>`) with no `any`, so
|
|
8
|
-
* the column's semantic type (`string | Date`,
|
|
9
|
-
* table's `$inferSelect`.
|
|
8
|
+
* the column's semantic type (`string | Date`, etc.) propagates to the consumer table's `$inferSelect`.
|
|
10
9
|
*
|
|
11
10
|
* **Precondition (a single drizzle copy)**: Drizzle's `SQL` is a **nominal** type carrying a private
|
|
12
11
|
* field `shouldInlineParams`, so if the kit and the consumer resolve different copies of drizzle,
|
|
@@ -23,7 +22,6 @@
|
|
|
23
22
|
*/
|
|
24
23
|
import { sql } from 'drizzle-orm';
|
|
25
24
|
import { customType } from 'drizzle-orm/mysql-core';
|
|
26
|
-
import { decimalNumberParams } from './decimal.js';
|
|
27
25
|
import { jstDateParams, jstDatetimeParams, jstTimestampParams } from './jst.js';
|
|
28
26
|
/**
|
|
29
27
|
* SQL expression for `ON UPDATE CURRENT_TIMESTAMP` (the MySQL session clock).
|
|
@@ -38,5 +36,3 @@ export const jstTimestamp = (name, opts) => customType(jstTimestampParams(opts?.
|
|
|
38
36
|
export const jstDatetime = (name, opts) => customType(jstDatetimeParams(opts?.fsp))(name);
|
|
39
37
|
/** MySQL `date` — on INSERT/UPDATE, normalizes ISO / empty strings to `YYYY-MM-DD` (via `toDriver`). */
|
|
40
38
|
export const jstDate = (name) => customType(jstDateParams())(name);
|
|
41
|
-
/** MySQL `decimal` — SELECT coerces string→number via `fromDriver`; writes bind the number as-is. */
|
|
42
|
-
export const decimalNumber = (name, config) => customType(decimalNumberParams(config))(name);
|
package/dist/db/database.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ import type { HyperdriveLike } from './connection.js';
|
|
|
6
6
|
* @remarks
|
|
7
7
|
* The two sides of the database are deliberately handled differently:
|
|
8
8
|
*
|
|
9
|
-
* - Reads go to the **replica** as raw SQL (`QueryRunner.query`)
|
|
10
|
-
*
|
|
9
|
+
* - Reads go to the **replica** as raw SQL (`QueryRunner.query`) by default, returning plain rows.
|
|
10
|
+
* Hyperdrive-backed databases also expose `query()` for freshness-sensitive primary SELECTs.
|
|
11
11
|
* - Writes and transactions go to the **primary** through the Drizzle ORM for type safety, but only
|
|
12
12
|
* via `write(fn)` / `transaction(fn)` — the raw query builder is never exposed. The builder is
|
|
13
13
|
* awaited inside those methods, which removes a foot-gun: a Drizzle builder is a lazy thenable, so
|
|
@@ -53,7 +53,8 @@ export type TxOf<TDrizzle> = TDrizzle extends {
|
|
|
53
53
|
*/
|
|
54
54
|
export interface Database<TDrizzle, TTx = TxOf<TDrizzle>> {
|
|
55
55
|
/**
|
|
56
|
-
* Run a raw SQL read against the replica
|
|
56
|
+
* Run a raw SQL read against the replica. Hyperdrive-backed databases retry deadlocks and repeat
|
|
57
|
+
* the SELECT once on a fresh connection after mysql2 reports a fatal connection error.
|
|
57
58
|
*
|
|
58
59
|
* @typeParam T - the row shape.
|
|
59
60
|
* @param sql - the SQL text, with `?` placeholders for `params`.
|
|
@@ -98,6 +99,55 @@ export interface DisposableDatabase<TDrizzle, TTx = TxOf<TDrizzle>> extends Data
|
|
|
98
99
|
*/
|
|
99
100
|
dispose(): Promise<void>;
|
|
100
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* A Hyperdrive-backed database with an explicit primary query path.
|
|
104
|
+
*
|
|
105
|
+
* @remarks
|
|
106
|
+
* Use `query()` only for SELECTs that require read-after-write consistency or cannot use the
|
|
107
|
+
* configured replica. Fatal connection errors recreate the primary connection and repeat the
|
|
108
|
+
* SELECT at most once. Writes and transactions are never repeated for connection errors.
|
|
109
|
+
* Read-only transactions are serialized on a separately cached primary connection so their
|
|
110
|
+
* snapshot boundaries cannot mix with ordinary primary operations or with each other. Do not call
|
|
111
|
+
* `readTransaction()` recursively from its own callback.
|
|
112
|
+
*
|
|
113
|
+
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
114
|
+
* @typeParam TTx - the transaction-handle type, inferred from `TDrizzle` by default.
|
|
115
|
+
*/
|
|
116
|
+
export interface HyperdriveDatabase<TDrizzle, TTx = TxOf<TDrizzle>> extends DisposableDatabase<TDrizzle, TTx> {
|
|
117
|
+
/**
|
|
118
|
+
* Run a raw SQL SELECT against the primary database.
|
|
119
|
+
*
|
|
120
|
+
* @typeParam T - the complete rows result type (for example, `User[]`).
|
|
121
|
+
* @param sql - the SQL text, with `?` placeholders for `params`.
|
|
122
|
+
* @param params - optional positional parameters.
|
|
123
|
+
* @returns the rows returned by the query, typed as `T`.
|
|
124
|
+
*/
|
|
125
|
+
query<T = unknown>(sql: string, params?: unknown[]): Promise<T>;
|
|
126
|
+
/**
|
|
127
|
+
* Run a repeatable-read, consistent snapshot on the primary database.
|
|
128
|
+
*
|
|
129
|
+
* @remarks
|
|
130
|
+
* The callback is repeated at most once on a fresh connection after a fatal connection error.
|
|
131
|
+
* This is safe only because the transaction is declared read-only. Use {@link transaction} for
|
|
132
|
+
* writes; write transactions are never repeated after connection loss. Calls are serialized on
|
|
133
|
+
* one dedicated connection, so recursive `readTransaction()` calls are not supported.
|
|
134
|
+
*/
|
|
135
|
+
readTransaction<T>(fn: (reader: ReadTransaction<TTx>) => Promise<T>): Promise<T>;
|
|
136
|
+
}
|
|
137
|
+
/** Primary read-only transaction handles that share one consistent snapshot. */
|
|
138
|
+
export interface ReadTransaction<TTx> {
|
|
139
|
+
/**
|
|
140
|
+
* The consumer's Drizzle transaction handle.
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* Drizzle's type does not distinguish read-only transactions. MySQL enforces read-only mode at
|
|
144
|
+
* runtime; applications may expose a narrower SELECT-only facade when they need compile-time
|
|
145
|
+
* enforcement.
|
|
146
|
+
*/
|
|
147
|
+
orm: TTx;
|
|
148
|
+
/** Run raw SQL on the same primary connection and snapshot. */
|
|
149
|
+
query: <T = unknown>(sql: string, params?: unknown[]) => Promise<T>;
|
|
150
|
+
}
|
|
101
151
|
/**
|
|
102
152
|
* Options for {@link createMysqlDatabase}.
|
|
103
153
|
*
|
|
@@ -155,17 +205,19 @@ export interface CreateHyperdriveDatabaseOptions<TDrizzle> {
|
|
|
155
205
|
connectionOptions?: Record<string, unknown>;
|
|
156
206
|
}
|
|
157
207
|
/**
|
|
158
|
-
* Create a {@link
|
|
208
|
+
* Create a {@link HyperdriveDatabase} that lazily opens its connections from Hyperdrive bindings.
|
|
159
209
|
*
|
|
160
210
|
* @remarks
|
|
161
211
|
* Construct one per request. Connections and the ORM are created on first use and reused for the
|
|
162
|
-
* lifetime of the instance
|
|
163
|
-
*
|
|
164
|
-
*
|
|
212
|
+
* lifetime of the instance. `read()` uses the replica, while `query()` provides an explicit
|
|
213
|
+
* primary SELECT path. Workers automatically cleans up connections at the end of the invocation,
|
|
214
|
+
* so callers do not need to close them manually. Either SELECT path is repeated once on a fresh
|
|
215
|
+
* connection after a fatal mysql2 connection error. Writes and transactions are never repeated
|
|
216
|
+
* for connection errors because their commit state can be ambiguous.
|
|
165
217
|
*
|
|
166
218
|
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
167
219
|
* @param options - the primary/replica Hyperdrive bindings, the ORM factory, and connection options.
|
|
168
|
-
* @returns a {@link
|
|
220
|
+
* @returns a {@link HyperdriveDatabase} whose compatibility `dispose()` method is a no-op; Workers
|
|
169
221
|
* cleans up its invocation-scoped connections automatically.
|
|
170
222
|
* @example
|
|
171
223
|
* ```ts
|
|
@@ -177,7 +229,7 @@ export interface CreateHyperdriveDatabaseOptions<TDrizzle> {
|
|
|
177
229
|
* await db.write((dz) => dz.insert(users).values(user));
|
|
178
230
|
* ```
|
|
179
231
|
*/
|
|
180
|
-
export declare function createHyperdriveDatabase<TDrizzle>(options: CreateHyperdriveDatabaseOptions<TDrizzle>):
|
|
232
|
+
export declare function createHyperdriveDatabase<TDrizzle>(options: CreateHyperdriveDatabaseOptions<TDrizzle>): HyperdriveDatabase<TDrizzle>;
|
|
181
233
|
/**
|
|
182
234
|
* Internal helper that assembles a {@link Database} from an ORM and a replica connection.
|
|
183
235
|
*
|
package/dist/db/database.js
CHANGED
|
@@ -24,17 +24,19 @@ export function createMysqlDatabase(options) {
|
|
|
24
24
|
return databaseFrom(options.orm, options.replica);
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* Create a {@link
|
|
27
|
+
* Create a {@link HyperdriveDatabase} that lazily opens its connections from Hyperdrive bindings.
|
|
28
28
|
*
|
|
29
29
|
* @remarks
|
|
30
30
|
* Construct one per request. Connections and the ORM are created on first use and reused for the
|
|
31
|
-
* lifetime of the instance
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* lifetime of the instance. `read()` uses the replica, while `query()` provides an explicit
|
|
32
|
+
* primary SELECT path. Workers automatically cleans up connections at the end of the invocation,
|
|
33
|
+
* so callers do not need to close them manually. Either SELECT path is repeated once on a fresh
|
|
34
|
+
* connection after a fatal mysql2 connection error. Writes and transactions are never repeated
|
|
35
|
+
* for connection errors because their commit state can be ambiguous.
|
|
34
36
|
*
|
|
35
37
|
* @typeParam TDrizzle - the consumer's Drizzle ORM type.
|
|
36
38
|
* @param options - the primary/replica Hyperdrive bindings, the ORM factory, and connection options.
|
|
37
|
-
* @returns a {@link
|
|
39
|
+
* @returns a {@link HyperdriveDatabase} whose compatibility `dispose()` method is a no-op; Workers
|
|
38
40
|
* cleans up its invocation-scoped connections automatically.
|
|
39
41
|
* @example
|
|
40
42
|
* ```ts
|
|
@@ -50,15 +52,84 @@ export function createHyperdriveDatabase(options) {
|
|
|
50
52
|
const { primaryHyperdrive, replicaHyperdrive, createOrm, connectionOptions } = options;
|
|
51
53
|
let primaryConn;
|
|
52
54
|
let replicaConn;
|
|
55
|
+
let readTransactionConn;
|
|
53
56
|
let orm;
|
|
57
|
+
let readTransactionOrm;
|
|
58
|
+
let readTransactionTail;
|
|
54
59
|
const primary = () => (primaryConn ??= connect(primaryHyperdrive, connectionOptions));
|
|
55
60
|
const replica = () => (replicaConn ??= connect(replicaHyperdrive, connectionOptions));
|
|
61
|
+
const readTransactionConnection = () => (readTransactionConn ??= connect(primaryHyperdrive, connectionOptions));
|
|
56
62
|
const ormFor = async () => (orm ??= createOrm(await primary()));
|
|
63
|
+
const readFrom = (connection, sql, params) => retryWhenDeadlock(async () => {
|
|
64
|
+
const [rows] = (await (await connection).query(sql, params));
|
|
65
|
+
return rows;
|
|
66
|
+
});
|
|
67
|
+
const readWithRecovery = async (connectionFor, reset, sql, params) => {
|
|
68
|
+
const connection = connectionFor();
|
|
69
|
+
const outcome = await readFrom(connection, sql, params).then((rows) => ({ ok: true, rows }), (error) => ({ ok: false, error }));
|
|
70
|
+
if (outcome.ok) {
|
|
71
|
+
return outcome.rows;
|
|
72
|
+
}
|
|
73
|
+
if (!isFatalConnectionError(outcome.error)) {
|
|
74
|
+
throw outcome.error;
|
|
75
|
+
}
|
|
76
|
+
reset(connection);
|
|
77
|
+
return readFrom(connectionFor(), sql, params);
|
|
78
|
+
};
|
|
79
|
+
const resetPrimary = (failedConnection) => {
|
|
80
|
+
if (primaryConn === failedConnection) {
|
|
81
|
+
primaryConn = undefined;
|
|
82
|
+
orm = undefined;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const resetReadTransaction = (failedConnection) => {
|
|
86
|
+
if (readTransactionConn === failedConnection) {
|
|
87
|
+
readTransactionConn = undefined;
|
|
88
|
+
readTransactionOrm = undefined;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const withReadTransactionLock = (fn) => {
|
|
92
|
+
const result = readTransactionTail ? readTransactionTail.then(fn, fn) : fn();
|
|
93
|
+
readTransactionTail = result.then(() => undefined, () => undefined);
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
const runReadTransaction = async (connection, fn) => {
|
|
97
|
+
const conn = await connection;
|
|
98
|
+
const dz = (readTransactionOrm ??= createOrm(conn));
|
|
99
|
+
return retryWhenDeadlock(async () => {
|
|
100
|
+
await conn.query('SET TRANSACTION READ ONLY');
|
|
101
|
+
return dz.transaction((tx) => fn({
|
|
102
|
+
orm: tx,
|
|
103
|
+
query: async (sql, params = []) => {
|
|
104
|
+
const [rows] = (await conn.query(sql, params));
|
|
105
|
+
return rows;
|
|
106
|
+
},
|
|
107
|
+
}), { isolationLevel: 'repeatable read', withConsistentSnapshot: true });
|
|
108
|
+
});
|
|
109
|
+
};
|
|
57
110
|
return {
|
|
58
111
|
read(sql, params = []) {
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
|
|
112
|
+
return readWithRecovery(replica, (failedConnection) => {
|
|
113
|
+
if (replicaConn === failedConnection) {
|
|
114
|
+
replicaConn = undefined;
|
|
115
|
+
}
|
|
116
|
+
}, sql, params);
|
|
117
|
+
},
|
|
118
|
+
query(sql, params = []) {
|
|
119
|
+
return readWithRecovery(primary, resetPrimary, sql, params);
|
|
120
|
+
},
|
|
121
|
+
async readTransaction(fn) {
|
|
122
|
+
return withReadTransactionLock(async () => {
|
|
123
|
+
const connection = readTransactionConnection();
|
|
124
|
+
const outcome = await runReadTransaction(connection, fn).then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
|
|
125
|
+
if (outcome.ok) {
|
|
126
|
+
return outcome.value;
|
|
127
|
+
}
|
|
128
|
+
if (!isFatalConnectionError(outcome.error)) {
|
|
129
|
+
throw outcome.error;
|
|
130
|
+
}
|
|
131
|
+
resetReadTransaction(connection);
|
|
132
|
+
return runReadTransaction(readTransactionConnection(), fn);
|
|
62
133
|
});
|
|
63
134
|
},
|
|
64
135
|
async write(fn) {
|
|
@@ -104,3 +175,22 @@ export function databaseFrom(orm, replica) {
|
|
|
104
175
|
function connect(hyperdrive, extra) {
|
|
105
176
|
return createConnection(hyperdriveConnectionOptions(hyperdrive, extra));
|
|
106
177
|
}
|
|
178
|
+
function isFatalConnectionError(error) {
|
|
179
|
+
let current = error;
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
while (typeof current === 'object' && current !== null && !seen.has(current)) {
|
|
182
|
+
seen.add(current);
|
|
183
|
+
const value = current;
|
|
184
|
+
if (value.fatal === true ||
|
|
185
|
+
value.code === 'PROTOCOL_CONNECTION_LOST' ||
|
|
186
|
+
value.code === 'ECONNRESET' ||
|
|
187
|
+
value.code === 'EPIPE' ||
|
|
188
|
+
(typeof value.message === 'string' &&
|
|
189
|
+
(value.message.includes('Connection lost:') ||
|
|
190
|
+
value.message.includes("Can't add new command when connection is in closed state")))) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
current = value.cause;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
package/dist/db/index.d.ts
CHANGED
|
@@ -11,15 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { retryWhenDeadlock } from './retry.js';
|
|
13
13
|
export { createMysqlDatabase, createHyperdriveDatabase, databaseFrom } from './database.js';
|
|
14
|
-
export type { Database, DisposableDatabase, QueryRunner, TxOf, CreateMysqlDatabaseOptions, CreateHyperdriveDatabaseOptions, Connection, Pool, } from './database.js';
|
|
14
|
+
export type { Database, DisposableDatabase, HyperdriveDatabase, ReadTransaction, QueryRunner, TxOf, CreateMysqlDatabaseOptions, CreateHyperdriveDatabaseOptions, Connection, Pool, } from './database.js';
|
|
15
15
|
export { insertIdOf, affectedRowsOf, insertedIdsOf } from './write-result.js';
|
|
16
16
|
export type { DzWriteResult } from './write-result.js';
|
|
17
17
|
export { hyperdriveConnectionOptions, withMysqlConnections } from './connection.js';
|
|
18
18
|
export type { HyperdriveLike, ExecutionContextLike } from './connection.js';
|
|
19
19
|
export { MYSQL_TIMEZONE, toJstDate, jstTimestampParams, jstDatetimeParams, jstDateParams } from './jst.js';
|
|
20
|
-
export {
|
|
21
|
-
export type { DecimalNumberConfig } from './decimal.js';
|
|
22
|
-
export { jstTimestamp, jstDatetime, jstDate, decimalNumber, jstOnUpdateNow } from './columns.js';
|
|
20
|
+
export { jstTimestamp, jstDatetime, jstDate, jstOnUpdateNow } from './columns.js';
|
|
23
21
|
export { DRIZZLE_ORM_OPTIONS, honoDrizzleConfig, resolveDbSecret } from './orm-config.js';
|
|
24
22
|
export type { HonoDrizzleConfigOptions, ResolvedDbSecret } from './orm-config.js';
|
|
25
23
|
export { baselineMigrations, readBaselineEntry } from './migrate.js';
|
package/dist/db/index.js
CHANGED
|
@@ -14,8 +14,7 @@ export { createMysqlDatabase, createHyperdriveDatabase, databaseFrom } from './d
|
|
|
14
14
|
export { insertIdOf, affectedRowsOf, insertedIdsOf } from './write-result.js';
|
|
15
15
|
export { hyperdriveConnectionOptions, withMysqlConnections } from './connection.js';
|
|
16
16
|
export { MYSQL_TIMEZONE, toJstDate, jstTimestampParams, jstDatetimeParams, jstDateParams } from './jst.js';
|
|
17
|
-
export {
|
|
18
|
-
export { jstTimestamp, jstDatetime, jstDate, decimalNumber, jstOnUpdateNow } from './columns.js';
|
|
17
|
+
export { jstTimestamp, jstDatetime, jstDate, jstOnUpdateNow } from './columns.js';
|
|
19
18
|
export { DRIZZLE_ORM_OPTIONS, honoDrizzleConfig, resolveDbSecret } from './orm-config.js';
|
|
20
19
|
export { baselineMigrations, readBaselineEntry } from './migrate.js';
|
|
21
20
|
export { reopenGuardedPaymentFailedSet } from './payment-failed.js';
|
package/dist/db/retry.js
CHANGED
|
@@ -32,8 +32,7 @@ export async function retryWhenDeadlock(fn, retries = 3, delay = 100) {
|
|
|
32
32
|
if (outcome.ok) {
|
|
33
33
|
return outcome.value;
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
if (code === 'ER_LOCK_DEADLOCK' && attempt < retries - 1) {
|
|
35
|
+
if (isDeadlock(outcome.error) && attempt < retries - 1) {
|
|
37
36
|
await new Promise((resolve) => setTimeout(resolve, delay * (attempt + 1)));
|
|
38
37
|
continue;
|
|
39
38
|
}
|
|
@@ -42,3 +41,16 @@ export async function retryWhenDeadlock(fn, retries = 3, delay = 100) {
|
|
|
42
41
|
// Unreachable: the loop returns on success and throws on the final failed attempt.
|
|
43
42
|
throw new Error('retryWhenDeadlock: exhausted retries');
|
|
44
43
|
}
|
|
44
|
+
function isDeadlock(error) {
|
|
45
|
+
let current = error;
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
while (typeof current === 'object' && current !== null && !seen.has(current)) {
|
|
48
|
+
seen.add(current);
|
|
49
|
+
const value = current;
|
|
50
|
+
if (value.code === 'ER_LOCK_DEADLOCK') {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
current = value.cause;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# API: `@rdlabo/workers-hono-kit/business-time`
|
|
2
|
+
|
|
3
|
+
String-level JST business-time conversions (Workers UTC instant ↔ business calendar date / date-time), with **no `mysql2` / `drizzle-orm` dependency**. This is a different layer from the `./db` column helpers (which handle the MySQL wire format): the DB stays on JST, and the app handles JST explicitly through this module instead of relying implicitly on the connection `timezone`.
|
|
4
|
+
|
|
5
|
+
| Export | Description |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `today(ref?)` | The JST business calendar date (`YYYY-MM-DD`) of `ref` (defaults to now). |
|
|
8
|
+
| `toBusinessDate(instant)` | UTC instant → JST business calendar date (`YYYY-MM-DD`). |
|
|
9
|
+
| `normalizeBusinessDate(value)` | Normalize a `string` / `Date` / nullish to `YYYY-MM-DD`; a `YYYY-MM-DD` string passes through unchanged, nullish/empty/invalid → `null`. |
|
|
10
|
+
| `toBusinessDateTime(instant)` | UTC instant → JST business date-time (`YYYY-MM-DD HH:mm:ss`). |
|
|
11
|
+
| `parseBusinessDateTime(value)` | JST business date-time string → UTC instant (accepts a space or `T` separator). |
|
|
12
|
+
| `formatBusinessDateTime(instant, pattern?)` | Format an instant in the business TZ (Nest `helper.formatDate`-compatible tokens). |
|
|
13
|
+
| `startOfBusinessDay(date)` / `endOfBusinessDay(date)` | UTC instant of `00:00:00` / `23:59:59` on a JST business date. |
|
|
14
|
+
| `businessDateTimeInstant(date, time)` | JST business date + wall-clock time → UTC instant. |
|
|
15
|
+
| `addBusinessDays(date, days)` | Add calendar days to a JST business date. |
|
|
16
|
+
| `ageOnBusinessDate(birthDate, asOfDate?)` | Full years of age on a business date (`asOfDate` defaults to `today()`). |
|
|
17
|
+
| `DEFAULT_BUSINESS_DATETIME_PATTERN` | Default `formatBusinessDateTime` pattern (`YYYY-MM-DDThh:mm:ss`). |
|
|
18
|
+
| `BUSINESS_TIMEZONE` / `BusinessDate` / `BusinessDateTime` | JST timezone constant and the business-date / date-time string types. |
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
toBusinessDate,
|
|
23
|
+
toBusinessDateTime,
|
|
24
|
+
formatBusinessDateTime,
|
|
25
|
+
addBusinessDays,
|
|
26
|
+
} from '@rdlabo/workers-hono-kit/business-time';
|
|
27
|
+
|
|
28
|
+
const now = new Date('2026-07-05T21:00:00Z');
|
|
29
|
+
toBusinessDate(now); // '2026-07-06' (JST)
|
|
30
|
+
toBusinessDateTime(now); // '2026-07-06 06:00:00'
|
|
31
|
+
formatBusinessDateTime(now); // '2026-07-06T06:00:00'
|
|
32
|
+
addBusinessDays('2026-07-06', 3); // '2026-07-09'
|
|
33
|
+
```
|
package/docs/api-db.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# API: `@rdlabo/workers-hono-kit/db`
|
|
2
|
+
|
|
3
|
+
Requires the `drizzle-orm` and `mysql2` peers. Reads use raw SQL against the replica by default, with an explicit primary read path for freshness; writes/transactions run against the primary through the Drizzle ORM with deadlock retry. The kit deliberately does not depend on the ORM's type identity — you pass the Drizzle instance in.
|
|
4
|
+
|
|
5
|
+
| Export | Description |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `createHyperdriveDatabase(options)` | `HyperdriveDatabase` that lazily opens primary/replica connections from Hyperdrive bindings per request. `read()` targets the replica, `query()` targets the primary, and `readTransaction()` pins raw and Drizzle reads to one primary consistent snapshot. Read transactions are serialized on a separately cached connection and must not be recursively nested. A SELECT or complete read-only transaction is repeated at most once on a fresh connection after a fatal mysql2 connection error. Writes and write transactions are not repeated. Workers cleans connections up at invocation end; the legacy `dispose()` is a no-op. |
|
|
8
|
+
| `createMysqlDatabase(options)` | Assemble a `Database` from an already-connected Drizzle ORM + replica `QueryRunner`. |
|
|
9
|
+
| `databaseFrom(orm, replica)` | Build a `Database` from an existing Drizzle instance + replica handle. |
|
|
10
|
+
| `Database` / `DisposableDatabase` / `HyperdriveDatabase` / `ReadTransaction` / `QueryRunner` / `TxOf` | The `read` / `query` / `readTransaction` / `write` / `transaction` API and its supporting types. |
|
|
11
|
+
| `hyperdriveConnectionOptions(hyperdrive, overrides?)` / `HyperdriveLike` / `ExecutionContextLike` | Build mysql2 `createConnection` options from a Hyperdrive binding (`disableEval`, `decimalNumbers`, `timezone '+09:00'` by default). `timezone` controls mysql2's JavaScript `Date` conversion; it does not change the MySQL session timezone. |
|
|
12
|
+
| `withMysqlConnections(...)` | Open primary/replica connections in parallel and run a function. Workers cleans them up at invocation end. |
|
|
13
|
+
| `retryWhenDeadlock(fn, retries?, delay?)` | Same deadlock-retry helper as the root export. |
|
|
14
|
+
| `insertIdOf` / `affectedRowsOf` / `insertedIdsOf` / `DzWriteResult` | Extract `insertId` / `affectedRows` (and derive contiguous bulk-insert ids) from a mysql2 write result. |
|
|
15
|
+
| `toJstDate` / `jstTimestampParams` / `jstDatetimeParams` / `jstDateParams` | JST date/time normalization params (advanced use). |
|
|
16
|
+
| `MYSQL_TIMEZONE` | Default mysql2 connection `timezone` (`'+09:00'`) for the JST DB deployment. |
|
|
17
|
+
| `jstTimestamp` / `jstDatetime` / `jstDate` | Drizzle column helpers (no repo-side wrapper needed). |
|
|
18
|
+
| `jstOnUpdateNow` | SQL expression for `ON UPDATE CURRENT_TIMESTAMP`. The `jstTimestamp` customType (and friends) do not support `.onUpdateNow()`, so pair it with `.$onUpdateFn(() => jstOnUpdateNow(fsp))`. |
|
|
19
|
+
| `DRIZZLE_ORM_OPTIONS` / `honoDrizzleConfig(options)` / `HonoDrizzleConfigOptions` | Shared Drizzle casing (`snake_case`) for both the runtime `drizzle()` call and `drizzle.config.ts`, keeping config ↔ runtime in sync. |
|
|
20
|
+
| `resolveDbSecret()` / `ResolvedDbSecret` | Resolve DB connection info from the `DB_SECRET` env var (an AWS RDS managed-secret JSON string) for CI migrate / local tooling. Returns `undefined` when `DB_SECRET` is unset; throws on invalid JSON or a missing required key. |
|
|
21
|
+
| `baselineMigrations(options)` / `readBaselineEntry(migrationsFolder)` / `BaselineMigrationsOptions` / `BaselineResult` / `BaselineEntry` | Brownfield first-deploy helper: mark an existing `0000_*` migration as applied without re-running DDL. |
|
|
22
|
+
|
|
23
|
+
## Drizzle column helpers (`jstTimestamp`, etc.)
|
|
24
|
+
|
|
25
|
+
- `drizzle-orm` is a **peer** only. The kit does not include `drizzle-orm` as a dependency (even after publishing, it uses the consumer's single copy).
|
|
26
|
+
- The consumer just keeps `drizzle-orm` in its `dependencies` as usual. **No `overrides` in `package.json` are needed.**
|
|
27
|
+
- The npm-published artifact contains no `devDependencies`, so installing it does not add a kit-specific `drizzle-orm` (there is only the one peer copy).
|
|
28
|
+
- The column helpers `import` the consumer's `drizzle-orm` at runtime, and the types are the `customType` inference as-is (`MySqlCustomColumnBuilder<…>`). No `any` is used, so the column's semantic type propagates to the consumer table's `$inferSelect`.
|
|
29
|
+
- **Precondition: resolve drizzle to a single copy.** Drizzle's `SQL` is a **nominal** type carrying a private field `shouldInlineParams`, so if the kit and the consumer resolve different copies, `jstTimestamp(…).default(sql\`…\`)` fails the whole schema with `TS2345 separate declarations of a private property 'shouldInlineParams'`. Under `file:`-link development, `drizzle-orm` nests under the kit and becomes a second copy, so **pin `drizzle-orm` to the consumer's own single copy in `tsconfig.json`**:
|
|
30
|
+
|
|
31
|
+
```jsonc
|
|
32
|
+
// tsconfig.json compilerOptions (merge with existing paths if any)
|
|
33
|
+
"paths": {
|
|
34
|
+
"drizzle-orm": ["./node_modules/drizzle-orm"],
|
|
35
|
+
"drizzle-orm/*": ["./node_modules/drizzle-orm/*"]
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
With `moduleResolution: "Bundler"`, `baseUrl` is not required (if `baseUrl` is already set, drop the leading `./`). On the published package (a single copy) these `paths` are harmless. **No `overrides` needed.**
|
|
40
|
+
- When developing against the kit via a direct `file:` link, run `npm install` in the kit repo itself to satisfy its peers (do not add `overrides` on the consumer side).
|
|
41
|
+
|
|
42
|
+
## `CURRENT_TIMESTAMP` vs the connection `timezone:'+09:00'`
|
|
43
|
+
|
|
44
|
+
| Path | Who decides the time | Relationship to JST |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| The app binds a `Date` (INSERT/UPDATE) | mysql2 + connection `timezone:'+09:00'` | Treated as JST on the wire (`datetime-wire` test) |
|
|
47
|
+
| `DEFAULT CURRENT_TIMESTAMP` / `ON UPDATE CURRENT_TIMESTAMP` | The MySQL server (session `time_zone`) | A **separate path** from the connection option. JST if the RDS `time_zone` is `+09:00`, UTC if UTC |
|
|
48
|
+
|
|
49
|
+
`jstTimestamp` / `jstDatetime` only handle read/write pass-through and DATE normalization; they do not change the timezone of server-side defaults. For columns that need `ON UPDATE`, keep the DDL intent with `.$onUpdateFn(() => jstOnUpdateNow(6))`.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# API: `@rdlabo/workers-hono-kit/offline`
|
|
2
|
+
|
|
3
|
+
Table-agnostic building blocks for product-owned REST ↔ DB method converters and their offline replica wire values. This subpath does not define table projections, Zod object shapes, public-column allowlists, schema hashes, or domain rules; those remain in each Hono application.
|
|
4
|
+
|
|
5
|
+
This is an additive subpath: existing root and subpath exports are unchanged. Consumers can migrate converter internals independently without changing REST payloads, schema hashes, or persisted SQLite rows. For an `AUTO_INCREMENT` table, omit `id` from a create method's table scheme; keep the client-generated UUID in `local_id` and keep `server_id` null until the server confirms its id.
|
|
6
|
+
|
|
7
|
+
| Export | Description |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `defineRestDbMethodConverter(converter)` | Type a product-owned, pure `MethodScheme ↔ TableScheme` converter without hiding HTTP or persistence side effects. |
|
|
10
|
+
| `RestDbMethodConverter` | Product-owned converter contract. Select and insert bundles may differ; every represented table and column remains required. |
|
|
11
|
+
| `CompleteRestDbTableScheme` | Compile-time lock requiring every represented table key and row column. |
|
|
12
|
+
| `toReplicaIsoDatetime(value)` | `Date` / datetime string → canonical UTC ISO-8601 wire value. |
|
|
13
|
+
| `toReplicaDateOnly(value)` | `Date` / date string / `null` → canonical `YYYY-MM-DD` / `null`. |
|
|
14
|
+
| `replicaTimestampMs(value)` | Replica datetime → epoch milliseconds for legacy DTOs. |
|
|
15
|
+
| `toTinyIntFlag(value)` / `fromTinyIntFlag(value)` | Boolean-like value ↔ numeric tinyint flag. |
|
|
16
|
+
| `replicaNowIso(clock?)` | Injectable wall clock → canonical UTC ISO-8601 wire value. |
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import {
|
|
20
|
+
defineRestDbMethodConverter,
|
|
21
|
+
replicaNowIso,
|
|
22
|
+
toReplicaIsoDatetime,
|
|
23
|
+
} from '@rdlabo/workers-hono-kit/offline';
|
|
24
|
+
|
|
25
|
+
type Tables = {
|
|
26
|
+
foods: FoodRow[];
|
|
27
|
+
allergens: AllergenRow[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const foodMethodConverter = defineRestDbMethodConverter<FoodMethodScheme, Tables>({
|
|
31
|
+
toMethodScheme: ({ foods, allergens }) => ({
|
|
32
|
+
...foods[0],
|
|
33
|
+
allergens: allergens.map(({ value }) => value),
|
|
34
|
+
}),
|
|
35
|
+
toTableScheme: (method) => ({
|
|
36
|
+
foods: [{ id: method.id, memo: method.memo ?? null }],
|
|
37
|
+
allergens: method.allergens.map((value) => ({ threadId: method.id, value })),
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`toTableScheme` requires every key represented by its DB row types. This includes nullable/default columns that Drizzle marks optional in `$inferInsert`; write `memo: method.memo ?? null` instead of omitting `memo`. If a REST method intentionally does not own an `AUTO_INCREMENT` column, remove it from that method's product-owned table scheme explicitly:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
type CreateTables = {
|
|
46
|
+
foods: Omit<typeof foods.$inferInsert, 'id'>[];
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The converter then cannot demand or manufacture `id`; the server adds the generated id to the confirmed response before it is stored as `server_id`.
|
|
51
|
+
|
|
52
|
+
When a write needs authenticated ownership or scope that is intentionally absent from the public REST body, use separate select/insert bundles and an explicit write context. The original two-generic form remains valid.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
defineRestDbMethodConverter<Method, SelectTables, InsertTables, { userId: number }>({
|
|
56
|
+
toMethodScheme: ({ foods, allergens }) => composeFood(foods, allergens),
|
|
57
|
+
toTableScheme: (method, { userId }) => ({
|
|
58
|
+
foods: [{ userId, name: method.name, memo: method.memo ?? null }],
|
|
59
|
+
allergens: method.allergens.map((value) => ({ value })),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
replicaNowIso(() => new Date('2026-07-23T10:00:00Z')); // '2026-07-23T10:00:00.000Z'
|
|
66
|
+
toReplicaIsoDatetime('2026-07-23T19:00:00+09:00'); // '2026-07-23T10:00:00.000Z'
|
|
67
|
+
```
|
package/docs/api-root.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# API: `@rdlabo/workers-hono-kit`
|
|
2
|
+
|
|
3
|
+
The root export is web-standard only: it runs on `workerd` and never depends on Node.js APIs or `mysql2`. The table below lists the helpers exported from the root entry point.
|
|
4
|
+
|
|
5
|
+
| Export | Description |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `finalizeResponse()` | Middleware that adds a weak `ETag` (delegates to `hono/etag`; also handles `If-None-Match` → `304`). |
|
|
8
|
+
| `validate(target, schema, options?)` | Zod validator → NestJS `ValidationPipe`-shaped `400` (`{ statusCode, message[], error }`). `options.onValidationError(err, c)` to report (e.g. Sentry). |
|
|
9
|
+
| `createValidate({ sentry? })` | Bound `validate` factory. Pass `sentry` on Sentry apps; omit for console-only (review, cbs-ai). |
|
|
10
|
+
| `createSentryValidate(sentry)` | **Deprecated** — use `createValidate({ sentry })`. |
|
|
11
|
+
| `zNum` / `zNumWithDefault` / `zNumOptional` / `zNumNullable` | Number-coercion zod schemas (mirror class-transformer `@Transform`). |
|
|
12
|
+
| `getAuthenticationSecret<T>(options, secretId)` / `AwsSecretsOptions` | Fetch a secret from AWS Secrets Manager (SigV4 `fetch`, per-isolate cache). |
|
|
13
|
+
| `getTemporaryCredentials(options)` / `GetTemporaryCredentialsOptions` / `StsCredentials` | STS `AssumeRole` via SigV4 `fetch` (global `sts.amazonaws.com`); returns temporary credentials for browser S3 uploads. |
|
|
14
|
+
| `getCloudFrontSignedUrl(url, privateKeyPem, keyPairId, dateLessThan)` | CloudFront signed URL (canned policy, RSA-SHA1, URL-safe base64) — Web Crypto reimpl of `@aws-sdk/cloudfront-signer`, byte-identical query order. |
|
|
15
|
+
| `JoseFirebaseVerifier` / `FirebaseVerifier` / `DecodedIdToken` | Firebase ID-token verification (`verifyIdToken`, `getUser`, `deleteUser`). |
|
|
16
|
+
| `createRemoteFirebaseVerifier(projectId)` | Convenience factory: production verifier with a cached remote JWKS (verification only). |
|
|
17
|
+
| `createServiceAccountVerifier(serviceAccountJson)` | Cached verifier built from a service-account JSON, **with `IdentityToolkit`** (getUser/deleteUser). One per isolate, re-created only when the SA JSON changes. |
|
|
18
|
+
| `IdentityToolkit` / `ServiceAccount` / `SECURETOKEN_JWK_URL` | Identity Toolkit REST client + constants for `getUser` / `deleteUser`. |
|
|
19
|
+
| `retryWhenDeadlock(fn, retries?, delay?)` | Retry on MySQL `ER_LOCK_DEADLOCK` with exponential backoff. |
|
|
20
|
+
| `getUserProtocol(c)` / `IUserProtocol` | Read client IP / UA (`CF-Connecting-IP` → `X-Forwarded-For`). |
|
|
21
|
+
| `getAppInfo(c)` / `AppInfo` | Read `x-amz-meta-version` / `x-amz-meta-uuid`. |
|
|
22
|
+
| `resolveAppEnv(env)` / `isProductionEnv(env)` / `AppEnv` | Resolve `'development'` / `'production'` from `env.APP_ENV` (defaults to `'production'` for safety). |
|
|
23
|
+
| `HttpStatus` | Standard HTTP status code enum (IANA registry). |
|
|
24
|
+
| `createHttpErrorHandler(options?)` / `HttpErrorHandlerOptions` | `app.onError()` handler that maps a thrown `HTTPException` to `{ statusCode, message, error? }` (`401` omits `error`). Optional custom error predicate and unhandled-error report hook. Unhandled errors log via `console.error` (mysql2 errors include `sqlMessage` / `errno` when detectable). |
|
|
25
|
+
| `createAppErrorHandler(options?)` / `CreateAppErrorHandlerOptions` | Standard `app.onError`: {@link createQueryFailedErrorHandler} + default {@link classifyGenericMysqlDriverError} + optional `sentry` (Sentry apps), `getReportError` / `reportError` (tests / container), or neither (no external reporting). |
|
|
26
|
+
| `createQueryFailedErrorHandler(options)` / `QueryFailedClassifier` / `ClassifiedDbError` | Lower-level compose when you need full control over `classify` + `onUnhandledError` without defaults. |
|
|
27
|
+
| `classifyGenericMysqlDriverError(err)` | Default classifier: any mysql2 driver error → `{ statusCode: 500, message: 'Internal server error' }`; non-DB errors → `null`. |
|
|
28
|
+
| `findMysqlDriverError(err)` / `logMysqlDriverError(err, statusCode)` | Low-level mysql2 driver-error detection (follows `err.cause`) and structured logging. For custom classifiers (e.g. odss). |
|
|
29
|
+
| `notFoundHandler(c)` | `app.notFound()` handler with `{ message: 'Cannot METHOD path', error, statusCode }` 404 body. |
|
|
30
|
+
| `normalizeTrailingSlash(request)` | Strip trailing slash(es) from the request URL before routing (Express/Nest parity). Does **not** 301-redirect — preserves POST/PUT/DELETE bodies. |
|
|
31
|
+
| `HTTP_ERROR_PHRASES` | `{ 400, 401, 403, 404 }` → standard `error` field phrases. |
|
|
32
|
+
| `createAuthMiddleware(options)` / `AuthMiddlewareOptions` | Factory for a Firebase-token auth middleware: reads the token header, verifies, resolves the DB user id, and stashes the result on the context. Omit `resolveUserId` for a token-only (login) guard. |
|
|
33
|
+
| `createIdentityAuthFailureBody()` / `createLegacyIdentityAuthFailureBody()` / `createAuthFailureBody(scope, code, message)` / `AuthFailureScope` | Stable wire contract for distinguishing a lost global identity (`identity`) from recent-login (`reauthentication`) and feature credential (`credential`) failures. The legacy helper tags products whose installed clients still require auth failure as `403`. |
|
|
34
|
+
| `perfLog(options?)` / `PerfLogOptions` / `AnalyticsEngineDatasetLike` | Middleware that records one per-request latency data point (`t_app`, colo, cold/warm, route, status) and emits it to **Workers Logs** (`console.log`) and/or **Workers Analytics Engine** (`writeDataPoint`). Lets you measure low-traffic Workers without a live `wrangler tail`. |
|
|
35
|
+
| `createMaintenanceMiddleware(options)` / `createMaintenanceWaitHandler(options)` / `isMaintenanceEnabled(env)` / `MAINTENANCE_CODE` / `MAINTENANCE_WAIT_PATH` | Fleet maintenance short-circuit: when enabled (`MAINTENANCE=1`), every non-allowlisted request returns `503` + `{ statusCode, message, code: 'MAINTENANCE' }` **before** container/DB. Pair with `GET /public/maintenance/wait` SSE (`event: ping` / `event: ended`) so clients can auto-dismiss a lock UI. Mount after `cors`, before `containerMiddleware`. |
|
|
36
|
+
| `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createHttpErrorHandler`'s `onUnhandledError`. |
|
|
37
|
+
| `createSentryErrorReporter(sentry)` / `SentryExceptionReporterLike` | Build an `ErrorReporter` that forwards to Sentry with an optional `request_id` tag (no hard `@sentry/cloudflare` dependency). |
|
|
38
|
+
| `DeferExecutor` / `defaultDefer` / `createWaitUntilDefer(ctx)` | Fire-and-forget executor for Workers: both variants log background rejections without propagating them; `createWaitUntilDefer` also registers work via `ctx.waitUntil`. |
|
|
39
|
+
| `configureHibernationAutoResponse` / `upgradeHibernationWebSocket` / `broadcastHibernationWebSockets` | Hibernation WebSocket room primitives: runtime ping/pong without waking JavaScript, attachment-before-accept upgrade, and broadcast through sockets restored by `getWebSockets()`. |
|
|
40
|
+
| `acknowledgeHibernationWebSocketClose` / `closeHibernationWebSocket` | Safe close helpers, including normalization of reserved received-only close codes. |
|
|
41
|
+
| `retryDurableObjectOperation(operation, options?)` / `isRetryableDurableObjectError(error)` | Retry idempotent DO work only for `retryable && !overloaded`, with jittered exponential backoff. `operation` runs per attempt so callers create a fresh stub after an exception. |
|
|
42
|
+
| `createIdempotencyInput(...)` / `runIdempotentMutation(...)` | Canonical payload hashing and a transaction-bound mutation state machine. Missing keys preserve legacy behavior; replay/conflict/in-flight semantics are shared while each app owns its schema and ORM adapter. |
|
|
43
|
+
| `withIdempotencyHttpErrors(run)` | Maps only standard idempotency failures to 400/409/503 and rethrows unrelated failures. |
|
|
44
|
+
| `createAiGatewayProvider(config)` / `AiGatewayConfig` / `AiGatewayProvider` | Route `@ai-sdk` models through the Cloudflare AI Gateway, via either a Workers `AI` binding or REST credentials (`accountId` / `gateway` / `token`). |
|
|
45
|
+
| `KVCache` / `KVNamespace` / `KVCacheOptions` / `KVCacheErrorContext` / `KVCacheOperation` | Workers-KV cache-aside helper (key `appName+version+table_type_column`, sha256 for string ids, TTL clamped ≥60s). Set `appName` / `version` per application; optional `onError(error, context)` observes fail-soft read/parse/serialize/write/delete failures. Context contains only the operation and logical table, not cache types, keys, ids, or values. |
|
|
46
|
+
| `createStripeClient(secret, opts?)` / `verifyStripeWebhook(...)` / `CreateStripeClientOptions` | Workers-native Stripe client (fetch transport) + async webhook verification (SubtleCrypto). `apiVersion` optional (pin to a fixed Stripe API version). |
|
|
47
|
+
| `extractStripeFailureReason(source)` / `StripeFailureReason` | Duck-type a Stripe `PaymentIntent` / `Invoice` / `{ paymentIntent?, invoice? }` / thrown error into a normalized `{ code, declineCode, message, paymentIntentId, invoiceId, subscriptionId }` (SDK-free), or `null`. |
|
|
48
|
+
| `stripeFailureMessageJa(reason)` | Render a `StripeFailureReason` (or `null`) as a single user-facing Japanese sentence (`decline_code` > `code`; fraud codes masked; unknown → generic). |
|
|
49
|
+
| `PaymentDeclinedError` / `toPaymentDeclinedError(error, status?)` / `PaymentDeclinedBody` | `HTTPException` carrying a verbatim `{ statusCode, message, code?, declineCode? }` body for a synchronous card decline (defaults to `400`). `toPaymentDeclinedError` returns `null` for non-declines (re-throw → 500). |
|
|
50
|
+
| `classifyStripeReconcile(subscription)` / `StripeReconcileAction` | Classify an expanded Stripe subscription into `trial` / `clear` / `canceled` / `failed` / `action_required` / `none` (termination evaluated before `succeeded`). Consumer does the DB write. |
|
|
51
|
+
| `serializePaymentFailure(record)` / `parsePaymentFailure(receipt)` / `PaymentFailureRecord` / `PaymentFailureReason` / `PaymentFailureSource` | (De)serialize the `payment_failed.receipt` JSON. `parsePaymentFailure` restores both a full Stripe record and a bare IAP reason. |
|
|
52
|
+
| `serializeIapFailureReason(reason)` / `IapFailureReason` | Serialize an IAP reason (`billing_retry` / `auto_renew_off` / `subscription_canceled` / `subscription_gone` + provider codes) directly, without the source/timestamp wrapper. |
|
|
53
|
+
| `paymentFailureMessageJa(input)` / `PaymentFailureStatus` / `PaymentFailureType` / `UNRESOLVED_PAYMENT_STATUSES` | Provider-agnostic Japanese message for a `payment_failed` row (`canceled` re-subscribe prompt, IAP `failed` App Store/Google Play prompt, else Stripe wording). `UNRESOLVED_PAYMENT_STATUSES` = everything except `resolved` for read/resolve `WHERE`. |
|
|
54
|
+
| `iapFailureKey(input)` | Provider-native `payment_failed.recursions_id`: iOS `${original_transaction_id}:${expires_date_ms}`, Android `${orderId}` (provider is in the `type` column). |
|
|
55
|
+
| `verifyAppleReceipt(receipt, opts)` / `classifyAppleRenewal(verify, now)` / `AppleRenewalClassification` / `AppleRenewalState` / `AppleVerifyReceiptResponse` / `ApplePendingRenewalInfo` / `AppleLatestReceiptInfo` | Verify an App Store receipt (production → sandbox fallback; inject `password` / `fetchImpl`) and classify it into `billing_retry` / `lapsed` / `active` / `unknown` plus the raw fields used (`statusCode` / `billingRetryStatus` / `autoRenewStatus`, latest `original_transaction_id` / `expires_date_ms`). |
|
|
56
|
+
| `googleAccessToken(creds, fetch?)` / `getGoogleSubscription(opts)` / `classifyGoogleSubscription(purchase, now)` / `GoogleSubscriptionClassification` / `GoogleSubscriptionState` / `GoogleSubscriptionPurchase` / `GoogleOAuthCredentials` | Exchange a refresh token for an Android Publisher access token (throws on `invalid_grant`), fetch a subscription purchase, and classify it into `canceled` / `gone` / `active` / `unknown` plus raw `statusCode` / `cancelReason`. |
|
|
57
|
+
| `sendInChunks(queue, messages, options?)` / `QueueLike` / `QueueSendMessage` | Send queue messages in bounded chunks to stay under the Workers subrequest cap per invocation. `options.chunkSize` sets the per-batch size (defaults to and is capped at 100). |
|
|
58
|
+
| `processBatch(batch, handler, options?)` / `isNonRetryableQueueError(error)` / `NonRetryableQueueErrorLike` / `MessageBatchLike` / `QueueMessageLike` / `ProcessBatchOptions` / `ProcessBatchResult` | Process a queue batch with bounded concurrency. Errors explicitly tagged with `queueDisposition: 'discard'` are reported and acked as permanent failures; all other errors are retried. |
|
|
59
|
+
| `createQueueErrorHandler(options)` / `CreateQueueErrorHandlerOptions` | Factory for `processBatch`'s `onError`: logs every failure; optional Sentry capture with queue/message context; optional `maxRetries` gate (report only on final attempt, except permanent failures which are reported immediately). |
|
|
60
|
+
| `assertStripeCustomerUpdated(options)` | Preserve the shared Stripe UPDATE→existence-check algorithm. `createNotFoundError(customerId)` can supply a domain-specific error without forking the algorithm. |
|
|
61
|
+
| `ExecutionContextLike` | Minimal `waitUntil`-only Workers execution context shape used by lifecycle-compatible APIs and deferred work helpers. |
|
|
62
|
+
|
|
63
|
+
Permanent Queue failures must opt in with the Queue-specific marker; unrelated `retryable` fields are ignored:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import type { NonRetryableQueueErrorLike } from '@rdlabo/workers-hono-kit';
|
|
67
|
+
|
|
68
|
+
class CustomerLinkMissingError extends Error implements NonRetryableQueueErrorLike {
|
|
69
|
+
readonly queueDisposition = 'discard' as const;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# API: `@rdlabo/workers-hono-kit/testing`
|
|
2
|
+
|
|
3
|
+
Requires the `drizzle-orm` and `mysql2` peers. Consolidates duplicated test boilerplate.
|
|
4
|
+
|
|
5
|
+
| Export | Description |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `createTestDb(options)` / `TestDb` / `CreateTestDbOptions` / `TestDbConnection` | Test database built from committed Drizzle migrations as the single source of truth: `resetSchema` / `createTestPool` / `truncateAll` / `seed` / `mysqlReachable`. |
|
|
8
|
+
| `FakeFirebaseVerifier` | In-memory `FirebaseVerifier` for offline route tests (`register` / `verifyIdToken` / `getUser` / `deleteUser`). |
|
|
9
|
+
| `createPoolDatabase(options)` / `CreatePoolDatabaseOptions` | A `Database` backed by a single pool used as both primary and replica. |
|
|
10
|
+
| `createNoopDatabase()` | A `Database` stub that throws on `write` / `transaction` to catch accidental DB use in DB-less routes. |
|
|
11
|
+
| `authHeaders(token, opts?)` | Build interceptor-compatible auth headers for requests. |
|
|
12
|
+
| `registerFirebaseToken(firebase, uid, record?, token?)` | Register a token in a `FakeFirebaseVerifier` (no DB). |
|
|
13
|
+
| `provisionUser(pool, firebase, opts)` | Register a token and provision a conventional `users(id, firebase_uid, agree)` row; returns the user id (idempotent). |
|
|
14
|
+
| `configurableFake(impl, name?)` | Build a test double from a partial implementation; un-stubbed members throw `"${name}.${method} not configured"`. |
|
|
15
|
+
| `fakeApiList` / `fakePaymentIntent` / `fakeStripeEvent` / `fakeCheckoutSession` / `fakeCustomer` / `fakePrice` / `fakeSubscription` | Stripe object fixtures with sensible defaults, overridable per test. |
|
|
16
|
+
| `fakeKv()` / `fakeQueue()` / `FakeQueue` | In-memory Workers KV / Queues producer doubles (`sent` + `batchCount` on queues for subrequest-bound assertions). |
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# API
|
|
2
|
+
|
|
3
|
+
`@rdlabo/workers-hono-kit` exposes these entry points. This page maps each entry point to its dedicated reference page. For feature-level examples, see [HTTP and Authentication](./http-auth.md), [Data Layer](./data-layer.md), [Realtime and Offline](./realtime-offline.md), and [Testing and Operations](./testing-operations.md).
|
|
4
|
+
|
|
5
|
+
| Entry point | Description | Reference |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `@rdlabo/workers-hono-kit` | Web-standard helpers (middleware, HTTP, Firebase, AWS, AI, Stripe, KV, queues, idempotency). | [Root](./api-root.md) |
|
|
8
|
+
| `@rdlabo/workers-hono-kit/db` | MySQL data layer (mysql2 + Drizzle), JST column helpers, baseline migrations. | [DB](./api-db.md) |
|
|
9
|
+
| `@rdlabo/workers-hono-kit/business-time` | JST business calendar and date-time conversions. | [Business time](./api-business-time.md) |
|
|
10
|
+
| `@rdlabo/workers-hono-kit/offline` | Table-agnostic REST/DB method converters and replica wire helpers. | [Offline](./api-offline.md) |
|
|
11
|
+
| `@rdlabo/workers-hono-kit/realtime` | Durable Object WebSocket and retry helpers. | [Realtime and Offline](./realtime-offline.md) |
|
|
12
|
+
| `@rdlabo/workers-hono-kit/testing` | Drizzle-backed test DB, fakes, fixtures, and binding doubles. | [Testing](./api-testing.md) |
|