@rdlabo/workers-hono-kit 0.3.6 → 0.4.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.
Files changed (55) hide show
  1. package/README.md +40 -1
  2. package/dist/business-time/index.d.ts +49 -0
  3. package/dist/business-time/index.js +149 -0
  4. package/dist/business-time/types.d.ts +9 -0
  5. package/dist/business-time/types.js +5 -0
  6. package/dist/db/columns.d.ts +46 -0
  7. package/dist/db/columns.js +36 -0
  8. package/dist/db/connection.d.ts +2 -10
  9. package/dist/db/connection.js +2 -1
  10. package/dist/db/decimal.d.ts +27 -0
  11. package/dist/db/decimal.js +50 -0
  12. package/dist/db/index.d.ts +4 -1
  13. package/dist/db/index.js +3 -1
  14. package/dist/db/jst.d.ts +10 -72
  15. package/dist/db/jst.js +10 -82
  16. package/dist/http/execution-context.d.ts +11 -0
  17. package/dist/http/execution-context.js +1 -0
  18. package/dist/index.d.ts +1 -0
  19. package/package.json +7 -3
  20. package/src/ai/gateway.ts +0 -120
  21. package/src/aws/cloudfront.ts +0 -105
  22. package/src/aws/secrets-manager.ts +0 -112
  23. package/src/cache/kv-cache.ts +0 -316
  24. package/src/db/connection.ts +0 -115
  25. package/src/db/database.ts +0 -269
  26. package/src/db/index.ts +0 -39
  27. package/src/db/jst.ts +0 -122
  28. package/src/db/migrate.ts +0 -155
  29. package/src/db/orm-config.ts +0 -171
  30. package/src/db/retry.ts +0 -43
  31. package/src/db/write-result.ts +0 -46
  32. package/src/firebase/firebase-verifier.ts +0 -76
  33. package/src/firebase/identity-toolkit.ts +0 -179
  34. package/src/firebase/jose-firebase-verifier.ts +0 -159
  35. package/src/firebase/remote-verifier.ts +0 -98
  36. package/src/http/app-env.ts +0 -53
  37. package/src/http/app-info.ts +0 -38
  38. package/src/http/http-status.ts +0 -71
  39. package/src/http/nest-error.ts +0 -207
  40. package/src/http/trailing-slash.ts +0 -28
  41. package/src/http/user-protocol.ts +0 -36
  42. package/src/index.ts +0 -76
  43. package/src/middleware/auth.ts +0 -129
  44. package/src/middleware/finalize-response.ts +0 -90
  45. package/src/middleware/validation.ts +0 -158
  46. package/src/middleware/zod-coerce.ts +0 -124
  47. package/src/queue/consumer.ts +0 -146
  48. package/src/queue/send.ts +0 -129
  49. package/src/stripe/client.ts +0 -85
  50. package/src/testing/auth.ts +0 -110
  51. package/src/testing/configurable-fake.ts +0 -45
  52. package/src/testing/db.ts +0 -194
  53. package/src/testing/fakes.ts +0 -153
  54. package/src/testing/index.ts +0 -31
  55. package/src/testing/stripe-fixtures.ts +0 -175
package/README.md CHANGED
@@ -41,6 +41,7 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
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
45
  > | `./testing` | `@rdlabo/workers-hono-kit/testing` | Test helpers (mysql2 + Drizzle + fakes/fixtures). |
45
46
 
46
47
  ## API
@@ -66,12 +67,16 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
66
67
  | `HttpStatus` | HTTP status enum identical to NestJS `@nestjs/common`. |
67
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
69
  | `nestNotFoundHandler(c)` | `app.notFound()` handler with the Express/Nest default `{ message: 'Cannot METHOD path', error, statusCode }` 404 body. |
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. |
69
71
  | `NEST_REASON_PHRASES` | `{ 400, 401, 403, 404 }` → NestJS reason phrases. |
70
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. |
71
73
  | `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createNestErrorHandler`'s `onUnhandledError`. |
72
74
  | `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`). |
73
75
  | `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. |
74
76
  | `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
+ | `processBatch(batch, handler, options?)` / `MessageBatchLike` / `QueueMessageLike` / `ProcessBatchOptions` / `ProcessBatchResult` | Process a queue batch with bounded concurrency (consumer-side counterpart to `sendInChunks`). |
79
+ | `ExecutionContextLike` | Minimal `waitUntil`-only Workers execution context shape (for `withMysqlConnections` in worker entry modules without importing `./db`). |
75
80
 
76
81
  ### Data layer — `@rdlabo/workers-hono-kit/db`
77
82
 
@@ -81,13 +86,47 @@ Requires the `drizzle-orm` and `mysql2` peers. Reads run against a replica via r
81
86
  | --- | --- |
82
87
  | `createHyperdriveDatabase(options)` | `DisposableDatabase` that lazily opens primary/replica connections from Hyperdrive bindings per request; `dispose()` closes them. |
