@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/db/index.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
*/
|
|
12
|
-
|
|
13
|
-
export { retryWhenDeadlock } from './retry.js';
|
|
14
|
-
|
|
15
|
-
export { createMysqlDatabase, createHyperdriveDatabase, databaseFrom } from './database.js';
|
|
16
|
-
export type {
|
|
17
|
-
Database,
|
|
18
|
-
DisposableDatabase,
|
|
19
|
-
QueryRunner,
|
|
20
|
-
TxOf,
|
|
21
|
-
CreateMysqlDatabaseOptions,
|
|
22
|
-
CreateHyperdriveDatabaseOptions,
|
|
23
|
-
Connection,
|
|
24
|
-
Pool,
|
|
25
|
-
} from './database.js';
|
|
26
|
-
|
|
27
|
-
export { insertIdOf, affectedRowsOf, insertedIdsOf } from './write-result.js';
|
|
28
|
-
export type { DzWriteResult } from './write-result.js';
|
|
29
|
-
|
|
30
|
-
export { hyperdriveConnectionOptions, withMysqlConnections } from './connection.js';
|
|
31
|
-
export type { HyperdriveLike, ExecutionContextLike } from './connection.js';
|
|
32
|
-
|
|
33
|
-
export { toJstDate, jstTimestampParams, jstDatetimeParams, jstDateParams } from './jst.js';
|
|
34
|
-
|
|
35
|
-
export { DRIZZLE_ORM_OPTIONS, honoDrizzleConfig, resolveDbSecret } from './orm-config.js';
|
|
36
|
-
export type { HonoDrizzleConfigOptions, ResolvedDbSecret } from './orm-config.js';
|
|
37
|
-
|
|
38
|
-
export { baselineMigrations, readBaselineEntry } from './migrate.js';
|
|
39
|
-
export type { BaselineMigrationsOptions, BaselineResult, BaselineEntry } from './migrate.js';
|
package/src/db/jst.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared building blocks for normalizing JST (Asia/Tokyo) date/time values.
|
|
3
|
-
*
|
|
4
|
-
* @remarks
|
|
5
|
-
* JST normalization is applied at the column boundary as a write-time side effect, and neither
|
|
6
|
-
* values nor types from `drizzle-orm` are imported here on purpose. Exporting a fully-built
|
|
7
|
-
* `customType` column from the kit would cause type collisions when the kit and the consumer
|
|
8
|
-
* resolve separate copies of `drizzle-orm` (the private `SQL` brand stops being nominally
|
|
9
|
-
* compatible). Instead the kit ships only the params and helpers, and the consumer builds the
|
|
10
|
-
* column with its own `customType`:
|
|
11
|
-
*
|
|
12
|
-
* ```ts
|
|
13
|
-
* import { customType } from 'drizzle-orm/mysql-core';
|
|
14
|
-
* import { jstTimestampParams, jstDateParams } from '@rdlabo/workers-hono-kit/db';
|
|
15
|
-
*
|
|
16
|
-
* export const jstTimestamp = (name: string, opts?: { fsp?: number }) =>
|
|
17
|
-
* customType<{ data: string | Date; driverData: string | Date }>(jstTimestampParams(opts?.fsp))(name);
|
|
18
|
-
* export const jstDate = (name: string) =>
|
|
19
|
-
* customType<{ data: string | null; driverData: string | null }>(jstDateParams())(name);
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* `timestamp`/`datetime` columns omit `toDriver` and pass `Date` values straight through, so the
|
|
23
|
-
* connection's `timezone: '+09:00'` default makes mysql2 format them as JST; pre-formatted strings
|
|
24
|
-
* also pass through. Drizzle's native `mode: 'date'` is avoided because it stringifies `Date` to
|
|
25
|
-
* UTC before the timezone layer, shifting values by -9h. `date` columns keep `toJstDate` because
|
|
26
|
-
* MySQL `DATE` rejects ISO/empty strings and a JST day-boundary normalization is required.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
const JST_OFFSET_MS = 9 * 60 * 60 * 1000;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Normalize a client-supplied date to the `YYYY-MM-DD` (JST) form accepted by a MySQL `DATE` column.
|
|
33
|
-
*
|
|
34
|
-
* Accepts ISO 8601 (`...Z`), `YYYY-MM-DD`, or an empty string. Nullish, empty, or unparseable input
|
|
35
|
-
* resolves to `null`.
|
|
36
|
-
*
|
|
37
|
-
* @remarks
|
|
38
|
-
* MySQL `DATE` rejects ISO strings with `ER_TRUNCATED_WRONG_VALUE`, so this cannot be handled by the
|
|
39
|
-
* driver alone; it is needed as the `toDriver` transform for a `date` column.
|
|
40
|
-
*
|
|
41
|
-
* @param value - the raw date string from the client (ISO 8601, `YYYY-MM-DD`, or empty), or nullish.
|
|
42
|
-
* @returns the JST calendar date as `YYYY-MM-DD`, or `null` when the input is empty or unparseable.
|
|
43
|
-
*/
|
|
44
|
-
export function toJstDate(value: string | null | undefined): string | null {
|
|
45
|
-
if (!value) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
const ms = new Date(value).getTime();
|
|
49
|
-
if (Number.isNaN(ms)) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
const jst = new Date(ms + JST_OFFSET_MS);
|
|
53
|
-
const p = (n: number): string => String(n).padStart(2, '0');
|
|
54
|
-
return `${jst.getUTCFullYear()}-${p(jst.getUTCMonth() + 1)}-${p(jst.getUTCDate())}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Build the params for a `customType` backing a MySQL `timestamp` column with `Date` pass-through.
|
|
59
|
-
*
|
|
60
|
-
* The column omits `toDriver`, so `Date` values flow straight to mysql2 and are formatted as JST by
|
|
61
|
-
* the connection's `timezone: '+09:00'` default.
|
|
62
|
-
*
|
|
63
|
-
* @param fsp - optional fractional-seconds precision; when provided, emits `timestamp(fsp)`.
|
|
64
|
-
* @returns the `customType` params object exposing the column's `dataType`.
|
|
65
|
-
* @example
|
|
66
|
-
* ```ts
|
|
67
|
-
* import { customType } from 'drizzle-orm/mysql-core';
|
|
68
|
-
* import { jstTimestampParams } from '@rdlabo/workers-hono-kit/db';
|
|
69
|
-
*
|
|
70
|
-
* const jstTimestamp = (name: string) =>
|
|
71
|
-
* customType<{ data: string | Date; driverData: string | Date }>(jstTimestampParams())(name);
|
|
72
|
-
* ```
|
|
73
|
-
*/
|
|
74
|
-
export const jstTimestampParams = (fsp?: number): { dataType: () => string } => ({
|
|
75
|
-
dataType: () => (fsp != null ? `timestamp(${fsp})` : 'timestamp'),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Build the params for a `customType` backing a MySQL `datetime` column with `Date` pass-through.
|
|
80
|
-
*
|
|
81
|
-
* Behaves like {@link jstTimestampParams} but emits a `datetime` data type; `Date` values pass
|
|
82
|
-
* through and are formatted as JST by the connection's `timezone: '+09:00'` default.
|
|
83
|
-
*
|
|
84
|
-
* @param fsp - optional fractional-seconds precision; when provided, emits `datetime(fsp)`.
|
|
85
|
-
* @returns the `customType` params object exposing the column's `dataType`.
|
|
86
|
-
* @example
|
|
87
|
-
* ```ts
|
|
88
|
-
* import { customType } from 'drizzle-orm/mysql-core';
|
|
89
|
-
* import { jstDatetimeParams } from '@rdlabo/workers-hono-kit/db';
|
|
90
|
-
*
|
|
91
|
-
* const jstDatetime = (name: string) =>
|
|
92
|
-
* customType<{ data: string | Date; driverData: string | Date }>(jstDatetimeParams())(name);
|
|
93
|
-
* ```
|
|
94
|
-
*/
|
|
95
|
-
export const jstDatetimeParams = (fsp?: number): { dataType: () => string } => ({
|
|
96
|
-
dataType: () => (fsp != null ? `datetime(${fsp})` : 'datetime'),
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Build the params for a `customType` backing a MySQL `date` column with JST normalization.
|
|
101
|
-
*
|
|
102
|
-
* Unlike the timestamp/datetime params, this defines a `toDriver` transform that runs
|
|
103
|
-
* {@link toJstDate} so client-supplied ISO/empty strings are normalized to a JST `YYYY-MM-DD` value
|
|
104
|
-
* the column accepts.
|
|
105
|
-
*
|
|
106
|
-
* @returns the `customType` params object exposing the column's `dataType` and `toDriver`.
|
|
107
|
-
* @example
|
|
108
|
-
* ```ts
|
|
109
|
-
* import { customType } from 'drizzle-orm/mysql-core';
|
|
110
|
-
* import { jstDateParams } from '@rdlabo/workers-hono-kit/db';
|
|
111
|
-
*
|
|
112
|
-
* const jstDate = (name: string) =>
|
|
113
|
-
* customType<{ data: string | null; driverData: string | null }>(jstDateParams())(name);
|
|
114
|
-
* ```
|
|
115
|
-
*/
|
|
116
|
-
export const jstDateParams = (): {
|
|
117
|
-
dataType: () => string;
|
|
118
|
-
toDriver: (value: string | null) => string | null;
|
|
119
|
-
} => ({
|
|
120
|
-
dataType: () => 'date',
|
|
121
|
-
toDriver: (value: string | null) => toJstDate(value),
|
|
122
|
-
});
|
package/src/db/migrate.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Brownfield baseline for Drizzle MySQL migrations.
|
|
3
|
-
*
|
|
4
|
-
* 既存(現行サービス)の DB は先にスキーマが存在するため、コミット済みの baseline マイグレーション
|
|
5
|
-
* (`drizzle/0000_*.sql` = 現行スキーマを introspect した CREATE TABLE 群)を `db:migrate` で流すと
|
|
6
|
-
* 全テーブルが衝突して失敗する。そこで 0000 を **実行せず「適用済み」として記録**する。
|
|
7
|
-
*
|
|
8
|
-
* 適用判定の根拠(drizzle-orm/mysql-core dialect.migrate): `__drizzle_migrations(id, hash,
|
|
9
|
-
* created_at)` の **`created_at` 最大値のみ**で未適用判定し、`max(created_at) < entry.when` の
|
|
10
|
-
* migration だけ実行する。hash は保存されるが判定には使われない。よって 0000 の marker として
|
|
11
|
-
* `(hash, created_at=当該 when)` を 1 行入れれば、以後 `db:migrate` は when がより大きい 0001+ だけを
|
|
12
|
-
* 適用し、0000 は skip する。新規/テスト DB は marker が無いのでフルチェーンが走る(挙動不変)。
|
|
13
|
-
*
|
|
14
|
-
* この関数は `drizzle-orm` に依存しない(journal/SQL を自前で読み、hash は drizzle と同じ sha256)。
|
|
15
|
-
* QueryRunner(mysql2 の `Connection`/`Pool` が構造的に代入可能)に対して生 SQL を実行する。
|
|
16
|
-
*
|
|
17
|
-
* @packageDocumentation
|
|
18
|
-
*/
|
|
19
|
-
import { createHash } from 'node:crypto';
|
|
20
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
-
import { join } from 'node:path';
|
|
22
|
-
|
|
23
|
-
import type { QueryRunner } from './database.js';
|
|
24
|
-
|
|
25
|
-
/** drizzle が使う既定のマイグレーション管理テーブル名。 */
|
|
26
|
-
const MIGRATIONS_TABLE = '__drizzle_migrations';
|
|
27
|
-
|
|
28
|
-
/** `meta/_journal.json` の 1 エントリ(必要なフィールドのみ)。 */
|
|
29
|
-
interface JournalEntry {
|
|
30
|
-
idx: number;
|
|
31
|
-
when: number;
|
|
32
|
-
tag: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** baseline(=最初の)マイグレーションの識別情報。 */
|
|
36
|
-
export interface BaselineEntry {
|
|
37
|
-
/** マイグレーション tag(例 `0000_melted_weapon_omega`)。 */
|
|
38
|
-
tag: string;
|
|
39
|
-
/** `_journal.json` の `when`(= drizzle の `created_at`/`folderMillis`)。 */
|
|
40
|
-
when: number;
|
|
41
|
-
/** `<tag>.sql` の生内容の sha256(drizzle と同一アルゴリズム)。 */
|
|
42
|
-
hash: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* `migrationsFolder`(drizzle の `out`、例 `./drizzle`)から baseline(最初の)エントリを読む。
|
|
47
|
-
*
|
|
48
|
-
* @param migrationsFolder - `meta/_journal.json` と `<tag>.sql` を含むフォルダ。
|
|
49
|
-
* @returns baseline エントリ(tag/when/hash)。
|
|
50
|
-
* @throws journal が無い / エントリが空 / `<tag>.sql` が無い場合。
|
|
51
|
-
*/
|
|
52
|
-
export function readBaselineEntry(migrationsFolder: string): BaselineEntry {
|
|
53
|
-
const journalPath = join(migrationsFolder, 'meta', '_journal.json');
|
|
54
|
-
if (!existsSync(journalPath)) {
|
|
55
|
-
throw new Error(`Can't find meta/_journal.json under ${migrationsFolder}. Run \`drizzle-kit generate\` first.`);
|
|
56
|
-
}
|
|
57
|
-
const journal = JSON.parse(readFileSync(journalPath, 'utf8')) as { entries?: JournalEntry[] };
|
|
58
|
-
const entries = journal.entries ?? [];
|
|
59
|
-
if (entries.length === 0) {
|
|
60
|
-
throw new Error(`No migration entries in ${journalPath}.`);
|
|
61
|
-
}
|
|
62
|
-
// 起点は必ず最初のエントリ(0000)。以降 0001+ は「新しい変更」なので既存 DB でも実行されるべき。
|
|
63
|
-
const first = entries[0];
|
|
64
|
-
const sqlPath = join(migrationsFolder, `${first.tag}.sql`);
|
|
65
|
-
if (!existsSync(sqlPath)) {
|
|
66
|
-
throw new Error(`Can't find ${first.tag}.sql under ${migrationsFolder}.`);
|
|
67
|
-
}
|
|
68
|
-
const sql = readFileSync(sqlPath, 'utf8');
|
|
69
|
-
return { tag: first.tag, when: first.when, hash: createHash('sha256').update(sql).digest('hex') };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** {@link baselineMigrations} のオプション。 */
|
|
73
|
-
export interface BaselineMigrationsOptions {
|
|
74
|
-
/** 生 SQL を実行する QueryRunner(mysql2 `Connection`/`Pool` が代入可能)。対象 DB に接続済みのこと。 */
|
|
75
|
-
db: QueryRunner;
|
|
76
|
-
/** drizzle の `out` フォルダ(既定 `./drizzle`)。 */
|
|
77
|
-
migrationsFolder?: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** {@link baselineMigrations} の結果。 */
|
|
81
|
-
export type BaselineResult =
|
|
82
|
-
| { status: 'inserted'; tag: string; when: number; hash: string }
|
|
83
|
-
| { status: 'already-baselined'; tag: string; when: number };
|
|
84
|
-
|
|
85
|
-
async function rowsOf(db: QueryRunner, sql: string, params?: unknown[]): Promise<Record<string, unknown>[]> {
|
|
86
|
-
const result = (await db.query(sql, params)) as [Record<string, unknown>[] | undefined, unknown];
|
|
87
|
-
return result[0] ?? [];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* 既存 DB へ baseline(0000)を「適用済み」として記録する。冪等・安全ガード付き。
|
|
92
|
-
*
|
|
93
|
-
* @remarks
|
|
94
|
-
* ガード:
|
|
95
|
-
* - 既に baseline marker(`created_at = when`)が在れば **no-op**(`already-baselined`)。
|
|
96
|
-
* - marker は無いが `__drizzle_migrations` に別の行が在る → **中断**(想定外の状態)。
|
|
97
|
-
* - 対象 DB に base table が 1 つも無い(空 DB)→ **中断**(空 DB は 0000 を skip すると
|
|
98
|
-
* テーブルが作られない。新規 DB には `db:migrate` を使う)。
|
|
99
|
-
*
|
|
100
|
-
* @param options - 接続と migrations フォルダ。{@link BaselineMigrationsOptions} 参照。
|
|
101
|
-
* @returns 挿入したか既に baseline 済みか。
|
|
102
|
-
* @throws 上記ガードに該当する場合。
|
|
103
|
-
*/
|
|
104
|
-
export async function baselineMigrations(options: BaselineMigrationsOptions): Promise<BaselineResult> {
|
|
105
|
-
const { db, migrationsFolder = './drizzle' } = options;
|
|
106
|
-
const baseline = readBaselineEntry(migrationsFolder);
|
|
107
|
-
|
|
108
|
-
// migrator と同一 DDL(存在すれば no-op)。
|
|
109
|
-
await db.query(
|
|
110
|
-
`create table if not exists \`${MIGRATIONS_TABLE}\` (
|
|
111
|
-
id serial primary key,
|
|
112
|
-
hash text not null,
|
|
113
|
-
created_at bigint
|
|
114
|
-
)`,
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
// 既に baseline marker があれば冪等 no-op。
|
|
118
|
-
const existing = await rowsOf(db, `select id from \`${MIGRATIONS_TABLE}\` where created_at = ? limit 1`, [
|
|
119
|
-
baseline.when,
|
|
120
|
-
]);
|
|
121
|
-
if (existing.length > 0) {
|
|
122
|
-
return { status: 'already-baselined', tag: baseline.tag, when: baseline.when };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// marker は無いが行が在る=既に別の状態。誤爆防止で中断。
|
|
126
|
-
const countRows = await rowsOf(db, `select count(*) as n from \`${MIGRATIONS_TABLE}\``);
|
|
127
|
-
const rowCount = Number(countRows[0]?.n ?? 0);
|
|
128
|
-
if (rowCount > 0) {
|
|
129
|
-
throw new Error(
|
|
130
|
-
`${MIGRATIONS_TABLE} already has ${rowCount} row(s) but no baseline marker (created_at=${baseline.when}). ` +
|
|
131
|
-
`Migration state is unexpected — refusing to insert. Inspect \`${MIGRATIONS_TABLE}\` manually.`,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// 空 DB への baseline は危険(0000 を skip 扱いにするとテーブルが作られない)。brownfield 確認。
|
|
136
|
-
const tableRows = await rowsOf(
|
|
137
|
-
db,
|
|
138
|
-
`select count(*) as n from information_schema.tables
|
|
139
|
-
where table_schema = DATABASE() and table_type = 'BASE TABLE' and table_name <> ?`,
|
|
140
|
-
[MIGRATIONS_TABLE],
|
|
141
|
-
);
|
|
142
|
-
const baseTableCount = Number(tableRows[0]?.n ?? 0);
|
|
143
|
-
if (baseTableCount === 0) {
|
|
144
|
-
throw new Error(
|
|
145
|
-
`Target DB has no base tables. baseline records 0000 as applied WITHOUT creating tables — this is only ` +
|
|
146
|
-
`for existing (brownfield) DBs. For a fresh/empty DB run \`drizzle-kit migrate\` instead.`,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
await db.query(`insert into \`${MIGRATIONS_TABLE}\` (\`hash\`, \`created_at\`) values (?, ?)`, [
|
|
151
|
-
baseline.hash,
|
|
152
|
-
baseline.when,
|
|
153
|
-
]);
|
|
154
|
-
return { status: 'inserted', tag: baseline.tag, when: baseline.when, hash: baseline.hash };
|
|
155
|
-
}
|
package/src/db/orm-config.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Centralizes Drizzle column-name casing so it is fixed (standard: `snake_case`) in both the
|
|
3
|
-
* config and the runtime ORM.
|
|
4
|
-
*
|
|
5
|
-
* @remarks
|
|
6
|
-
* Casing is configured in two distinct places:
|
|
7
|
-
*
|
|
8
|
-
* 1. The top-level `casing` in `drizzle.config.ts` decides the column names that `db:generate`
|
|
9
|
-
* **creates** (see {@link honoDrizzleConfig}).
|
|
10
|
-
* 2. The `drizzle(conn, { …casing })` call decides the column names the **runtime write builder**
|
|
11
|
-
* resolves to (see {@link DRIZZLE_ORM_OPTIONS}).
|
|
12
|
-
*
|
|
13
|
-
* If these two disagree, a multi-word camelCase column without an explicit column name will be
|
|
14
|
-
* generated with one name but queried with another, producing a runtime `Unknown column` error —
|
|
15
|
-
* something neither the type-check nor the migration surface, so it is caught late. Sourcing both
|
|
16
|
-
* from here makes the mismatch structurally impossible. Casing is ignored for columns that declare
|
|
17
|
-
* an explicit name, so this is a pure safety net that does not change existing behavior.
|
|
18
|
-
*
|
|
19
|
-
* The runtime `drizzle()` call itself is made by the consuming app with its own `drizzle-orm`; the
|
|
20
|
-
* kit only ever provides values, never the ORM instance, to avoid splitting `drizzle-orm` into two
|
|
21
|
-
* copies and breaking type identity.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Runtime ORM options shared by the consuming app's `drizzle()` call.
|
|
26
|
-
*
|
|
27
|
-
* Spread into the runtime ORM as `drizzle(conn, { schema, ...DRIZZLE_ORM_OPTIONS })` so the write
|
|
28
|
-
* builder resolves column names as `snake_case`, matching what `db:generate` creates.
|
|
29
|
-
*
|
|
30
|
-
* @remarks
|
|
31
|
-
* Fixes `mode: 'default'` and `casing: 'snake_case'`. See the module-level documentation for why
|
|
32
|
-
* the same casing must be used by both the config and the runtime ORM.
|
|
33
|
-
*/
|
|
34
|
-
export const DRIZZLE_ORM_OPTIONS = { mode: 'default', casing: 'snake_case' } as const;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Options for {@link honoDrizzleConfig}.
|
|
38
|
-
*/
|
|
39
|
-
export interface HonoDrizzleConfigOptions {
|
|
40
|
-
/** drizzle-kit `dbCredentials.database` — the database name to connect to. */
|
|
41
|
-
database: string;
|
|
42
|
-
/** Database host; defaults to `process.env.DB_HOST` then `127.0.0.1`. */
|
|
43
|
-
host?: string;
|
|
44
|
-
/** Database port; defaults to `process.env.DB_PORT` then `3306`. */
|
|
45
|
-
port?: number;
|
|
46
|
-
/** Database user; defaults to `process.env.DB_USER` then `root`. */
|
|
47
|
-
user?: string;
|
|
48
|
-
/** Database password; defaults to `process.env.DB_PASSWORD` then `root`. */
|
|
49
|
-
password?: string;
|
|
50
|
-
/** Path to the schema directory; defaults to `'./src/db/schemes'`. */
|
|
51
|
-
schema?: string;
|
|
52
|
-
/** Output directory for generated migrations; defaults to `'./drizzle'`. */
|
|
53
|
-
out?: string;
|
|
54
|
-
/**
|
|
55
|
-
* Optional table allow-list. Use this to restrict drizzle-kit to the schema's own tables when the
|
|
56
|
-
* database is shared with another application.
|
|
57
|
-
*/
|
|
58
|
-
tablesFilter?: string[];
|
|
59
|
-
/**
|
|
60
|
-
* Optional `db:introspect` (DB → JS) casing. This is an independent axis from the generation-side
|
|
61
|
-
* `casing: 'snake_case'` and only affects introspection output.
|
|
62
|
-
*/
|
|
63
|
-
introspect?: { casing: 'camel' | 'preserve' };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Build a `drizzle.config.ts` configuration object with the kit's standard defaults.
|
|
68
|
-
*
|
|
69
|
-
* Fixes `casing: 'snake_case'`, the `schema`/`out` paths, and `dbCredentials` (with env-based
|
|
70
|
-
* defaults), while leaving `tablesFilter` and `introspect` opt-in.
|
|
71
|
-
*
|
|
72
|
-
* @remarks
|
|
73
|
-
* Returns a plain object rather than a typed drizzle-kit config so that `drizzle-kit` need not be a
|
|
74
|
-
* dependency of the kit; the drizzle-kit CLI only reads the default export.
|
|
75
|
-
*
|
|
76
|
-
* @param options - configuration overrides; only `database` is required.
|
|
77
|
-
* @returns a plain configuration object suitable for `export default` in `drizzle.config.ts`.
|
|
78
|
-
* @example
|
|
79
|
-
* ```ts
|
|
80
|
-
* // drizzle.config.ts
|
|
81
|
-
* import { honoDrizzleConfig } from '@rdlabo/workers-hono-kit/db';
|
|
82
|
-
*
|
|
83
|
-
* export default honoDrizzleConfig({ database: 'app' });
|
|
84
|
-
* ```
|
|
85
|
-
*/
|
|
86
|
-
export function honoDrizzleConfig(options: HonoDrizzleConfigOptions) {
|
|
87
|
-
const {
|
|
88
|
-
database,
|
|
89
|
-
host,
|
|
90
|
-
port,
|
|
91
|
-
user,
|
|
92
|
-
password,
|
|
93
|
-
schema = './src/db/schemes',
|
|
94
|
-
out = './drizzle',
|
|
95
|
-
tablesFilter,
|
|
96
|
-
introspect,
|
|
97
|
-
} = options;
|
|
98
|
-
// CI/本番の migrate は AWS Secrets Manager の RDS マネージド secret(キー
|
|
99
|
-
// host/port/dbname/username/password)を `DB_SECRET` にまるごと渡す運用を吸収する。JSON.parse で
|
|
100
|
-
// 解釈するので secret のキー名(host≠DB_HOST)差を map でき、password の特殊文字もシェル安全。
|
|
101
|
-
// 未設定(ローカル/db:generate)は従来どおり個別 env → デフォルトにフォールバック。
|
|
102
|
-
// DB_SECRET が在れば「完全な secret」として全接続情報をそれで確定する(欠損/不正は throw)。
|
|
103
|
-
// 未設定時のみ従来の個別 env → デフォルトにフォールバックする。
|
|
104
|
-
const secret = resolveDbSecret();
|
|
105
|
-
const dbCredentials = secret
|
|
106
|
-
? {
|
|
107
|
-
host: secret.host,
|
|
108
|
-
port: secret.port,
|
|
109
|
-
user: secret.username,
|
|
110
|
-
password: secret.password,
|
|
111
|
-
database: secret.dbname,
|
|
112
|
-
}
|
|
113
|
-
: {
|
|
114
|
-
host: host ?? process.env.DB_HOST ?? '127.0.0.1',
|
|
115
|
-
port: port ?? Number(process.env.DB_PORT ?? 3306),
|
|
116
|
-
user: user ?? process.env.DB_USER ?? 'root',
|
|
117
|
-
password: password ?? process.env.DB_PASSWORD ?? 'root',
|
|
118
|
-
database,
|
|
119
|
-
};
|
|
120
|
-
return {
|
|
121
|
-
dialect: 'mysql' as const,
|
|
122
|
-
schema,
|
|
123
|
-
out,
|
|
124
|
-
casing: 'snake_case' as const,
|
|
125
|
-
...(tablesFilter ? { tablesFilter } : {}),
|
|
126
|
-
...(introspect ? { introspect } : {}),
|
|
127
|
-
dbCredentials,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** {@link resolveDbSecret} の戻り値(正規化済みの接続情報)。 */
|
|
132
|
-
export interface ResolvedDbSecret {
|
|
133
|
-
host: string;
|
|
134
|
-
port: number;
|
|
135
|
-
dbname: string;
|
|
136
|
-
username: string;
|
|
137
|
-
password: string;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* AWS RDS マネージド secret(`DB_SECRET` に入れた JSON 文字列)を解決する。
|
|
142
|
-
*
|
|
143
|
-
* @remarks
|
|
144
|
-
* - `DB_SECRET` 未設定 → `undefined`(ローカル/`db:generate` の正常フォールバック)。
|
|
145
|
-
* - 設定されている場合は「完全な接続情報」であることを要求し、**不正 JSON / 必須キー欠損は throw**
|
|
146
|
-
* (静かに localhost へフォールバックして事故らせない)。`port` のみ欠損時は 3306 を補う。
|
|
147
|
-
*
|
|
148
|
-
* `honoDrizzleConfig`(db:migrate)と `workers-hono-kit-db-baseline` bin の双方が同じ解釈を使う。
|
|
149
|
-
*/
|
|
150
|
-
export function resolveDbSecret(): ResolvedDbSecret | undefined {
|
|
151
|
-
const raw = process.env.DB_SECRET;
|
|
152
|
-
if (!raw) {
|
|
153
|
-
return undefined;
|
|
154
|
-
}
|
|
155
|
-
let parsed: Record<string, unknown>;
|
|
156
|
-
try {
|
|
157
|
-
parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
158
|
-
} catch {
|
|
159
|
-
throw new Error('DB_SECRET is set but is not valid JSON (expected an AWS RDS managed secret string).');
|
|
160
|
-
}
|
|
161
|
-
const { host, dbname, username, password } = parsed;
|
|
162
|
-
if (
|
|
163
|
-
typeof host !== 'string' ||
|
|
164
|
-
typeof dbname !== 'string' ||
|
|
165
|
-
typeof username !== 'string' ||
|
|
166
|
-
typeof password !== 'string'
|
|
167
|
-
) {
|
|
168
|
-
throw new Error('DB_SECRET must contain string host, dbname, username, password (AWS RDS managed secret shape).');
|
|
169
|
-
}
|
|
170
|
-
return { host, dbname, username, password, port: parsed.port === undefined ? 3306 : Number(parsed.port) };
|
|
171
|
-
}
|
package/src/db/retry.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Run an async unit of work, retrying it on MySQL deadlock errors with exponential backoff.
|
|
3
|
-
*
|
|
4
|
-
* Retries are triggered only by the `ER_LOCK_DEADLOCK` error code. Each failed attempt waits
|
|
5
|
-
* `delay * attempt` milliseconds (linear growth of the base delay) before the next try, and any
|
|
6
|
-
* non-deadlock error is rethrown immediately without retrying.
|
|
7
|
-
*
|
|
8
|
-
* @remarks
|
|
9
|
-
* MySQL rolls back the entire transaction when it detects a deadlock, so re-running the same unit
|
|
10
|
-
* of work is safe. Pass a `fn` that represents one complete unit — a single statement or an entire
|
|
11
|
-
* transaction — because the whole `fn` is re-executed on each retry.
|
|
12
|
-
*
|
|
13
|
-
* @typeParam T - resolved value produced by `fn`.
|
|
14
|
-
* @param fn - the unit of work to execute; it is invoked again from scratch on each retry.
|
|
15
|
-
* @param retries - maximum number of attempts (default `3`).
|
|
16
|
-
* @param delay - base backoff in milliseconds; attempt N waits `delay * N` (default `100`).
|
|
17
|
-
* @returns the value resolved by the first successful call to `fn`.
|
|
18
|
-
* @throws the last error thrown by `fn` once retries are exhausted, or any non-deadlock error on
|
|
19
|
-
* the first occurrence.
|
|
20
|
-
* @example
|
|
21
|
-
* ```ts
|
|
22
|
-
* await retryWhenDeadlock(() => db.transaction(async (tx) => {
|
|
23
|
-
* await tx.insert(orders).values(order);
|
|
24
|
-
* await tx.update(stock).set({ qty: sql`qty - 1` }).where(eq(stock.id, order.itemId));
|
|
25
|
-
* }));
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
export async function retryWhenDeadlock<T>(fn: () => Promise<T>, retries = 3, delay = 100): Promise<T> {
|
|
29
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
30
|
-
try {
|
|
31
|
-
return await fn();
|
|
32
|
-
} catch (error) {
|
|
33
|
-
const code = (error as { code?: string }).code;
|
|
34
|
-
if (code === 'ER_LOCK_DEADLOCK' && attempt < retries - 1) {
|
|
35
|
-
await new Promise((resolve) => setTimeout(resolve, delay * (attempt + 1)));
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
// Unreachable: the loop returns on success and throws on the final failed attempt.
|
|
42
|
-
throw new Error('retryWhenDeadlock: exhausted retries');
|
|
43
|
-
}
|
package/src/db/write-result.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shape of a Drizzle (mysql2) write result, narrowed to the fields callers actually read.
|
|
3
|
-
*
|
|
4
|
-
* @remarks
|
|
5
|
-
* A mysql2 INSERT/UPDATE/DELETE result is the tuple `[ResultSetHeader, FieldPacket[]]`. Typing the
|
|
6
|
-
* result this way lets repositories extract the common values without exposing the raw query
|
|
7
|
-
* builder or the full `ResultSetHeader` to the rest of the codebase.
|
|
8
|
-
*/
|
|
9
|
-
export type DzWriteResult = readonly [{ insertId: number; affectedRows: number }, ...unknown[]];
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Extract the auto-increment `insertId` from a write result.
|
|
13
|
-
*
|
|
14
|
-
* @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
|
|
15
|
-
* @returns the `insertId` reported by mysql2 (the id of the first inserted row).
|
|
16
|
-
*/
|
|
17
|
-
export function insertIdOf(result: DzWriteResult): number {
|
|
18
|
-
return result[0].insertId;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Extract the number of affected rows from a write result.
|
|
23
|
-
*
|
|
24
|
-
* @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
|
|
25
|
-
* @returns the `affectedRows` count reported by mysql2.
|
|
26
|
-
*/
|
|
27
|
-
export function affectedRowsOf(result: DzWriteResult): number {
|
|
28
|
-
return result[0].affectedRows;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Reconstruct the auto-increment ids assigned by a bulk INSERT.
|
|
33
|
-
*
|
|
34
|
-
* @remarks
|
|
35
|
-
* mysql2 reports only the first `insertId` for a multi-row INSERT, so the remaining ids are derived
|
|
36
|
-
* by assuming a contiguous sequence (`base`, `base + 1`, …). This holds for tables with a standard
|
|
37
|
-
* `AUTO_INCREMENT` column and the default `innodb_autoinc_lock_mode`.
|
|
38
|
-
*
|
|
39
|
-
* @param result - the result of a bulk INSERT.
|
|
40
|
-
* @param count - the number of rows that were inserted.
|
|
41
|
-
* @returns an array of the `count` auto-increment ids, starting at the reported `insertId`.
|
|
42
|
-
*/
|
|
43
|
-
export function insertedIdsOf(result: DzWriteResult, count: number): number[] {
|
|
44
|
-
const base = result[0].insertId;
|
|
45
|
-
return Array.from({ length: count }, (_, i) => base + i);
|
|
46
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Decoded Firebase ID token payload.
|
|
3
|
-
*
|
|
4
|
-
* Shaped to match the subset of `firebase-admin`'s `DecodedIdToken` that consumers
|
|
5
|
-
* typically rely on: a stable `uid` plus an optional `email`. The index signature keeps
|
|
6
|
-
* every other JWT claim (e.g. `name`, `picture`, custom claims) accessible without
|
|
7
|
-
* enumerating them here.
|
|
8
|
-
*
|
|
9
|
-
* @remarks
|
|
10
|
-
* This is the return type of {@link FirebaseVerifier.verifyIdToken}. The `uid` is derived
|
|
11
|
-
* from the token's `sub` claim.
|
|
12
|
-
*/
|
|
13
|
-
export interface DecodedIdToken {
|
|
14
|
-
/** The authenticated user's unique id, taken from the token's `sub` claim. */
|
|
15
|
-
uid: string;
|
|
16
|
-
/** The user's email address, when present on the token. */
|
|
17
|
-
email?: string;
|
|
18
|
-
/** Any additional JWT claim carried by the token (custom claims, `name`, `picture`, ...). */
|
|
19
|
-
[claim: string]: unknown;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Abstract authentication boundary that replaces the `firebase-admin` Auth surface
|
|
24
|
-
* (`verifyIdToken` / `getUser` / `deleteUser`) for environments where the Node SDK cannot
|
|
25
|
-
* run, such as Cloudflare Workers.
|
|
26
|
-
*
|
|
27
|
-
* @remarks
|
|
28
|
-
* Implementations verify Firebase ID tokens and look up or delete accounts without the
|
|
29
|
-
* `firebase-admin` Node dependency. See `JoseFirebaseVerifier` for the `jose`-based
|
|
30
|
-
* implementation and the `createRemoteFirebaseVerifier` / `createServiceAccountVerifier`
|
|
31
|
-
* factories for ready-made instances.
|
|
32
|
-
*/
|
|
33
|
-
export interface FirebaseVerifier {
|
|
34
|
-
/**
|
|
35
|
-
* Verify a Firebase ID token and return its decoded payload.
|
|
36
|
-
*
|
|
37
|
-
* Mirrors `firebase-admin` `getAuth().verifyIdToken()`.
|
|
38
|
-
*
|
|
39
|
-
* @param idToken - The raw Firebase ID token (JWT) to verify.
|
|
40
|
-
* @returns The decoded token payload.
|
|
41
|
-
* @throws If the token signature, issuer, audience, expiry, or other required claims are invalid.
|
|
42
|
-
*/
|
|
43
|
-
verifyIdToken(idToken: string): Promise<DecodedIdToken>;
|
|
44
|
-
/**
|
|
45
|
-
* Look up a user record by uid.
|
|
46
|
-
*
|
|
47
|
-
* Mirrors `firebase-admin` `getAuth().getUser()`.
|
|
48
|
-
*
|
|
49
|
-
* @param uid - The user's unique id.
|
|
50
|
-
* @returns The user's `uid` and optional `email`, or `null` when the user does not exist.
|
|
51
|
-
* @throws If the backing user-management service is not configured or the lookup fails.
|
|
52
|
-
*/
|
|
53
|
-
getUser(uid: string): Promise<{ uid: string; email?: string } | null>;
|
|
54
|
-
/**
|
|
55
|
-
* Look up multiple user records by uid in as few requests as the backing service allows.
|
|
56
|
-
*
|
|
57
|
-
* Batched equivalent of {@link getUser}, intended to replace N single-uid lookups with a
|
|
58
|
-
* handful of requests.
|
|
59
|
-
*
|
|
60
|
-
* @param uids - The users' unique ids to look up.
|
|
61
|
-
* @returns The `uid`/`email` of every matching user. Uids that do not resolve to a user are
|
|
62
|
-
* simply absent from the result (never `null` entries).
|
|
63
|
-
* @throws If the backing user-management service is not configured or the lookup fails.
|
|
64
|
-
*/
|
|
65
|
-
getUsers(uids: string[]): Promise<{ uid: string; email?: string }[]>;
|
|
66
|
-
/**
|
|
67
|
-
* Delete a user by uid.
|
|
68
|
-
*
|
|
69
|
-
* Mirrors `firebase-admin` `getAuth().deleteUser()`.
|
|
70
|
-
*
|
|
71
|
-
* @param uid - The user's unique id.
|
|
72
|
-
* @returns A promise that resolves once the user has been deleted.
|
|
73
|
-
* @throws If the backing user-management service is not configured or the deletion fails.
|
|
74
|
-
*/
|
|
75
|
-
deleteUser(uid: string): Promise<void>;
|
|
76
|
-
}
|