@rdlabo/workers-hono-kit 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,13 +35,13 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
35
35
  > **Compiled ESM, with types.** The package is published as compiled ES modules (`./dist/*.js`) plus
36
36
  > declaration files (`./dist/*.d.ts`) via the `exports` field. It depends only on Web-standard APIs
37
37
  > (`fetch`, `crypto.subtle`, `Response`) available on Cloudflare Workers (`workerd`) and other edge
38
- > runtimes, and requires Node.js ≥ 20 for tooling. Three entry points are exposed:
38
+ > runtimes, and requires Node.js ≥ 20 for tooling. Four entry points are exposed:
39
39
  >
40
40
  > | Subpath | Import | Use |
41
41
  > | --- | --- | --- |
42
42
  > | `.` | `@rdlabo/workers-hono-kit` | Web-standard helpers (middleware, HTTP, Firebase, AWS, AI, Stripe, KV). |
43
43
  > | `./db` | `@rdlabo/workers-hono-kit/db` | MySQL data layer (mysql2 + Drizzle). |
44
- > | `./business-time` | `@rdlabo/workers-hono-kit/business-time` | JST 業務時刻 API(`toBusinessDateTime` / `normalizeBusinessDate` 等)。 |
44
+ > | `./business-time` | `@rdlabo/workers-hono-kit/business-time` | JST business-time API (`toBusinessDateTime` / `normalizeBusinessDate` / `formatBusinessDateTime`, etc.). |
45
45
  > | `./testing` | `@rdlabo/workers-hono-kit/testing` | Test helpers (mysql2 + Drizzle + fakes/fixtures). |
46
46
 
47
47
  ## API
@@ -65,16 +65,19 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
65
65
  | `getAppInfo(c)` / `AppInfo` | Read `x-amz-meta-version` / `x-amz-meta-uuid`. |
66
66
  | `resolveAppEnv(env)` / `isProductionEnv(env)` / `AppEnv` | Resolve `'development'` / `'production'` from `env.APP_ENV` (defaults to `'production'` for safety). |
67
67
  | `HttpStatus` | HTTP status enum identical to NestJS `@nestjs/common`. |
68
- | `createNestErrorHandler(options?)` / `NestErrorHandlerOptions` | `app.onError()` handler that maps a thrown `HTTPException` to the NestJS exception-filter body (`{ statusCode, message, error? }`; `401` omits `error`). Configurable field order, reason phrases, error predicate, and unhandled-error report hook. |
68
+ | `createNestErrorHandler(options?)` / `NestErrorHandlerOptions` | `app.onError()` handler that maps a thrown `HTTPException` to the NestJS exception-filter body (`{ statusCode, message, error? }`; `401` omits `error`). Configurable field order, reason phrases, error predicate, and unhandled-error report hook. Unhandled errors log via `console.error` (mysql2 errors include `sqlMessage` / `errno` when detectable). |
69
+ | `createQueryFailedNestErrorHandler(options)` / `QueryFailedClassifier` / `ClassifiedDbError` | Compose a NestJS `QueryFailedExceptionFilter` analog **before** `createNestErrorHandler`: consumer supplies `classify(err)` (parity-critical messages stay in the app); classified DB errors log (`warn` for 400, `error` for 500) and trigger `onUnhandledError` on 500 only. |
70
+ | `findMysqlDriverError(err)` / `logMysqlDriverError(err, statusCode)` / `reportClassifiedDbError(...)` | Low-level mysql2 driver-error detection (follows `err.cause`), structured logging, and classify-path reporting helpers. |
69
71
  | `nestNotFoundHandler(c)` | `app.notFound()` handler with the Express/Nest default `{ message: 'Cannot METHOD path', error, statusCode }` 404 body. |
70
72
  | `normalizeTrailingSlash(request)` | Strip trailing slash(es) from the request URL before routing (Express/Nest parity). Does **not** 301-redirect — preserves POST/PUT/DELETE bodies. |
71
73
  | `NEST_REASON_PHRASES` | `{ 400, 401, 403, 404 }` → NestJS reason phrases. |
72
74
  | `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. |
75
+ | `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`. |
73
76
  | `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createNestErrorHandler`'s `onUnhandledError`. |
74
77
  | `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`). |
75
78
  | `KVCache` / `KVNamespace` / `KVCacheOptions` | Workers-KV cache-aside helper (key `appName+version+table_type_column`, sha256 for string ids, TTL clamped ≥60s). Set `appName` / `version` per application. |
76
79
  | `createStripeClient(secret, opts?)` / `verifyStripeWebhook(...)` / `CreateStripeClientOptions` | Workers-native Stripe client (fetch transport) + async webhook verification (SubtleCrypto). `apiVersion` optional (pin to a fixed Stripe API version). |
77
- | `sendInChunks(queue, messages, chunkSize?)` / `QueueLike` / `QueueSendMessage` | Send queue messages in bounded chunks to stay under the Workers subrequest cap per invocation. |
80
+ | `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). |
78
81
  | `processBatch(batch, handler, options?)` / `MessageBatchLike` / `QueueMessageLike` / `ProcessBatchOptions` / `ProcessBatchResult` | Process a queue batch with bounded concurrency (consumer-side counterpart to `sendInChunks`). |
79
82
  | `ExecutionContextLike` | Minimal `waitUntil`-only Workers execution context shape (for `withMysqlConnections` in worker entry modules without importing `./db`). |
80
83
 
@@ -88,45 +91,83 @@ Requires the `drizzle-orm` and `mysql2` peers. Reads run against a replica via r
88
91
  | `createMysqlDatabase(options)` | Assemble a `Database` from an already-connected Drizzle ORM + replica `QueryRunner`. |
