@rdlabo/workers-hono-kit 0.4.0 → 0.4.3

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.
@@ -1,36 +1,42 @@
1
1
  /**
2
- * Drizzle 列ヘルパー(フリート共通)。各 repo `custom-types.ts` / `columns.ts` 薄いラッパーは不要。
2
+ * Shared Drizzle column helpers. Removes the need for a thin `custom-types.ts` / `columns.ts` wrapper
3
+ * in each repo.
3
4
  *
4
5
  * @remarks
5
- * `drizzle-orm` **peer**(consumer 1 本解決)。kit `drizzle-orm` bundled しない。
6
- * 戻り型は `customType` 推論そのまま(`MySqlCustomColumnBuilder<…>`)で `any` を使わない。
7
- * これにより consumer テーブルの `$inferSelect` に列の意味型(`string | Date` / `number | null` など)が伝播する。
6
+ * `drizzle-orm` is a **peer** (the consumer resolves a single copy); the kit does not bundle it. The
7
+ * return types are the `customType` inference as-is (`MySqlCustomColumnBuilder<…>`) with no `any`, so
8
+ * the column's semantic type (`string | Date`, `number | null`, etc.) propagates to the consumer
9
+ * table's `$inferSelect`.
8
10
  *
9
- * **前提(単一 drizzle コピー)**: drizzle `SQL` private フィールド `shouldInlineParams` を持つ
10
- * **名目型**なので、kit consumer drizzle の別コピーを解決すると
11
- * `jstTimestamp(…).default(sql\`…\`)` `TS2345: separate declarations of a private property
12
- * 'shouldInlineParams'` で落ちる。フリートは kit `file:` リンク参照するため kit 配下に drizzle
13
- * ネストし二重コピーになりやすい。consumer tsconfig `paths` `drizzle-orm` **自身の 1 コピーへ
14
- * 固定**して単一化すること(README「Drizzle 列ヘルパー」参照)。published 版(単一コピー)ではそのまま単一。
11
+ * **Precondition (a single drizzle copy)**: Drizzle's `SQL` is a **nominal** type carrying a private
12
+ * field `shouldInlineParams`, so if the kit and the consumer resolve different copies of drizzle,
13
+ * `jstTimestamp(…).default(sql\`…\`)` fails with `TS2345: separate declarations of a private property
14
+ * 'shouldInlineParams'`. The fleet references the kit via a `file:` link, which tends to nest a second
15
+ * copy of drizzle under the kit. Pin `drizzle-orm` to the consumer's **own single copy** with tsconfig
16
+ * `paths` (see the "Drizzle column helpers" section of the README). The published package (a single
17
+ * copy) is already unified.
15
18
  *
16
- * **DEFAULT / ON UPDATE CURRENT_TIMESTAMP** MySQL サーバ側の既定値(列を省略した INSERT / UPDATE)。
17
- * 接続 `timezone:'+09:00'`({@link hyperdriveConnectionOptions})が効くのは **アプリから Date を bind するとき**。
18
- * 両者を混同しないこと(`datetime-wire` / `drizzle-smoke` JST テスト参照)。
19
+ * **DEFAULT / ON UPDATE CURRENT_TIMESTAMP** is a server-side default (an INSERT / UPDATE that omits the
20
+ * column). The connection's `timezone:'+09:00'` ({@link hyperdriveConnectionOptions}) only applies when
21
+ * the **app binds a `Date`**. Do not conflate the two (see the `datetime-wire` / `drizzle-smoke` JST
22
+ * tests).
19
23
  */
20
24
  import { sql } from 'drizzle-orm';
21
25
  import { customType } from 'drizzle-orm/mysql-core';
22
26
  import { decimalNumberParams } from './decimal.js';
23
27
  import { jstDateParams, jstDatetimeParams, jstTimestampParams } from './jst.js';
24
28
  /**
25
- * `ON UPDATE CURRENT_TIMESTAMP` 用式(MySQL セッション時刻)。
26
- * customType 列は `.onUpdateNow()` が無いため `.$onUpdateFn(() => jstOnUpdateNow(fsp))` と併用する。
29
+ * SQL expression for `ON UPDATE CURRENT_TIMESTAMP` (the MySQL session clock).
30
+ * customType columns have no `.onUpdateNow()`, so pair this with `.$onUpdateFn(() => jstOnUpdateNow(fsp))`.
31
+ *
32
+ * @param fsp - optional fractional-seconds precision; when provided, emits `CURRENT_TIMESTAMP(fsp)`.
27
33
  */
28
34
  export const jstOnUpdateNow = (fsp) => fsp != null ? sql `(CURRENT_TIMESTAMP(${sql.raw(String(fsp))}))` : sql `(CURRENT_TIMESTAMP)`;
29
- /** MySQL `timestamp` — pass-throughDate は接続 `timezone:'+09:00'` で mysql2 が JST 整形。 */
35
+ /** MySQL `timestamp` — pass-through. A `Date` is formatted as JST by mysql2 via the connection `timezone:'+09:00'`. */
30
36
  export const jstTimestamp = (name, opts) => customType(jstTimestampParams(opts?.fsp))(name);
