@rdlabo/workers-hono-kit 0.11.2 → 0.12.0-beta.pr48.sha371f5792ce8a

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 (56) hide show
  1. package/README.md +73 -37
  2. package/dist/aws/sts.d.ts +2 -2
  3. package/dist/aws/sts.js +4 -4
  4. package/dist/business-time/index.d.ts +68 -122
  5. package/dist/business-time/index.js +55 -222
  6. package/dist/container/middleware.d.ts +1 -2
  7. package/dist/container/middleware.js +1 -1
  8. package/dist/db/index.d.ts +95 -20
  9. package/dist/db/index.js +56 -15
  10. package/dist/db/payment-failed.d.ts +1 -1
  11. package/dist/db/payment-failed.js +1 -1
  12. package/dist/index.d.ts +0 -3
  13. package/dist/index.js +0 -3
  14. package/dist/mysql/index.d.ts +9 -0
  15. package/dist/mysql/index.js +8 -0
  16. package/dist/testing/auth.d.ts +1 -1
  17. package/dist/testing/db.d.ts +19 -106
  18. package/dist/testing/db.js +3 -95
  19. package/dist/testing/fakes.d.ts +15 -97
  20. package/dist/testing/fakes.js +12 -104
  21. package/dist/testing/index.d.ts +8 -3
  22. package/dist/testing/index.js +8 -2
  23. package/docs/api-business-time.md +47 -30
  24. package/docs/api-db.md +48 -29
  25. package/docs/api-offline.md +11 -15
  26. package/docs/api-root.md +64 -58
  27. package/docs/api-testing.md +41 -13
  28. package/docs/api.md +35 -12
  29. package/docs/cli.md +11 -7
  30. package/docs/data-layer.md +68 -5
  31. package/docs/development.md +125 -5
  32. package/docs/http-auth.md +15 -0
  33. package/docs/realtime-offline.md +15 -0
  34. package/docs/role-policies.md +4 -4
  35. package/docs/testing-operations.md +25 -2
  36. package/package.json +24 -18
  37. package/scripts/db-baseline.mjs +3 -68
  38. package/scripts/workspace-package-smoke.mjs +206 -0
  39. package/dist/business-time/types.d.ts +0 -9
  40. package/dist/business-time/types.js +0 -5
  41. package/dist/db/columns.d.ts +0 -38
  42. package/dist/db/columns.js +0 -38
  43. package/dist/db/connection.d.ts +0 -74
  44. package/dist/db/connection.js +0 -67
  45. package/dist/db/database.d.ts +0 -249
  46. package/dist/db/database.js +0 -196
  47. package/dist/db/jst.d.ts +0 -45
  48. package/dist/db/jst.js +0 -47
  49. package/dist/db/migrate.d.ts +0 -51
  50. package/dist/db/migrate.js +0 -109
  51. package/dist/db/orm-config.d.ts +0 -127
  52. package/dist/db/orm-config.js +0 -122
  53. package/dist/db/retry.d.ts +0 -28
  54. package/dist/db/retry.js +0 -56
  55. package/dist/db/write-result.d.ts +0 -39
  56. package/dist/db/write-result.js +0 -34
