@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.
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
@@ -70,11 +70,12 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
70
70
  | `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
71
  | `NEST_REASON_PHRASES` | `{ 400, 401, 403, 404 }` → NestJS reason phrases. |
72
72
  | `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. |
73
+ | `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
74
  | `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createNestErrorHandler`'s `onUnhandledError`. |
74
75
  | `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
76
  | `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
77
  | `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. |
78
+ | `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
79
  | `processBatch(batch, handler, options?)` / `MessageBatchLike` / `QueueMessageLike` / `ProcessBatchOptions` / `ProcessBatchResult` | Process a queue batch with bounded concurrency (consumer-side counterpart to `sendInChunks`). |
79
80
  | `ExecutionContextLike` | Minimal `waitUntil`-only Workers execution context shape (for `withMysqlConnections` in worker entry modules without importing `./db`). |
80
81
 
@@ -88,45 +89,83 @@ Requires the `drizzle-orm` and `mysql2` peers. Reads run against a replica via r
88
89
  | `createMysqlDatabase(options)` | Assemble a `Database` from an already-connected Drizzle ORM + replica `QueryRunner`. |
89
90
  | `databaseFrom(orm, replica)` | Build a `Database` from an existing Drizzle instance + replica handle. |
90
91
  | `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). |
92
+ | `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
93
  | `withMysqlConnections(...)` | Open primary/replica connections, run a function, close them in `finally` (via `ctx.waitUntil`). |
93
94
  | `retryWhenDeadlock(fn, retries?, delay?)` | Same deadlock-retry helper as the root export. |
94
95
  | `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` 列ヘルパーで十分)。 |
96
+ | `toJstDate` / `jstTimestampParams` / `jstDatetimeParams` / `jstDateParams` | JST date/time normalization params (advanced use). |
97
+ | `MYSQL_TIMEZONE` | Default mysql2 connection `timezone` (`'+09:00'`) for the JST DB deployment. |
98
+ | `jstTimestamp` / `jstDatetime` / `jstDate` / `decimalNumber` | Drizzle column helpers (no repo-side wrapper needed). |
99
+ | `jstOnUpdateNow` | SQL expression for `ON UPDATE CURRENT_TIMESTAMP`. The `jstTimestamp` customType (and friends) do not support `.onUpdateNow()`, so pair it with `.$onUpdateFn(() => jstOnUpdateNow(fsp))`. |
100
+ | `coerceDecimalNumber` / `decimalNumberParams` | DECIMAL normalization params (the `decimalNumber` column helper is usually enough). |
99
101
  | `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. |
102
+ | `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
103
  | `baselineMigrations(options)` / `readBaselineEntry(migrationsFolder)` / `BaselineMigrationsOptions` / `BaselineResult` / `BaselineEntry` | Brownfield first-deploy helper: mark an existing `0000_*` migration as applied without re-running DDL. |
102
104
 
103
- #### Drizzle 列ヘルパー(`jstTimestamp` / `decimalNumber` 等)
105
+ #### Drizzle column helpers (`jstTimestamp` / `decimalNumber`, etc.)
104
106
 
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 コピーへ固定**する:
107
+ - `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).
108
+ - The consumer just keeps `drizzle-orm` in its `dependencies` as usual. **No `overrides` in `package.json` are needed.**
109
+ - 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).
110
+ - 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`.
111
+ - **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
112
 
111
113
  ```jsonc
112
- // tsconfig.json compilerOptions(既存 paths があればマージ)
114
+ // tsconfig.json compilerOptions (merge with existing paths if any)
113
115
  "paths": {
114
116
  "drizzle-orm": ["./node_modules/drizzle-orm"],
115
117
  "drizzle-orm/*": ["./node_modules/drizzle-orm/*"]
116
118
  }
