@rdlabo/workers-hono-kit 0.3.7 → 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 (52) hide show
  1. package/README.md +33 -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.js +2 -1
  9. package/dist/db/decimal.d.ts +27 -0
  10. package/dist/db/decimal.js +50 -0
  11. package/dist/db/index.d.ts +4 -1
  12. package/dist/db/index.js +3 -1
  13. package/dist/db/jst.d.ts +10 -72
  14. package/dist/db/jst.js +10 -82
  15. package/package.json +7 -3
  16. package/src/ai/gateway.ts +0 -120
  17. package/src/aws/cloudfront.ts +0 -105
  18. package/src/aws/secrets-manager.ts +0 -112
  19. package/src/cache/kv-cache.ts +0 -316
  20. package/src/db/connection.ts +0 -107
  21. package/src/db/database.ts +0 -269
  22. package/src/db/index.ts +0 -39
  23. package/src/db/jst.ts +0 -122
  24. package/src/db/migrate.ts +0 -155
  25. package/src/db/orm-config.ts +0 -171
  26. package/src/db/retry.ts +0 -43
  27. package/src/db/write-result.ts +0 -46
  28. package/src/firebase/firebase-verifier.ts +0 -76
  29. package/src/firebase/identity-toolkit.ts +0 -179
  30. package/src/firebase/jose-firebase-verifier.ts +0 -159
  31. package/src/firebase/remote-verifier.ts +0 -98
  32. package/src/http/app-env.ts +0 -53
  33. package/src/http/app-info.ts +0 -38
  34. package/src/http/execution-context.ts +0 -11
  35. package/src/http/http-status.ts +0 -71
  36. package/src/http/nest-error.ts +0 -207
  37. package/src/http/trailing-slash.ts +0 -28
  38. package/src/http/user-protocol.ts +0 -36
  39. package/src/index.ts +0 -77
  40. package/src/middleware/auth.ts +0 -129
  41. package/src/middleware/finalize-response.ts +0 -90
  42. package/src/middleware/validation.ts +0 -158
  43. package/src/middleware/zod-coerce.ts +0 -124
  44. package/src/queue/consumer.ts +0 -146
  45. package/src/queue/send.ts +0 -129
  46. package/src/stripe/client.ts +0 -85
  47. package/src/testing/auth.ts +0 -110
  48. package/src/testing/configurable-fake.ts +0 -45
  49. package/src/testing/db.ts +0 -194
  50. package/src/testing/fakes.ts +0 -153
  51. package/src/testing/index.ts +0 -31
  52. 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
@@ -91,11 +92,42 @@ Requires the `drizzle-orm` and `mysql2` peers. Reads run against a replica via r
91
92
  | `withMysqlConnections(...)` | Open primary/replica connections, run a function, close them in `finally` (via `ctx.waitUntil`). |
92
93
  | `retryWhenDeadlock(fn, retries?, delay?)` | Same deadlock-retry helper as the root export. |
93
94
  | `insertIdOf` / `affectedRowsOf` / `insertedIdsOf` / `DzWriteResult` | Extract `insertId` / `affectedRows` (and derive contiguous bulk-insert ids) from a mysql2 write result. |
94
- | `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` 列ヘルパーで十分)。 |
95
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. |
96
100
  | `resolveDbSecret(options, secretId?)` / `ResolvedDbSecret` | Resolve RDS-managed or plain DB credentials from AWS Secrets Manager for CI migrate / local tooling. |
97
101
  | `baselineMigrations(options)` / `readBaselineEntry(migrationsFolder)` / `BaselineMigrationsOptions` / `BaselineResult` / `BaselineEntry` | Brownfield first-deploy helper: mark an existing `0000_*` migration as applied without re-running DDL. |
98
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 意図を維持する。
130
+
99
131
  ### Testing — `@rdlabo/workers-hono-kit/testing`
100
132
 
101
133
  Requires the `drizzle-orm` and `mysql2` peers. Consolidates duplicated test boilerplate.
@@ -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,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;