83
88
  | `createMysqlDatabase(options)` | Assemble a `Database` from an already-connected Drizzle ORM + replica `QueryRunner`. |
89
+ | `databaseFrom(orm, replica)` | Build a `Database` from an existing Drizzle instance + replica handle. |
84
90
  | `Database` / `DisposableDatabase` / `QueryRunner` / `TxOf` | The `read` / `write` / `transaction` API and its supporting types. |
85
91
  | `hyperdriveConnectionOptions(hyperdrive, overrides?)` / `HyperdriveLike` / `ExecutionContextLike` | Build mysql2 `createConnection` options from a Hyperdrive binding (`disableEval`, `decimalNumbers`, `timezone '+09:00'` by default). |
86
92
  | `withMysqlConnections(...)` | Open primary/replica connections, run a function, close them in `finally` (via `ctx.waitUntil`). |
87
93
  | `retryWhenDeadlock(fn, retries?, delay?)` | Same deadlock-retry helper as the root export. |
88
94
  | `insertIdOf` / `affectedRowsOf` / `insertedIdsOf` / `DzWriteResult` | Extract `insertId` / `affectedRows` (and derive contiguous bulk-insert ids) from a mysql2 write result. |
89
- | `toJstDate` / `jstTimestampParams` / `jstDatetimeParams` / `jstDateParams` | JST date/time normalization applied at the Drizzle `customType` column boundary. |
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` 列ヘルパーで十分)。 |
90
99
  | `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. |
101
+ | `baselineMigrations(options)` / `readBaselineEntry(migrationsFolder)` / `BaselineMigrationsOptions` / `BaselineResult` / `BaselineEntry` | Brownfield first-deploy helper: mark an existing `0000_*` migration as applied without re-running DDL. |
102
+
103
+ #### Drizzle 列ヘルパー(`jstTimestamp` / `decimalNumber` 等)
104
+
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 コピーへ固定**する:
110
+
111
+ ```jsonc
112
+ // tsconfig.json compilerOptions(既存 paths があればマージ)
113
+ "paths": {
114
+ "drizzle-orm": ["./node_modules/drizzle-orm"],
115
+ "drizzle-orm/*": ["./node_modules/drizzle-orm/*"]
116
+ }
117
+ ```
118
+
119
+ `moduleResolution: "Bundler"` なら `baseUrl` 不要(`baseUrl` 設定済みなら先頭 `./` は外す)。published 版(単一コピー)ではこの `paths` は無害。**overrides は不要。**
120
+ - `file:` で kit を直リンクする開発では、kit リポジトリ側で `npm install` して peer を満たす(consumer 側で overrides を足さない)。
121
+
122
+ **`CURRENT_TIMESTAMP` と接続 `timezone:'+09:00'` の違い**
123
+
124
+ | 経路 | 誰が時刻を決めるか | JST との関係 |
125
+ | --- | --- | --- |
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
+
129
+ `jstTimestamp` / `jstDatetime` は読書の pass-through と DATE 正規化のみ担当し、DB 既定値の時刻帯は変えない。`ON UPDATE` が必要な列は `.$onUpdateFn(() => jstOnUpdateNow(6))` で DDL 意図を維持する。
91
130
 
92
131
  ### Testing — `@rdlabo/workers-hono-kit/testing`
93
132
 