31
- /** MySQL `datetime` — {@link jstTimestamp} と同じ pass-through 方針。 */
37
+ /** MySQL `datetime` — same pass-through policy as {@link jstTimestamp}. */
32
38
  export const jstDatetime = (name, opts) => customType(jstDatetimeParams(opts?.fsp))(name);
33
- /** MySQL `date` — INSERT/UPDATE 時に ISO / 空文字を `YYYY-MM-DD` へ正規化(`toDriver`)。 */
39
+ /** MySQL `date` — on INSERT/UPDATE, normalizes ISO / empty strings to `YYYY-MM-DD` (via `toDriver`). */
34
40
  export const jstDate = (name) => customType(jstDateParams())(name);
35
- /** MySQL `decimal` — SELECT `fromDriver` string→number、書込は number をそのまま bind。 */
41
+ /** MySQL `decimal` — SELECT coerces string→number via `fromDriver`; writes bind the number as-is. */
36
42
  export const decimalNumber = (name, config) => customType(decimalNumberParams(config))(name);
@@ -1,24 +1,32 @@
1
1
  /**
2
- * MySQL `DECIMAL` 列向け Drizzle `customType` params。
2
+ * Drizzle `customType` params for a MySQL `DECIMAL` column.
3
3
  *
4
4
  * @remarks
5
- * - **読込(SELECT)**: `fromDriver` driver 値(`number` / `string` / `null`)を JS `number | null` に統一。
6
- * 接続 `decimalNumbers: true`({@link hyperdriveConnectionOptions} 既定)と併用し、Drizzle builder 経路でも
7
- * 文字列 `"0"` / `"100.00"` が混ざったときに 0 を潰さず number へ揃える。
8
- * - **書込(INSERT/UPDATE)**: `toDriver` number をそのまま mysql2 に bind(`String()` 変換不要)。
9
- * - SQL `db.read` は接続 `decimalNumbers: true` が効く。列型の `fromDriver` Drizzle `select` 経路向け。
5
+ * - **Reads (SELECT)**: `fromDriver` unifies the driver value (`number` / `string` / `null`) to a JS
6
+ * `number | null`. Combined with the connection's `decimalNumbers: true`
7
+ * ({@link hyperdriveConnectionOptions} default), it aligns values to numbers even on the Drizzle
8
+ * builder path when strings like `"0"` / `"100.00"` slip in, without dropping `0`.
9
+ * - **Writes (INSERT/UPDATE)**: `toDriver` binds the number to mysql2 as-is (no `String()` conversion).
10
+ * - Raw-SQL `db.read` relies on the connection's `decimalNumbers: true`; the column's `fromDriver` is
11
+ * for the Drizzle `select` path.
10
12
  */
11
13
  export interface DecimalNumberConfig {
12
14
  precision: number;
13
15
  scale: number;
14
16
  }
15
17
  /**
16
- * mysql2 / Drizzle から届いた DECIMAL 値を JS `number | null` へ正規化する。
17
- * `0` falsy 落ちしないようそのまま保持する。
18
+ * Normalize a DECIMAL value coming from mysql2 / Drizzle to a JS `number | null`.
19
+ * `0` is preserved as-is so it is not dropped as falsy.
20
+ *
21
+ * @param value - the raw driver value (`number` / `string` / `bigint` / nullish).
22
+ * @returns the coerced finite number, or `null` when it cannot be resolved.
18
23
  */
19
24
  export declare function coerceDecimalNumber(value: unknown): number | null;
20
25
  /**
21
- * `customType` params。高度な用途向け。通常は {@link decimalNumber} 列ヘルパーを使う。
26
+ * Params for a `customType`. For advanced use; the {@link decimalNumber} column helper is usually enough.
27
+ *
28
+ * @param config - the DECIMAL `precision` / `scale`.
29
+ * @returns the `customType` params (`dataType` / `fromDriver` / `toDriver`).
22
30
  */