89
92
  | `databaseFrom(orm, replica)` | Build a `Database` from an existing Drizzle instance + replica handle. |
90
93
  | `Database` / `DisposableDatabase` / `QueryRunner` / `TxOf` | The `read` / `write` / `transaction` API and its supporting types. |
91
- | `hyperdriveConnectionOptions(hyperdrive, overrides?)` / `HyperdriveLike` / `ExecutionContextLike` | Build mysql2 `createConnection` options from a Hyperdrive binding (`disableEval`, `decimalNumbers`, `timezone '+09:00'` by default). |
94
+ | `hyperdriveConnectionOptions(hyperdrive, overrides?)` / `HyperdriveLike` / `ExecutionContextLike` | Build mysql2 `createConnection` options from a Hyperdrive binding (`disableEval`, `decimalNumbers`, `timezone '+09:00'` by default). `ExecutionContextLike` is the same type as the root export, re-exported here so `withMysqlConnections` callers don't need the root import. |
92
95
  | `withMysqlConnections(...)` | Open primary/replica connections, run a function, close them in `finally` (via `ctx.waitUntil`). |
93
96
  | `retryWhenDeadlock(fn, retries?, delay?)` | Same deadlock-retry helper as the root export. |
94
97
  | `insertIdOf` / `affectedRowsOf` / `insertedIdsOf` / `DzWriteResult` | Extract `insertId` / `affectedRows` (and derive contiguous bulk-insert ids) from a mysql2 write result. |
95
- | `toJstDate` / `jstTimestampParams` / `jstDatetimeParams` / `jstDateParams` | JST date/time normalization params(高度な用途)。 |
96
- | `jstTimestamp` / `jstDatetime` / `jstDate` / `decimalNumber` | Drizzle 列ヘルパー(repo 側ラッパー不要)。 |
97
- | `jstOnUpdateNow` | `ON UPDATE CURRENT_TIMESTAMP` SQL 式。`jstTimestamp` 等の customType `.onUpdateNow()` 非対応のため `.$onUpdateFn(() => jstOnUpdateNow(fsp))` と併用。 |
98
- | `coerceDecimalNumber` / `decimalNumberParams` | DECIMAL 正規化 params(通常は `decimalNumber` 列ヘルパーで十分)。 |
98
+ | `toJstDate` / `jstTimestampParams` / `jstDatetimeParams` / `jstDateParams` | JST date/time normalization params (advanced use). |
99
+ | `MYSQL_TIMEZONE` | Default mysql2 connection `timezone` (`'+09:00'`) for the JST DB deployment. |
100
+ | `jstTimestamp` / `jstDatetime` / `jstDate` / `decimalNumber` | Drizzle column helpers (no repo-side wrapper needed). |
101
+ | `jstOnUpdateNow` | SQL expression for `ON UPDATE CURRENT_TIMESTAMP`. The `jstTimestamp` customType (and friends) do not support `.onUpdateNow()`, so pair it with `.$onUpdateFn(() => jstOnUpdateNow(fsp))`. |
102
+ | `coerceDecimalNumber` / `decimalNumberParams` | DECIMAL normalization params (the `decimalNumber` column helper is usually enough). |
99
103
  | `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. |
100
- | `resolveDbSecret(options, secretId?)` / `ResolvedDbSecret` | Resolve RDS-managed or plain DB credentials from AWS Secrets Manager for CI migrate / local tooling. |
104
+ | `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. |
101
105
  | `baselineMigrations(options)` / `readBaselineEntry(migrationsFolder)` / `BaselineMigrationsOptions` / `BaselineResult` / `BaselineEntry` | Brownfield first-deploy helper: mark an existing `0000_*` migration as applied without re-running DDL. |
102
106
 
103
- #### Drizzle 列ヘルパー(`jstTimestamp` / `decimalNumber` 等)
107
+ #### Drizzle column helpers (`jstTimestamp` / `decimalNumber`, etc.)
104
108
 
105
- - `drizzle-orm` **peer** のみ。kit `drizzle-orm` を依存に含めない(publish 後も consumer 1 本を使う)。
106
- - consumer は通常どおり `drizzle-orm` `dependencies` に置くだけでよい。**`package.json` `overrides` は不要**。
107
- - npm publish 物には `devDependencies` は含まれないため、インストール先で kit 専用の `drizzle-orm` は増えない(peer 1 本のみ)。
108
- - 列ヘルパーは runtime consumer `drizzle-orm` `import` し、型は `customType` 推論そのまま(`MySqlCustomColumnBuilder<…>`)。`any` は使わないので consumer テーブルの `$inferSelect` に列の意味型が伝播する。
109
- - **前提: drizzle を単一コピーに解決すること。** drizzle `SQL` private フィールド `shouldInlineParams` を持つ**名目型**で、kit consumer が別コピーを解決すると `jstTimestamp(…).default(sql\`…\`)` `TS2345 separate declarations of a private property 'shouldInlineParams'` で全 schema 落ちする。`file:` リンク開発では kit 配下に `drizzle-orm` がネストして二重コピーになるため、**consumer `tsconfig.json` `drizzle-orm` を自身の 1 コピーへ固定**する:
109
+ - `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).
110
+ - The consumer just keeps `drizzle-orm` in its `dependencies` as usual. **No `overrides` in `package.json` are needed.**
111
+ - 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).
112
+ - 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`.
113
+ - **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`**:
110
114
 
111
115
  ```jsonc