117
119
  ```
118
120
 
119
- `moduleResolution: "Bundler"` なら `baseUrl` 不要(`baseUrl` 設定済みなら先頭 `./` は外す)。published 版(単一コピー)ではこの `paths` は無害。**overrides は不要。**
120
- - `file:` kit を直リンクする開発では、kit リポジトリ側で `npm install` して peer を満たす(consumer 側で overrides を足さない)。
121
+ 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.**
122
+ - 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
123
 
122
- **`CURRENT_TIMESTAMP` と接続 `timezone:'+09:00'` の違い**
124
+ **`CURRENT_TIMESTAMP` vs the connection `timezone:'+09:00'`**
123
125
 
124
- | 経路 | 誰が時刻を決めるか | JST との関係 |
126
+ | Path | Who decides the time | Relationship to JST |
125
127
  | --- | --- | --- |
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 |
128
+ | The app binds a `Date` (INSERT/UPDATE) | mysql2 + connection `timezone:'+09:00'` | Treated as JST on the wire (`datetime-wire` test) |
129
+ | `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
130
 
129
- `jstTimestamp` / `jstDatetime` は読書の pass-through DATE 正規化のみ担当し、DB 既定値の時刻帯は変えない。`ON UPDATE` が必要な列は `.$onUpdateFn(() => jstOnUpdateNow(6))` で DDL 意図を維持する。
131
+ `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))`.
132
+
133
+ ### Business time — `@rdlabo/workers-hono-kit/business-time`
134
+
135
+ String-level JST business-time conversions (Workers UTC instant ↔ business calendar date / date-time),
136
+ with **no `mysql2` / `drizzle-orm` dependency**. This is a different layer from the `./db` column helpers
137
+ (which handle the MySQL wire format): the DB stays on JST, and the app handles JST explicitly through
138
+ this module instead of relying implicitly on the connection `timezone`.
139
+
140
+ | Export | Description |
141
+ | --- | --- |
142
+ | `today(ref?)` | The JST business calendar date (`YYYY-MM-DD`) of `ref` (defaults to now). |
143
+ | `toBusinessDate(instant)` | UTC instant → JST business calendar date (`YYYY-MM-DD`). |
144
+ | `normalizeBusinessDate(value)` | Normalize a `string` / `Date` / nullish to `YYYY-MM-DD`; a `YYYY-MM-DD` string passes through unchanged, nullish/empty/invalid → `null`. |
145
+ | `toBusinessDateTime(instant)` | UTC instant → JST business date-time (`YYYY-MM-DD HH:mm:ss`). |
146
+ | `parseBusinessDateTime(value)` | JST business date-time string → UTC instant (accepts a space or `T` separator). |
147
+ | `formatBusinessDateTime(instant, pattern?)` | Format an instant in the business TZ (Nest `helper.formatDate`-compatible tokens). |
148
+ | `startOfBusinessDay(date)` / `endOfBusinessDay(date)` | UTC instant of `00:00:00` / `23:59:59` on a JST business date. |
149
+ | `businessDateTimeInstant(date, time)` | JST business date + wall-clock time → UTC instant. |
150
+ | `addBusinessDays(date, days)` | Add calendar days to a JST business date. |
151
+ | `ageOnBusinessDate(birthDate, asOfDate?)` | Full years of age on a business date (`asOfDate` defaults to `today()`). |
152
+ | `DEFAULT_BUSINESS_DATETIME_PATTERN` | Default `formatBusinessDateTime` pattern (`YYYY-MM-DDThh:mm:ss`). |
153
+ | `BUSINESS_TIMEZONE` / `BusinessDate` / `BusinessDateTime` | JST timezone constant and the business-date / date-time string types. |
154
+
155
+ ```ts
156
+ import {
157
+ toBusinessDate,
158
+ toBusinessDateTime,
159
+ formatBusinessDateTime,
160
+ addBusinessDays,
161
+ } from '@rdlabo/workers-hono-kit/business-time';
162
+
163
+ const now = new Date('2026-07-05T21:00:00Z');
164
+ toBusinessDate(now); // '2026-07-06' (JST)
165
+ toBusinessDateTime(now); // '2026-07-06 06:00:00'
166
+ formatBusinessDateTime(now); // '2026-07-06T06:00:00'
167
+ addBusinessDays('2026-07-06', 3); // '2026-07-09'
168
+ ```
130
169
 