23
31
  export declare const decimalNumberParams: (config: DecimalNumberConfig) => {
24
32
  dataType: () => string;
@@ -1,16 +1,21 @@
1
1
  /**
2
- * MySQL `DECIMAL` 列向け Drizzle `customType` params。
2
+ * Drizzle `customType` params for a MySQL `DECIMAL` column.
3
3
  *
4
4
  * @remarks
5
- * - **読込(SELECT)**: `fromDriver` driver 値(`number` / `string` / `null`)を JS `number | null` に統一。
6
- * 接続 `decimalNumbers: true`({@link hyperdriveConnectionOptions} 既定)と併用し、Drizzle builder 経路でも
7
- * 文字列 `"0"` / `"100.00"` が混ざったときに 0 を潰さず number へ揃える。
8
- * - **書込(INSERT/UPDATE)**: `toDriver` number をそのまま mysql2 に bind(`String()` 変換不要)。
9
- * - SQL `db.read` は接続 `decimalNumbers: true` が効く。列型の `fromDriver` Drizzle `select` 経路向け。
5
+ * - **Reads (SELECT)**: `fromDriver` unifies the driver value (`number` / `string` / `null`) to a JS
6
+ * `number | null`. Combined with the connection's `decimalNumbers: true`
7
+ * ({@link hyperdriveConnectionOptions} default), it aligns values to numbers even on the Drizzle
8
+ * builder path when strings like `"0"` / `"100.00"` slip in, without dropping `0`.
9
+ * - **Writes (INSERT/UPDATE)**: `toDriver` binds the number to mysql2 as-is (no `String()` conversion).
10
+ * - Raw-SQL `db.read` relies on the connection's `decimalNumbers: true`; the column's `fromDriver` is
11
+ * for the Drizzle `select` path.
10
12
  */
11
13
  /**
12
- * mysql2 / Drizzle から届いた DECIMAL 値を JS `number | null` へ正規化する。
13
- * `0` falsy 落ちしないようそのまま保持する。
14
+ * Normalize a DECIMAL value coming from mysql2 / Drizzle to a JS `number | null`.
15
+ * `0` is preserved as-is so it is not dropped as falsy.
16
+ *
17
+ * @param value - the raw driver value (`number` / `string` / `bigint` / nullish).
18
+ * @returns the coerced finite number, or `null` when it cannot be resolved.
14
19
  */
15
20
  export function coerceDecimalNumber(value) {
16
21
  if (value === null || value === undefined) {
@@ -33,7 +38,10 @@ export function coerceDecimalNumber(value) {
33
38
  return null;
34
39
  }
35
40
  /**
36
- * `customType` params。高度な用途向け。通常は {@link decimalNumber} 列ヘルパーを使う。
41
+ * Params for a `customType`. For advanced use; the {@link decimalNumber} column helper is usually enough.
42
+ *
43
+ * @param config - the DECIMAL `precision` / `scale`.
44
+ * @returns the `customType` params (`dataType` / `fromDriver` / `toDriver`).
37
45
  */
38
46
  export const decimalNumberParams = (config) => ({
39
47
  dataType: () => `decimal(${config.precision},${config.scale})`,
package/dist/db/jst.d.ts CHANGED
@@ -1,16 +1,21 @@
1
1
  /**
2
- * MySQL / Drizzle 向け JST ワイヤ変換と DATE 列正規化。
2
+ * JST wire conversion and DATE-column normalization for MySQL / Drizzle.
3
3
  *
4
4
  * @remarks
5
- * 業務時刻の意味論は {@link ../business-time/index.js | business-time} に集約する。
6
- * このモジュールは「MySQL 接続既定」「DATE 列の toDriver」、列 `customType` params のみを担う。
5
+ * Business-time semantics are consolidated in {@link ../business-time/index.js | business-time}. This
6
+ * module only owns the MySQL connection default, the DATE column's `toDriver`, and the column
7
+ * `customType` params.
7
8
  */
8
9
  import type { BusinessDate } from '../business-time/index.js';
9
- /** mysql2 接続 `timezone` 既定(既存 JST DB 運用)。 */
10
+ /** Default mysql2 connection `timezone` (for the existing JST DB deployment). */
10
11
  export declare const MYSQL_TIMEZONE = "+09:00";
11
12
  /**
12
- * クライアント入力を MySQL `DATE` 列向け `YYYY-MM-DD`(JST 業務暦日)へ正規化。
13
- * ISO 8601 / `YYYY-MM-DD` / 空文字を受け付ける。`YYYY-MM-DD` Date 化せずそのまま渡す。
13
+ * Normalize a client input to `YYYY-MM-DD` (a JST business calendar date) for a MySQL `DATE` column.
14
+ * Accepts ISO 8601 / `YYYY-MM-DD` / empty strings. A `YYYY-MM-DD` value is passed through without
15
+ * constructing a `Date`.
16
+ *
17
+ * @param value - the string or nullish input to normalize.
18
+ * @returns the business date as `YYYY-MM-DD`, or `null` when the input cannot be resolved.
14
19
  */
15
20
  export declare function toJstDate(value: string | null | undefined): BusinessDate | null;
16
21
  /**
package/dist/db/jst.js CHANGED
@@ -1,16 +1,21 @@
1
1
  /**
2
- * MySQL / Drizzle 向け JST ワイヤ変換と DATE 列正規化。
2
+ * JST wire conversion and DATE-column normalization for MySQL / Drizzle.
3
3
  *
4
4
  * @remarks
5
- * 業務時刻の意味論は {@link ../business-time/index.js | business-time} に集約する。
6
- * このモジュールは「MySQL 接続既定」「DATE 列の toDriver」、列 `customType` params のみを担う。
5
+ * Business-time semantics are consolidated in {@link ../business-time/index.js | business-time}. This
6
+ * module only owns the MySQL connection default, the DATE column's `toDriver`, and the column
7
+ * `customType` params.
7
8
  */
8
9
  import { normalizeBusinessDate } from '../business-time/index.js';
9
- /** mysql2 接続 `timezone` 既定(既存 JST DB 運用)。 */
10
+ /** Default mysql2 connection `timezone` (for the existing JST DB deployment). */
10
11
  export const MYSQL_TIMEZONE = '+09:00';
11
12
  /**
12
- * クライアント入力を MySQL `DATE` 列向け `YYYY-MM-DD`(JST 業務暦日)へ正規化。
13
- * ISO 8601 / `YYYY-MM-DD` / 空文字を受け付ける。`YYYY-MM-DD` Date 化せずそのまま渡す。
13
+ * Normalize a client input to `YYYY-MM-DD` (a JST business calendar date) for a MySQL `DATE` column.
14
+ * Accepts ISO 8601 / `YYYY-MM-DD` / empty strings. A `YYYY-MM-DD` value is passed through without
15
+ * constructing a `Date`.
16
+ *
17
+ * @param value - the string or nullish input to normalize.
18
+ * @returns the business date as `YYYY-MM-DD`, or `null` when the input cannot be resolved.
14
19
  */
15
20
  export function toJstDate(value) {
16
21
  return normalizeBusinessDate(value ?? null);
@@ -1,29 +1,29 @@
1
1
  import type { QueryRunner } from './database.js';
2
- /** baseline(=最初の)マイグレーションの識別情報。 */
2
+ /** Identifying info for the baseline (i.e. first) migration. */
3
3
  export interface BaselineEntry {
4
- /** マイグレーション tag(例 `0000_melted_weapon_omega`)。 */
4
+ /** The migration tag (e.g. `0000_melted_weapon_omega`). */
5
5
  tag: string;
6
- /** `_journal.json` `when`(= drizzle `created_at`/`folderMillis`)。 */
6
+ /** The `when` from `_journal.json` (= drizzle's `created_at` / `folderMillis`). */
7
7
  when: number;
8
- /** `<tag>.sql` の生内容の sha256(drizzle と同一アルゴリズム)。 */
8
+ /** The sha256 of the raw `<tag>.sql` contents (the same algorithm as drizzle). */
9
9
  hash: string;
10
10
  }
11
11
  /**
12
- * `migrationsFolder`(drizzle `out`、例 `./drizzle`)から baseline(最初の)エントリを読む。
12
+ * Read the baseline (first) entry from `migrationsFolder` (drizzle's `out`, e.g. `./drizzle`).
13
13
  *
14
- * @param migrationsFolder - `meta/_journal.json` `<tag>.sql` を含むフォルダ。
15
- * @returns baseline エントリ(tag/when/hash)。
16
- * @throws journal が無い / エントリが空 / `<tag>.sql` が無い場合。
14
+ * @param migrationsFolder - the folder containing `meta/_journal.json` and `<tag>.sql`.
15
+ * @returns the baseline entry (tag/when/hash).
16
+ * @throws Error when the journal is missing, the entries are empty, or `<tag>.sql` is missing.
17
17
  */
18
18
  export declare function readBaselineEntry(migrationsFolder: string): BaselineEntry;
19
- /** {@link baselineMigrations} のオプション。 */
19
+ /** Options for {@link baselineMigrations}. */
20
20
  export interface BaselineMigrationsOptions {
21
- /** SQL を実行する QueryRunner(mysql2 `Connection`/`Pool` が代入可能)。対象 DB に接続済みのこと。 */
21
+ /** A QueryRunner for raw SQL (a mysql2 `Connection`/`Pool` is assignable). Must already be connected to the target DB. */
22
22
  db: QueryRunner;
23
- /** drizzle `out` フォルダ(既定 `./drizzle`)。 */
23
+ /** Drizzle's `out` folder (defaults to `./drizzle`). */
24
24
  migrationsFolder?: string;
25
25
  }
26
- /** {@link baselineMigrations} の結果。 */
26
+ /** The result of {@link baselineMigrations}. */
27
27
  export type BaselineResult = {
28
28
  status: 'inserted';
29
29
  tag: string;
@@ -35,17 +35,17 @@ export type BaselineResult = {
35
35
  when: number;
36
36
  };
37
37
  /**
38
- * 既存 DB baseline(0000)を「適用済み」として記録する。冪等・安全ガード付き。
38
+ * Record the baseline (0000) as "applied" on an existing DB. Idempotent, with safety guards.
39
39
  *
40
40
  * @remarks
41
- * ガード:
42
- * - 既に baseline marker(`created_at = when`)が在れば **no-op**(`already-baselined`)。
43
- * - marker は無いが `__drizzle_migrations` に別の行が在る**中断**(想定外の状態)。
44
- * - 対象 DB base table 1 つも無い(空 DB)→ **中断**(空 DB 0000 skip すると
45
- * テーブルが作られない。新規 DB には `db:migrate` を使う)。
41
+ * Guards:
42
+ * - If a baseline marker (`created_at = when`) already exists → **no-op** (`already-baselined`).
43
+ * - If there is no marker but `__drizzle_migrations` has other rows **abort** (unexpected state).
44
+ * - If the target DB has no base tables (an empty DB) **abort** (skipping 0000 on an empty DB would
45
+ * never create the tables; use `db:migrate` for a fresh DB).
46
46
  *
47
- * @param options - 接続と migrations フォルダ。{@link BaselineMigrationsOptions} 参照。
48
- * @returns 挿入したか既に baseline 済みか。
49
- * @throws 上記ガードに該当する場合。
47
+ * @param options - the connection and migrations folder; see {@link BaselineMigrationsOptions}.
48
+ * @returns whether a marker was inserted or the DB was already baselined.
49
+ * @throws Error when one of the guards above trips.
50
50
  */
51
51
  export declare function baselineMigrations(options: BaselineMigrationsOptions): Promise<BaselineResult>;
@@ -1,32 +1,35 @@
1
1
  /**
2
2
  * Brownfield baseline for Drizzle MySQL migrations.
3
3
  *
4
- * 既存(現行サービス)の DB は先にスキーマが存在するため、コミット済みの baseline マイグレーション
5
- * (`drizzle/0000_*.sql` = 現行スキーマを introspect した CREATE TABLE 群)を `db:migrate` で流すと
6
- * 全テーブルが衝突して失敗する。そこで 0000 **実行せず「適用済み」として記録**する。
4
+ * An existing (in-production) DB already has its schema, so running the committed baseline migration
5
+ * (`drizzle/0000_*.sql` = the CREATE TABLE statements introspected from the current schema) via
6
+ * `db:migrate` fails as every table collides. Instead, 0000 is **recorded as "applied" without being
7
+ * executed**.
7
8
  *
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 が無いのでフルチェーンが走る(挙動不変)。
9
+ * How "applied" is decided (drizzle-orm/mysql-core dialect.migrate): it determines the pending set from
10
+ * **only the maximum `created_at`** in `__drizzle_migrations(id, hash, created_at)`, running just the
11
+ * migrations where `max(created_at) < entry.when`. The hash is stored but not used for the decision. So
12
+ * inserting one row as the 0000 marker — `(hash, created_at = that entry's when)` makes subsequent
13
+ * `db:migrate` runs apply only the later 0001+ (larger `when`) and skip 0000. A fresh / test DB has no
14
+ * marker, so the full chain runs (behavior unchanged).
13
15
  *
14
- * この関数は `drizzle-orm` に依存しない(journal/SQL を自前で読み、hash drizzle と同じ sha256)。
15
- * QueryRunnermysql2 `Connection`/`Pool` が構造的に代入可能)に対して生 SQL を実行する。
16
+ * This function does not depend on `drizzle-orm` (it reads the journal/SQL itself and hashes with the
17
+ * same sha256 as drizzle). It runs raw SQL against a QueryRunner (a mysql2 `Connection`/`Pool` is
18
+ * structurally assignable).
16
19
  *
17
20
  * @packageDocumentation
18
21
  */
19
22
  import { createHash } from 'node:crypto';
20
23
  import { existsSync, readFileSync } from 'node:fs';
21
24
  import { join } from 'node:path';
22
- /** drizzle が使う既定のマイグレーション管理テーブル名。 */
25
+ /** The default migration-tracking table name used by drizzle. */
23
26
  const MIGRATIONS_TABLE = '__drizzle_migrations';
24
27
  /**
25
- * `migrationsFolder`(drizzle `out`、例 `./drizzle`)から baseline(最初の)エントリを読む。
28
+ * Read the baseline (first) entry from `migrationsFolder` (drizzle's `out`, e.g. `./drizzle`).
26
29
  *
27
- * @param migrationsFolder - `meta/_journal.json` `<tag>.sql` を含むフォルダ。
28
- * @returns baseline エントリ(tag/when/hash)。
29
- * @throws journal が無い / エントリが空 / `<tag>.sql` が無い場合。
30
+ * @param migrationsFolder - the folder containing `meta/_journal.json` and `<tag>.sql`.
31
+ * @returns the baseline entry (tag/when/hash).
32
+ * @throws Error when the journal is missing, the entries are empty, or `<tag>.sql` is missing.
30
33
  */
31
34
  export function readBaselineEntry(migrationsFolder) {
32
35
  const journalPath = join(migrationsFolder, 'meta', '_journal.json');
@@ -38,7 +41,8 @@ export function readBaselineEntry(migrationsFolder) {
38
41
  if (entries.length === 0) {
39
42
  throw new Error(`No migration entries in ${journalPath}.`);
40
43
  }
41
- // 起点は必ず最初のエントリ(0000)。以降 0001+ は「新しい変更」なので既存 DB でも実行されるべき。
44
+ // The origin is always the first entry (0000). Later 0001+ are "new changes" that should run even on
45
+ // an existing DB.
42
46
  const first = entries[0];
43
47
  const sqlPath = join(migrationsFolder, `${first.tag}.sql`);
44
48
  if (!existsSync(sqlPath)) {
@@ -52,43 +56,44 @@ async function rowsOf(db, sql, params) {
52
56
  return result[0] ?? [];
53
57
  }
54
58
  /**
55
- * 既存 DB baseline(0000)を「適用済み」として記録する。冪等・安全ガード付き。
59
+ * Record the baseline (0000) as "applied" on an existing DB. Idempotent, with safety guards.
56
60
  *
57
61
  * @remarks
58
- * ガード:
59
- * - 既に baseline marker(`created_at = when`)が在れば **no-op**(`already-baselined`)。
60
- * - marker は無いが `__drizzle_migrations` に別の行が在る**中断**(想定外の状態)。
61
- * - 対象 DB base table 1 つも無い(空 DB)→ **中断**(空 DB 0000 skip すると
62
- * テーブルが作られない。新規 DB には `db:migrate` を使う)。
62
+ * Guards:
63
+ * - If a baseline marker (`created_at = when`) already exists → **no-op** (`already-baselined`).
64
+ * - If there is no marker but `__drizzle_migrations` has other rows **abort** (unexpected state).
65
+ * - If the target DB has no base tables (an empty DB) **abort** (skipping 0000 on an empty DB would
66
+ * never create the tables; use `db:migrate` for a fresh DB).
63
67
  *
64
- * @param options - 接続と migrations フォルダ。{@link BaselineMigrationsOptions} 参照。
65
- * @returns 挿入したか既に baseline 済みか。
66
- * @throws 上記ガードに該当する場合。
68
+ * @param options - the connection and migrations folder; see {@link BaselineMigrationsOptions}.
69
+ * @returns whether a marker was inserted or the DB was already baselined.
70
+ * @throws Error when one of the guards above trips.
67
71
  */
68
72
  export async function baselineMigrations(options) {
69
73
  const { db, migrationsFolder = './drizzle' } = options;
70
74
  const baseline = readBaselineEntry(migrationsFolder);
71
- // migrator と同一 DDL(存在すれば no-op)。
75
+ // Same DDL as the migrator (a no-op if it already exists).
72
76
  await db.query(`create table if not exists \`${MIGRATIONS_TABLE}\` (
73
77
  id serial primary key,
74
78
  hash text not null,
75
79
  created_at bigint
76
80
  )`);
77
- // 既に baseline marker があれば冪等 no-op
81
+ // If a baseline marker already exists, this is an idempotent no-op.
78
82
  const existing = await rowsOf(db, `select id from \`${MIGRATIONS_TABLE}\` where created_at = ? limit 1`, [
79
83
  baseline.when,
80
84
  ]);
81
85
  if (existing.length > 0) {
82
86
  return { status: 'already-baselined', tag: baseline.tag, when: baseline.when };
83
87
  }
84
- // marker は無いが行が在る=既に別の状態。誤爆防止で中断。
88
+ // No marker but rows exist = already in some other state. Abort to avoid misfiring.
85
89
  const countRows = await rowsOf(db, `select count(*) as n from \`${MIGRATIONS_TABLE}\``);
86
90
  const rowCount = Number(countRows[0]?.n ?? 0);
87
91
  if (rowCount > 0) {
88
92
  throw new Error(`${MIGRATIONS_TABLE} already has ${rowCount} row(s) but no baseline marker (created_at=${baseline.when}). ` +
89
93
  `Migration state is unexpected — refusing to insert. Inspect \`${MIGRATIONS_TABLE}\` manually.`);
90
94
  }
91
- // DB への baseline は危険(0000 skip 扱いにするとテーブルが作られない)。brownfield 確認。
95
+ // Baselining an empty DB is dangerous (treating 0000 as skipped would never create the tables).
96
+ // Confirm this is a brownfield DB.
92
97
  const tableRows = await rowsOf(db, `select count(*) as n from information_schema.tables
93
98
  where table_schema = DATABASE() and table_type = 'BASE TABLE' and table_name <> ?`, [MIGRATIONS_TABLE]);
94
99
  const baseTableCount = Number(tableRows[0]?.n ?? 0);
@@ -102,7 +102,7 @@ export declare function honoDrizzleConfig(options: HonoDrizzleConfigOptions): {
102
102
  out: string;
103
103
  casing: "snake_case";
104
104
  };
105
- /** {@link resolveDbSecret} の戻り値(正規化済みの接続情報)。 */
105
+ /** The return value of {@link resolveDbSecret} (normalized connection info). */
106
106
  export interface ResolvedDbSecret {
107
107
  host: string;
108
108
  port: number;
@@ -111,13 +111,17 @@ export interface ResolvedDbSecret {
111
111
  password: string;
112
112
  }
113
113
  /**
114
- * AWS RDS マネージド secret(`DB_SECRET` に入れた JSON 文字列)を解決する。
114
+ * Resolve an AWS RDS managed secret (a JSON string placed in `DB_SECRET`).
115
115
  *
116
116
  * @remarks
117
- * - `DB_SECRET` 未設定 → `undefined`(ローカル/`db:generate` の正常フォールバック)。
118
- * - 設定されている場合は「完全な接続情報」であることを要求し、**不正 JSON / 必須キー欠損は throw**
119
- * (静かに localhost へフォールバックして事故らせない)。`port` のみ欠損時は 3306 を補う。
117
+ * - `DB_SECRET` unset → `undefined` (the normal local / `db:generate` fallback).
118
+ * - When set, it must be complete connection info: **invalid JSON / a missing required key throws**
119
+ * (rather than silently falling back to localhost and causing an incident). A missing `port` alone
120
+ * defaults to 3306.
120
121
  *
121
- * `honoDrizzleConfig`(db:migrate)と `workers-hono-kit-db-baseline` bin の双方が同じ解釈を使う。
122
+ * Both `honoDrizzleConfig` (db:migrate) and the `workers-hono-kit-db-baseline` bin use this same logic.
123
+ *
124
+ * @returns the resolved connection info, or `undefined` when `DB_SECRET` is unset.
125
+ * @throws Error when `DB_SECRET` is set but is not valid JSON or is missing a required key.
122
126
  */
123
127
  export declare function resolveDbSecret(): ResolvedDbSecret | undefined;
@@ -53,12 +53,12 @@ export const DRIZZLE_ORM_OPTIONS = { mode: 'default', casing: 'snake_case' };
53
53
  */
54
54
  export function honoDrizzleConfig(options) {
55
55
  const { database, host, port, user, password, schema = './src/db/schemes', out = './drizzle', tablesFilter, introspect, } = options;
56
- // CI/本番の migrate AWS Secrets Manager RDS マネージド secret(キー
57
- // host/port/dbname/username/password)を `DB_SECRET` にまるごと渡す運用を吸収する。JSON.parse
58
- // 解釈するので secret のキー名(host≠DB_HOST)差を map でき、password の特殊文字もシェル安全。
59
- // 未設定(ローカル/db:generate)は従来どおり個別 env デフォルトにフォールバック。
60
- // DB_SECRET が在れば「完全な secret」として全接続情報をそれで確定する(欠損/不正は throw)。
61
- // 未設定時のみ従来の個別 env デフォルトにフォールバックする。
56
+ // CI/production migrate absorbs the pattern of passing a whole AWS Secrets Manager RDS managed secret
57
+ // (keys host/port/dbname/username/password) via `DB_SECRET`. It is parsed with JSON.parse, so key-name
58
+ // differences (host DB_HOST) can be mapped and special characters in the password stay shell-safe.
59
+ // When `DB_SECRET` is set, it is treated as a complete secret and fully determines the connection
60
+ // (missing/invalid throw). Only when it is unset do we fall back to the individual DB_* env vars
61
+ // and then the defaults (the local / db:generate path).
62
62
  const secret = resolveDbSecret();
63
63
  const dbCredentials = secret
64
64
  ? {
@@ -86,14 +86,18 @@ export function honoDrizzleConfig(options) {
86
86
  };
87
87
  }
88
88
  /**
89
- * AWS RDS マネージド secret(`DB_SECRET` に入れた JSON 文字列)を解決する。
89
+ * Resolve an AWS RDS managed secret (a JSON string placed in `DB_SECRET`).
90
90
  *
91
91
  * @remarks
92
- * - `DB_SECRET` 未設定 → `undefined`(ローカル/`db:generate` の正常フォールバック)。
93
- * - 設定されている場合は「完全な接続情報」であることを要求し、**不正 JSON / 必須キー欠損は throw**
94
- * (静かに localhost へフォールバックして事故らせない)。`port` のみ欠損時は 3306 を補う。
92
+ * - `DB_SECRET` unset → `undefined` (the normal local / `db:generate` fallback).
93
+ * - When set, it must be complete connection info: **invalid JSON / a missing required key throws**
94
+ * (rather than silently falling back to localhost and causing an incident). A missing `port` alone
95
+ * defaults to 3306.
95
96
  *
96
- * `honoDrizzleConfig`(db:migrate)と `workers-hono-kit-db-baseline` bin の双方が同じ解釈を使う。
97
+ * Both `honoDrizzleConfig` (db:migrate) and the `workers-hono-kit-db-baseline` bin use this same logic.
98
+ *
99
+ * @returns the resolved connection info, or `undefined` when `DB_SECRET` is unset.
100
+ * @throws Error when `DB_SECRET` is set but is not valid JSON or is missing a required key.
97
101
  */
98
102
  export function resolveDbSecret() {
99
103
  const raw = process.env.DB_SECRET;
package/dist/index.d.ts CHANGED
@@ -15,6 +15,8 @@ export type { ValidateOptions, ValidationTarget, ZodErrorLike, SentryLike, Sentr
15
15
  export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
16
16
  export { createAuthMiddleware } from './middleware/auth.js';
17
17
  export type { AuthMiddlewareOptions } from './middleware/auth.js';
18
+ export { perfLog } from './middleware/perf-log.js';
19
+ export type { PerfLogOptions, AnalyticsEngineDatasetLike } from './middleware/perf-log.js';
18
20
  export { getUserProtocol } from './http/user-protocol.js';
19
21
  export type { IUserProtocol } from './http/user-protocol.js';
20
22
  export { getAppInfo } from './http/app-info.js';
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ export { finalizeResponse } from './middleware/finalize-response.js';
14
14
  export { validate, createSentryValidate } from './middleware/validation.js';
15
15
  export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
16
16
  export { createAuthMiddleware } from './middleware/auth.js';
17
+ export { perfLog } from './middleware/perf-log.js';
17
18
  // http
18
19
  export { getUserProtocol } from './http/user-protocol.js';
19
20
  export { getAppInfo } from './http/app-info.js';
@@ -0,0 +1,77 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ /**
3
+ * Minimal shape of a Workers Analytics Engine dataset binding.
4
+ *
5
+ * Declared locally so consumers are not forced to depend on `@cloudflare/workers-types`. Only the
6
+ * single write operation the emitter needs is modeled. A real `AnalyticsEngineDataset` binding is
7
+ * assignable. Writes are non-blocking and add no latency to the request.
8
+ *
9
+ * @see https://developers.cloudflare.com/analytics/analytics-engine/
10
+ */
11
+ export interface AnalyticsEngineDatasetLike {
12
+ writeDataPoint(event: {
13
+ doubles?: number[];
14
+ blobs?: (string | null)[];
15
+ indexes?: string[];
16
+ }): void;
17
+ }
18
+ /** Options for {@link perfLog}. Both sinks are optional; enable either or both. */
19
+ export interface PerfLogOptions {
20
+ /**
21
+ * When `true`, emit one `console.log(JSON.stringify({ perf }))` per request. With
22
+ * `[observability] enabled = true` these lines are captured by **Workers Logs** (retained up to
23
+ * 7 days, queryable via the dashboard Query Builder or the Observability REST API) — no live
24
+ * `wrangler tail` needed, which is what makes this practical for low-traffic Workers.
25
+ *
26
+ * Explicit wins over the `PERF_LOG` env fallback in both directions: `true` forces on, `false` forces
27
+ * off (even when `PERF_LOG === '1'`), `undefined` (default) defers to `PERF_LOG`.
28
+ */
29
+ console?: boolean;
30
+ /**
31
+ * When provided, write one data point per request to a **Workers Analytics Engine** dataset. Query
32
+ * percentiles by route/colo with the SQL API (≈90-day retention). Non-blocking. Layout:
33
+ * `doubles = [t_app_ms, cold(0|1), status]`, `blobs = [path, colo, method]`, `indexes = [path]`.
34
+ */
35
+ dataset?: AnalyticsEngineDatasetLike;
36
+ /**
37
+ * In-code sampling in `[0, 1]` (default `1` = every request; values are clamped to the range).
38
+ * Thins **Analytics Engine writes only** — Workers Logs volume is controlled separately by the
39
+ * observability `head_sampling_rate`. Low-traffic Workers should leave it at `1`.
40
+ */
41
+ sampleRate?: number;
42
+ }
43
+ /**
44
+ * Create a Hono middleware that records a per-request latency data point and emits it to Workers
45
+ * Logs (`console`) and/or Workers Analytics Engine (`dataset`).
46
+ *
47
+ * Register it first (`app.use('*', perfLog(...))`) so `t_app` covers the whole in-app path. Route
48
+ * grouping uses the matched route pattern (e.g. `/user/:id`) rather than the raw path so ids do not
49
+ * explode cardinality; unmatched requests collapse to `(unmatched)`. Colo comes from `request.cf.colo`;
50
+ * cold/warm from an isolate-scoped flag. See {@link PerfLogOptions} for the two sinks and the note
51
+ * above for exactly what `t_app` includes (it depends on where secrets/DB-connect are wired).
52
+ *
53
+ * Two wiring styles, both A/B-capable (Workers Logs and/or Analytics Engine):
54
+ *
55
+ * @example Bare, when the app is served with `app.fetch(req, env, ctx)` — reads `PERF` (Analytics
56
+ * Engine binding) and `PERF_LOG === '1'` (Workers Logs) straight off `c.env`:
57
+ * ```ts
58
+ * app.use('*', perfLog());
59
+ * ```
60
+ *
61
+ * @example Explicit, when the app is built without Hono `env` (e.g. `createApp(container).fetch(req)`)
62
+ * — thread the bindings in:
63
+ * ```ts
64
+ * app.use('*', perfLog({ console: env.PERF_LOG === '1', dataset: env.PERF }));
65
+ * ```
66
+ *
67
+ * @example Query Analytics Engine (SQL API), handler p50/p90 by route and colo:
68
+ * ```sql
69
+ * SELECT blob1 AS path, blob2 AS colo,
70
+ * quantileWeighted(0.5)(double1, _sample_interval) AS p50,
71
+ * quantileWeighted(0.9)(double1, _sample_interval) AS p90,
72
+ * sum(_sample_interval) AS n
73
+ * FROM your_dataset WHERE timestamp > now() - INTERVAL '7' DAY
74
+ * GROUP BY path, colo ORDER BY n DESC
75
+ * ```
76
+ */
77
+ export declare function perfLog(options?: PerfLogOptions): MiddlewareHandler;