112
- // tsconfig.json compilerOptions(既存 paths があればマージ)
116
+ // tsconfig.json compilerOptions (merge with existing paths if any)
113
117
  "paths": {
114
118
  "drizzle-orm": ["./node_modules/drizzle-orm"],
115
119
  "drizzle-orm/*": ["./node_modules/drizzle-orm/*"]
116
120
  }
117
121
  ```
118
122
 
119
- `moduleResolution: "Bundler"` なら `baseUrl` 不要(`baseUrl` 設定済みなら先頭 `./` は外す)。published 版(単一コピー)ではこの `paths` は無害。**overrides は不要。**
120
- - `file:` kit を直リンクする開発では、kit リポジトリ側で `npm install` して peer を満たす(consumer 側で overrides を足さない)。
123
+ 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.**
124
+ - 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).
121
125
 
122
- **`CURRENT_TIMESTAMP` と接続 `timezone:'+09:00'` の違い**
126
+ **`CURRENT_TIMESTAMP` vs the connection `timezone:'+09:00'`**
123
127
 
124
- | 経路 | 誰が時刻を決めるか | JST との関係 |
128
+ | Path | Who decides the time | Relationship to JST |
125
129
  | --- | --- | --- |
126
- | アプリが `Date` を bind(INSERT/UPDATE | mysql2 + 接続 `timezone:'+09:00'` | ワイヤ上は JST として扱われる(`datetime-wire` テスト) |
127
- | `DEFAULT CURRENT_TIMESTAMP` / `ON UPDATE CURRENT_TIMESTAMP` | MySQL サーバ(セッション `time_zone`) | 接続オプションとは**別経路**。RDS `time_zone` `+09:00` なら JST、UTC なら UTC |
130
+ | The app binds a `Date` (INSERT/UPDATE) | mysql2 + connection `timezone:'+09:00'` | Treated as JST on the wire (`datetime-wire` test) |
131
+ | `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 |
128
132
 
129
- `jstTimestamp` / `jstDatetime` は読書の pass-through DATE 正規化のみ担当し、DB 既定値の時刻帯は変えない。`ON UPDATE` が必要な列は `.$onUpdateFn(() => jstOnUpdateNow(6))` で DDL 意図を維持する。
133
+ `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))`.
134
+
135
+ ### Business time — `@rdlabo/workers-hono-kit/business-time`
136
+
137
+ String-level JST business-time conversions (Workers UTC instant ↔ business calendar date / date-time),
138
+ with **no `mysql2` / `drizzle-orm` dependency**. This is a different layer from the `./db` column helpers
139
+ (which handle the MySQL wire format): the DB stays on JST, and the app handles JST explicitly through
140
+ this module instead of relying implicitly on the connection `timezone`.
141
+
142
+ | Export | Description |
143
+ | --- | --- |
144
+ | `today(ref?)` | The JST business calendar date (`YYYY-MM-DD`) of `ref` (defaults to now). |
145
+ | `toBusinessDate(instant)` | UTC instant → JST business calendar date (`YYYY-MM-DD`). |
146
+ | `normalizeBusinessDate(value)` | Normalize a `string` / `Date` / nullish to `YYYY-MM-DD`; a `YYYY-MM-DD` string passes through unchanged, nullish/empty/invalid → `null`. |
147
+ | `toBusinessDateTime(instant)` | UTC instant → JST business date-time (`YYYY-MM-DD HH:mm:ss`). |
148
+ | `parseBusinessDateTime(value)` | JST business date-time string → UTC instant (accepts a space or `T` separator). |
149
+ | `formatBusinessDateTime(instant, pattern?)` | Format an instant in the business TZ (Nest `helper.formatDate`-compatible tokens). |
150
+ | `startOfBusinessDay(date)` / `endOfBusinessDay(date)` | UTC instant of `00:00:00` / `23:59:59` on a JST business date. |
151
+ | `businessDateTimeInstant(date, time)` | JST business date + wall-clock time → UTC instant. |
152
+ | `addBusinessDays(date, days)` | Add calendar days to a JST business date. |
153
+ | `ageOnBusinessDate(birthDate, asOfDate?)` | Full years of age on a business date (`asOfDate` defaults to `today()`). |
154
+ | `DEFAULT_BUSINESS_DATETIME_PATTERN` | Default `formatBusinessDateTime` pattern (`YYYY-MM-DDThh:mm:ss`). |
155
+ | `BUSINESS_TIMEZONE` / `BusinessDate` / `BusinessDateTime` | JST timezone constant and the business-date / date-time string types. |
156
+
157
+ ```ts
158
+ import {
159
+ toBusinessDate,
160
+ toBusinessDateTime,
161
+ formatBusinessDateTime,
162
+ addBusinessDays,
163
+ } from '@rdlabo/workers-hono-kit/business-time';
164
+
165
+ const now = new Date('2026-07-05T21:00:00Z');
166
+ toBusinessDate(now); // '2026-07-06' (JST)
167
+ toBusinessDateTime(now); // '2026-07-06 06:00:00'
168
+ formatBusinessDateTime(now); // '2026-07-06T06:00:00'
169
+ addBusinessDays('2026-07-06', 3); // '2026-07-09'
170
+ ```
130
171
 
131
172
  ### Testing — `@rdlabo/workers-hono-kit/testing`
132
173
 
@@ -259,6 +300,24 @@ app.onError(
259
300
  );
