@end-close/relay-sqlite 0.12.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.
@@ -0,0 +1,45 @@
1
+ /** kv key prefix for per-route pause flags: `route_paused.<routeId>` = '1'. */
2
+ export const ROUTE_PAUSED_PREFIX = 'route_paused.';
3
+ export class KvRepo {
4
+ db;
5
+ constructor(db) {
6
+ this.db = db;
7
+ }
8
+ get(key) {
9
+ const row = this.db.prepare('SELECT value FROM kv WHERE key = ?').get(key);
10
+ return row?.value;
11
+ }
12
+ set(key, value) {
13
+ this.db
14
+ .prepare(`INSERT INTO kv (key, value, updated_at) VALUES (?, ?, ?)
15
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`)
16
+ .run(key, value, new Date().toISOString());
17
+ }
18
+ delete(key) {
19
+ this.db.prepare('DELETE FROM kv WHERE key = ?').run(key);
20
+ }
21
+ globalKillswitch() {
22
+ const v = this.get('killswitch.global');
23
+ return v === 'pause' || v === 'panic' ? v : 'none';
24
+ }
25
+ setGlobalKillswitch(state) {
26
+ this.set('killswitch.global', state);
27
+ }
28
+ isRoutePaused(routeId) {
29
+ return this.get(ROUTE_PAUSED_PREFIX + routeId) === '1';
30
+ }
31
+ setRoutePaused(routeId, paused) {
32
+ if (paused)
33
+ this.set(ROUTE_PAUSED_PREFIX + routeId, '1');
34
+ else
35
+ this.delete(ROUTE_PAUSED_PREFIX + routeId);
36
+ }
37
+ /** Every paused route id in one query. */
38
+ pausedRoutes() {
39
+ const rows = this.db
40
+ .prepare(`SELECT key FROM kv WHERE key LIKE ? AND value = '1'`)
41
+ .all(ROUTE_PAUSED_PREFIX + '%');
42
+ return new Set(rows.map((r) => r.key.slice(ROUTE_PAUSED_PREFIX.length)));
43
+ }
44
+ }
45
+ //# sourceMappingURL=kv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kv.js","sourceRoot":"","sources":["../../src/repo/kv.ts"],"names":[],"mappings":"AAKA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAG,eAAe,CAAA;AAElD,MAAM,OAAO,MAAM;IACG;IAApB,YAAoB,EAAM;QAAN,OAAE,GAAF,EAAE,CAAI;IAAG,CAAC;IAE9B,GAAG,CAAC,GAAW;QACb,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC,GAAG,CAAC,GAAG,CAE5D,CAAA;QACb,OAAO,GAAG,EAAE,KAAK,CAAA;IACnB,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,KAAa;QAC5B,IAAI,CAAC,EAAE;aACJ,OAAO,CACN;kGAC0F,CAC3F;aACA,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAA;IAC9C,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC1D,CAAC;IAED,gBAAgB;QACd,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;QACvC,OAAO,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IACpD,CAAC;IAED,mBAAmB,CAAC,KAAuB;QACzC,IAAI,CAAC,GAAG,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAA;IACtC,CAAC;IAED,aAAa,CAAC,OAAe;QAC3B,OAAO,IAAI,CAAC,GAAG,CAAC,mBAAmB,GAAG,OAAO,CAAC,KAAK,GAAG,CAAA;IACxD,CAAC;IAED,cAAc,CAAC,OAAe,EAAE,MAAe;QAC7C,IAAI,MAAM;YAAE,IAAI,CAAC,GAAG,CAAC,mBAAmB,GAAG,OAAO,EAAE,GAAG,CAAC,CAAA;;YACnD,IAAI,CAAC,MAAM,CAAC,mBAAmB,GAAG,OAAO,CAAC,CAAA;IACjD,CAAC;IAED,0CAA0C;IAC1C,YAAY;QACV,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE;aACjB,OAAO,CAAC,qDAAqD,CAAC;aAC9D,GAAG,CAAC,mBAAmB,GAAG,GAAG,CAAsB,CAAA;QACtD,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC1E,CAAC;CACF"}
@@ -0,0 +1,52 @@
1
+ import type { Db } from './db.js';
2
+ import { EventsRepo } from './repo/events.js';
3
+ import { KvRepo } from './repo/kv.js';
4
+ import { type ControlStore, type Logger, type EventRecord, type EventStatus, type EventStore, type EventStoreAdmin, type EventSummary, type InsertResult, type Killswitch, type Lease, type NewEvent, type RouteStats } from '@end-close/relay';
5
+ /** Run one synchronous better-sqlite3 call with busy retry and error classification. */
6
+ export declare function runSqlite<T>(op: string, fn: () => T, opts?: {
7
+ attempts?: number;
8
+ logger?: Logger;
9
+ }): Promise<T>;
10
+ export declare class SqliteEventStore implements EventStore, EventStoreAdmin {
11
+ readonly repo: EventsRepo;
12
+ private log;
13
+ constructor(db: Db, opts?: {
14
+ logger?: Logger;
15
+ });
16
+ private run;
17
+ insert(e: NewEvent): Promise<InsertResult>;
18
+ routesWithDueEvents(now: string): Promise<string[]>;
19
+ claimDue(routeId: string, now: string, limit: number, lease: Lease): Promise<EventRecord[]>;
20
+ markDelivered(ids: string[], deliveredAt: string, bulkRequestId: string | null): Promise<void>;
21
+ markFailed(ids: string[], nextAttemptAt: string, error: string): Promise<void>;
22
+ markParked(ids: string[], error: string): Promise<void>;
23
+ releaseDelivering(ids: string[], nextAttemptAt: string, error: string): Promise<number>;
24
+ recoverDelivering(now: string, owner?: string): Promise<number>;
25
+ parkExpired(now: string, maxAgeMs: number): Promise<number>;
26
+ pruneBatch(now: string, deliveredDays: number, ledgerDays: number, limit: number): Promise<{
27
+ wiped: number;
28
+ deleted: number;
29
+ }>;
30
+ getById(id: string): Promise<EventRecord | undefined>;
31
+ list(filter: {
32
+ status?: EventStatus;
33
+ route?: string;
34
+ limit?: number;
35
+ }): Promise<EventSummary[]>;
36
+ countByStatus(): Promise<Record<string, number>>;
37
+ perRouteStats(): Promise<RouteStats[]>;
38
+ replay(id: string): Promise<boolean>;
39
+ replayAllParked(): Promise<number>;
40
+ }
41
+ export declare class SqliteControlStore implements ControlStore {
42
+ readonly kv: KvRepo;
43
+ private log;
44
+ constructor(db: Db, opts?: {
45
+ logger?: Logger;
46
+ });
47
+ getKillswitch(): Promise<Killswitch>;
48
+ setKillswitch(state: Killswitch): Promise<void>;
49
+ isRoutePaused(routeId: string): Promise<boolean>;
50
+ setRoutePaused(routeId: string, paused: boolean): Promise<void>;
51
+ }
52
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/dist/store.js ADDED
@@ -0,0 +1,121 @@
1
+ import { EventsRepo } from './repo/events.js';
2
+ import { KvRepo } from './repo/kv.js';
3
+ import { BUSY_RETRY_ATTEMPTS, INGEST_BUSY_RETRY_ATTEMPTS, isSqliteBusy, withBusyRetry } from './busy.js';
4
+ import { noopLogger, StoreError, StoreUnavailableError, } from '@end-close/relay';
5
+ // SQLite implementations of the engine's storage contracts. Lock contention (SQLITE_BUSY,
6
+ // common on network filesystems such as EFS) is retried here and surfaced as
7
+ // StoreUnavailableError; every other failure becomes a StoreError carrying the operation
8
+ // name, with the original error as `cause`.
9
+ /** Run one synchronous better-sqlite3 call with busy retry and error classification. */
10
+ export async function runSqlite(op, fn, opts = {}) {
11
+ try {
12
+ return await withBusyRetry(op, fn, { attempts: opts.attempts ?? BUSY_RETRY_ATTEMPTS, logger: opts.logger ?? noopLogger });
13
+ }
14
+ catch (err) {
15
+ if (isSqliteBusy(err))
16
+ throw new StoreUnavailableError(err.message, op, { cause: err });
17
+ if (err instanceof StoreError)
18
+ throw err;
19
+ throw new StoreError(err.message, op, { cause: err });
20
+ }
21
+ }
22
+ function toRecord(row) {
23
+ const { payload_enc, payload_iv, ...rest } = row;
24
+ return {
25
+ ...rest,
26
+ id: String(row.id),
27
+ payload: payload_enc,
28
+ payload_iv: payload_iv.length === 0 ? null : payload_iv,
29
+ };
30
+ }
31
+ function toSummary(row) {
32
+ return { ...row, id: String(row.id) };
33
+ }
34
+ const numericIds = (ids) => ids.map(Number);
35
+ export class SqliteEventStore {
36
+ repo;
37
+ log;
38
+ constructor(db, opts = {}) {
39
+ this.repo = new EventsRepo(db);
40
+ this.log = opts.logger ?? noopLogger;
41
+ }
42
+ run(op, fn, attempts) {
43
+ return runSqlite(op, fn, { logger: this.log, ...(attempts !== undefined ? { attempts } : {}) });
44
+ }
45
+ async insert(e) {
46
+ const { payload, payload_iv, ...rest } = e;
47
+ const row = { ...rest, payload_enc: payload, payload_iv: payload_iv ?? Buffer.alloc(0) };
48
+ const id = await this.run('insert', () => this.repo.insert(row), INGEST_BUSY_RETRY_ATTEMPTS);
49
+ return id === null ? { duplicate: true } : { duplicate: false, id: String(id) };
50
+ }
51
+ routesWithDueEvents(now) {
52
+ return this.run('routesWithDueEvents', () => this.repo.routesWithDueEvents(now));
53
+ }
54
+ async claimDue(routeId, now, limit, lease) {
55
+ const rows = await this.run('claimDue', () => this.repo.claimDue(routeId, now, limit, lease.owner, lease.until));
56
+ return rows.map(toRecord);
57
+ }
58
+ markDelivered(ids, deliveredAt, bulkRequestId) {
59
+ return this.run('markDelivered', () => this.repo.markDelivered(numericIds(ids), deliveredAt, bulkRequestId));
60
+ }
61
+ markFailed(ids, nextAttemptAt, error) {
62
+ return this.run('markFailed', () => this.repo.markFailed(numericIds(ids), nextAttemptAt, error));
63
+ }
64
+ markParked(ids, error) {
65
+ return this.run('markParked', () => this.repo.markParked(numericIds(ids), error));
66
+ }
67
+ releaseDelivering(ids, nextAttemptAt, error) {
68
+ return this.run('releaseDelivering', () => this.repo.releaseDelivering(numericIds(ids), nextAttemptAt, error));
69
+ }
70
+ recoverDelivering(now, owner) {
71
+ return this.run('recoverDelivering', () => this.repo.recoverDelivering(now, owner));
72
+ }
73
+ parkExpired(now, maxAgeMs) {
74
+ return this.run('parkExpired', () => this.repo.parkExpired(now, maxAgeMs));
75
+ }
76
+ pruneBatch(now, deliveredDays, ledgerDays, limit) {
77
+ return this.run('prune', () => this.repo.pruneBatch(now, deliveredDays, ledgerDays, limit));
78
+ }
79
+ // ── admin capability ──
80
+ async getById(id) {
81
+ const row = await this.run('getById', () => this.repo.getById(Number(id)));
82
+ return row ? toRecord(row) : undefined;
83
+ }
84
+ async list(filter) {
85
+ const rows = await this.run('list', () => this.repo.list(filter));
86
+ return rows.map(toSummary);
87
+ }
88
+ countByStatus() {
89
+ return this.run('countByStatus', () => this.repo.countByStatus());
90
+ }
91
+ perRouteStats() {
92
+ return this.run('perRouteStats', () => this.repo.perRouteStats());
93
+ }
94
+ replay(id) {
95
+ return this.run('replay', () => this.repo.replay(Number(id)));
96
+ }
97
+ replayAllParked() {
98
+ return this.run('replayAllParked', () => this.repo.replayAllParked());
99
+ }
100
+ }
101
+ export class SqliteControlStore {
102
+ kv;
103
+ log;
104
+ constructor(db, opts = {}) {
105
+ this.kv = new KvRepo(db);
106
+ this.log = opts.logger ?? noopLogger;
107
+ }
108
+ getKillswitch() {
109
+ return runSqlite('killswitch', () => this.kv.globalKillswitch(), { logger: this.log });
110
+ }
111
+ setKillswitch(state) {
112
+ return runSqlite('setKillswitch', () => this.kv.setGlobalKillswitch(state), { logger: this.log });
113
+ }
114
+ isRoutePaused(routeId) {
115
+ return runSqlite('isPaused', () => this.kv.isRoutePaused(routeId), { logger: this.log });
116
+ }
117
+ setRoutePaused(routeId, paused) {
118
+ return runSqlite('setPaused', () => this.kv.setRoutePaused(routeId, paused), { logger: this.log });
119
+ }
120
+ }
121
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@end-close/relay-sqlite",
3
+ "version": "0.12.0",
4
+ "description": "SQLite EventStore and ControlStore for @end-close/relay (rollback journal + synchronous=FULL; safe on network filesystems).",
5
+ "keywords": [
6
+ "endclose",
7
+ "webhook",
8
+ "relay",
9
+ "sqlite",
10
+ "better-sqlite3"
11
+ ],
12
+ "license": "MIT",
13
+ "homepage": "https://github.com/End-Close/endclose-relay/tree/main/packages/store-sqlite#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/End-Close/endclose-relay/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/End-Close/endclose-relay.git",
20
+ "directory": "packages/store-sqlite"
21
+ },
22
+ "type": "module",
23
+ "sideEffects": false,
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "!dist/tsconfig.tsbuildinfo",
36
+ "src"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "provenance": true
41
+ },
42
+ "engines": {
43
+ "node": ">=22.12"
44
+ },
45
+ "dependencies": {
46
+ "better-sqlite3": "^11.10.0"
47
+ },
48
+ "peerDependencies": {
49
+ "@end-close/relay": "^0.12.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/better-sqlite3": "^7.6.12",
53
+ "@end-close/relay": "0.12.0",
54
+ "@end-close/relay-store-contract": "0.12.0"
55
+ },
56
+ "scripts": {
57
+ "build": "tsc -b tsconfig.json",
58
+ "typecheck": "tsc --noEmit -p tsconfig.json"
59
+ }
60
+ }
package/src/busy.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { noopLogger, type Logger } from '@end-close/relay'
2
+
3
+ export const SQLITE_BUSY_TIMEOUT_MS = 15_000
4
+ export const BUSY_RETRY_ATTEMPTS = 3
5
+ export const INGEST_BUSY_RETRY_ATTEMPTS = 2
6
+
7
+ const DEFAULT_DELAY_MS = 200
8
+
9
+ export function isSqliteBusy(err: unknown): boolean {
10
+ const code = (err as { code?: string } | null)?.code
11
+ return typeof code === 'string' && (code === 'SQLITE_BUSY' || code.startsWith('SQLITE_BUSY_'))
12
+ }
13
+
14
+ export interface BusyRetryOpts {
15
+ attempts?: number
16
+ delayMs?: number
17
+ logger?: Logger
18
+ }
19
+
20
+ /** Retry a synchronous SQLite call after SQLITE_BUSY; sqlite's busy_timeout already waited once. */
21
+ export async function withBusyRetry<T>(
22
+ op: string,
23
+ fn: () => T,
24
+ opts: BusyRetryOpts = {},
25
+ ): Promise<T> {
26
+ const attempts = opts.attempts ?? BUSY_RETRY_ATTEMPTS
27
+ const delayMs = opts.delayMs ?? DEFAULT_DELAY_MS
28
+ const logger = opts.logger ?? noopLogger
29
+ let last: unknown
30
+ for (let i = 0; i < attempts; i++) {
31
+ try {
32
+ return fn()
33
+ } catch (err) {
34
+ last = err
35
+ if (!isSqliteBusy(err) || i === attempts - 1) {
36
+ if (isSqliteBusy(err)) {
37
+ logger.warn('sqlite busy exhausted', {
38
+ op,
39
+ attempts,
40
+ error: (err as Error).message,
41
+ })
42
+ }
43
+ throw err
44
+ }
45
+ logger.warn('sqlite busy, retrying', {
46
+ op,
47
+ attempt: i + 1,
48
+ error: (err as Error).message,
49
+ })
50
+ await sleep(delayMs * (i + 1))
51
+ }
52
+ }
53
+ throw last
54
+ }
55
+
56
+ function sleep(ms: number): Promise<void> {
57
+ return new Promise((r) => setTimeout(r, ms))
58
+ }
package/src/db.ts ADDED
@@ -0,0 +1,21 @@
1
+ import Database from 'better-sqlite3'
2
+ import { mkdirSync } from 'node:fs'
3
+ import { dirname } from 'node:path'
4
+ import { SQLITE_BUSY_TIMEOUT_MS } from './busy.js'
5
+
6
+ export type Db = Database.Database
7
+
8
+ export function openDb(path: string): Db {
9
+ if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true })
10
+ const db = new Database(path)
11
+ // DELETE (rollback journal), not WAL: WAL needs shared-memory primitives that network
12
+ // filesystems (EFS/NFS) do not provide correctly. Durability comes from synchronous=FULL
13
+ // below — an acked webhook must survive power loss (the store-and-forward contract).
14
+ db.pragma('journal_mode = DELETE')
15
+ db.pragma('synchronous = FULL')
16
+ db.pragma('foreign_keys = ON')
17
+ // EFS lock/fsync can exceed a few seconds; sqlite retries internally up to this timeout
18
+ // before throwing SQLITE_BUSY. Application-level retries sit on top (see withBusyRetry).
19
+ db.pragma(`busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`)
20
+ return db
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ // @end-close/relay-sqlite — SQLite storage for the End Close relay engine.
2
+ export { openDb, type Db } from './db.js'
3
+ export { migrate, MIGRATIONS, type Migration } from './migrations.js'
4
+ export { withBusyRetry, isSqliteBusy } from './busy.js'
5
+ export { SqliteEventStore, SqliteControlStore, runSqlite } from './store.js'
6
+ export { EventsRepo, type EventRow } from './repo/events.js'
7
+ export { KvRepo, ROUTE_PAUSED_PREFIX, type GlobalKillswitch } from './repo/kv.js'
@@ -0,0 +1,80 @@
1
+ import type { Db } from './db.js'
2
+
3
+ // Schema migrations, inlined so the package is bundler-friendly. Names are the keys in
4
+ // schema_migrations, so they must never change once shipped. `001_init.sql` once created
5
+ // the application's tables too; those now live in the application's own migration set, which
6
+ // uses IF NOT EXISTS so both fresh and upgraded databases converge.
7
+
8
+ export interface Migration {
9
+ name: string
10
+ sql: string
11
+ }
12
+
13
+ export const MIGRATIONS: Migration[] = [
14
+ {
15
+ name: '001_init.sql',
16
+ sql: `
17
+ CREATE TABLE events (
18
+ id INTEGER PRIMARY KEY,
19
+ route_id TEXT NOT NULL,
20
+ source TEXT NOT NULL,
21
+ event_id TEXT NOT NULL,
22
+ event_type TEXT,
23
+ payload_enc BLOB NOT NULL,
24
+ payload_iv BLOB NOT NULL,
25
+ headers_json TEXT NOT NULL,
26
+ received_at TEXT NOT NULL,
27
+ status TEXT NOT NULL DEFAULT 'pending'
28
+ CHECK (status IN ('pending','retry','delivering','delivered','parked','dropped_by_filter')),
29
+ attempts INTEGER NOT NULL DEFAULT 0,
30
+ next_attempt_at TEXT,
31
+ delivered_at TEXT,
32
+ bulk_request_id TEXT,
33
+ last_error TEXT,
34
+ idempotency_key TEXT NOT NULL,
35
+ UNIQUE (idempotency_key)
36
+ );
37
+ CREATE INDEX idx_events_dispatch ON events (status, next_attempt_at, route_id, id);
38
+ CREATE INDEX idx_events_route_status ON events (route_id, status);
39
+
40
+ CREATE TABLE kv (
41
+ key TEXT PRIMARY KEY,
42
+ value TEXT NOT NULL,
43
+ updated_at TEXT NOT NULL
44
+ );
45
+ `,
46
+ },
47
+ {
48
+ // Lease-based claiming: a crashed or slow instance's rows are recoverable by others.
49
+ name: '002_leases.sql',
50
+ sql: `
51
+ ALTER TABLE events ADD COLUMN claimed_by TEXT;
52
+ ALTER TABLE events ADD COLUMN lease_until TEXT;
53
+ `,
54
+ },
55
+ ]
56
+
57
+ export function migrate(db: Db, sets: Migration[][] = [MIGRATIONS]): void {
58
+ db.exec(
59
+ 'CREATE TABLE IF NOT EXISTS schema_migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)',
60
+ )
61
+ const applied = new Set(
62
+ db
63
+ .prepare('SELECT name FROM schema_migrations')
64
+ .all()
65
+ .map((r) => (r as { name: string }).name),
66
+ )
67
+ for (const set of sets) {
68
+ for (const m of set) {
69
+ if (applied.has(m.name)) continue
70
+ const run = db.transaction(() => {
71
+ db.exec(m.sql)
72
+ db.prepare('INSERT INTO schema_migrations (name, applied_at) VALUES (?, ?)').run(
73
+ m.name,
74
+ new Date().toISOString(),
75
+ )
76
+ })
77
+ run()
78
+ }
79
+ }
80
+ }