@rdlabo/workers-hono-kit 0.4.0 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -24
- 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/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/middleware/perf-log.d.ts +77 -0
- package/dist/middleware/perf-log.js +97 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/workers-bindings.d.ts +49 -0
- package/dist/testing/workers-bindings.js +62 -0
- package/package.json +1 -1
- package/scripts/db-baseline.mjs +10 -9
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;
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export type { ValidateOptions, ValidationTarget, ZodErrorLike, SentryLike, Sentr
|
|
|
15
15
|
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
|
|
16
16
|
export { createAuthMiddleware } from './middleware/auth.js';
|
|
17
17
|
export type { AuthMiddlewareOptions } from './middleware/auth.js';
|
|
18
|
+
export { perfLog } from './middleware/perf-log.js';
|
|
19
|
+
export type { PerfLogOptions, AnalyticsEngineDatasetLike } from './middleware/perf-log.js';
|
|
18
20
|
export { getUserProtocol } from './http/user-protocol.js';
|
|
19
21
|
export type { IUserProtocol } from './http/user-protocol.js';
|
|
20
22
|
export { getAppInfo } from './http/app-info.js';
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export { finalizeResponse } from './middleware/finalize-response.js';
|
|
|
14
14
|
export { validate, createSentryValidate } from './middleware/validation.js';
|
|
15
15
|
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
|
|
16
16
|
export { createAuthMiddleware } from './middleware/auth.js';
|
|
17
|
+
export { perfLog } from './middleware/perf-log.js';
|
|
17
18
|
// http
|
|
18
19
|
export { getUserProtocol } from './http/user-protocol.js';
|
|
19
20
|
export { getAppInfo } from './http/app-info.js';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal shape of a Workers Analytics Engine dataset binding.
|
|
4
|
+
*
|
|
5
|
+
* Declared locally so consumers are not forced to depend on `@cloudflare/workers-types`. Only the
|
|
6
|
+
* single write operation the emitter needs is modeled. A real `AnalyticsEngineDataset` binding is
|
|
7
|
+
* assignable. Writes are non-blocking and add no latency to the request.
|
|
8
|
+
*
|
|
9
|
+
* @see https://developers.cloudflare.com/analytics/analytics-engine/
|
|
10
|
+
*/
|
|
11
|
+
export interface AnalyticsEngineDatasetLike {
|
|
12
|
+
writeDataPoint(event: {
|
|
13
|
+
doubles?: number[];
|
|
14
|
+
blobs?: (string | null)[];
|
|
15
|
+
indexes?: string[];
|
|
16
|
+
}): void;
|
|
17
|
+
}
|
|
18
|
+
/** Options for {@link perfLog}. Both sinks are optional; enable either or both. */
|
|
19
|
+
export interface PerfLogOptions {
|
|
20
|
+
/**
|
|
21
|
+
* When `true`, emit one `console.log(JSON.stringify({ perf }))` per request. With
|
|
22
|
+
* `[observability] enabled = true` these lines are captured by **Workers Logs** (retained up to
|
|
23
|
+
* 7 days, queryable via the dashboard Query Builder or the Observability REST API) — no live
|
|
24
|
+
* `wrangler tail` needed, which is what makes this practical for low-traffic Workers.
|
|
25
|
+
*
|
|
26
|
+
* Explicit wins over the `PERF_LOG` env fallback in both directions: `true` forces on, `false` forces
|
|
27
|
+
* off (even when `PERF_LOG === '1'`), `undefined` (default) defers to `PERF_LOG`.
|
|
28
|
+
*/
|
|
29
|
+
console?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* When provided, write one data point per request to a **Workers Analytics Engine** dataset. Query
|
|
32
|
+
* percentiles by route/colo with the SQL API (≈90-day retention). Non-blocking. Layout:
|
|
33
|
+
* `doubles = [t_app_ms, cold(0|1), status]`, `blobs = [path, colo, method]`, `indexes = [path]`.
|
|
34
|
+
*/
|
|
35
|
+
dataset?: AnalyticsEngineDatasetLike;
|
|
36
|
+
/**
|
|
37
|
+
* In-code sampling in `[0, 1]` (default `1` = every request; values are clamped to the range).
|
|
38
|
+
* Thins **Analytics Engine writes only** — Workers Logs volume is controlled separately by the
|
|
39
|
+
* observability `head_sampling_rate`. Low-traffic Workers should leave it at `1`.
|
|
40
|
+
*/
|
|
41
|
+
sampleRate?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create a Hono middleware that records a per-request latency data point and emits it to Workers
|
|
45
|
+
* Logs (`console`) and/or Workers Analytics Engine (`dataset`).
|
|
46
|
+
*
|
|
47
|
+
* Register it first (`app.use('*', perfLog(...))`) so `t_app` covers the whole in-app path. Route
|
|
48
|
+
* grouping uses the matched route pattern (e.g. `/user/:id`) rather than the raw path so ids do not
|
|
49
|
+
* explode cardinality; unmatched requests collapse to `(unmatched)`. Colo comes from `request.cf.colo`;
|
|
50
|
+
* cold/warm from an isolate-scoped flag. See {@link PerfLogOptions} for the two sinks and the note
|
|
51
|
+
* above for exactly what `t_app` includes (it depends on where secrets/DB-connect are wired).
|
|
52
|
+
*
|
|
53
|
+
* Two wiring styles, both A/B-capable (Workers Logs and/or Analytics Engine):
|
|
54
|
+
*
|
|
55
|
+
* @example Bare, when the app is served with `app.fetch(req, env, ctx)` — reads `PERF` (Analytics
|
|
56
|
+
* Engine binding) and `PERF_LOG === '1'` (Workers Logs) straight off `c.env`:
|
|
57
|
+
* ```ts
|
|
58
|
+
* app.use('*', perfLog());
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @example Explicit, when the app is built without Hono `env` (e.g. `createApp(container).fetch(req)`)
|
|
62
|
+
* — thread the bindings in:
|
|
63
|
+
* ```ts
|
|
64
|
+
* app.use('*', perfLog({ console: env.PERF_LOG === '1', dataset: env.PERF }));
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @example Query Analytics Engine (SQL API), handler p50/p90 by route and colo:
|
|
68
|
+
* ```sql
|
|
69
|
+
* SELECT blob1 AS path, blob2 AS colo,
|
|
70
|
+
* quantileWeighted(0.5)(double1, _sample_interval) AS p50,
|
|
71
|
+
* quantileWeighted(0.9)(double1, _sample_interval) AS p90,
|
|
72
|
+
* sum(_sample_interval) AS n
|
|
73
|
+
* FROM your_dataset WHERE timestamp > now() - INTERVAL '7' DAY
|
|
74
|
+
* GROUP BY path, colo ORDER BY n DESC
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare function perfLog(options?: PerfLogOptions): MiddlewareHandler;
|