260
301
  ```
261
302
 
303
+ **Important:** `Sentry.withSentry` does **not** capture errors handled by `app.onError`. Wire
304
+ `onUnhandledError` → `Sentry.captureException` explicitly (mirrors Nest `SentryGlobalFilter`).
305
+
306
+ Repos with a Nest `QueryFailedExceptionFilter` (e.g. odss-mobile) should use
307
+ `createQueryFailedNestErrorHandler` so classified DB errors still log and report to Sentry:
308
+
309
+ ```ts
310
+ import { createQueryFailedNestErrorHandler } from '@rdlabo/workers-hono-kit';
311
+
312
+ app.onError(
313
+ createQueryFailedNestErrorHandler({
314
+ fieldOrder: 'message-first',
315
+ classify: classifyQueryFailed, // app-local parity (Japanese messages, errno rules)
316
+ onUnhandledError: (err, c) => container.reportError?.(err, { requestId: c.get('requestId') }),
317
+ }),
318
+ );
319
+ ```
320
+
262
321
  ### Auth middleware
263
322
 
264
323
  Encodes the shared skeleton (read token header → verify → `getAppInfo` → resolve user id →
@@ -291,6 +350,47 @@ const tokenAuth = createAuthMiddleware<AppEnv, UserRecord>({
291
350
  });
292
351
  ```
293
352
 
353
+ ### Latency instrumentation (`perfLog`)
354
+
355
+ Records one data point per request — `t_app` (time inside the app), `colo`, `cold`/`warm`, matched
356
+ route, `status` — and ships it to **Workers Logs** and/or **Workers Analytics Engine**. This lets you
357
+ measure a low-traffic Worker after the fact (retained + queryable) instead of watching a live
358
+ `wrangler tail`. Register it first so it wraps everything.
359
+
360
+ ```ts
361
+ import { perfLog } from '@rdlabo/workers-hono-kit';
362
+
363
+ // A) app served with env (`app.fetch(req, env, ctx)`): bare — reads `PERF` (Analytics Engine
364
+ // dataset binding) and `PERF_LOG === '1'` (Workers Logs) off `c.env`.
365
+ app.use('*', perfLog());
366
+
367
+ // B) app built without Hono env (`createApp(container).fetch(req)`): pass bindings explicitly.
368
+ app.use('*', perfLog({ console: env.PERF_LOG === '1', dataset: env.PERF }));
369
+ ```
370
+
371
+ ```toml
372
+ # wrangler.toml — dataset is created on first write (no provisioning); needs [observability] for Logs.
373
+ [[analytics_engine_datasets]]
374
+ binding = "PERF"
375
+ dataset = "myapp_perf"
376
+ ```
377
+
378
+ Query percentiles by route/colo with the Analytics Engine SQL API:
379
+
380
+ ```sql
381
+ SELECT blob1 AS path, blob2 AS colo,
382
+ quantileWeighted(0.5)(double1, _sample_interval) AS p50,
383
+ quantileWeighted(0.9)(double1, _sample_interval) AS p90
384
+ FROM myapp_perf WHERE timestamp > now() - INTERVAL '7' DAY
385
+ GROUP BY path, colo ORDER BY p90 DESC
386
+ ```
387
+
388
+ > **Scope of `t_app`**: it covers everything *inside* the app; work done in `fetch` *before* the app
389
+ > (e.g. secrets fetch / DB connect in `createApp(container)` vs. building the container in `fetch`) is
390
+ > not comparable across differently-wired apps. Instrument the `fetch` seam if you need a secrets/connect
391
+ > cold breakdown. On production Workers `Date.now()` only advances at I/O boundaries, so `t_app` ≈ I/O
392
+ > wait, not CPU time.
393
+
294
394
  ### AI Gateway
295
395
 
296
396
  Route `@ai-sdk` models through the Cloudflare AI Gateway — either with a Workers `AI` binding
@@ -363,7 +463,7 @@ await testDb.resetSchema();
363
463
  const pool = testDb.createTestPool();
364
464
 
365
465
  const firebase = new FakeFirebaseVerifier();
366
- firebase.register('uid-1', { email: 'a@example.com' });
466
+ firebase.register('token-1', { uid: 'uid-1', email: 'a@example.com' });
367
467
 
368
468
  const gateway = configurableFake<PaymentGateway>({ charge: async () => ({ ok: true }) }, 'PaymentGateway');
369
469
  ```
@@ -387,6 +487,16 @@ If you consume this package via a local path (e.g. `"@rdlabo/workers-hono-kit":
387
487
 
388
488
  When installed from npm normally, package managers dedupe `zod` to a single copy and this is not needed.
389
489
 
490
+ ## CLI
491
+
492
+ The package ships three `bin` commands (run via `npx` or an npm script in the consuming app):
493
+
494
+ | Command | Use |
495
+ | --- | --- |
496
+ | `workers-hono-kit-sync-dev-aws <wrangler-args…>` | Launch `wrangler` with AWS credentials injected as `--var`, resolved from the active AWS profile (honors `AWS_PROFILE`, supports short-lived SSO/temporary creds). Nothing is written to disk — replaces `.dev.vars`. Wire it as the `dev` script, e.g. `AWS_PROFILE=<p> workers-hono-kit-sync-dev-aws dev --var APP_ENV:development`. |
497
+ | `workers-hono-kit-check-subrequest-fanout [dir…]` | CI gate that greps for per-item external-call fan-outs (`runWithConcurrency(` / `PromisePool` / `.withConcurrency(`) that would eventually exceed the Workers subrequest cap. Annotate a genuinely-safe site with `subrequest-ok`. Scans `src` by default; exits 1 on an un-annotated marker. |
498
+ | `workers-hono-kit-db-baseline [--migrations ./drizzle]` | Brownfield first-deploy helper: record the baseline `0000` migration as *already applied* on an existing MySQL DB without running its DDL (the CLI wrapper around `baselineMigrations` / `readBaselineEntry`). Reads DB credentials from `DB_SECRET` (AWS RDS managed secret) or the individual `DB_*` env vars. |
499
+
390
500
  ## Development