@@ -0,0 +1,49 @@
1
+ /**
2
+ * JST 業務時刻の明示 API(Workers UTC instant ↔ 業務暦日/日時)。
3
+ *
4
+ * @remarks
5
+ * - DB は JST 運用のまま。アプリは mysql2 `timezone` に暗黙依存せず、ここ経由で JST を扱う。
6
+ * - MySQL ワイヤ形式への変換は {@link ../db/jst.js | db/jst} の責務。
7
+ * - `Date` の local getter(`getHours` 等)は業務判定に使わない。
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ import { BUSINESS_TIMEZONE } from './types.js';
12
+ import type { BusinessDate, BusinessDateTime } from './types.js';
13
+ export { BUSINESS_TIMEZONE, type BusinessDate, type BusinessDateTime };
14
+ /** 参照 instant の JST 業務暦日。 */
15
+ export declare function today(ref?: Date): BusinessDate;
16
+ /** UTC instant → JST 業務暦日。 */
17
+ export declare function toBusinessDate(instant: Date): BusinessDate;
18
+ /**
19
+ * クライアント / DB 入力を JST 業務暦日 `YYYY-MM-DD` へ正規化する。
20
+ *
21
+ * - 既に `YYYY-MM-DD` の文字列は **Date 化せず**そのまま返す(誕生日は instant ではない)。
22
+ * - ISO 8601 等は instant 経由で JST 暦日へ変換。
23
+ * - nullish / 空 / 不正は `null`。
24
+ */
25
+ export declare function normalizeBusinessDate(value: string | Date | null | undefined): BusinessDate | null;
26
+ /** UTC instant → JST 業務日時(`YYYY-MM-DD HH:mm:ss`)。 */
27
+ export declare function toBusinessDateTime(instant: Date): BusinessDateTime;
28
+ /** Nest / foodlabel / winecode `helper.formatDate` 既定パターン。 */
29
+ export declare const DEFAULT_BUSINESS_DATETIME_PATTERN: "YYYY-MM-DDThh:mm:ss";
30
+ /**
31
+ * Nest `helper.formatDate` 互換のパターン整形(業務 TZ)。
32
+ * `S` トークンは元 instant のミリ秒(Nest 正本)。
33
+ */
34
+ export declare function formatBusinessDateTime(instant: Date, pattern?: string): string;
35
+ /** JST 業務日時文字列 → UTC instant。`YYYY-MM-DD HH:mm:ss` / `T` 区切りを受け付ける。 */
36
+ export declare function parseBusinessDateTime(value: BusinessDateTime): Date;
37
+ /** JST 業務暦日の 00:00:00 を表す UTC instant。 */
38
+ export declare function startOfBusinessDay(date: BusinessDate): Date;
39
+ /** JST 業務暦日の 23:59:59 を表す UTC instant。 */
40
+ export declare function endOfBusinessDay(date: BusinessDate): Date;
41
+ /**
42
+ * JST 業務暦日 + 壁時計時刻 → UTC instant。
43
+ * @example businessDateTimeInstant('2026-07-05', '06:00:00')
44
+ */
45
+ export declare function businessDateTimeInstant(date: BusinessDate, time: string): Date;
46
+ /** JST 業務暦日に日数を加算(暦日単位)。 */
47
+ export declare function addBusinessDays(date: BusinessDate, days: number): BusinessDate;
48
+ /** 業務暦日基準の満年齢(誕生日は instant ではなく BusinessDate)。 */
49
+ export declare function ageOnBusinessDate(birthDate: BusinessDate, asOfDate?: BusinessDate): number;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * JST 業務時刻の明示 API(Workers UTC instant ↔ 業務暦日/日時)。
3
+ *
4
+ * @remarks
5
+ * - DB は JST 運用のまま。アプリは mysql2 `timezone` に暗黙依存せず、ここ経由で JST を扱う。
6
+ * - MySQL ワイヤ形式への変換は {@link ../db/jst.js | db/jst} の責務。
7
+ * - `Date` の local getter(`getHours` 等)は業務判定に使わない。
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ import { BUSINESS_TIMEZONE } from './types.js';
12
+ export { BUSINESS_TIMEZONE };
13
+ const pad2 = (n) => String(n).padStart(2, '0');
14
+ /** instant を業務 TZ 壁時計として読むためのシフト(`getUTC*` で成分を得る)。 */
15
+ function toWallClock(instant) {
16
+ return new Date(instant.getTime() + BUSINESS_TIMEZONE.offsetMinutes * 60_000);
17
+ }
18
+ function parseYmd(date) {
19
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
20
+ if (!m) {
21
+ throw new RangeError(`Invalid BusinessDate: ${date}`);
22
+ }
23
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
24
+ }
25
+ function parseHms(time) {
26
+ const m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec(time);
27
+ if (!m) {
28
+ throw new RangeError(`Invalid business time: ${time}`);
29
+ }
30
+ return [Number(m[1]), Number(m[2]), Number(m[3] || 0)];
31
+ }
32
+ /** 参照 instant の JST 業務暦日。 */
33
+ export function today(ref = new Date()) {
34
+ return toBusinessDate(ref);
35
+ }
36
+ /** UTC instant → JST 業務暦日。 */
37
+ export function toBusinessDate(instant) {
38
+ const wall = toWallClock(instant);
39
+ return `${wall.getUTCFullYear()}-${pad2(wall.getUTCMonth() + 1)}-${pad2(wall.getUTCDate())}`;
40
+ }
41
+ /**
42
+ * クライアント / DB 入力を JST 業務暦日 `YYYY-MM-DD` へ正規化する。
43
+ *
44
+ * - 既に `YYYY-MM-DD` の文字列は **Date 化せず**そのまま返す(誕生日は instant ではない)。
45
+ * - ISO 8601 等は instant 経由で JST 暦日へ変換。
46
+ * - nullish / 空 / 不正は `null`。
47
+ */
48
+ export function normalizeBusinessDate(value) {
49
+ if (value == null) {
50
+ return null;
51
+ }
52
+ if (value instanceof Date) {
53
+ if (Number.isNaN(value.getTime())) {
54
+ return null;
55
+ }
56
+ return toBusinessDate(value);
57
+ }
58
+ const trimmed = value.trim();
59
+ if (!trimmed) {
60
+ return null;
61
+ }
62
+ if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
63
+ return trimmed;
64
+ }
65
+ const isoDatePrefix = /^(\d{4}-\d{2}-\d{2})/.exec(trimmed);
66
+ if (isoDatePrefix && !trimmed.includes('T') && !trimmed.includes(' ')) {
67
+ return isoDatePrefix[1];
68
+ }
69
+ const ms = new Date(trimmed).getTime();
70
+ if (Number.isNaN(ms)) {
71
+ return null;
72
+ }
73
+ return toBusinessDate(new Date(ms));
74
+ }
75
+ /** UTC instant → JST 業務日時(`YYYY-MM-DD HH:mm:ss`)。 */
76
+ export function toBusinessDateTime(instant) {
77
+ const wall = toWallClock(instant);
78
+ return `${wall.getUTCFullYear()}-${pad2(wall.getUTCMonth() + 1)}-${pad2(wall.getUTCDate())} ${pad2(wall.getUTCHours())}:${pad2(wall.getUTCMinutes())}:${pad2(wall.getUTCSeconds())}`;
79
+ }
80
+ /** Nest / foodlabel / winecode `helper.formatDate` 既定パターン。 */
81
+ export const DEFAULT_BUSINESS_DATETIME_PATTERN = 'YYYY-MM-DDThh:mm:ss';
82
+ /**
83
+ * Nest `helper.formatDate` 互換のパターン整形(業務 TZ)。
84
+ * `S` トークンは元 instant のミリ秒(Nest 正本)。
85
+ */
86
+ export function formatBusinessDateTime(instant, pattern = DEFAULT_BUSINESS_DATETIME_PATTERN) {
87
+ const wall = toWallClock(instant);
88
+ let out = pattern;
89
+ out = out.replace(/YYYY/g, String(wall.getUTCFullYear()));
90
+ out = out.replace(/MM/g, pad2(wall.getUTCMonth() + 1));
91
+ out = out.replace(/DD/g, pad2(wall.getUTCDate()));
92
+ out = out.replace(/hh/g, pad2(wall.getUTCHours()));
93
+ out = out.replace(/mm/g, pad2(wall.getUTCMinutes()));
94
+ out = out.replace(/ss/g, pad2(wall.getUTCSeconds()));
95
+ const matched = out.match(/S/g);
96
+ if (matched) {
97
+ const milliSeconds = String(instant.getMilliseconds()).padStart(3, '0');
98
+ const length = matched.length;
99
+ for (let i = 0; i < length; i++) {
100
+ out = out.replace(/S/, milliSeconds.substring(i, i + 1));
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+ /** JST 業務日時文字列 → UTC instant。`YYYY-MM-DD HH:mm:ss` / `T` 区切りを受け付ける。 */
106
+ export function parseBusinessDateTime(value) {
107
+ const normalized = value.includes('T') ? value.replace('T', ' ') : value;
108
+ const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/.exec(normalized);
109
+ if (!m) {
110
+ throw new RangeError(`Invalid BusinessDateTime: ${value}`);
111
+ }
112
+ const [, y, mo, d, h, mi, s] = m;
113
+ const offsetHours = BUSINESS_TIMEZONE.offsetMinutes / 60;
114
+ return new Date(Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h) - offsetHours, Number(mi), Number(s), 0));
115
+ }
116
+ /** JST 業務暦日の 00:00:00 を表す UTC instant。 */
117
+ export function startOfBusinessDay(date) {
118
+ return businessDateTimeInstant(date, '00:00:00');
119
+ }
120
+ /** JST 業務暦日の 23:59:59 を表す UTC instant。 */
121
+ export function endOfBusinessDay(date) {
122
+ return businessDateTimeInstant(date, '23:59:59');
123
+ }
124
+ /**
125
+ * JST 業務暦日 + 壁時計時刻 → UTC instant。
126
+ * @example businessDateTimeInstant('2026-07-05', '06:00:00')
127
+ */
128
+ export function businessDateTimeInstant(date, time) {
129
+ const [y, mo, d] = parseYmd(date);
130
+ const [h, mi, s] = parseHms(time);
131
+ const offsetHours = BUSINESS_TIMEZONE.offsetMinutes / 60;
132
+ return new Date(Date.UTC(y, mo - 1, d, h - offsetHours, mi, s, 0));
133
+ }
134
+ /** JST 業務暦日に日数を加算(暦日単位)。 */
135
+ export function addBusinessDays(date, days) {
136
+ const anchor = businessDateTimeInstant(date, '12:00:00');
137
+ return toBusinessDate(new Date(anchor.getTime() + days * 24 * 60 * 60 * 1000));
138
+ }
139
+ /** 業務暦日基準の満年齢(誕生日は instant ではなく BusinessDate)。 */
140
+ export function ageOnBusinessDate(birthDate, asOfDate) {
141
+ const asOf = asOfDate ?? today();
142
+ const [by, bm, bd] = parseYmd(birthDate);
143
+ const [ay, am, ad] = parseYmd(asOf);
144
+ let age = ay - by;
145
+ if (am < bm || (am === bm && ad < bd)) {
146
+ age--;
147
+ }
148
+ return age;
149
+ }
@@ -0,0 +1,9 @@
1
+ /** JST 業務暦日 `YYYY-MM-DD`(instant ではない)。 */
2
+ export type BusinessDate = string;
3
+ /** JST 業務日時 `YYYY-MM-DD HH:mm:ss`(MySQL DATETIME 互換の壁時計表現)。 */
4
+ export type BusinessDateTime = string;
5
+ /** JST 業務タイムゾーン定数(Workers は UTC instant、業務はここで明示)。 */
6
+ export declare const BUSINESS_TIMEZONE: {
7
+ readonly iana: "Asia/Tokyo";
8
+ readonly offsetMinutes: 540;
9
+ };
@@ -0,0 +1,5 @@
1
+ /** JST 業務タイムゾーン定数(Workers は UTC instant、業務はここで明示)。 */
2
+ export const BUSINESS_TIMEZONE = {
3
+ iana: 'Asia/Tokyo',
4
+ offsetMinutes: 540,
5
+ };
@@ -0,0 +1,46 @@
1
+ import type { DecimalNumberConfig } from './decimal.js';
2
+ /**
3
+ * `ON UPDATE CURRENT_TIMESTAMP` 用式(MySQL セッション時刻)。
4
+ * customType 列は `.onUpdateNow()` が無いため `.$onUpdateFn(() => jstOnUpdateNow(fsp))` と併用する。
5
+ */
6
+ export declare const jstOnUpdateNow: (fsp?: number) => import("drizzle-orm").SQL<unknown>;
7
+ /** MySQL `timestamp` — pass-through。Date は接続 `timezone:'+09:00'` で mysql2 が JST 整形。 */
8
+ export declare const jstTimestamp: (name: string, opts?: {
9
+ fsp?: number;
10
+ }) => import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
11
+ name: string;
12
+ dataType: "custom";
13
+ columnType: "MySqlCustomColumn";
14
+ data: string | Date;
15
+ driverParam: string | Date;
16
+ enumValues: undefined;
17
+ }>;
18
+ /** MySQL `datetime` — {@link jstTimestamp} と同じ pass-through 方針。 */
19
+ export declare const jstDatetime: (name: string, opts?: {
20
+ fsp?: number;
21
+ }) => import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
22
+ name: string;
23
+ dataType: "custom";
24
+ columnType: "MySqlCustomColumn";
25
+ data: string | Date;
26
+ driverParam: string | Date;
27
+ enumValues: undefined;
28
+ }>;
29
+ /** MySQL `date` — INSERT/UPDATE 時に ISO / 空文字を `YYYY-MM-DD` へ正規化(`toDriver`)。 */
30
+ export declare const jstDate: (name: string) => import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
31
+ name: string;
32
+ dataType: "custom";
33
+ columnType: "MySqlCustomColumn";
34
+ data: string | null;
35
+ driverParam: string | null;
36
+ enumValues: undefined;
37
+ }>;
38
+ /** MySQL `decimal` — SELECT は `fromDriver` で string→number、書込は number をそのまま bind。 */
39
+ export declare const decimalNumber: (name: string, config: DecimalNumberConfig) => import("drizzle-orm/mysql-core").MySqlCustomColumnBuilder<{
40
+ name: string;
41
+ dataType: "custom";
42
+ columnType: "MySqlCustomColumn";
43
+ data: number | null;
44
+ driverParam: string | number | null;
45
+ enumValues: undefined;
46
+ }>;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Drizzle 列ヘルパー(フリート共通)。各 repo の `custom-types.ts` / `columns.ts` 薄いラッパーは不要。
3
+ *
4
+ * @remarks
5
+ * `drizzle-orm` は **peer**(consumer が 1 本解決)。kit は `drizzle-orm` を bundled しない。
6
+ * 戻り型は `customType` 推論そのまま(`MySqlCustomColumnBuilder<…>`)で `any` を使わない。
7
+ * これにより consumer テーブルの `$inferSelect` に列の意味型(`string | Date` / `number | null` など)が伝播する。
8
+ *
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 版(単一コピー)ではそのまま単一。
15
+ *
16
+ * **DEFAULT / ON UPDATE CURRENT_TIMESTAMP** … MySQL サーバ側の既定値(列を省略した INSERT / UPDATE)。
17
+ * 接続 `timezone:'+09:00'`({@link hyperdriveConnectionOptions})が効くのは **アプリから Date を bind するとき**。
18
+ * 両者を混同しないこと(`datetime-wire` / `drizzle-smoke` の JST テスト参照)。
19
+ */
20
+ import { sql } from 'drizzle-orm';
21
+ import { customType } from 'drizzle-orm/mysql-core';
22
+ import { decimalNumberParams } from './decimal.js';
23
+ import { jstDateParams, jstDatetimeParams, jstTimestampParams } from './jst.js';
24
+ /**
25
+ * `ON UPDATE CURRENT_TIMESTAMP` 用式(MySQL セッション時刻)。
26
+ * customType 列は `.onUpdateNow()` が無いため `.$onUpdateFn(() => jstOnUpdateNow(fsp))` と併用する。
27
+ */
28
+ export const jstOnUpdateNow = (fsp) => fsp != null ? sql `(CURRENT_TIMESTAMP(${sql.raw(String(fsp))}))` : sql `(CURRENT_TIMESTAMP)`;
29
+ /** MySQL `timestamp` — pass-through。Date は接続 `timezone:'+09:00'` で mysql2 が JST 整形。 */
30
+ export const jstTimestamp = (name, opts) => customType(jstTimestampParams(opts?.fsp))(name);
31
+ /** MySQL `datetime` — {@link jstTimestamp} と同じ pass-through 方針。 */
32
+ export const jstDatetime = (name, opts) => customType(jstDatetimeParams(opts?.fsp))(name);
33
+ /** MySQL `date` — INSERT/UPDATE 時に ISO / 空文字を `YYYY-MM-DD` へ正規化(`toDriver`)。 */
34
+ export const jstDate = (name) => customType(jstDateParams())(name);
35
+ /** MySQL `decimal` — SELECT は `fromDriver` で string→number、書込は number をそのまま bind。 */
36
+ export const decimalNumber = (name, config) => customType(decimalNumberParams(config))(name);
@@ -1,4 +1,6 @@
1
1
  import type { Connection } from 'mysql2/promise';
