Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
805 changes: 33 additions & 772 deletions README.md

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions docs/api-business-time.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# API: `@rdlabo/workers-hono-kit/business-time`

String-level JST business-time conversions (Workers UTC instant ↔ business calendar date / date-time), with **no `mysql2` / `drizzle-orm` dependency**. This is a different layer from the `./db` column helpers (which handle the MySQL wire format): the DB stays on JST, and the app handles JST explicitly through this module instead of relying implicitly on the connection `timezone`.

| Export | Description |
| --- | --- |
| `today(ref?)` | The JST business calendar date (`YYYY-MM-DD`) of `ref` (defaults to now). |
| `toBusinessDate(instant)` | UTC instant → JST business calendar date (`YYYY-MM-DD`). |
| `normalizeBusinessDate(value)` | Normalize a `string` / `Date` / nullish to `YYYY-MM-DD`; a `YYYY-MM-DD` string passes through unchanged, nullish/empty/invalid → `null`. |
| `toBusinessDateTime(instant)` | UTC instant → JST business date-time (`YYYY-MM-DD HH:mm:ss`). |
| `parseBusinessDateTime(value)` | JST business date-time string → UTC instant (accepts a space or `T` separator). |
| `formatBusinessDateTime(instant, pattern?)` | Format an instant in the business TZ (Nest `helper.formatDate`-compatible tokens). |
| `startOfBusinessDay(date)` / `endOfBusinessDay(date)` | UTC instant of `00:00:00` / `23:59:59` on a JST business date. |
| `businessDateTimeInstant(date, time)` | JST business date + wall-clock time → UTC instant. |
| `addBusinessDays(date, days)` | Add calendar days to a JST business date. |
| `ageOnBusinessDate(birthDate, asOfDate?)` | Full years of age on a business date (`asOfDate` defaults to `today()`). |
| `DEFAULT_BUSINESS_DATETIME_PATTERN` | Default `formatBusinessDateTime` pattern (`YYYY-MM-DDThh:mm:ss`). |
| `BUSINESS_TIMEZONE` / `BusinessDate` / `BusinessDateTime` | JST timezone constant and the business-date / date-time string types. |

```ts
import {
toBusinessDate,
toBusinessDateTime,
formatBusinessDateTime,
addBusinessDays,
} from '@rdlabo/workers-hono-kit/business-time';

const now = new Date('2026-07-05T21:00:00Z');
toBusinessDate(now); // '2026-07-06' (JST)
toBusinessDateTime(now); // '2026-07-06 06:00:00'
formatBusinessDateTime(now); // '2026-07-06T06:00:00'
addBusinessDays('2026-07-06', 3); // '2026-07-09'
```
49 changes: 49 additions & 0 deletions docs/api-db.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# API: `@rdlabo/workers-hono-kit/db`

Requires the `drizzle-orm` and `mysql2` peers. Reads run against a replica via raw SQL; writes/transactions run against the primary through the Drizzle ORM with deadlock retry. The kit deliberately does not depend on the ORM's type identity — you pass the Drizzle instance in.

| Export | Description |
| --- | --- |
| `createHyperdriveDatabase(options)` | `DisposableDatabase` that lazily opens primary/replica connections from Hyperdrive bindings per request. Workers cleans them up at invocation end; the legacy `dispose()` is a no-op. |
| `createMysqlDatabase(options)` | Assemble a `Database` from an already-connected Drizzle ORM + replica `QueryRunner`. |
| `databaseFrom(orm, replica)` | Build a `Database` from an existing Drizzle instance + replica handle. |
| `Database` / `DisposableDatabase` / `QueryRunner` / `TxOf` | The `read` / `write` / `transaction` API and its supporting types. |
| `hyperdriveConnectionOptions(hyperdrive, overrides?)` / `HyperdriveLike` / `ExecutionContextLike` | Build mysql2 `createConnection` options from a Hyperdrive binding (`disableEval`, `decimalNumbers`, `timezone '+09:00'` by default). `timezone` controls mysql2's JavaScript `Date` conversion; it does not change the MySQL session timezone. |
| `withMysqlConnections(...)` | Open primary/replica connections in parallel and run a function. Workers cleans them up at invocation end. |
| `retryWhenDeadlock(fn, retries?, delay?)` | Same deadlock-retry helper as the root export. |
| `insertIdOf` / `affectedRowsOf` / `insertedIdsOf` / `DzWriteResult` | Extract `insertId` / `affectedRows` (and derive contiguous bulk-insert ids) from a mysql2 write result. |
| `toJstDate` / `jstTimestampParams` / `jstDatetimeParams` / `jstDateParams` | JST date/time normalization params (advanced use). |
| `MYSQL_TIMEZONE` | Default mysql2 connection `timezone` (`'+09:00'`) for the JST DB deployment. |
| `jstTimestamp` / `jstDatetime` / `jstDate` | Drizzle column helpers (no repo-side wrapper needed). |
| `jstOnUpdateNow` | SQL expression for `ON UPDATE CURRENT_TIMESTAMP`. The `jstTimestamp` customType (and friends) do not support `.onUpdateNow()`, so pair it with `.$onUpdateFn(() => jstOnUpdateNow(fsp))`. |
| `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. |
| `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. |
| `baselineMigrations(options)` / `readBaselineEntry(migrationsFolder)` / `BaselineMigrationsOptions` / `BaselineResult` / `BaselineEntry` | Brownfield first-deploy helper: mark an existing `0000_*` migration as applied without re-running DDL. |