131
170
  ### Testing — `@rdlabo/workers-hono-kit/testing`
132
171
 
@@ -143,6 +182,7 @@ Requires the `drizzle-orm` and `mysql2` peers. Consolidates duplicated test boil
143
182
  | `provisionUser(pool, firebase, opts)` | Register a token and provision a conventional `users(id, firebase_uid, agree)` row; returns the user id (idempotent). |
144
183
  | `configurableFake(impl, name?)` | Build a test double from a partial implementation; un-stubbed members throw `"${name}.${method} not configured"`. |
145
184
  | `fakeApiList` / `fakePaymentIntent` / `fakeStripeEvent` / `fakeCheckoutSession` / `fakeCustomer` / `fakePrice` / `fakeSubscription` | Stripe object fixtures with sensible defaults, overridable per test. |
185
+ | `fakeKv()` / `fakeQueue()` / `FakeQueue` | In-memory Workers KV / Queues producer doubles (`sent` + `batchCount` on queues for subrequest-bound assertions). |
146
186
 
147
187
  ## Usage
148
188
 
@@ -290,6 +330,47 @@ const tokenAuth = createAuthMiddleware<AppEnv, UserRecord>({
290
330
  });
291
331
  ```
292
332
 
333
+ ### Latency instrumentation (`perfLog`)
334
+
335
+ Records one data point per request — `t_app` (time inside the app), `colo`, `cold`/`warm`, matched
336
+ route, `status` — and ships it to **Workers Logs** and/or **Workers Analytics Engine**. This lets you
337
+ measure a low-traffic Worker after the fact (retained + queryable) instead of watching a live
338
+ `wrangler tail`. Register it first so it wraps everything.
339
+
340
+ ```ts
341
+ import { perfLog } from '@rdlabo/workers-hono-kit';
342
+
343
+ // A) app served with env (`app.fetch(req, env, ctx)`): bare — reads `PERF` (Analytics Engine
344
+ // dataset binding) and `PERF_LOG === '1'` (Workers Logs) off `c.env`.
345
+ app.use('*', perfLog());
346
+
347
+ // B) app built without Hono env (`createApp(container).fetch(req)`): pass bindings explicitly.
348
+ app.use('*', perfLog({ console: env.PERF_LOG === '1', dataset: env.PERF }));
349
+ ```
350
+
351
+ ```toml
352
+ # wrangler.toml — dataset is created on first write (no provisioning); needs [observability] for Logs.
353
+ [[analytics_engine_datasets]]
354
+ binding = "PERF"
355
+ dataset = "myapp_perf"
356
+ ```
357
+
358
+ Query percentiles by route/colo with the Analytics Engine SQL API:
359
+
360
+ ```sql
361
+ SELECT blob1 AS path, blob2 AS colo,
362
+ quantileWeighted(0.5)(double1, _sample_interval) AS p50,
363
+ quantileWeighted(0.9)(double1, _sample_interval) AS p90
364
+ FROM myapp_perf WHERE timestamp > now() - INTERVAL '7' DAY
365
+ GROUP BY path, colo ORDER BY p90 DESC
366
+ ```
367
+
368
+ > **Scope of `t_app`**: it covers everything *inside* the app; work done in `fetch` *before* the app
369
+ > (e.g. secrets fetch / DB connect in `createApp(container)` vs. building the container in `fetch`) is
370
+ > not comparable across differently-wired apps. Instrument the `fetch` seam if you need a secrets/connect
371
+ > cold breakdown. On production Workers `Date.now()` only advances at I/O boundaries, so `t_app` ≈ I/O
372
+ > wait, not CPU time.
373
+
293
374
  ### AI Gateway
294
375
 
295
376
  Route `@ai-sdk` models through the Cloudflare AI Gateway — either with a Workers `AI` binding
@@ -362,7 +443,7 @@ await testDb.resetSchema();
362
443
  const pool = testDb.createTestPool();
363
444
 
364
445
  const firebase = new FakeFirebaseVerifier();
365
- firebase.register('uid-1', { email: 'a@example.com' });
446
+ firebase.register('token-1', { uid: 'uid-1', email: 'a@example.com' });
366
447
 
367
448
  const gateway = configurableFake<PaymentGateway>({ charge: async () => ({ ok: true }) }, 'PaymentGateway');
368
449
  ```