2
+ import type { ExecutionContextLike } from '../http/execution-context.js';
3
+ export type { ExecutionContextLike } from '../http/execution-context.js';
2
4
  /**
3
5
  * Minimal structural shape of a Cloudflare Hyperdrive binding.
4
6
  *
@@ -40,16 +42,6 @@ export interface HyperdriveLike {
40
42
  * @returns a plain options object to pass to mysql2 `createConnection`.
41
43
  */
42
44
  export declare function hyperdriveConnectionOptions(hyperdrive: HyperdriveLike, extra?: Record<string, unknown>): Record<string, unknown>;
43
- /**
44
- * Minimal structural shape of a Workers `ExecutionContext`, limited to `waitUntil`.
45
- *
46
- * @remarks
47
- * Declared structurally to avoid a dependency on `@cloudflare/workers-types`.
48
- */
49
- export interface ExecutionContextLike {
50
- /** Extend the request's lifetime until `promise` settles (used to close connections after the response). */
51
- waitUntil(promise: Promise<unknown>): void;
52
- }
53
45
  /**
54
46
  * Open primary and replica connections, run `fn` with them, and close both afterwards.
55
47
  *
@@ -1,4 +1,5 @@
1
1
  import { createConnection } from 'mysql2/promise';
2
+ import { MYSQL_TIMEZONE } from './jst.js';
2
3
  /**
3
4
  * Build mysql2 `createConnection` options from a Hyperdrive binding, applying the kit's defaults.
4
5
  *
@@ -29,7 +30,7 @@ export function hyperdriveConnectionOptions(hyperdrive, extra) {
29
30
  port: hyperdrive.port,
30
31
  disableEval: true,
31
32
  decimalNumbers: true,
32
- timezone: '+09:00',
33
+ timezone: MYSQL_TIMEZONE,
33
34
  ...extra,
34
35
  };
35
36
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * MySQL `DECIMAL` 列向け Drizzle `customType` params。
3
+ *
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` 経路向け。
10
+ */
11
+ export interface DecimalNumberConfig {
12
+ precision: number;
13
+ scale: number;
14
+ }
15
+ /**
16
+ * mysql2 / Drizzle から届いた DECIMAL 値を JS `number | null` へ正規化する。
17
+ * `0` は falsy 落ちしないようそのまま保持する。
18
+ */
19
+ export declare function coerceDecimalNumber(value: unknown): number | null;
20
+ /**
21
+ * `customType` 用 params。高度な用途向け。通常は {@link decimalNumber} 列ヘルパーを使う。
22
+ */
23
+ export declare const decimalNumberParams: (config: DecimalNumberConfig) => {
24
+ dataType: () => string;
25
+ fromDriver: (value: unknown) => number | null;
26
+ toDriver: (value: number | string | null) => number | string | null;
27
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * MySQL `DECIMAL` 列向け Drizzle `customType` params。
3
+ *
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` 経路向け。
10
+ */
11
+ /**
12
+ * mysql2 / Drizzle から届いた DECIMAL 値を JS `number | null` へ正規化する。
13
+ * `0` は falsy 落ちしないようそのまま保持する。
14
+ */
15
+ export function coerceDecimalNumber(value) {
16
+ if (value === null || value === undefined) {
17
+ return null;
18
+ }
19
+ if (typeof value === 'number') {
20
+ return Number.isFinite(value) ? value : null;
21
+ }
22
+ if (typeof value === 'string') {
23
+ const trimmed = value.trim();
24
+ if (trimmed === '') {
25
+ return null;
26
+ }
27
+ const n = Number(trimmed);
28
+ return Number.isFinite(n) ? n : null;
29
+ }
30
+ if (typeof value === 'bigint') {
31
+ return Number(value);
32
+ }
33
+ return null;
34
+ }
35
+ /**
36
+ * `customType` 用 params。高度な用途向け。通常は {@link decimalNumber} 列ヘルパーを使う。
37
+ */
38
+ export const decimalNumberParams = (config) => ({
39
+ dataType: () => `decimal(${config.precision},${config.scale})`,
40
+ fromDriver: (value) => coerceDecimalNumber(value),
41
+ toDriver: (value) => {
42
+ if (value === null) {
43
+ return null;
44
+ }
45
+ if (typeof value === 'number') {
46
+ return value;
47
+ }
48
+ return coerceDecimalNumber(value);
49
+ },
50
+ });
@@ -16,7 +16,10 @@ export { insertIdOf, affectedRowsOf, insertedIdsOf } from './write-result.js';
16
16
  export type { DzWriteResult } from './write-result.js';
17
17
  export { hyperdriveConnectionOptions, withMysqlConnections } from './connection.js';
18
18
  export type { HyperdriveLike, ExecutionContextLike } from './connection.js';
19
- export { toJstDate, jstTimestampParams, jstDatetimeParams, jstDateParams } from './jst.js';
19
+ export { MYSQL_TIMEZONE, toJstDate, jstTimestampParams, jstDatetimeParams, jstDateParams } from './jst.js';
20
+ export { coerceDecimalNumber, decimalNumberParams } from './decimal.js';
21
+ export type { DecimalNumberConfig } from './decimal.js';
22
+ export { jstTimestamp, jstDatetime, jstDate, decimalNumber, jstOnUpdateNow } from './columns.js';
20
23
  export { DRIZZLE_ORM_OPTIONS, honoDrizzleConfig, resolveDbSecret } from './orm-config.js';
21
24
  export type { HonoDrizzleConfigOptions, ResolvedDbSecret } from './orm-config.js';
22
25
  export { baselineMigrations, readBaselineEntry } from './migrate.js';
package/dist/db/index.js CHANGED
@@ -13,6 +13,8 @@ export { retryWhenDeadlock } from './retry.js';
13
13
  export { createMysqlDatabase, createHyperdriveDatabase, databaseFrom } from './database.js';
14
14
  export { insertIdOf, affectedRowsOf, insertedIdsOf } from './write-result.js';
15
15
  export { hyperdriveConnectionOptions, withMysqlConnections } from './connection.js';
16
- export { toJstDate, jstTimestampParams, jstDatetimeParams, jstDateParams } from './jst.js';
16
+ export { MYSQL_TIMEZONE, toJstDate, jstTimestampParams, jstDatetimeParams, jstDateParams } from './jst.js';
17
+ export { coerceDecimalNumber, decimalNumberParams } from './decimal.js';
18
+ export { jstTimestamp, jstDatetime, jstDate, decimalNumber, jstOnUpdateNow } from './columns.js';
17
19
  export { DRIZZLE_ORM_OPTIONS, honoDrizzleConfig, resolveDbSecret } from './orm-config.js';
18
20
  export { baselineMigrations, readBaselineEntry } from './migrate.js';
package/dist/db/jst.d.ts CHANGED
@@ -1,60 +1,22 @@
1
1
  /**
2
- * Shared building blocks for normalizing JST (Asia/Tokyo) date/time values.
2
+ * MySQL / Drizzle 向け JST ワイヤ変換と DATE 列正規化。
3
3
  *
4
4
  * @remarks
5
- * JST normalization is applied at the column boundary as a write-time side effect, and neither
6
- * values nor types from `drizzle-orm` are imported here on purpose. Exporting a fully-built
7
- * `customType` column from the kit would cause type collisions when the kit and the consumer
8
- * resolve separate copies of `drizzle-orm` (the private `SQL` brand stops being nominally
9
- * compatible). Instead the kit ships only the params and helpers, and the consumer builds the
10
- * column with its own `customType`:
11
- *
12
- * ```ts
13
- * import { customType } from 'drizzle-orm/mysql-core';
14
- * import { jstTimestampParams, jstDateParams } from '@rdlabo/workers-hono-kit/db';
15
- *
16
- * export const jstTimestamp = (name: string, opts?: { fsp?: number }) =>
17
- * customType<{ data: string | Date; driverData: string | Date }>(jstTimestampParams(opts?.fsp))(name);
18
- * export const jstDate = (name: string) =>
19
- * customType<{ data: string | null; driverData: string | null }>(jstDateParams())(name);
20
- * ```
21
- *
22
- * `timestamp`/`datetime` columns omit `toDriver` and pass `Date` values straight through, so the
23
- * connection's `timezone: '+09:00'` default makes mysql2 format them as JST; pre-formatted strings
24
- * also pass through. Drizzle's native `mode: 'date'` is avoided because it stringifies `Date` to
25
- * UTC before the timezone layer, shifting values by -9h. `date` columns keep `toJstDate` because
26
- * MySQL `DATE` rejects ISO/empty strings and a JST day-boundary normalization is required.
5
+ * 業務時刻の意味論は {@link ../business-time/index.js | business-time} に集約する。
6
+ * このモジュールは「MySQL 接続既定」「DATE 列の toDriver」、列 `customType` params のみを担う。
27
7
  */
8
+ import type { BusinessDate } from '../business-time/index.js';
9
+ /** mysql2 接続 `timezone` 既定(既存 JST DB 運用)。 */
10
+ export declare const MYSQL_TIMEZONE = "+09:00";
28
11
  /**
29
- * Normalize a client-supplied date to the `YYYY-MM-DD` (JST) form accepted by a MySQL `DATE` column.
30
- *
31
- * Accepts ISO 8601 (`...Z`), `YYYY-MM-DD`, or an empty string. Nullish, empty, or unparseable input
32
- * resolves to `null`.
33
- *
34
- * @remarks
35
- * MySQL `DATE` rejects ISO strings with `ER_TRUNCATED_WRONG_VALUE`, so this cannot be handled by the
36
- * driver alone; it is needed as the `toDriver` transform for a `date` column.
37
- *
38
- * @param value - the raw date string from the client (ISO 8601, `YYYY-MM-DD`, or empty), or nullish.
39
- * @returns the JST calendar date as `YYYY-MM-DD`, or `null` when the input is empty or unparseable.
12
+ * クライアント入力を MySQL `DATE` 列向け `YYYY-MM-DD`(JST 業務暦日)へ正規化。
13
+ * ISO 8601 / `YYYY-MM-DD` / 空文字を受け付ける。`YYYY-MM-DD` は Date 化せずそのまま渡す。
40
14
  */
41
- export declare function toJstDate(value: string | null | undefined): string | null;
15
+ export declare function toJstDate(value: string | null | undefined): BusinessDate | null;
42
16
  /**
43
17
  * Build the params for a `customType` backing a MySQL `timestamp` column with `Date` pass-through.
44
18
  *
45
- * The column omits `toDriver`, so `Date` values flow straight to mysql2 and are formatted as JST by
46
- * the connection's `timezone: '+09:00'` default.
47
- *
48
19
  * @param fsp - optional fractional-seconds precision; when provided, emits `timestamp(fsp)`.
49
- * @returns the `customType` params object exposing the column's `dataType`.
50
- * @example
51
- * ```ts
52
- * import { customType } from 'drizzle-orm/mysql-core';
53
- * import { jstTimestampParams } from '@rdlabo/workers-hono-kit/db';
54
- *
55
- * const jstTimestamp = (name: string) =>
56
- * customType<{ data: string | Date; driverData: string | Date }>(jstTimestampParams())(name);
57
- * ```
58
20
  */
59
21
  export declare const jstTimestampParams: (fsp?: number) => {
60
22
  dataType: () => string;
@@ -62,19 +24,7 @@ export declare const jstTimestampParams: (fsp?: number) => {
62
24
  /**
63
25
  * Build the params for a `customType` backing a MySQL `datetime` column with `Date` pass-through.
64
26
  *
65
- * Behaves like {@link jstTimestampParams} but emits a `datetime` data type; `Date` values pass
66
- * through and are formatted as JST by the connection's `timezone: '+09:00'` default.
67
- *
68
27
  * @param fsp - optional fractional-seconds precision; when provided, emits `datetime(fsp)`.
69
- * @returns the `customType` params object exposing the column's `dataType`.
70
- * @example
71
- * ```ts
72
- * import { customType } from 'drizzle-orm/mysql-core';
73
- * import { jstDatetimeParams } from '@rdlabo/workers-hono-kit/db';
74
- *
75
- * const jstDatetime = (name: string) =>
76
- * customType<{ data: string | Date; driverData: string | Date }>(jstDatetimeParams())(name);
77
- * ```
78
28
  */
79
29
  export declare const jstDatetimeParams: (fsp?: number) => {
80
30
  dataType: () => string;
@@ -82,19 +32,7 @@ export declare const jstDatetimeParams: (fsp?: number) => {
82
32
  /**
83
33
  * Build the params for a `customType` backing a MySQL `date` column with JST normalization.
84
34
  *
85
- * Unlike the timestamp/datetime params, this defines a `toDriver` transform that runs
86
- * {@link toJstDate} so client-supplied ISO/empty strings are normalized to a JST `YYYY-MM-DD` value
87
- * the column accepts.
88
- *
89
- * @returns the `customType` params object exposing the column's `dataType` and `toDriver`.
90
- * @example
91
- * ```ts
92
- * import { customType } from 'drizzle-orm/mysql-core';
93
- * import { jstDateParams } from '@rdlabo/workers-hono-kit/db';
94
- *
95
- * const jstDate = (name: string) =>
96
- * customType<{ data: string | null; driverData: string | null }>(jstDateParams())(name);
97
- * ```
35
+ * @returns params with `toDriver` running {@link toJstDate}.
98
36
  */
99
37
  export declare const jstDateParams: () => {
100
38
  dataType: () => string;