## Drizzle column helpers (`jstTimestamp`, etc.)

- `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).
- The consumer just keeps `drizzle-orm` in its `dependencies` as usual. **No `overrides` in `package.json` are needed.**
- 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).
- 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`.
- **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`**:

```jsonc
// tsconfig.json compilerOptions (merge with existing paths if any)
"paths": {
"drizzle-orm": ["./node_modules/drizzle-orm"],
"drizzle-orm/*": ["./node_modules/drizzle-orm/*"]
}
```

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.**
- 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).

## `CURRENT_TIMESTAMP` vs the connection `timezone:'+09:00'`

| Path | Who decides the time | Relationship to JST |
| --- | --- | --- |
| The app binds a `Date` (INSERT/UPDATE) | mysql2 + connection `timezone:'+09:00'` | Treated as JST on the wire (`datetime-wire` test) |
| `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 |

`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))`.
67 changes: 67 additions & 0 deletions docs/api-offline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# API: `@rdlabo/workers-hono-kit/offline`

Table-agnostic building blocks for product-owned REST ↔ DB method converters and their offline replica wire values. This subpath does not define table projections, Zod object shapes, public-column allowlists, schema hashes, or domain rules; those remain in each Hono application.

This is an additive subpath: existing root and subpath exports are unchanged. Consumers can migrate converter internals independently without changing REST payloads, schema hashes, or persisted SQLite rows. For an `AUTO_INCREMENT` table, omit `id` from a create method's table scheme; keep the client-generated UUID in `local_id` and keep `server_id` null until the server confirms its id.

| Export | Description |
| --- | --- |
| `defineRestDbMethodConverter(converter)` | Type a product-owned, pure `MethodScheme ↔ TableScheme` converter without hiding HTTP or persistence side effects. |
| `RestDbMethodConverter` | Product-owned converter contract. Select and insert bundles may differ; every represented table and column remains required. |
| `CompleteRestDbTableScheme` | Compile-time lock requiring every represented table key and row column. |
| `toReplicaIsoDatetime(value)` | `Date` / datetime string → canonical UTC ISO-8601 wire value. |
| `toReplicaDateOnly(value)` | `Date` / date string / `null` → canonical `YYYY-MM-DD` / `null`. |
| `replicaTimestampMs(value)` | Replica datetime → epoch milliseconds for legacy DTOs. |
| `toTinyIntFlag(value)` / `fromTinyIntFlag(value)` | Boolean-like value ↔ numeric tinyint flag. |
| `replicaNowIso(clock?)` | Injectable wall clock → canonical UTC ISO-8601 wire value. |

```ts
import {
defineRestDbMethodConverter,
replicaNowIso,
toReplicaIsoDatetime,
} from '@rdlabo/workers-hono-kit/offline';

type Tables = {
foods: FoodRow[];
allergens: AllergenRow[];
};

export const foodMethodConverter = defineRestDbMethodConverter<FoodMethodScheme, Tables>({
toMethodScheme: ({ foods, allergens }) => ({
...foods[0],
allergens: allergens.map(({ value }) => value),
}),
toTableScheme: (method) => ({
foods: [{ id: method.id, memo: method.memo ?? null }],
allergens: method.allergens.map((value) => ({ threadId: method.id, value })),
}),
});
```

`toTableScheme` requires every key represented by its DB row types. This includes nullable/default columns that Drizzle marks optional in `$inferInsert`; write `memo: method.memo ?? null` instead of omitting `memo`. If a REST method intentionally does not own an `AUTO_INCREMENT` column, remove it from that method's product-owned table scheme explicitly:

```ts
type CreateTables = {
foods: Omit<typeof foods.$inferInsert, 'id'>[];
};
```

The converter then cannot demand or manufacture `id`; the server adds the generated id to the confirmed response before it is stored as `server_id`.

When a write needs authenticated ownership or scope that is intentionally absent from the public REST body, use separate select/insert bundles and an explicit write context. The original two-generic form remains valid.

```ts
defineRestDbMethodConverter<Method, SelectTables, InsertTables, { userId: number }>({
toMethodScheme: ({ foods, allergens }) => composeFood(foods, allergens),
toTableScheme: (method, { userId }) => ({
foods: [{ userId, name: method.name, memo: method.memo ?? null }],
allergens: method.allergens.map((value) => ({ value })),
}),
});
```

```ts
replicaNowIso(() => new Date('2026-07-23T10:00:00Z')); // '2026-07-23T10:00:00.000Z'
toReplicaIsoDatetime('2026-07-23T19:00:00+09:00'); // '2026-07-23T10:00:00.000Z'
```
Loading
Loading