391
501
 
392
502
  ```bash
@@ -1,49 +1,126 @@
1
1
  /**
2
- * JST 業務時刻の明示 APIWorkers UTC instant ↔ 業務暦日/日時)。
2
+ * Explicit JST business-time API (Workers UTC instant ↔ business calendar date / date-time).
3
3
  *
4
4
  * @remarks
5
- * - DB JST 運用のまま。アプリは mysql2 `timezone` に暗黙依存せず、ここ経由で JST を扱う。
6
- * - MySQL ワイヤ形式への変換は {@link ../db/jst.js | db/jst} の責務。
7
- * - `Date` local getter(`getHours` 等)は業務判定に使わない。
5
+ * - The DB stays on JST. The app does not implicitly rely on the mysql2 `timezone` option; it goes
6
+ * through this module to handle JST.
7
+ * - Converting to the MySQL wire format is the responsibility of {@link ../db/jst.js | db/jst}.
8
+ * - Do not use a `Date`'s local getters (`getHours`, etc.) for business-time decisions.
8
9
  *
9
10
  * @packageDocumentation
10
11
  */
11
12
  import { BUSINESS_TIMEZONE } from './types.js';
12
13
  import type { BusinessDate, BusinessDateTime } from './types.js';
13
14
  export { BUSINESS_TIMEZONE, type BusinessDate, type BusinessDateTime };
14
- /** 参照 instant の JST 業務暦日。 */
15
+ /**
16
+ * The JST business calendar date of a reference instant.
17
+ *
18
+ * @param ref - the instant to read; defaults to now.
19
+ * @returns the business date as `YYYY-MM-DD`.
20
+ * @example
21
+ * today(new Date('2026-07-05T20:00:00Z')); // → '2026-07-06' (JST)
22
+ */
15
23
  export declare function today(ref?: Date): BusinessDate;
16
- /** UTC instant → JST 業務暦日。 */
24
+ /**
25
+ * Convert a UTC instant to a JST business calendar date.
26
+ *
27
+ * @param instant - the UTC instant to convert.
28
+ * @returns the business date as `YYYY-MM-DD`.
29
+ */
17
30
  export declare function toBusinessDate(instant: Date): BusinessDate;
18
31
  /**
19
- * クライアント / DB 入力を JST 業務暦日 `YYYY-MM-DD` へ正規化する。
32
+ * Normalize a client / DB input to a JST business calendar date `YYYY-MM-DD`.
33
+ *
34
+ * - A string already in `YYYY-MM-DD` form is returned as-is, **without** constructing a `Date`
35
+ * (a birthday is a calendar day, not an instant).
36
+ * - ISO 8601 and similar values are converted to a JST calendar date via their instant.
37
+ * - Nullish / empty / invalid inputs yield `null`.
20
38
  *
21
- * - 既に `YYYY-MM-DD` の文字列は **Date 化せず**そのまま返す(誕生日は instant ではない)。
22
- * - ISO 8601 等は instant 経由で JST 暦日へ変換。
23
- * - nullish / 空 / 不正は `null`。
39
+ * @param value - the string, `Date`, or nullish value to normalize.
40
+ * @returns the business date as `YYYY-MM-DD`, or `null` when the input cannot be resolved.
41
+ * @example
42
+ * normalizeBusinessDate('1990-01-15'); // → '1990-01-15' (unchanged)
43
+ * normalizeBusinessDate('2026-07-05T20:00:00Z'); // → '2026-07-06' (JST)
44
+ * normalizeBusinessDate(''); // → null
24
45
  */
25
46
  export declare function normalizeBusinessDate(value: string | Date | null | undefined): BusinessDate | null;
26
- /** UTC instant → JST 業務日時(`YYYY-MM-DD HH:mm:ss`)。 */
47
+ /**
48
+ * Convert a UTC instant to a JST business date-time (`YYYY-MM-DD HH:mm:ss`).
49
+ *
50
+ * @param instant - the UTC instant to convert.
51
+ * @returns the business date-time string.
52
+ * @example
53
+ * toBusinessDateTime(new Date('2026-07-05T21:00:00Z')); // → '2026-07-06 06:00:00' (JST)
54
+ */
27
55
  export declare function toBusinessDateTime(instant: Date): BusinessDateTime;
28
- /** Nest / foodlabel / winecode `helper.formatDate` 既定パターン。 */
56
+ /** Default pattern for the Nest / foodlabel / winecode `helper.formatDate`. */
29
57
  export declare const DEFAULT_BUSINESS_DATETIME_PATTERN: "YYYY-MM-DDThh:mm:ss";
30
58
  /**
31
- * Nest `helper.formatDate` 互換のパターン整形(業務 TZ)。
32
- * `S` トークンは元 instant のミリ秒(Nest 正本)。
59
+ * Format an instant in the business TZ, compatible with the Nest `helper.formatDate`.
60
+ *
61
+ * Supported tokens: `YYYY` / `MM` / `DD` / `hh` / `mm` / `ss`, plus `S` for the source instant's
62
+ * milliseconds (matching the Nest reference implementation).
63
+ *
64
+ * @param instant - the UTC instant to format.
65
+ * @param pattern - the format pattern; defaults to {@link DEFAULT_BUSINESS_DATETIME_PATTERN}.
66
+ * @returns the formatted string.
67
+ * @example
68
+ * formatBusinessDateTime(new Date('2026-07-05T21:00:00Z')); // → '2026-07-06T06:00:00' (JST)
33
69
  */
34
70
  export declare function formatBusinessDateTime(instant: Date, pattern?: string): string;
35
- /** JST 業務日時文字列 → UTC instant。`YYYY-MM-DD HH:mm:ss` / `T` 区切りを受け付ける。 */
71
+ /**
72
+ * Parse a JST business date-time string into a UTC instant. Accepts a space or `T` separator
73
+ * (`YYYY-MM-DD HH:mm:ss` or `YYYY-MM-DDTHH:mm:ss`).
74
+ *
75
+ * @param value - the business date-time string to parse.
76
+ * @returns the corresponding UTC instant.
77
+ * @throws RangeError when `value` is not a valid business date-time.
78
+ * @example
79
+ * parseBusinessDateTime('2026-07-06 06:00:00'); // → 2026-07-05T21:00:00Z
80
+ */
36
81
  export declare function parseBusinessDateTime(value: BusinessDateTime): Date;
37
- /** JST 業務暦日の 00:00:00 を表す UTC instant。 */
82
+ /**
83
+ * The UTC instant of `00:00:00` on a JST business calendar date.
84
+ *
85
+ * @param date - the business date as `YYYY-MM-DD`.
86
+ * @returns the UTC instant at the start of that business day.
87
+ */
38
88
  export declare function startOfBusinessDay(date: BusinessDate): Date;
39
- /** JST 業務暦日の 23:59:59 を表す UTC instant。 */
89
+ /**
90
+ * The UTC instant of `23:59:59` on a JST business calendar date.
91
+ *
92
+ * @param date - the business date as `YYYY-MM-DD`.
93
+ * @returns the UTC instant at the end of that business day.
94
+ */
40
95
  export declare function endOfBusinessDay(date: BusinessDate): Date;
41
96
  /**
42
- * JST 業務暦日 + 壁時計時刻 UTC instant
43
- * @example businessDateTimeInstant('2026-07-05', '06:00:00')
97
+ * Convert a JST business calendar date + wall-clock time to a UTC instant.
98
+ *
99
+ * @param date - the business date as `YYYY-MM-DD`.
100
+ * @param time - the wall-clock time as `HH:mm:ss` (or `HH:mm`).
101
+ * @returns the corresponding UTC instant.
102
+ * @throws RangeError when `date` or `time` is malformed.
103
+ * @example
104
+ * businessDateTimeInstant('2026-07-06', '06:00:00'); // → 2026-07-05T21:00:00Z
44
105
  */
45
106
  export declare function businessDateTimeInstant(date: BusinessDate, time: string): Date;
46
- /** JST 業務暦日に日数を加算(暦日単位)。 */
107
+ /**
108
+ * Add a number of calendar days to a JST business calendar date.
109
+ *
110
+ * @param date - the starting business date as `YYYY-MM-DD`.
111
+ * @param days - the number of calendar days to add (may be negative).
112
+ * @returns the resulting business date as `YYYY-MM-DD`.
113
+ * @example
114
+ * addBusinessDays('2026-07-06', 3); // → '2026-07-09'
115
+ */
47
116
  export declare function addBusinessDays(date: BusinessDate, days: number): BusinessDate;
48
- /** 業務暦日基準の満年齢(誕生日は instant ではなく BusinessDate)。 */
117
+ /**
118
+ * The full years of age on a business calendar date (a birthday is a `BusinessDate`, not an instant).
119
+ *
120
+ * @param birthDate - the birth date as `YYYY-MM-DD`.
121
+ * @param asOfDate - the reference business date; defaults to {@link today}.
122
+ * @returns the age in completed years.
123
+ * @example
124
+ * ageOnBusinessDate('1990-07-10', '2026-07-06'); // → 35
125
+ */
49
126
  export declare function ageOnBusinessDate(birthDate: BusinessDate, asOfDate?: BusinessDate): number;
@@ -1,17 +1,18 @@
1
1
  /**
2
- * JST 業務時刻の明示 APIWorkers UTC instant ↔ 業務暦日/日時)。
2
+ * Explicit JST business-time API (Workers UTC instant ↔ business calendar date / date-time).
3
3
  *
4
4
  * @remarks
5
- * - DB JST 運用のまま。アプリは mysql2 `timezone` に暗黙依存せず、ここ経由で JST を扱う。
6
- * - MySQL ワイヤ形式への変換は {@link ../db/jst.js | db/jst} の責務。
7
- * - `Date` local getter(`getHours` 等)は業務判定に使わない。
5
+ * - The DB stays on JST. The app does not implicitly rely on the mysql2 `timezone` option; it goes
6
+ * through this module to handle JST.
7
+ * - Converting to the MySQL wire format is the responsibility of {@link ../db/jst.js | db/jst}.
8
+ * - Do not use a `Date`'s local getters (`getHours`, etc.) for business-time decisions.
8
9
  *
9
10
  * @packageDocumentation
10
11
  */
11
12
  import { BUSINESS_TIMEZONE } from './types.js';
12
13
  export { BUSINESS_TIMEZONE };
13
14
  const pad2 = (n) => String(n).padStart(2, '0');
14
- /** instant を業務 TZ 壁時計として読むためのシフト(`getUTC*` で成分を得る)。 */
15
+ /** Shift an instant so it can be read as a business-TZ wall clock (extract fields with `getUTC*`). */
15
16
  function toWallClock(instant) {
16
17
  return new Date(instant.getTime() + BUSINESS_TIMEZONE.offsetMinutes * 60_000);
17
18
  }
@@ -29,21 +30,41 @@ function parseHms(time) {
29
30
  }
30
31
  return [Number(m[1]), Number(m[2]), Number(m[3] || 0)];
31
32
  }
32
- /** 参照 instant の JST 業務暦日。 */
33
+ /**
34
+ * The JST business calendar date of a reference instant.
35
+ *
36
+ * @param ref - the instant to read; defaults to now.
37
+ * @returns the business date as `YYYY-MM-DD`.
38
+ * @example
39
+ * today(new Date('2026-07-05T20:00:00Z')); // → '2026-07-06' (JST)
40
+ */
33
41
  export function today(ref = new Date()) {
34
42
  return toBusinessDate(ref);
35
43
  }
36
- /** UTC instant → JST 業務暦日。 */
44
+ /**
45
+ * Convert a UTC instant to a JST business calendar date.
46
+ *
47
+ * @param instant - the UTC instant to convert.
48
+ * @returns the business date as `YYYY-MM-DD`.
49
+ */
37
50
  export function toBusinessDate(instant) {
38
51
  const wall = toWallClock(instant);
39
52
  return `${wall.getUTCFullYear()}-${pad2(wall.getUTCMonth() + 1)}-${pad2(wall.getUTCDate())}`;
40
53
  }
41
54
  /**
42
- * クライアント / DB 入力を JST 業務暦日 `YYYY-MM-DD` へ正規化する。
55
+ * Normalize a client / DB input to a JST business calendar date `YYYY-MM-DD`.
56
+ *
57
+ * - A string already in `YYYY-MM-DD` form is returned as-is, **without** constructing a `Date`
58
+ * (a birthday is a calendar day, not an instant).
59
+ * - ISO 8601 and similar values are converted to a JST calendar date via their instant.
60
+ * - Nullish / empty / invalid inputs yield `null`.
43
61
  *
44
- * - 既に `YYYY-MM-DD` の文字列は **Date 化せず**そのまま返す(誕生日は instant ではない)。
45
- * - ISO 8601 等は instant 経由で JST 暦日へ変換。
46
- * - nullish / 空 / 不正は `null`。
62
+ * @param value - the string, `Date`, or nullish value to normalize.
63
+ * @returns the business date as `YYYY-MM-DD`, or `null` when the input cannot be resolved.
64
+ * @example
65
+ * normalizeBusinessDate('1990-01-15'); // → '1990-01-15' (unchanged)
66
+ * normalizeBusinessDate('2026-07-05T20:00:00Z'); // → '2026-07-06' (JST)
67
+ * normalizeBusinessDate(''); // → null
47
68
  */
48
69
  export function normalizeBusinessDate(value) {
49
70
  if (value == null) {
@@ -72,16 +93,31 @@ export function normalizeBusinessDate(value) {
72
93
  }
73
94
  return toBusinessDate(new Date(ms));
74
95
  }
75
- /** UTC instant → JST 業務日時(`YYYY-MM-DD HH:mm:ss`)。 */
96
+ /**
97
+ * Convert a UTC instant to a JST business date-time (`YYYY-MM-DD HH:mm:ss`).
98
+ *
99
+ * @param instant - the UTC instant to convert.
100
+ * @returns the business date-time string.
101
+ * @example
102
+ * toBusinessDateTime(new Date('2026-07-05T21:00:00Z')); // → '2026-07-06 06:00:00' (JST)
103
+ */
76
104
  export function toBusinessDateTime(instant) {
77
105
  const wall = toWallClock(instant);
78
106
  return `${wall.getUTCFullYear()}-${pad2(wall.getUTCMonth() + 1)}-${pad2(wall.getUTCDate())} ${pad2(wall.getUTCHours())}:${pad2(wall.getUTCMinutes())}:${pad2(wall.getUTCSeconds())}`;
79
107
  }
80
- /** Nest / foodlabel / winecode `helper.formatDate` 既定パターン。 */
108
+ /** Default pattern for the Nest / foodlabel / winecode `helper.formatDate`. */
81
109
  export const DEFAULT_BUSINESS_DATETIME_PATTERN = 'YYYY-MM-DDThh:mm:ss';
82
110
  /**
83
- * Nest `helper.formatDate` 互換のパターン整形(業務 TZ)。
84
- * `S` トークンは元 instant のミリ秒(Nest 正本)。
111
+ * Format an instant in the business TZ, compatible with the Nest `helper.formatDate`.
112
+ *
113
+ * Supported tokens: `YYYY` / `MM` / `DD` / `hh` / `mm` / `ss`, plus `S` for the source instant's
114
+ * milliseconds (matching the Nest reference implementation).
115
+ *
116
+ * @param instant - the UTC instant to format.
117
+ * @param pattern - the format pattern; defaults to {@link DEFAULT_BUSINESS_DATETIME_PATTERN}.
118
+ * @returns the formatted string.
119
+ * @example
120
+ * formatBusinessDateTime(new Date('2026-07-05T21:00:00Z')); // → '2026-07-06T06:00:00' (JST)
85
121
  */
86
122
  export function formatBusinessDateTime(instant, pattern = DEFAULT_BUSINESS_DATETIME_PATTERN) {
87
123
  const wall = toWallClock(instant);
@@ -102,7 +138,16 @@ export function formatBusinessDateTime(instant, pattern = DEFAULT_BUSINESS_DATET
102
138
  }
103
139
  return out;
104
140
  }
105
- /** JST 業務日時文字列 → UTC instant。`YYYY-MM-DD HH:mm:ss` / `T` 区切りを受け付ける。 */
141
+ /**
142
+ * Parse a JST business date-time string into a UTC instant. Accepts a space or `T` separator
143
+ * (`YYYY-MM-DD HH:mm:ss` or `YYYY-MM-DDTHH:mm:ss`).
144
+ *
145
+ * @param value - the business date-time string to parse.
146
+ * @returns the corresponding UTC instant.
147
+ * @throws RangeError when `value` is not a valid business date-time.
148
+ * @example
149
+ * parseBusinessDateTime('2026-07-06 06:00:00'); // → 2026-07-05T21:00:00Z
150
+ */
106
151
  export function parseBusinessDateTime(value) {
107
152
  const normalized = value.includes('T') ? value.replace('T', ' ') : value;
108
153
  const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/.exec(normalized);
@@ -113,17 +158,33 @@ export function parseBusinessDateTime(value) {
113
158
  const offsetHours = BUSINESS_TIMEZONE.offsetMinutes / 60;
114
159
  return new Date(Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h) - offsetHours, Number(mi), Number(s), 0));
115
160
  }
116
- /** JST 業務暦日の 00:00:00 を表す UTC instant。 */
161
+ /**
162
+ * The UTC instant of `00:00:00` on a JST business calendar date.
163
+ *
164
+ * @param date - the business date as `YYYY-MM-DD`.
165
+ * @returns the UTC instant at the start of that business day.
166
+ */
117
167
  export function startOfBusinessDay(date) {
118
168
  return businessDateTimeInstant(date, '00:00:00');
119
169
  }
120
- /** JST 業務暦日の 23:59:59 を表す UTC instant。 */
170
+ /**
171
+ * The UTC instant of `23:59:59` on a JST business calendar date.
172
+ *
173
+ * @param date - the business date as `YYYY-MM-DD`.
174
+ * @returns the UTC instant at the end of that business day.
175
+ */
121
176
  export function endOfBusinessDay(date) {
122
177
  return businessDateTimeInstant(date, '23:59:59');
123
178
  }
124
179
  /**
125
- * JST 業務暦日 + 壁時計時刻 UTC instant
126
- * @example businessDateTimeInstant('2026-07-05', '06:00:00')
180
+ * Convert a JST business calendar date + wall-clock time to a UTC instant.
181
+ *
182
+ * @param date - the business date as `YYYY-MM-DD`.
183
+ * @param time - the wall-clock time as `HH:mm:ss` (or `HH:mm`).
184
+ * @returns the corresponding UTC instant.
185
+ * @throws RangeError when `date` or `time` is malformed.
186
+ * @example
187
+ * businessDateTimeInstant('2026-07-06', '06:00:00'); // → 2026-07-05T21:00:00Z
127
188
  */
128
189
  export function businessDateTimeInstant(date, time) {
129
190
  const [y, mo, d] = parseYmd(date);
@@ -131,12 +192,28 @@ export function businessDateTimeInstant(date, time) {
131
192
  const offsetHours = BUSINESS_TIMEZONE.offsetMinutes / 60;
132
193
  return new Date(Date.UTC(y, mo - 1, d, h - offsetHours, mi, s, 0));
133
194
  }
134
- /** JST 業務暦日に日数を加算(暦日単位)。 */
195
+ /**
196
+ * Add a number of calendar days to a JST business calendar date.
197
+ *
198
+ * @param date - the starting business date as `YYYY-MM-DD`.
199
+ * @param days - the number of calendar days to add (may be negative).
200
+ * @returns the resulting business date as `YYYY-MM-DD`.
201
+ * @example
202
+ * addBusinessDays('2026-07-06', 3); // → '2026-07-09'
203
+ */
135
204
  export function addBusinessDays(date, days) {
136
205
  const anchor = businessDateTimeInstant(date, '12:00:00');
137
206
  return toBusinessDate(new Date(anchor.getTime() + days * 24 * 60 * 60 * 1000));
138
207
  }
139
- /** 業務暦日基準の満年齢(誕生日は instant ではなく BusinessDate)。 */
208
+ /**
209
+ * The full years of age on a business calendar date (a birthday is a `BusinessDate`, not an instant).
210
+ *
211
+ * @param birthDate - the birth date as `YYYY-MM-DD`.
212
+ * @param asOfDate - the reference business date; defaults to {@link today}.
213
+ * @returns the age in completed years.
214
+ * @example
215
+ * ageOnBusinessDate('1990-07-10', '2026-07-06'); // → 35
216
+ */
140
217
  export function ageOnBusinessDate(birthDate, asOfDate) {
141
218
  const asOf = asOfDate ?? today();
142
219
  const [by, bm, bd] = parseYmd(birthDate);
@@ -1,8 +1,8 @@
1
- /** JST 業務暦日 `YYYY-MM-DD`(instant ではない)。 */
1
+ /** A JST business calendar date `YYYY-MM-DD` (a calendar day, not an instant). */
2
2
  export type BusinessDate = string;
3
- /** JST 業務日時 `YYYY-MM-DD HH:mm:ss`(MySQL DATETIME 互換の壁時計表現)。 */
3
+ /** A JST business date-time `YYYY-MM-DD HH:mm:ss` (a wall-clock value, MySQL `DATETIME`-compatible). */
4
4
  export type BusinessDateTime = string;
5
- /** JST 業務タイムゾーン定数(Workers UTC instant、業務はここで明示)。 */
5
+ /** JST business timezone constant. Workers run in UTC instants; business time is made explicit here. */
6
6
  export declare const BUSINESS_TIMEZONE: {
7
7
  readonly iana: "Asia/Tokyo";
8
8
  readonly offsetMinutes: 540;
@@ -1,4 +1,4 @@
1
- /** JST 業務タイムゾーン定数(Workers UTC instant、業務はここで明示)。 */
1
+ /** JST business timezone constant. Workers run in UTC instants; business time is made explicit here. */
2
2
  export const BUSINESS_TIMEZONE = {
3
3
  iana: 'Asia/Tokyo',
4
4
  offsetMinutes: 540,