@@ -1,109 +0,0 @@
1
- /**
2
- * Brownfield baseline for Drizzle MySQL migrations.
3
- *
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**.
8
- *
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).
15
- *
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).
19
- *
20
- * @packageDocumentation
21
- */
22
- import { createHash } from 'node:crypto';
23
- import { existsSync, readFileSync } from 'node:fs';
24
- import { join } from 'node:path';
25
- /** The default migration-tracking table name used by drizzle. */
26
- const MIGRATIONS_TABLE = '__drizzle_migrations';
27
- /**
28
- * Read the baseline (first) entry from `migrationsFolder` (drizzle's `out`, e.g. `./drizzle`).
29
- *
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.
33
- */
34
- export function readBaselineEntry(migrationsFolder) {
35
- const journalPath = join(migrationsFolder, 'meta', '_journal.json');
36
- if (!existsSync(journalPath)) {
37
- throw new Error(`Can't find meta/_journal.json under ${migrationsFolder}. Run \`drizzle-kit generate\` first.`);
38
- }
39
- const journal = JSON.parse(readFileSync(journalPath, 'utf8'));
40
- const entries = journal.entries ?? [];
41
- if (entries.length === 0) {
42
- throw new Error(`No migration entries in ${journalPath}.`);
43
- }
44
- // The origin is always the first entry (0000). Later 0001+ are "new changes" that should run even on
45
- // an existing DB.
46
- const first = entries[0];
47
- const sqlPath = join(migrationsFolder, `${first.tag}.sql`);
48
- if (!existsSync(sqlPath)) {
49
- throw new Error(`Can't find ${first.tag}.sql under ${migrationsFolder}.`);
50
- }
51
- const sql = readFileSync(sqlPath, 'utf8');
52
- return { tag: first.tag, when: first.when, hash: createHash('sha256').update(sql).digest('hex') };
53
- }
54
- async function rowsOf(db, sql, params) {
55
- const result = (await db.query(sql, params));
56
- return result[0] ?? [];
57
- }
58
- /**
59
- * Record the baseline (0000) as "applied" on an existing DB. Idempotent, with safety guards.
60
- *
61
- * @remarks
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).
67
- *
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.
71
- */
72
- export async function baselineMigrations(options) {
73
- const { db, migrationsFolder = './drizzle' } = options;
74
- const baseline = readBaselineEntry(migrationsFolder);
75
- // Same DDL as the migrator (a no-op if it already exists).
76
- await db.query(`create table if not exists \`${MIGRATIONS_TABLE}\` (
77
- id serial primary key,
78
- hash text not null,
79
- created_at bigint
80
- )`);
81
- // If a baseline marker already exists, this is an idempotent no-op.
82
- const existing = await rowsOf(db, `select id from \`${MIGRATIONS_TABLE}\` where created_at = ? limit 1`, [
83
- baseline.when,
84
- ]);
85
- if (existing.length > 0) {
86
- return { status: 'already-baselined', tag: baseline.tag, when: baseline.when };
87
- }
88
- // No marker but rows exist = already in some other state. Abort to avoid misfiring.
89
- const countRows = await rowsOf(db, `select count(*) as n from \`${MIGRATIONS_TABLE}\``);
90
- const rowCount = Number(countRows[0]?.n ?? 0);
91
- if (rowCount > 0) {
92
- throw new Error(`${MIGRATIONS_TABLE} already has ${rowCount} row(s) but no baseline marker (created_at=${baseline.when}). ` +
93
- `Migration state is unexpected — refusing to insert. Inspect \`${MIGRATIONS_TABLE}\` manually.`);
94
- }
95
- // Baselining an empty DB is dangerous (treating 0000 as skipped would never create the tables).
96
- // Confirm this is a brownfield DB.
97
- const tableRows = await rowsOf(db, `select count(*) as n from information_schema.tables
98
- where table_schema = DATABASE() and table_type = 'BASE TABLE' and table_name <> ?`, [MIGRATIONS_TABLE]);
99
- const baseTableCount = Number(tableRows[0]?.n ?? 0);
100
- if (baseTableCount === 0) {
101
- throw new Error(`Target DB has no base tables. baseline records 0000 as applied WITHOUT creating tables — this is only ` +
102
- `for existing (brownfield) DBs. For a fresh/empty DB run \`drizzle-kit migrate\` instead.`);
103
- }
104
- await db.query(`insert into \`${MIGRATIONS_TABLE}\` (\`hash\`, \`created_at\`) values (?, ?)`, [
105
- baseline.hash,
106
- baseline.when,
107
- ]);
108
- return { status: 'inserted', tag: baseline.tag, when: baseline.when, hash: baseline.hash };
109
- }
@@ -1,127 +0,0 @@
1
- /**
2
- * Centralizes Drizzle column-name casing so it is fixed (standard: `snake_case`) in both the
3
- * config and the runtime ORM.
4
- *
5
- * @remarks
6
- * Casing is configured in two distinct places:
7
- *
8
- * 1. The top-level `casing` in `drizzle.config.ts` decides the column names that `db:generate`
9
- * **creates** (see {@link honoDrizzleConfig}).
10
- * 2. The `drizzle(conn, { …casing })` call decides the column names the **runtime write builder**
11
- * resolves to (see {@link DRIZZLE_ORM_OPTIONS}).
12
- *
13
- * If these two disagree, a multi-word camelCase column without an explicit column name will be
14
- * generated with one name but queried with another, producing a runtime `Unknown column` error —
15
- * something neither the type-check nor the migration surface, so it is caught late. Sourcing both
16
- * from here makes the mismatch structurally impossible. Casing is ignored for columns that declare
17
- * an explicit name, so this is a pure safety net that does not change existing behavior.
18
- *
19
- * The runtime `drizzle()` call itself is made by the consuming app with its own `drizzle-orm`; the
20
- * kit only ever provides values, never the ORM instance, to avoid splitting `drizzle-orm` into two
21
- * copies and breaking type identity.
22
- */
23
- /**
24
- * Runtime ORM options shared by the consuming app's `drizzle()` call.
25
- *
26
- * Spread into the runtime ORM as `drizzle(conn, { schema, ...DRIZZLE_ORM_OPTIONS })` so the write
27
- * builder resolves column names as `snake_case`, matching what `db:generate` creates.
28
- *
29
- * @remarks
30
- * Fixes `mode: 'default'` and `casing: 'snake_case'`. See the module-level documentation for why
31
- * the same casing must be used by both the config and the runtime ORM.
32
- */
33
- export declare const DRIZZLE_ORM_OPTIONS: {
34
- readonly mode: "default";
35
- readonly casing: "snake_case";
36
- };
37
- /**
38
- * Options for {@link honoDrizzleConfig}.
39
- */
40
- export interface HonoDrizzleConfigOptions {
41
- /** drizzle-kit `dbCredentials.database` — the database name to connect to. */
42
- database: string;
43
- /** Database host; defaults to `process.env.DB_HOST` then `127.0.0.1`. */
44
- host?: string;
45
- /** Database port; defaults to `process.env.DB_PORT` then `3306`. */
46
- port?: number;
47
- /** Database user; defaults to `process.env.DB_USER` then `root`. */
48
- user?: string;
49
- /** Database password; defaults to `process.env.DB_PASSWORD` then `root`. */
50
- password?: string;
51
- /** Path to the schema directory; defaults to `'./src/db/schemes'`. */
52
- schema?: string;
53
- /** Output directory for generated migrations; defaults to `'./drizzle'`. */
54
- out?: string;
55
- /**
56
- * Optional table allow-list. Use this to restrict drizzle-kit to the schema's own tables when the
57
- * database is shared with another application.
58
- */
59
- tablesFilter?: string[];
60
- /**
61
- * Optional `db:introspect` (DB → JS) casing. This is an independent axis from the generation-side
62
- * `casing: 'snake_case'` and only affects introspection output.
63
- */
64
- introspect?: {
65
- casing: 'camel' | 'preserve';
66
- };
67
- }
68
- /**
69
- * Build a `drizzle.config.ts` configuration object with the kit's standard defaults.
70
- *
71
- * Fixes `casing: 'snake_case'`, the `schema`/`out` paths, and `dbCredentials` (with env-based
72
- * defaults), while leaving `tablesFilter` and `introspect` opt-in.
73
- *
74
- * @remarks
75
- * Returns a plain object rather than a typed drizzle-kit config so that `drizzle-kit` need not be a
76
- * dependency of the kit; the drizzle-kit CLI only reads the default export.
77
- *
78
- * @param options - configuration overrides; only `database` is required.
79
- * @returns a plain configuration object suitable for `export default` in `drizzle.config.ts`.
80
- * @example
81
- * ```ts
82
- * // drizzle.config.ts
83
- * import { honoDrizzleConfig } from '@rdlabo/workers-hono-kit/db';
84
- *
85
- * export default honoDrizzleConfig({ database: 'app' });
86
- * ```
87
- */
88
- export declare function honoDrizzleConfig(options: HonoDrizzleConfigOptions): {
89
- dbCredentials: {
90
- host: string;
91
- port: number;
92
- user: string;
93
- password: string;
94
- database: string;
95
- };
96
- introspect?: {
97
- casing: "camel" | "preserve";
98
- } | undefined;
99
- tablesFilter?: string[] | undefined;
100
- dialect: "mysql";
101
- schema: string;
102
- out: string;
103
- casing: "snake_case";
104
- };
105
- /** The return value of {@link resolveDbSecret} (normalized connection info). */
106
- export interface ResolvedDbSecret {
107
- host: string;
108
- port: number;
109
- dbname: string;
110
- username: string;
111
- password: string;
112
- }
113
- /**
114
- * Resolve an AWS RDS managed secret (a JSON string placed in `DB_SECRET`).
115
- *
116
- * @remarks
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.
121
- *
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.
126
- */
127
- export declare function resolveDbSecret(): ResolvedDbSecret | undefined;
@@ -1,122 +0,0 @@
1
- /**
2
- * Centralizes Drizzle column-name casing so it is fixed (standard: `snake_case`) in both the
3
- * config and the runtime ORM.
4
- *
5
- * @remarks
6
- * Casing is configured in two distinct places:
7
- *
8
- * 1. The top-level `casing` in `drizzle.config.ts` decides the column names that `db:generate`
9
- * **creates** (see {@link honoDrizzleConfig}).
10
- * 2. The `drizzle(conn, { …casing })` call decides the column names the **runtime write builder**
11
- * resolves to (see {@link DRIZZLE_ORM_OPTIONS}).
12
- *
13
- * If these two disagree, a multi-word camelCase column without an explicit column name will be
14
- * generated with one name but queried with another, producing a runtime `Unknown column` error —
15
- * something neither the type-check nor the migration surface, so it is caught late. Sourcing both
16
- * from here makes the mismatch structurally impossible. Casing is ignored for columns that declare
17
- * an explicit name, so this is a pure safety net that does not change existing behavior.
18
- *
19
- * The runtime `drizzle()` call itself is made by the consuming app with its own `drizzle-orm`; the
20
- * kit only ever provides values, never the ORM instance, to avoid splitting `drizzle-orm` into two
21
- * copies and breaking type identity.
22
- */
23
- /**
24
- * Runtime ORM options shared by the consuming app's `drizzle()` call.
25
- *
26
- * Spread into the runtime ORM as `drizzle(conn, { schema, ...DRIZZLE_ORM_OPTIONS })` so the write
27
- * builder resolves column names as `snake_case`, matching what `db:generate` creates.
28
- *
29
- * @remarks
30
- * Fixes `mode: 'default'` and `casing: 'snake_case'`. See the module-level documentation for why
31
- * the same casing must be used by both the config and the runtime ORM.
32
- */
33
- export const DRIZZLE_ORM_OPTIONS = { mode: 'default', casing: 'snake_case' };
34
- /**
35
- * Build a `drizzle.config.ts` configuration object with the kit's standard defaults.
36
- *
37
- * Fixes `casing: 'snake_case'`, the `schema`/`out` paths, and `dbCredentials` (with env-based
38
- * defaults), while leaving `tablesFilter` and `introspect` opt-in.
39
- *
40
- * @remarks
41
- * Returns a plain object rather than a typed drizzle-kit config so that `drizzle-kit` need not be a
42
- * dependency of the kit; the drizzle-kit CLI only reads the default export.
43
- *
44
- * @param options - configuration overrides; only `database` is required.
45
- * @returns a plain configuration object suitable for `export default` in `drizzle.config.ts`.
46
- * @example
47
- * ```ts
48
- * // drizzle.config.ts
49
- * import { honoDrizzleConfig } from '@rdlabo/workers-hono-kit/db';
50
- *
51
- * export default honoDrizzleConfig({ database: 'app' });
52
- * ```
53
- */
54
- export function honoDrizzleConfig(options) {
55
- const { database, host, port, user, password, schema = './src/db/schemes', out = './drizzle', tablesFilter, introspect, } = options;
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
- const secret = resolveDbSecret();
63
- const dbCredentials = secret
64
- ? {
65
- host: secret.host,
66
- port: secret.port,
67
- user: secret.username,
68
- password: secret.password,
69
- database: secret.dbname,
70
- }
71
- : {
72
- host: host ?? process.env.DB_HOST ?? '127.0.0.1',
73
- port: port ?? Number(process.env.DB_PORT ?? 3306),
74
- user: user ?? process.env.DB_USER ?? 'root',
75
- password: password ?? process.env.DB_PASSWORD ?? 'root',
76
- database,
77
- };
78
- return {
79
- dialect: 'mysql',
80
- schema,
81
- out,
82
- casing: 'snake_case',
83
- ...(tablesFilter ? { tablesFilter } : {}),
84
- ...(introspect ? { introspect } : {}),
85
- dbCredentials,
86
- };
87
- }
88
- /**
89
- * Resolve an AWS RDS managed secret (a JSON string placed in `DB_SECRET`).
90
- *
91
- * @remarks
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.
96
- *
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.
101
- */
102
- export function resolveDbSecret() {
103
- const raw = process.env.DB_SECRET;
104
- if (!raw) {
105
- return undefined;
106
- }
107
- let parsed;
108
- try {
109
- parsed = JSON.parse(raw);
110
- }
111
- catch {
112
- throw new Error('DB_SECRET is set but is not valid JSON (expected an AWS RDS managed secret string).');
113
- }
114
- const { host, dbname, username, password } = parsed;
115
- if (typeof host !== 'string' ||
116
- typeof dbname !== 'string' ||
117
- typeof username !== 'string' ||
118
- typeof password !== 'string') {
119
- throw new Error('DB_SECRET must contain string host, dbname, username, password (AWS RDS managed secret shape).');
120
- }
121
- return { host, dbname, username, password, port: parsed.port === undefined ? 3306 : Number(parsed.port) };
122
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * Run an async unit of work, retrying it on MySQL deadlock errors with exponential backoff.
3
- *
4
- * Retries are triggered only by the `ER_LOCK_DEADLOCK` error code. Each failed attempt waits
5
- * `delay * attempt` milliseconds (linear growth of the base delay) before the next try, and any
6
- * non-deadlock error is rethrown immediately without retrying.
7
- *
8
- * @remarks
9
- * MySQL rolls back the entire transaction when it detects a deadlock, so re-running the same unit
10
- * of work is safe. Pass a `fn` that represents one complete unit — a single statement or an entire
11
- * transaction — because the whole `fn` is re-executed on each retry.
12
- *
13
- * @typeParam T - resolved value produced by `fn`.
14
- * @param fn - the unit of work to execute; it is invoked again from scratch on each retry.
15
- * @param retries - maximum number of attempts (default `3`).
16
- * @param delay - base backoff in milliseconds; attempt N waits `delay * N` (default `100`).
17
- * @returns the value resolved by the first successful call to `fn`.
18
- * @throws the last error thrown by `fn` once retries are exhausted, or any non-deadlock error on
19
- * the first occurrence.
20
- * @example
21
- * ```ts
22
- * await retryWhenDeadlock(() => db.transaction(async (tx) => {
23
- * await tx.insert(orders).values(order);
24
- * await tx.update(stock).set({ qty: sql`qty - 1` }).where(eq(stock.id, order.itemId));
25
- * }));
26
- * ```
27
- */
28
- export declare function retryWhenDeadlock<T>(fn: () => Promise<T>, retries?: number, delay?: number): Promise<T>;
package/dist/db/retry.js DELETED
@@ -1,56 +0,0 @@
1
- /**
2
- * Run an async unit of work, retrying it on MySQL deadlock errors with exponential backoff.
3
- *
4
- * Retries are triggered only by the `ER_LOCK_DEADLOCK` error code. Each failed attempt waits
5
- * `delay * attempt` milliseconds (linear growth of the base delay) before the next try, and any
6
- * non-deadlock error is rethrown immediately without retrying.
7
- *
8
- * @remarks
9
- * MySQL rolls back the entire transaction when it detects a deadlock, so re-running the same unit
10
- * of work is safe. Pass a `fn` that represents one complete unit — a single statement or an entire
11
- * transaction — because the whole `fn` is re-executed on each retry.
12
- *
13
- * @typeParam T - resolved value produced by `fn`.
14
- * @param fn - the unit of work to execute; it is invoked again from scratch on each retry.
15
- * @param retries - maximum number of attempts (default `3`).
16
- * @param delay - base backoff in milliseconds; attempt N waits `delay * N` (default `100`).
17
- * @returns the value resolved by the first successful call to `fn`.
18
- * @throws the last error thrown by `fn` once retries are exhausted, or any non-deadlock error on
19
- * the first occurrence.
20
- * @example
21
- * ```ts
22
- * await retryWhenDeadlock(() => db.transaction(async (tx) => {
23
- * await tx.insert(orders).values(order);
24
- * await tx.update(stock).set({ qty: sql`qty - 1` }).where(eq(stock.id, order.itemId));
25
- * }));
26
- * ```
27
- */
28
- export async function retryWhenDeadlock(fn, retries = 3, delay = 100) {
29
- for (let attempt = 0; attempt < retries; attempt++) {
30
- const invoke = async () => fn();
31
- const outcome = await invoke().then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
32
- if (outcome.ok) {
33
- return outcome.value;
34
- }
35
- if (isDeadlock(outcome.error) && attempt < retries - 1) {
36
- await new Promise((resolve) => setTimeout(resolve, delay * (attempt + 1)));
37
- continue;
38
- }
39
- throw outcome.error;
40
- }
41
- // Unreachable: the loop returns on success and throws on the final failed attempt.
42
- throw new Error('retryWhenDeadlock: exhausted retries');
43
- }
44
- function isDeadlock(error) {
45
- let current = error;
46
- const seen = new Set();
47
- while (typeof current === 'object' && current !== null && !seen.has(current)) {
48
- seen.add(current);
49
- const value = current;
50
- if (value.code === 'ER_LOCK_DEADLOCK') {
51
- return true;
52
- }
53
- current = value.cause;
54
- }
55
- return false;
56
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * Shape of a Drizzle (mysql2) write result, narrowed to the fields callers actually read.
3
- *
4
- * @remarks
5
- * A mysql2 INSERT/UPDATE/DELETE result is the tuple `[ResultSetHeader, FieldPacket[]]`. Typing the
6
- * result this way lets repositories extract the common values without exposing the raw query
7
- * builder or the full `ResultSetHeader` to the rest of the codebase.
8
- */
9
- export type DzWriteResult = readonly [{
10
- insertId: number;
11
- affectedRows: number;
12
- }, ...unknown[]];
13
- /**
14
- * Extract the auto-increment `insertId` from a write result.
15
- *
16
- * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
17
- * @returns the `insertId` reported by mysql2 (the id of the first inserted row).
18
- */
19
- export declare function insertIdOf(result: DzWriteResult): number;
20
- /**
21
- * Extract the number of affected rows from a write result.
22
- *
23
- * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
24
- * @returns the `affectedRows` count reported by mysql2.
25
- */
26
- export declare function affectedRowsOf(result: DzWriteResult): number;
27
- /**
28
- * Reconstruct the auto-increment ids assigned by a bulk INSERT.
29
- *
30
- * @remarks
31
- * mysql2 reports only the first `insertId` for a multi-row INSERT, so the remaining ids are derived
32
- * by assuming a contiguous sequence (`base`, `base + 1`, …). This holds for tables with a standard
33
- * `AUTO_INCREMENT` column and the default `innodb_autoinc_lock_mode`.
34
- *
35
- * @param result - the result of a bulk INSERT.
36
- * @param count - the number of rows that were inserted.
37
- * @returns an array of the `count` auto-increment ids, starting at the reported `insertId`.
38
- */
39
- export declare function insertedIdsOf(result: DzWriteResult, count: number): number[];
@@ -1,34 +0,0 @@
1
- /**
2
- * Extract the auto-increment `insertId` from a write result.
3
- *
4
- * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
5
- * @returns the `insertId` reported by mysql2 (the id of the first inserted row).
6
- */
7
- export function insertIdOf(result) {
8
- return result[0].insertId;
9
- }
10
- /**
11
- * Extract the number of affected rows from a write result.
12
- *
13
- * @param result - the result of a Drizzle (mysql2) INSERT/UPDATE/DELETE.
14
- * @returns the `affectedRows` count reported by mysql2.
15
- */
16
- export function affectedRowsOf(result) {
17
- return result[0].affectedRows;
18
- }
19
- /**
20
- * Reconstruct the auto-increment ids assigned by a bulk INSERT.
21
- *
22
- * @remarks
23
- * mysql2 reports only the first `insertId` for a multi-row INSERT, so the remaining ids are derived
24
- * by assuming a contiguous sequence (`base`, `base + 1`, …). This holds for tables with a standard
25
- * `AUTO_INCREMENT` column and the default `innodb_autoinc_lock_mode`.
26
- *
27
- * @param result - the result of a bulk INSERT.
28
- * @param count - the number of rows that were inserted.
29
- * @returns an array of the `count` auto-increment ids, starting at the reported `insertId`.
30
- */
31
- export function insertedIdsOf(result, count) {
32
- const base = result[0].insertId;
33
- return Array.from({ length: count }, (_, i) => base + i);
34
- }