@@ -386,6 +467,16 @@ If you consume this package via a local path (e.g. `"@rdlabo/workers-hono-kit":
386
467
 
387
468
  When installed from npm normally, package managers dedupe `zod` to a single copy and this is not needed.
388
469
 
470
+ ## CLI
471
+
472
+ The package ships three `bin` commands (run via `npx` or an npm script in the consuming app):
473
+
474
+ | Command | Use |
475
+ | --- | --- |
476
+ | `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`. |
477
+ | `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. |
478
+ | `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. |
479
+
389
480
  ## Development
390
481
 
391
482
  ```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,
@@ -1,10 +1,12 @@
1
1
  import type { DecimalNumberConfig } from './decimal.js';
2
2
  /**
3
- * `ON UPDATE CURRENT_TIMESTAMP` 用式(MySQL セッション時刻)。
4
- * customType 列は `.onUpdateNow()` が無いため `.$onUpdateFn(() => jstOnUpdateNow(fsp))` と併用する。
3
+ * SQL expression for `ON UPDATE CURRENT_TIMESTAMP` (the MySQL session clock).
4
+ * customType columns have no `.onUpdateNow()`, so pair this with `.$onUpdateFn(() => jstOnUpdateNow(fsp))`.
5
+ *
6
+ * @param fsp - optional fractional-seconds precision; when provided, emits `CURRENT_TIMESTAMP(fsp)`.
5
7
  */
6
8
  export declare const jstOnUpdateNow: (fsp?: number) => import("drizzle-orm").SQL<unknown>;
7
- /** MySQL `timestamp` — pass-throughDate は接続 `timezone:'+09:00'` で mysql2 が JST 整形。 */
9
+ /** MySQL `timestamp` — pass-through. A `Date` is formatted as JST by mysql2 via the connection `timezone:'+09:00'`. */
8
10
  export declare const jstTimestamp: (name: string, opts?: {
9
11
  fsp?: number;
10
12
  }) => import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
@@ -15,7 +17,7 @@ export declare const jstTimestamp: (name: string, opts?: {
15
17
  driverParam: string | Date;
16
18
  enumValues: undefined;
17
19
  }>;
18
- /** MySQL `datetime` — {@link jstTimestamp} と同じ pass-through 方針。 */
20
+ /** MySQL `datetime` — same pass-through policy as {@link jstTimestamp}. */
19
21
  export declare const jstDatetime: (name: string, opts?: {
20
22
  fsp?: number;
21
23
  }) => import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
@@ -26,7 +28,7 @@ export declare const jstDatetime: (name: string, opts?: {
26
28
  driverParam: string | Date;
27
29
  enumValues: undefined;
28
30
  }>;
29
- /** MySQL `date` — INSERT/UPDATE 時に ISO / 空文字を `YYYY-MM-DD` へ正規化(`toDriver`)。 */
31
+ /** MySQL `date` — on INSERT/UPDATE, normalizes ISO / empty strings to `YYYY-MM-DD` (via `toDriver`). */
30
32
  export declare const jstDate: (name: string) => import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
31
33
  name: string;
32
34
  dataType: "custom";
@@ -35,7 +37,7 @@ export declare const jstDate: (name: string) => import("drizzle-orm/mysql-core")
35
37
  driverParam: string | null;
36
38
  enumValues: undefined;
37
39
  }>;
38
- /** MySQL `decimal` — SELECT `fromDriver` string→number、書込は number をそのまま bind。 */
40
+ /** MySQL `decimal` — SELECT coerces string→number via `fromDriver`; writes bind the number as-is. */
39
41
  export declare const decimalNumber: (name: string, config: DecimalNumberConfig) => import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
40
42
  name: string;
41
43
  dataType: "custom";