@rdlabo/workers-hono-kit 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -25
- package/dist/business-time/index.d.ts +98 -21
- package/dist/business-time/index.js +99 -22
- package/dist/business-time/types.d.ts +3 -3
- package/dist/business-time/types.js +1 -1
- package/dist/db/columns.d.ts +8 -6
- package/dist/db/columns.js +25 -19
- package/dist/db/decimal.d.ts +17 -9
- package/dist/db/decimal.js +17 -9
- package/dist/db/jst.d.ts +11 -6
- package/dist/db/jst.js +11 -6
- package/dist/db/migrate.d.ts +21 -21
- package/dist/db/migrate.js +34 -29
- package/dist/db/orm-config.d.ts +10 -6
- package/dist/db/orm-config.js +15 -11
- package/dist/http/mysql-driver-error.d.ts +25 -0
- package/dist/http/mysql-driver-error.js +35 -0
- package/dist/http/nest-error.js +7 -1
- package/dist/http/query-failed-error.d.ts +29 -0
- package/dist/http/query-failed-error.js +43 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/middleware/perf-log.d.ts +77 -0
- package/dist/middleware/perf-log.js +97 -0
- package/package.json +1 -1
- package/scripts/db-baseline.mjs +10 -9
package/dist/db/columns.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { DecimalNumberConfig } from './decimal.js';
|
|
2
2
|
/**
|
|
3
|
-
* `ON UPDATE CURRENT_TIMESTAMP`
|
|
4
|
-
* customType
|
|
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-through
|
|
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}
|
|
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
|
|
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
|
|
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";
|
package/dist/db/columns.js
CHANGED
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Drizzle
|
|
2
|
+
* Shared Drizzle column helpers. Removes the need for a thin `custom-types.ts` / `columns.ts` wrapper
|
|
3
|
+
* in each repo.
|
|
3
4
|
*
|
|
4
5
|
* @remarks
|
|
5
|
-
* `drizzle-orm`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* `drizzle-orm` is a **peer** (the consumer resolves a single copy); the kit does not bundle it. The
|
|
7
|
+
* return types are the `customType` inference as-is (`MySqlCustomColumnBuilder<…>`) with no `any`, so
|
|
8
|
+
* the column's semantic type (`string | Date`, `number | null`, etc.) propagates to the consumer
|
|
9
|
+
* table's `$inferSelect`.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `jstTimestamp(…).default(sql\`…\`)`
|
|
12
|
-
* 'shouldInlineParams'
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* **Precondition (a single drizzle copy)**: Drizzle's `SQL` is a **nominal** type carrying a private
|
|
12
|
+
* field `shouldInlineParams`, so if the kit and the consumer resolve different copies of drizzle,
|
|
13
|
+
* `jstTimestamp(…).default(sql\`…\`)` fails with `TS2345: separate declarations of a private property
|
|
14
|
+
* 'shouldInlineParams'`. The fleet references the kit via a `file:` link, which tends to nest a second
|
|
15
|
+
* copy of drizzle under the kit. Pin `drizzle-orm` to the consumer's **own single copy** with tsconfig
|
|
16
|
+
* `paths` (see the "Drizzle column helpers" section of the README). The published package (a single
|
|
17
|
+
* copy) is already unified.
|
|
15
18
|
*
|
|
16
|
-
* **DEFAULT / ON UPDATE CURRENT_TIMESTAMP**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* **DEFAULT / ON UPDATE CURRENT_TIMESTAMP** is a server-side default (an INSERT / UPDATE that omits the
|
|
20
|
+
* column). The connection's `timezone:'+09:00'` ({@link hyperdriveConnectionOptions}) only applies when
|
|
21
|
+
* the **app binds a `Date`**. Do not conflate the two (see the `datetime-wire` / `drizzle-smoke` JST
|
|
22
|
+
* tests).
|
|
19
23
|
*/
|
|
20
24
|
import { sql } from 'drizzle-orm';
|
|
21
25
|
import { customType } from 'drizzle-orm/mysql-core';
|
|
22
26
|
import { decimalNumberParams } from './decimal.js';
|
|
23
27
|
import { jstDateParams, jstDatetimeParams, jstTimestampParams } from './jst.js';
|
|
24
28
|
/**
|
|
25
|
-
* `ON UPDATE CURRENT_TIMESTAMP`
|
|
26
|
-
* customType
|
|
29
|
+
* SQL expression for `ON UPDATE CURRENT_TIMESTAMP` (the MySQL session clock).
|
|
30
|
+
* customType columns have no `.onUpdateNow()`, so pair this with `.$onUpdateFn(() => jstOnUpdateNow(fsp))`.
|
|
31
|
+
*
|
|
32
|
+
* @param fsp - optional fractional-seconds precision; when provided, emits `CURRENT_TIMESTAMP(fsp)`.
|
|
27
33
|
*/
|
|
28
34
|
export const jstOnUpdateNow = (fsp) => fsp != null ? sql `(CURRENT_TIMESTAMP(${sql.raw(String(fsp))}))` : sql `(CURRENT_TIMESTAMP)`;
|
|
29
|
-
/** MySQL `timestamp` — pass-through
|
|
35
|
+
/** MySQL `timestamp` — pass-through. A `Date` is formatted as JST by mysql2 via the connection `timezone:'+09:00'`. */
|
|
30
36
|
export const jstTimestamp = (name, opts) => customType(jstTimestampParams(opts?.fsp))(name);
|
|
31
|
-
/** MySQL `datetime` — {@link jstTimestamp}
|
|
37
|
+
/** MySQL `datetime` — same pass-through policy as {@link jstTimestamp}. */
|
|
32
38
|
export const jstDatetime = (name, opts) => customType(jstDatetimeParams(opts?.fsp))(name);
|
|
33
|
-
/** MySQL `date` — INSERT/UPDATE
|
|
39
|
+
/** MySQL `date` — on INSERT/UPDATE, normalizes ISO / empty strings to `YYYY-MM-DD` (via `toDriver`). */
|
|
34
40
|
export const jstDate = (name) => customType(jstDateParams())(name);
|
|
35
|
-
/** MySQL `decimal` — SELECT
|
|
41
|
+
/** MySQL `decimal` — SELECT coerces string→number via `fromDriver`; writes bind the number as-is. */
|
|
36
42
|
export const decimalNumber = (name, config) => customType(decimalNumberParams(config))(name);
|
package/dist/db/decimal.d.ts
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Drizzle `customType` params for a MySQL `DECIMAL` column.
|
|
3
3
|
*
|
|
4
4
|
* @remarks
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
5
|
+
* - **Reads (SELECT)**: `fromDriver` unifies the driver value (`number` / `string` / `null`) to a JS
|
|
6
|
+
* `number | null`. Combined with the connection's `decimalNumbers: true`
|
|
7
|
+
* ({@link hyperdriveConnectionOptions} default), it aligns values to numbers even on the Drizzle
|
|
8
|
+
* builder path when strings like `"0"` / `"100.00"` slip in, without dropping `0`.
|
|
9
|
+
* - **Writes (INSERT/UPDATE)**: `toDriver` binds the number to mysql2 as-is (no `String()` conversion).
|
|
10
|
+
* - Raw-SQL `db.read` relies on the connection's `decimalNumbers: true`; the column's `fromDriver` is
|
|
11
|
+
* for the Drizzle `select` path.
|
|
10
12
|
*/
|
|
11
13
|
export interface DecimalNumberConfig {
|
|
12
14
|
precision: number;
|
|
13
15
|
scale: number;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
|
-
* mysql2 / Drizzle
|
|
17
|
-
* `0`
|
|
18
|
+
* Normalize a DECIMAL value coming from mysql2 / Drizzle to a JS `number | null`.
|
|
19
|
+
* `0` is preserved as-is so it is not dropped as falsy.
|
|
20
|
+
*
|
|
21
|
+
* @param value - the raw driver value (`number` / `string` / `bigint` / nullish).
|
|
22
|
+
* @returns the coerced finite number, or `null` when it cannot be resolved.
|
|
18
23
|
*/
|
|
19
24
|
export declare function coerceDecimalNumber(value: unknown): number | null;
|
|
20
25
|
/**
|
|
21
|
-
* `customType
|
|
26
|
+
* Params for a `customType`. For advanced use; the {@link decimalNumber} column helper is usually enough.
|
|
27
|
+
*
|
|
28
|
+
* @param config - the DECIMAL `precision` / `scale`.
|
|
29
|
+
* @returns the `customType` params (`dataType` / `fromDriver` / `toDriver`).
|
|
22
30
|
*/
|
|
23
31
|
export declare const decimalNumberParams: (config: DecimalNumberConfig) => {
|
|
24
32
|
dataType: () => string;
|
package/dist/db/decimal.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Drizzle `customType` params for a MySQL `DECIMAL` column.
|
|
3
3
|
*
|
|
4
4
|
* @remarks
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
5
|
+
* - **Reads (SELECT)**: `fromDriver` unifies the driver value (`number` / `string` / `null`) to a JS
|
|
6
|
+
* `number | null`. Combined with the connection's `decimalNumbers: true`
|
|
7
|
+
* ({@link hyperdriveConnectionOptions} default), it aligns values to numbers even on the Drizzle
|
|
8
|
+
* builder path when strings like `"0"` / `"100.00"` slip in, without dropping `0`.
|
|
9
|
+
* - **Writes (INSERT/UPDATE)**: `toDriver` binds the number to mysql2 as-is (no `String()` conversion).
|
|
10
|
+
* - Raw-SQL `db.read` relies on the connection's `decimalNumbers: true`; the column's `fromDriver` is
|
|
11
|
+
* for the Drizzle `select` path.
|
|
10
12
|
*/
|
|
11
13
|
/**
|
|
12
|
-
* mysql2 / Drizzle
|
|
13
|
-
* `0`
|
|
14
|
+
* Normalize a DECIMAL value coming from mysql2 / Drizzle to a JS `number | null`.
|
|
15
|
+
* `0` is preserved as-is so it is not dropped as falsy.
|
|
16
|
+
*
|
|
17
|
+
* @param value - the raw driver value (`number` / `string` / `bigint` / nullish).
|
|
18
|
+
* @returns the coerced finite number, or `null` when it cannot be resolved.
|
|
14
19
|
*/
|
|
15
20
|
export function coerceDecimalNumber(value) {
|
|
16
21
|
if (value === null || value === undefined) {
|
|
@@ -33,7 +38,10 @@ export function coerceDecimalNumber(value) {
|
|
|
33
38
|
return null;
|
|
34
39
|
}
|
|
35
40
|
/**
|
|
36
|
-
* `customType
|
|
41
|
+
* Params for a `customType`. For advanced use; the {@link decimalNumber} column helper is usually enough.
|
|
42
|
+
*
|
|
43
|
+
* @param config - the DECIMAL `precision` / `scale`.
|
|
44
|
+
* @returns the `customType` params (`dataType` / `fromDriver` / `toDriver`).
|
|
37
45
|
*/
|
|
38
46
|
export const decimalNumberParams = (config) => ({
|
|
39
47
|
dataType: () => `decimal(${config.precision},${config.scale})`,
|
package/dist/db/jst.d.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* JST wire conversion and DATE-column normalization for MySQL / Drizzle.
|
|
3
3
|
*
|
|
4
4
|
* @remarks
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Business-time semantics are consolidated in {@link ../business-time/index.js | business-time}. This
|
|
6
|
+
* module only owns the MySQL connection default, the DATE column's `toDriver`, and the column
|
|
7
|
+
* `customType` params.
|
|
7
8
|
*/
|
|
8
9
|
import type { BusinessDate } from '../business-time/index.js';
|
|
9
|
-
/** mysql2
|
|
10
|
+
/** Default mysql2 connection `timezone` (for the existing JST DB deployment). */
|
|
10
11
|
export declare const MYSQL_TIMEZONE = "+09:00";
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
* ISO 8601 / `YYYY-MM-DD` /
|
|
13
|
+
* Normalize a client input to `YYYY-MM-DD` (a JST business calendar date) for a MySQL `DATE` column.
|
|
14
|
+
* Accepts ISO 8601 / `YYYY-MM-DD` / empty strings. A `YYYY-MM-DD` value is passed through without
|
|
15
|
+
* constructing a `Date`.
|
|
16
|
+
*
|
|
17
|
+
* @param value - the string or nullish input to normalize.
|
|
18
|
+
* @returns the business date as `YYYY-MM-DD`, or `null` when the input cannot be resolved.
|
|
14
19
|
*/
|
|
15
20
|
export declare function toJstDate(value: string | null | undefined): BusinessDate | null;
|
|
16
21
|
/**
|
package/dist/db/jst.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* JST wire conversion and DATE-column normalization for MySQL / Drizzle.
|
|
3
3
|
*
|
|
4
4
|
* @remarks
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Business-time semantics are consolidated in {@link ../business-time/index.js | business-time}. This
|
|
6
|
+
* module only owns the MySQL connection default, the DATE column's `toDriver`, and the column
|
|
7
|
+
* `customType` params.
|
|
7
8
|
*/
|
|
8
9
|
import { normalizeBusinessDate } from '../business-time/index.js';
|
|
9
|
-
/** mysql2
|
|
10
|
+
/** Default mysql2 connection `timezone` (for the existing JST DB deployment). */
|
|
10
11
|
export const MYSQL_TIMEZONE = '+09:00';
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
* ISO 8601 / `YYYY-MM-DD` /
|
|
13
|
+
* Normalize a client input to `YYYY-MM-DD` (a JST business calendar date) for a MySQL `DATE` column.
|
|
14
|
+
* Accepts ISO 8601 / `YYYY-MM-DD` / empty strings. A `YYYY-MM-DD` value is passed through without
|
|
15
|
+
* constructing a `Date`.
|
|
16
|
+
*
|
|
17
|
+
* @param value - the string or nullish input to normalize.
|
|
18
|
+
* @returns the business date as `YYYY-MM-DD`, or `null` when the input cannot be resolved.
|
|
14
19
|
*/
|
|
15
20
|
export function toJstDate(value) {
|
|
16
21
|
return normalizeBusinessDate(value ?? null);
|
package/dist/db/migrate.d.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import type { QueryRunner } from './database.js';
|
|
2
|
-
/** baseline
|
|
2
|
+
/** Identifying info for the baseline (i.e. first) migration. */
|
|
3
3
|
export interface BaselineEntry {
|
|
4
|
-
/**
|
|
4
|
+
/** The migration tag (e.g. `0000_melted_weapon_omega`). */
|
|
5
5
|
tag: string;
|
|
6
|
-
/** `_journal.json`
|
|
6
|
+
/** The `when` from `_journal.json` (= drizzle's `created_at` / `folderMillis`). */
|
|
7
7
|
when: number;
|
|
8
|
-
/** `<tag>.sql`
|
|
8
|
+
/** The sha256 of the raw `<tag>.sql` contents (the same algorithm as drizzle). */
|
|
9
9
|
hash: string;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* `migrationsFolder
|
|
12
|
+
* Read the baseline (first) entry from `migrationsFolder` (drizzle's `out`, e.g. `./drizzle`).
|
|
13
13
|
*
|
|
14
|
-
* @param migrationsFolder - `meta/_journal.json`
|
|
15
|
-
* @returns baseline
|
|
16
|
-
* @throws journal
|
|
14
|
+
* @param migrationsFolder - the folder containing `meta/_journal.json` and `<tag>.sql`.
|
|
15
|
+
* @returns the baseline entry (tag/when/hash).
|
|
16
|
+
* @throws Error when the journal is missing, the entries are empty, or `<tag>.sql` is missing.
|
|
17
17
|
*/
|
|
18
18
|
export declare function readBaselineEntry(migrationsFolder: string): BaselineEntry;
|
|
19
|
-
/** {@link baselineMigrations}
|
|
19
|
+
/** Options for {@link baselineMigrations}. */
|
|
20
20
|
export interface BaselineMigrationsOptions {
|
|
21
|
-
/**
|
|
21
|
+
/** A QueryRunner for raw SQL (a mysql2 `Connection`/`Pool` is assignable). Must already be connected to the target DB. */
|
|
22
22
|
db: QueryRunner;
|
|
23
|
-
/**
|
|
23
|
+
/** Drizzle's `out` folder (defaults to `./drizzle`). */
|
|
24
24
|
migrationsFolder?: string;
|
|
25
25
|
}
|
|
26
|
-
/** {@link baselineMigrations}
|
|
26
|
+
/** The result of {@link baselineMigrations}. */
|
|
27
27
|
export type BaselineResult = {
|
|
28
28
|
status: 'inserted';
|
|
29
29
|
tag: string;
|
|
@@ -35,17 +35,17 @@ export type BaselineResult = {
|
|
|
35
35
|
when: number;
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* Record the baseline (0000) as "applied" on an existing DB. Idempotent, with safety guards.
|
|
39
39
|
*
|
|
40
40
|
* @remarks
|
|
41
|
-
*
|
|
42
|
-
* -
|
|
43
|
-
* - marker
|
|
44
|
-
* -
|
|
45
|
-
*
|
|
41
|
+
* Guards:
|
|
42
|
+
* - If a baseline marker (`created_at = when`) already exists → **no-op** (`already-baselined`).
|
|
43
|
+
* - If there is no marker but `__drizzle_migrations` has other rows → **abort** (unexpected state).
|
|
44
|
+
* - If the target DB has no base tables (an empty DB) → **abort** (skipping 0000 on an empty DB would
|
|
45
|
+
* never create the tables; use `db:migrate` for a fresh DB).
|
|
46
46
|
*
|
|
47
|
-
* @param options -
|
|
48
|
-
* @returns
|
|
49
|
-
* @throws
|
|
47
|
+
* @param options - the connection and migrations folder; see {@link BaselineMigrationsOptions}.
|
|
48
|
+
* @returns whether a marker was inserted or the DB was already baselined.
|
|
49
|
+
* @throws Error when one of the guards above trips.
|
|
50
50
|
*/
|
|
51
51
|
export declare function baselineMigrations(options: BaselineMigrationsOptions): Promise<BaselineResult>;
|
package/dist/db/migrate.js
CHANGED
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Brownfield baseline for Drizzle MySQL migrations.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* An existing (in-production) DB already has its schema, so running the committed baseline migration
|
|
5
|
+
* (`drizzle/0000_*.sql` = the CREATE TABLE statements introspected from the current schema) via
|
|
6
|
+
* `db:migrate` fails as every table collides. Instead, 0000 is **recorded as "applied" without being
|
|
7
|
+
* executed**.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
* created_at
|
|
10
|
-
*
|
|
11
|
-
* `(hash, created_at
|
|
12
|
-
*
|
|
9
|
+
* How "applied" is decided (drizzle-orm/mysql-core dialect.migrate): it determines the pending set from
|
|
10
|
+
* **only the maximum `created_at`** in `__drizzle_migrations(id, hash, created_at)`, running just the
|
|
11
|
+
* migrations where `max(created_at) < entry.when`. The hash is stored but not used for the decision. So
|
|
12
|
+
* inserting one row as the 0000 marker — `(hash, created_at = that entry's when)` — makes subsequent
|
|
13
|
+
* `db:migrate` runs apply only the later 0001+ (larger `when`) and skip 0000. A fresh / test DB has no
|
|
14
|
+
* marker, so the full chain runs (behavior unchanged).
|
|
13
15
|
*
|
|
14
|
-
*
|
|
15
|
-
* QueryRunner
|
|
16
|
+
* This function does not depend on `drizzle-orm` (it reads the journal/SQL itself and hashes with the
|
|
17
|
+
* same sha256 as drizzle). It runs raw SQL against a QueryRunner (a mysql2 `Connection`/`Pool` is
|
|
18
|
+
* structurally assignable).
|
|
16
19
|
*
|
|
17
20
|
* @packageDocumentation
|
|
18
21
|
*/
|
|
19
22
|
import { createHash } from 'node:crypto';
|
|
20
23
|
import { existsSync, readFileSync } from 'node:fs';
|
|
21
24
|
import { join } from 'node:path';
|
|
22
|
-
/** drizzle
|
|
25
|
+
/** The default migration-tracking table name used by drizzle. */
|
|
23
26
|
const MIGRATIONS_TABLE = '__drizzle_migrations';
|
|
24
27
|
/**
|
|
25
|
-
* `migrationsFolder
|
|
28
|
+
* Read the baseline (first) entry from `migrationsFolder` (drizzle's `out`, e.g. `./drizzle`).
|
|
26
29
|
*
|
|
27
|
-
* @param migrationsFolder - `meta/_journal.json`
|
|
28
|
-
* @returns baseline
|
|
29
|
-
* @throws journal
|
|
30
|
+
* @param migrationsFolder - the folder containing `meta/_journal.json` and `<tag>.sql`.
|
|
31
|
+
* @returns the baseline entry (tag/when/hash).
|
|
32
|
+
* @throws Error when the journal is missing, the entries are empty, or `<tag>.sql` is missing.
|
|
30
33
|
*/
|
|
31
34
|
export function readBaselineEntry(migrationsFolder) {
|
|
32
35
|
const journalPath = join(migrationsFolder, 'meta', '_journal.json');
|
|
@@ -38,7 +41,8 @@ export function readBaselineEntry(migrationsFolder) {
|
|
|
38
41
|
if (entries.length === 0) {
|
|
39
42
|
throw new Error(`No migration entries in ${journalPath}.`);
|
|
40
43
|
}
|
|
41
|
-
//
|
|
44
|
+
// The origin is always the first entry (0000). Later 0001+ are "new changes" that should run even on
|
|
45
|
+
// an existing DB.
|
|
42
46
|
const first = entries[0];
|
|
43
47
|
const sqlPath = join(migrationsFolder, `${first.tag}.sql`);
|
|
44
48
|
if (!existsSync(sqlPath)) {
|
|
@@ -52,43 +56,44 @@ async function rowsOf(db, sql, params) {
|
|
|
52
56
|
return result[0] ?? [];
|
|
53
57
|
}
|
|
54
58
|
/**
|
|
55
|
-
*
|
|
59
|
+
* Record the baseline (0000) as "applied" on an existing DB. Idempotent, with safety guards.
|
|
56
60
|
*
|
|
57
61
|
* @remarks
|
|
58
|
-
*
|
|
59
|
-
* -
|
|
60
|
-
* - marker
|
|
61
|
-
* -
|
|
62
|
-
*
|
|
62
|
+
* Guards:
|
|
63
|
+
* - If a baseline marker (`created_at = when`) already exists → **no-op** (`already-baselined`).
|
|
64
|
+
* - If there is no marker but `__drizzle_migrations` has other rows → **abort** (unexpected state).
|
|
65
|
+
* - If the target DB has no base tables (an empty DB) → **abort** (skipping 0000 on an empty DB would
|
|
66
|
+
* never create the tables; use `db:migrate` for a fresh DB).
|
|
63
67
|
*
|
|
64
|
-
* @param options -
|
|
65
|
-
* @returns
|
|
66
|
-
* @throws
|
|
68
|
+
* @param options - the connection and migrations folder; see {@link BaselineMigrationsOptions}.
|
|
69
|
+
* @returns whether a marker was inserted or the DB was already baselined.
|
|
70
|
+
* @throws Error when one of the guards above trips.
|
|
67
71
|
*/
|
|
68
72
|
export async function baselineMigrations(options) {
|
|
69
73
|
const { db, migrationsFolder = './drizzle' } = options;
|
|
70
74
|
const baseline = readBaselineEntry(migrationsFolder);
|
|
71
|
-
// migrator
|
|
75
|
+
// Same DDL as the migrator (a no-op if it already exists).
|
|
72
76
|
await db.query(`create table if not exists \`${MIGRATIONS_TABLE}\` (
|
|
73
77
|
id serial primary key,
|
|
74
78
|
hash text not null,
|
|
75
79
|
created_at bigint
|
|
76
80
|
)`);
|
|
77
|
-
//
|
|
81
|
+
// If a baseline marker already exists, this is an idempotent no-op.
|
|
78
82
|
const existing = await rowsOf(db, `select id from \`${MIGRATIONS_TABLE}\` where created_at = ? limit 1`, [
|
|
79
83
|
baseline.when,
|
|
80
84
|
]);
|
|
81
85
|
if (existing.length > 0) {
|
|
82
86
|
return { status: 'already-baselined', tag: baseline.tag, when: baseline.when };
|
|
83
87
|
}
|
|
84
|
-
// marker
|
|
88
|
+
// No marker but rows exist = already in some other state. Abort to avoid misfiring.
|
|
85
89
|
const countRows = await rowsOf(db, `select count(*) as n from \`${MIGRATIONS_TABLE}\``);
|
|
86
90
|
const rowCount = Number(countRows[0]?.n ?? 0);
|
|
87
91
|
if (rowCount > 0) {
|
|
88
92
|
throw new Error(`${MIGRATIONS_TABLE} already has ${rowCount} row(s) but no baseline marker (created_at=${baseline.when}). ` +
|
|
89
93
|
`Migration state is unexpected — refusing to insert. Inspect \`${MIGRATIONS_TABLE}\` manually.`);
|
|
90
94
|
}
|
|
91
|
-
//
|
|
95
|
+
// Baselining an empty DB is dangerous (treating 0000 as skipped would never create the tables).
|
|
96
|
+
// Confirm this is a brownfield DB.
|
|
92
97
|
const tableRows = await rowsOf(db, `select count(*) as n from information_schema.tables
|
|
93
98
|
where table_schema = DATABASE() and table_type = 'BASE TABLE' and table_name <> ?`, [MIGRATIONS_TABLE]);
|
|
94
99
|
const baseTableCount = Number(tableRows[0]?.n ?? 0);
|
package/dist/db/orm-config.d.ts
CHANGED
|
@@ -102,7 +102,7 @@ export declare function honoDrizzleConfig(options: HonoDrizzleConfigOptions): {
|
|
|
102
102
|
out: string;
|
|
103
103
|
casing: "snake_case";
|
|
104
104
|
};
|
|
105
|
-
/** {@link resolveDbSecret}
|
|
105
|
+
/** The return value of {@link resolveDbSecret} (normalized connection info). */
|
|
106
106
|
export interface ResolvedDbSecret {
|
|
107
107
|
host: string;
|
|
108
108
|
port: number;
|
|
@@ -111,13 +111,17 @@ export interface ResolvedDbSecret {
|
|
|
111
111
|
password: string;
|
|
112
112
|
}
|
|
113
113
|
/**
|
|
114
|
-
* AWS RDS
|
|
114
|
+
* Resolve an AWS RDS managed secret (a JSON string placed in `DB_SECRET`).
|
|
115
115
|
*
|
|
116
116
|
* @remarks
|
|
117
|
-
* - `DB_SECRET`
|
|
118
|
-
* -
|
|
119
|
-
*
|
|
117
|
+
* - `DB_SECRET` unset → `undefined` (the normal local / `db:generate` fallback).
|
|
118
|
+
* - When set, it must be complete connection info: **invalid JSON / a missing required key throws**
|
|
119
|
+
* (rather than silently falling back to localhost and causing an incident). A missing `port` alone
|
|
120
|
+
* defaults to 3306.
|
|
120
121
|
*
|
|
121
|
-
* `honoDrizzleConfig
|
|
122
|
+
* Both `honoDrizzleConfig` (db:migrate) and the `workers-hono-kit-db-baseline` bin use this same logic.
|
|
123
|
+
*
|
|
124
|
+
* @returns the resolved connection info, or `undefined` when `DB_SECRET` is unset.
|
|
125
|
+
* @throws Error when `DB_SECRET` is set but is not valid JSON or is missing a required key.
|
|
122
126
|
*/
|
|
123
127
|
export declare function resolveDbSecret(): ResolvedDbSecret | undefined;
|
package/dist/db/orm-config.js
CHANGED
|
@@ -53,12 +53,12 @@ export const DRIZZLE_ORM_OPTIONS = { mode: 'default', casing: 'snake_case' };
|
|
|
53
53
|
*/
|
|
54
54
|
export function honoDrizzleConfig(options) {
|
|
55
55
|
const { database, host, port, user, password, schema = './src/db/schemes', out = './drizzle', tablesFilter, introspect, } = options;
|
|
56
|
-
// CI
|
|
57
|
-
// host/port/dbname/username/password
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
56
|
+
// CI/production migrate absorbs the pattern of passing a whole AWS Secrets Manager RDS managed secret
|
|
57
|
+
// (keys host/port/dbname/username/password) via `DB_SECRET`. It is parsed with JSON.parse, so key-name
|
|
58
|
+
// differences (host ≠ DB_HOST) can be mapped and special characters in the password stay shell-safe.
|
|
59
|
+
// When `DB_SECRET` is set, it is treated as a complete secret and fully determines the connection
|
|
60
|
+
// (missing/invalid → throw). Only when it is unset do we fall back to the individual DB_* env vars
|
|
61
|
+
// and then the defaults (the local / db:generate path).
|
|
62
62
|
const secret = resolveDbSecret();
|
|
63
63
|
const dbCredentials = secret
|
|
64
64
|
? {
|
|
@@ -86,14 +86,18 @@ export function honoDrizzleConfig(options) {
|
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
* AWS RDS
|
|
89
|
+
* Resolve an AWS RDS managed secret (a JSON string placed in `DB_SECRET`).
|
|
90
90
|
*
|
|
91
91
|
* @remarks
|
|
92
|
-
* - `DB_SECRET`
|
|
93
|
-
* -
|
|
94
|
-
*
|
|
92
|
+
* - `DB_SECRET` unset → `undefined` (the normal local / `db:generate` fallback).
|
|
93
|
+
* - When set, it must be complete connection info: **invalid JSON / a missing required key throws**
|
|
94
|
+
* (rather than silently falling back to localhost and causing an incident). A missing `port` alone
|
|
95
|
+
* defaults to 3306.
|
|
95
96
|
*
|
|
96
|
-
* `honoDrizzleConfig
|
|
97
|
+
* Both `honoDrizzleConfig` (db:migrate) and the `workers-hono-kit-db-baseline` bin use this same logic.
|
|
98
|
+
*
|
|
99
|
+
* @returns the resolved connection info, or `undefined` when `DB_SECRET` is unset.
|
|
100
|
+
* @throws Error when `DB_SECRET` is set but is not valid JSON or is missing a required key.
|
|
97
101
|
*/
|
|
98
102
|
export function resolveDbSecret() {
|
|
99
103
|
const raw = process.env.DB_SECRET;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mysql2 driver error shape (= TypeORM QueryFailedError.driverError 相当).
|
|
3
|
+
* Drizzle / Error.cause で wrap される場合があるため cause も再帰探索する。
|
|
4
|
+
*/
|
|
5
|
+
export interface MysqlDriverErrorLike {
|
|
6
|
+
errno?: number;
|
|
7
|
+
sqlMessage?: string;
|
|
8
|
+
sqlState?: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
sql?: string;
|
|
11
|
+
code?: string;
|
|
12
|
+
cause?: unknown;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* err 本体 → `err.cause` を再帰的に辿り、mysql2 ドライバエラーを取り出す。
|
|
16
|
+
*
|
|
17
|
+
* @param err - handler / onError に渡された thrown value。
|
|
18
|
+
* @param seen - 循環参照防止(内部用)。
|
|
19
|
+
*/
|
|
20
|
+
export declare function findMysqlDriverError(err: unknown, seen?: Set<unknown>): MysqlDriverErrorLike | null;
|
|
21
|
+
/**
|
|
22
|
+
* Nest QueryFailedExceptionFilter の logger.error / logger.warn 相当。
|
|
23
|
+
* レスポンス body には載せず、Workers Logs 用に errno / sqlMessage / sql を残す。
|
|
24
|
+
*/
|
|
25
|
+
export declare function logMysqlDriverError(err: unknown, statusCode: number): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* err 本体 → `err.cause` を再帰的に辿り、mysql2 ドライバエラーを取り出す。
|
|
3
|
+
*
|
|
4
|
+
* @param err - handler / onError に渡された thrown value。
|
|
5
|
+
* @param seen - 循環参照防止(内部用)。
|
|
6
|
+
*/
|
|
7
|
+
export function findMysqlDriverError(err, seen = new Set()) {
|
|
8
|
+
if (err === null || err === undefined || seen.has(err)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
seen.add(err);
|
|
12
|
+
const e = err;
|
|
13
|
+
if (typeof e.errno === 'number' && (typeof e.sqlMessage === 'string' || typeof e.sqlState === 'string')) {
|
|
14
|
+
return e;
|
|
15
|
+
}
|
|
16
|
+
if (e.cause !== undefined) {
|
|
17
|
+
return findMysqlDriverError(e.cause, seen);
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Nest QueryFailedExceptionFilter の logger.error / logger.warn 相当。
|
|
23
|
+
* レスポンス body には載せず、Workers Logs 用に errno / sqlMessage / sql を残す。
|
|
24
|
+
*/
|
|
25
|
+
export function logMysqlDriverError(err, statusCode) {
|
|
26
|
+
const driver = findMysqlDriverError(err);
|
|
27
|
+
const rawMessage = driver?.sqlMessage ?? driver?.message ?? (err instanceof Error ? err.message : String(err));
|
|
28
|
+
const detail = driver === null ? undefined : { errno: driver.errno, sql: driver.sql, code: driver.code };
|
|
29
|
+
const line = `QueryFailedError (${statusCode}): ${rawMessage}`;
|
|
30
|
+
if (statusCode >= 500) {
|
|
31
|
+
console.error(line, detail);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.warn(line, detail);
|
|
35
|
+
}
|
package/dist/http/nest-error.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findMysqlDriverError, logMysqlDriverError } from './mysql-driver-error.js';
|
|
1
2
|
/**
|
|
2
3
|
* Reason phrases attached by the NestJS default exception filter, keyed by HTTP status code.
|
|
3
4
|
*
|
|
@@ -82,7 +83,12 @@ export function createNestErrorHandler(options = {}) {
|
|
|
82
83
|
catch {
|
|
83
84
|
// Reporting must never change the behavior of the error response.
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
+
if (findMysqlDriverError(err)) {
|
|
87
|
+
logMysqlDriverError(err, 500);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.error(err);
|
|
91
|
+
}
|
|
86
92
|
return c.json(internalServerErrorBody, 500);
|
|
87
93
|
};
|
|
88
94
|
}
|