@spfn/core 0.2.0-beta.9 → 0.3.0-beta.2
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/LICENSE +1 -1
- package/README.md +467 -305
- package/dist/authz/index.d.ts +34 -0
- package/dist/authz/index.js +810 -0
- package/dist/authz/index.js.map +1 -0
- package/dist/{boss-DI1r4kTS.d.ts → boss-D16fO2oG.d.ts} +41 -1
- package/dist/cache/index.js +42 -30
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +121 -13
- package/dist/codegen/index.js +212 -15
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +615 -6
- package/dist/config/index.js +124 -5
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +220 -0
- package/dist/contract/index.js +558 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/db/index.d.ts +528 -85
- package/dist/db/index.js +831 -122
- package/dist/db/index.js.map +1 -1
- package/dist/define-middleware-DfDP39Nq.d.ts +167 -0
- package/dist/env/index.d.ts +26 -2
- package/dist/env/index.js +15 -5
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +26 -19
- package/dist/env/loader.js +32 -25
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.js +418 -5
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +33 -3
- package/dist/event/index.js +24 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +42 -3
- package/dist/event/sse/client.js +128 -45
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +12 -5
- package/dist/event/sse/index.js +280 -32
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +59 -0
- package/dist/event/ws/client.js +273 -0
- package/dist/event/ws/client.js.map +1 -0
- package/dist/event/ws/index.d.ts +94 -0
- package/dist/event/ws/index.js +272 -0
- package/dist/event/ws/index.js.map +1 -0
- package/dist/job/index.d.ts +2 -2
- package/dist/job/index.js +155 -42
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.js +14 -0
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +347 -9
- package/dist/middleware/index.js +1462 -15
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +42 -28
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +35 -51
- package/dist/nextjs/server.js +126 -60
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +152 -0
- package/dist/ops/index.js +500 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/route/index.d.ts +8 -694
- package/dist/route/index.js +111 -22
- package/dist/route/index.js.map +1 -1
- package/dist/router-Qbssr11H.d.ts +676 -0
- package/dist/security/index.d.ts +83 -0
- package/dist/security/index.js +173 -0
- package/dist/security/index.js.map +1 -0
- package/dist/server/index.d.ts +491 -22
- package/dist/server/index.js +1887 -308
- package/dist/server/index.js.map +1 -1
- package/dist/token-manager-BT5EnUAR.d.ts +278 -0
- package/dist/types-2AbaW4Ie.d.ts +205 -0
- package/dist/{types-BOPTApC2.d.ts → types-9oszaJqp.d.ts} +7 -2
- package/dist/types-D1c57Ko-.d.ts +115 -0
- package/dist/types-ZQODsBft.d.ts +282 -0
- package/package.json +244 -208
- package/dist/router-Di7ENoah.d.ts +0 -151
- package/dist/types-B-e_f2dQ.d.ts +0 -121
- package/docs/cache.md +0 -133
- package/docs/codegen.md +0 -74
- package/docs/database.md +0 -346
- package/docs/entity.md +0 -539
- package/docs/env.md +0 -477
- package/docs/errors.md +0 -319
- package/docs/event.md +0 -116
- package/docs/job.md +0 -131
- package/docs/logger.md +0 -108
- package/docs/middleware.md +0 -337
- package/docs/nextjs.md +0 -241
- package/docs/repository.md +0 -496
- package/docs/route.md +0 -497
- package/docs/server.md +0 -307
package/dist/db/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
2
2
|
import { env } from '@spfn/core/config';
|
|
3
3
|
import { logger } from '@spfn/core/logger';
|
|
4
|
+
import net from 'net';
|
|
4
5
|
import postgres from 'postgres';
|
|
5
6
|
import { QueryError, ConnectionError, DeadlockError, TransactionError, ConstraintViolationError, DuplicateEntryError, DatabaseError } from '@spfn/core/errors';
|
|
6
7
|
import { parseNumber, parseBoolean } from '@spfn/core/env';
|
|
7
|
-
import { existsSync,
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
8
9
|
import { join, dirname, basename } from 'path';
|
|
9
|
-
import {
|
|
10
|
+
import { sql, count as count$1, lt, gt, and, desc, asc, eq } from 'drizzle-orm';
|
|
11
|
+
import { bigserial, timestamp, bigint, uuid as uuid$1, text, jsonb, pgSchema } from 'drizzle-orm/pg-core';
|
|
12
|
+
import { createHash, randomUUID } from 'crypto';
|
|
10
13
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
11
14
|
import { createMiddleware } from 'hono/factory';
|
|
12
|
-
import { randomUUID } from 'crypto';
|
|
13
|
-
import { count as count$1, sql, eq, and } from 'drizzle-orm';
|
|
14
15
|
|
|
15
16
|
// src/db/manager/factory.ts
|
|
16
17
|
function parseUniqueViolation(message) {
|
|
@@ -129,7 +130,16 @@ function fromPostgresError(error) {
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
// src/db/manager/connection.ts
|
|
133
|
+
function getSocketFamily() {
|
|
134
|
+
const family = process.env.DATABASE_SOCKET_FAMILY;
|
|
135
|
+
if (family === "4") return 4;
|
|
136
|
+
if (family === "6") return 6;
|
|
137
|
+
return void 0;
|
|
138
|
+
}
|
|
132
139
|
var dbLogger = logger.child("@spfn/core:database");
|
|
140
|
+
function isTransactionPooler(connectionString) {
|
|
141
|
+
return /:6543(?:[/?]|$)|[?&]pgbouncer=true\b|pooler\.supabase\.com/i.test(connectionString);
|
|
142
|
+
}
|
|
133
143
|
var DEFAULT_CONNECT_TIMEOUT = 10;
|
|
134
144
|
function delay(ms) {
|
|
135
145
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -189,10 +199,23 @@ async function createDatabaseConnection(connectionString, poolConfig, retryConfi
|
|
|
189
199
|
let client;
|
|
190
200
|
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
|
|
191
201
|
try {
|
|
202
|
+
const socketFamily = getSocketFamily();
|
|
203
|
+
const prepare = poolConfig.prepare ?? !isTransactionPooler(connectionString);
|
|
192
204
|
client = postgres(connectionString, {
|
|
193
205
|
max: poolConfig.max,
|
|
194
206
|
idle_timeout: poolConfig.idleTimeout,
|
|
195
|
-
connect_timeout: DEFAULT_CONNECT_TIMEOUT
|
|
207
|
+
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
|
|
208
|
+
prepare,
|
|
209
|
+
...socketFamily && {
|
|
210
|
+
socket: ({ host, port }) => new Promise((resolve, reject) => {
|
|
211
|
+
const socket = new net.Socket();
|
|
212
|
+
socket.on("error", reject);
|
|
213
|
+
socket.connect(
|
|
214
|
+
{ port: port[0], host: host[0], family: socketFamily },
|
|
215
|
+
() => resolve(socket)
|
|
216
|
+
);
|
|
217
|
+
})
|
|
218
|
+
}
|
|
196
219
|
});
|
|
197
220
|
await client`SELECT 1 as test`;
|
|
198
221
|
if (attempt > 0) {
|
|
@@ -325,10 +348,27 @@ function parseEnvBoolean(key, defaultValue) {
|
|
|
325
348
|
throw new Error(`${key}: ${message}`);
|
|
326
349
|
}
|
|
327
350
|
}
|
|
351
|
+
function parseEnvBooleanOptional(key) {
|
|
352
|
+
const value = process.env[key];
|
|
353
|
+
if (value === void 0) {
|
|
354
|
+
return void 0;
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
return parseBoolean(value);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
360
|
+
throw new Error(`${key}: ${message}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
328
363
|
function getPoolConfig(options) {
|
|
364
|
+
const max = options?.max ?? parseEnvNumber("DB_POOL_MAX", 20, 10);
|
|
329
365
|
return {
|
|
330
|
-
max
|
|
331
|
-
idleTimeout: options?.idleTimeout ?? parseEnvNumber("DB_POOL_IDLE_TIMEOUT", 30, 20)
|
|
366
|
+
max,
|
|
367
|
+
idleTimeout: options?.idleTimeout ?? parseEnvNumber("DB_POOL_IDLE_TIMEOUT", 30, 20),
|
|
368
|
+
// Defaults to the write `max`; DB_POOL_READ_MAX lets ops size the replica pool separately
|
|
369
|
+
readMax: options?.readMax ?? parseEnvNumber("DB_POOL_READ_MAX", max, max),
|
|
370
|
+
// Left undefined for connection-layer auto-detection unless explicitly overridden.
|
|
371
|
+
prepare: options?.prepare ?? parseEnvBooleanOptional("SPFN_DB_PREPARE")
|
|
332
372
|
};
|
|
333
373
|
}
|
|
334
374
|
function getRetryConfig() {
|
|
@@ -397,11 +437,12 @@ async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig
|
|
|
397
437
|
dbLogger2.error("Failed to connect to write database", errorObj);
|
|
398
438
|
throw new Error(`Write database connection failed: ${errorObj.message}`, { cause: error });
|
|
399
439
|
}
|
|
440
|
+
const readPoolConfig = { ...poolConfig, max: poolConfig.readMax ?? poolConfig.max };
|
|
400
441
|
try {
|
|
401
|
-
readClient = await createDatabaseConnection(readUrl,
|
|
442
|
+
readClient = await createDatabaseConnection(readUrl, readPoolConfig, retryConfig);
|
|
402
443
|
return {
|
|
403
|
-
write: drizzle(writeClient),
|
|
404
|
-
read: drizzle(readClient),
|
|
444
|
+
write: drizzle({ client: writeClient }),
|
|
445
|
+
read: drizzle({ client: readClient }),
|
|
405
446
|
writeClient,
|
|
406
447
|
readClient
|
|
407
448
|
};
|
|
@@ -417,8 +458,8 @@ async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig
|
|
|
417
458
|
}
|
|
418
459
|
);
|
|
419
460
|
return {
|
|
420
|
-
write: drizzle(writeClient),
|
|
421
|
-
read: drizzle(writeClient),
|
|
461
|
+
write: drizzle({ client: writeClient }),
|
|
462
|
+
read: drizzle({ client: writeClient }),
|
|
422
463
|
writeClient,
|
|
423
464
|
readClient: writeClient
|
|
424
465
|
};
|
|
@@ -426,7 +467,7 @@ async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig
|
|
|
426
467
|
}
|
|
427
468
|
async function createSingleClient(url, poolConfig, retryConfig) {
|
|
428
469
|
const client = await createDatabaseConnection(url, poolConfig, retryConfig);
|
|
429
|
-
const db = drizzle(client);
|
|
470
|
+
const db = drizzle({ client });
|
|
430
471
|
return {
|
|
431
472
|
write: db,
|
|
432
473
|
read: db,
|
|
@@ -478,6 +519,11 @@ async function createDatabaseFromEnv(options) {
|
|
|
478
519
|
throw new Error("No database pattern detected despite passing config check");
|
|
479
520
|
}
|
|
480
521
|
|
|
522
|
+
// src/db/manager/path-utils.ts
|
|
523
|
+
function toPosixPath(path) {
|
|
524
|
+
return path.split("\\").join("/");
|
|
525
|
+
}
|
|
526
|
+
|
|
481
527
|
// src/db/manager/global-state.ts
|
|
482
528
|
var getWriteInstance = () => globalThis.__SPFN_DB_WRITE__;
|
|
483
529
|
var setWriteInstance = (instance) => {
|
|
@@ -487,6 +533,10 @@ var getReadInstance = () => globalThis.__SPFN_DB_READ__;
|
|
|
487
533
|
var setReadInstance = (instance) => {
|
|
488
534
|
globalThis.__SPFN_DB_READ__ = instance;
|
|
489
535
|
};
|
|
536
|
+
var getDatabaseProvider = () => globalThis.__SPFN_DB_PROVIDER__;
|
|
537
|
+
var setDatabaseProviderInstance = (provider) => {
|
|
538
|
+
globalThis.__SPFN_DB_PROVIDER__ = provider;
|
|
539
|
+
};
|
|
490
540
|
var getWriteClient = () => globalThis.__SPFN_DB_WRITE_CLIENT__;
|
|
491
541
|
var setWriteClient = (client) => {
|
|
492
542
|
globalThis.__SPFN_DB_WRITE_CLIENT__ = client;
|
|
@@ -502,7 +552,32 @@ var setHealthCheckInterval = (interval) => {
|
|
|
502
552
|
var setMonitoringConfig = (config) => {
|
|
503
553
|
globalThis.__SPFN_DB_MONITORING__ = config;
|
|
504
554
|
};
|
|
555
|
+
var getInitOptions = () => globalThis.__SPFN_DB_INIT_OPTIONS__;
|
|
556
|
+
var setInitOptions = (options) => {
|
|
557
|
+
globalThis.__SPFN_DB_INIT_OPTIONS__ = options;
|
|
558
|
+
};
|
|
559
|
+
var getInitPromise = () => globalThis.__SPFN_DB_INIT_PROMISE__;
|
|
560
|
+
var setInitPromise = (promise) => {
|
|
561
|
+
globalThis.__SPFN_DB_INIT_PROMISE__ = promise;
|
|
562
|
+
};
|
|
563
|
+
var getInitProvider = () => globalThis.__SPFN_DB_INIT_PROVIDER__;
|
|
564
|
+
var setInitProvider = (provider) => {
|
|
565
|
+
globalThis.__SPFN_DB_INIT_PROVIDER__ = provider;
|
|
566
|
+
};
|
|
567
|
+
var getClosePromise = () => globalThis.__SPFN_DB_CLOSE_PROMISE__;
|
|
568
|
+
var setClosePromise = (promise) => {
|
|
569
|
+
globalThis.__SPFN_DB_CLOSE_PROMISE__ = promise;
|
|
570
|
+
};
|
|
571
|
+
var getIsClosing = () => globalThis.__SPFN_DB_CLOSING__ === true;
|
|
572
|
+
var setIsClosing = (closing) => {
|
|
573
|
+
globalThis.__SPFN_DB_CLOSING__ = closing;
|
|
574
|
+
};
|
|
505
575
|
var dbLogger3 = logger.child("@spfn/core:database");
|
|
576
|
+
var CLIENT_CLOSE_TIMEOUT = 5;
|
|
577
|
+
var isReconnecting = false;
|
|
578
|
+
function isReconnectingNow() {
|
|
579
|
+
return isReconnecting;
|
|
580
|
+
}
|
|
506
581
|
async function testDatabaseConnection(db) {
|
|
507
582
|
await db.execute("SELECT 1");
|
|
508
583
|
}
|
|
@@ -514,8 +589,17 @@ async function performHealthCheck(getDatabase2) {
|
|
|
514
589
|
await testDatabaseConnection(read);
|
|
515
590
|
}
|
|
516
591
|
}
|
|
517
|
-
async function
|
|
518
|
-
|
|
592
|
+
async function closeClient(client) {
|
|
593
|
+
try {
|
|
594
|
+
await client.end({ timeout: CLIENT_CLOSE_TIMEOUT });
|
|
595
|
+
} catch {
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async function reconnectAndRestore(options) {
|
|
599
|
+
if (getIsClosing()) {
|
|
600
|
+
dbLogger3.debug("reconnectAndRestore aborted: database is closing");
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
519
603
|
const result = await createDatabaseFromEnv(options);
|
|
520
604
|
if (!result.write) {
|
|
521
605
|
return false;
|
|
@@ -524,15 +608,33 @@ async function reconnectAndRestore(options, closeDatabase2) {
|
|
|
524
608
|
if (result.read && result.read !== result.write) {
|
|
525
609
|
await testDatabaseConnection(result.read);
|
|
526
610
|
}
|
|
611
|
+
if (getIsClosing()) {
|
|
612
|
+
dbLogger3.warn("reconnectAndRestore: close started mid-rebuild, discarding new pool");
|
|
613
|
+
if (result.writeClient) {
|
|
614
|
+
await closeClient(result.writeClient);
|
|
615
|
+
}
|
|
616
|
+
if (result.readClient && result.readClient !== result.writeClient) {
|
|
617
|
+
await closeClient(result.readClient);
|
|
618
|
+
}
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
const oldWriteClient = getWriteClient();
|
|
622
|
+
const oldReadClient = getReadClient();
|
|
527
623
|
setWriteInstance(result.write);
|
|
528
624
|
setReadInstance(result.read);
|
|
529
625
|
setWriteClient(result.writeClient);
|
|
530
626
|
setReadClient(result.readClient);
|
|
531
627
|
const monConfig = buildMonitoringConfig(options?.monitoring);
|
|
532
628
|
setMonitoringConfig(monConfig);
|
|
629
|
+
if (oldWriteClient) {
|
|
630
|
+
closeClient(oldWriteClient);
|
|
631
|
+
}
|
|
632
|
+
if (oldReadClient && oldReadClient !== oldWriteClient) {
|
|
633
|
+
closeClient(oldReadClient);
|
|
634
|
+
}
|
|
533
635
|
return true;
|
|
534
636
|
}
|
|
535
|
-
function startHealthCheck(config, options, getDatabase2
|
|
637
|
+
function startHealthCheck(config, options, getDatabase2) {
|
|
536
638
|
const healthCheck = getHealthCheckInterval();
|
|
537
639
|
if (healthCheck) {
|
|
538
640
|
dbLogger3.debug("Health check already running");
|
|
@@ -543,48 +645,85 @@ function startHealthCheck(config, options, getDatabase2, closeDatabase2) {
|
|
|
543
645
|
reconnect: config.reconnect
|
|
544
646
|
});
|
|
545
647
|
const interval = setInterval(async () => {
|
|
648
|
+
if (isReconnecting) {
|
|
649
|
+
dbLogger3.debug("Health check skipped: reconnection in progress");
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
546
652
|
try {
|
|
547
653
|
await performHealthCheck(getDatabase2);
|
|
548
654
|
} catch (error) {
|
|
549
655
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
550
656
|
dbLogger3.error("Database health check failed", { error: message });
|
|
551
657
|
if (config.reconnect) {
|
|
552
|
-
await attemptReconnection(config, options,
|
|
658
|
+
await attemptReconnection(config, options, "health_check_failed");
|
|
553
659
|
}
|
|
554
660
|
}
|
|
555
661
|
}, config.interval);
|
|
556
662
|
setHealthCheckInterval(interval);
|
|
557
663
|
}
|
|
558
|
-
async function
|
|
664
|
+
async function triggerForceReconnect(reason) {
|
|
665
|
+
if (getDatabaseProvider()) {
|
|
666
|
+
dbLogger3.debug("Force reconnect skipped: database is externally provided", { reason });
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
if (!getWriteInstance()) {
|
|
670
|
+
dbLogger3.warn("Force reconnect skipped: database not initialized", { reason });
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
if (getIsClosing()) {
|
|
674
|
+
dbLogger3.debug("Force reconnect skipped: database is closing", { reason });
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
const options = getInitOptions();
|
|
678
|
+
const config = buildHealthCheckConfig(options?.healthCheck);
|
|
679
|
+
dbLogger3.warn("Force reconnect triggered", { reason });
|
|
680
|
+
return await attemptReconnection(config, options, reason);
|
|
681
|
+
}
|
|
682
|
+
async function attemptReconnection(config, options, reason) {
|
|
683
|
+
if (getDatabaseProvider()) {
|
|
684
|
+
dbLogger3.debug("Reconnection skipped: database is externally provided", { reason });
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
if (isReconnecting) {
|
|
688
|
+
dbLogger3.debug("Reconnection coalesced: attempt already in progress", { reason });
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
isReconnecting = true;
|
|
559
692
|
dbLogger3.warn("Attempting database reconnection", {
|
|
693
|
+
reason,
|
|
560
694
|
maxRetries: config.maxRetries,
|
|
561
695
|
retryInterval: `${config.retryInterval}ms`
|
|
562
696
|
});
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
697
|
+
try {
|
|
698
|
+
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
|
|
699
|
+
try {
|
|
700
|
+
dbLogger3.debug(`Reconnection attempt ${attempt}/${config.maxRetries}`);
|
|
701
|
+
if (attempt > 1) {
|
|
702
|
+
await new Promise((resolve) => setTimeout(resolve, config.retryInterval));
|
|
703
|
+
}
|
|
704
|
+
const success = await reconnectAndRestore(options);
|
|
705
|
+
if (success) {
|
|
706
|
+
dbLogger3.info("Database reconnection successful", { attempt });
|
|
707
|
+
return true;
|
|
708
|
+
} else {
|
|
709
|
+
dbLogger3.error(`Reconnection attempt ${attempt} failed: No write database instance created`);
|
|
710
|
+
}
|
|
711
|
+
} catch (error) {
|
|
712
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
713
|
+
dbLogger3.error(`Reconnection attempt ${attempt} failed`, {
|
|
714
|
+
error: message,
|
|
715
|
+
attempt,
|
|
716
|
+
maxRetries: config.maxRetries
|
|
717
|
+
});
|
|
568
718
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
dbLogger3.info("Database reconnection successful", { attempt });
|
|
572
|
-
return;
|
|
573
|
-
} else {
|
|
574
|
-
dbLogger3.error(`Reconnection attempt ${attempt} failed: No write database instance created`);
|
|
719
|
+
if (attempt === config.maxRetries) {
|
|
720
|
+
dbLogger3.error("Max reconnection attempts reached, will retry on next health check");
|
|
575
721
|
}
|
|
576
|
-
} catch (error) {
|
|
577
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
578
|
-
dbLogger3.error(`Reconnection attempt ${attempt} failed`, {
|
|
579
|
-
error: message,
|
|
580
|
-
attempt,
|
|
581
|
-
maxRetries: config.maxRetries
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
if (attempt === config.maxRetries) {
|
|
585
|
-
dbLogger3.error("Max reconnection attempts reached, giving up");
|
|
586
722
|
}
|
|
723
|
+
} finally {
|
|
724
|
+
isReconnecting = false;
|
|
587
725
|
}
|
|
726
|
+
return true;
|
|
588
727
|
}
|
|
589
728
|
function stopHealthCheck() {
|
|
590
729
|
const healthCheck = getHealthCheckInterval();
|
|
@@ -593,6 +732,7 @@ function stopHealthCheck() {
|
|
|
593
732
|
setHealthCheckInterval(void 0);
|
|
594
733
|
dbLogger3.info("Database health check stopped");
|
|
595
734
|
}
|
|
735
|
+
isReconnecting = false;
|
|
596
736
|
}
|
|
597
737
|
|
|
598
738
|
// src/db/manager/manager.ts
|
|
@@ -603,8 +743,6 @@ var STACK_TRACE_PATTERNS = {
|
|
|
603
743
|
withParens: /\((.+):(\d+):(\d+)\)/,
|
|
604
744
|
withoutParens: /at (.+):(\d+):(\d+)/
|
|
605
745
|
};
|
|
606
|
-
var initPromise = null;
|
|
607
|
-
var isClosing = false;
|
|
608
746
|
async function cleanupDatabaseConnections(writeClient, readClient) {
|
|
609
747
|
const cleanupPromises = [];
|
|
610
748
|
if (writeClient) {
|
|
@@ -642,13 +780,40 @@ async function testDatabaseConnections(write, read) {
|
|
|
642
780
|
}
|
|
643
781
|
}
|
|
644
782
|
}
|
|
783
|
+
function validateDatabaseProvider(provider) {
|
|
784
|
+
if (typeof provider.kind !== "string" || !provider.kind.trim()) {
|
|
785
|
+
throw new Error("Database provider kind must be a non-empty string");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function registerDatabaseProvider(provider) {
|
|
789
|
+
setDatabaseProviderInstance(provider);
|
|
790
|
+
setWriteInstance(provider.write);
|
|
791
|
+
setReadInstance(provider.read ?? provider.write);
|
|
792
|
+
return {
|
|
793
|
+
write: provider.write,
|
|
794
|
+
read: provider.read ?? provider.write
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
async function closeDatabaseProvider(provider, context) {
|
|
798
|
+
if (!provider.close) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
await provider.close();
|
|
803
|
+
} catch (error) {
|
|
804
|
+
const closeError = error instanceof Error ? error : new Error(String(error));
|
|
805
|
+
dbLogger4.error(`Error closing ${provider.kind || "unknown"} database provider`, closeError, {
|
|
806
|
+
context
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
645
810
|
function getCallerInfo() {
|
|
646
811
|
try {
|
|
647
812
|
const stack = new Error().stack;
|
|
648
813
|
if (!stack) return void 0;
|
|
649
814
|
const lines = stack.split("\n");
|
|
650
815
|
for (let i = STACK_TRACE_SKIP_LINES; i < lines.length; i++) {
|
|
651
|
-
const line = lines[i];
|
|
816
|
+
const line = toPosixPath(lines[i]);
|
|
652
817
|
if (!line.includes("node_modules") && !line.includes("/db/manager/")) {
|
|
653
818
|
const match = line.match(STACK_TRACE_PATTERNS.withParens) || line.match(STACK_TRACE_PATTERNS.withoutParens);
|
|
654
819
|
if (match) {
|
|
@@ -701,24 +866,107 @@ function getDatabase(type) {
|
|
|
701
866
|
return writeInst;
|
|
702
867
|
}
|
|
703
868
|
function setDatabase(write, read) {
|
|
869
|
+
if (getIsClosing()) {
|
|
870
|
+
throw new Error("Cannot set database while closing");
|
|
871
|
+
}
|
|
872
|
+
if (getInitPromise()) {
|
|
873
|
+
throw new Error("Cannot set database while initialization is in progress");
|
|
874
|
+
}
|
|
875
|
+
if (getDatabaseProvider()) {
|
|
876
|
+
throw new Error(
|
|
877
|
+
"An external database provider is active. Call closeDatabase() before setting a database manually."
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
if (getWriteClient() || getReadClient()) {
|
|
881
|
+
throw new Error(
|
|
882
|
+
"Managed database connections are active. Call closeDatabase() before setting a database manually."
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
setDatabaseProviderInstance(void 0);
|
|
704
886
|
setWriteInstance(write);
|
|
705
887
|
setReadInstance(read ?? write);
|
|
706
888
|
}
|
|
889
|
+
function setDatabaseProvider(provider) {
|
|
890
|
+
validateDatabaseProvider(provider);
|
|
891
|
+
const activeProvider = getDatabaseProvider();
|
|
892
|
+
if (getIsClosing()) {
|
|
893
|
+
throw new Error("Cannot set database provider while closing");
|
|
894
|
+
}
|
|
895
|
+
if (getInitPromise()) {
|
|
896
|
+
throw new Error("Cannot set database provider while initialization is in progress");
|
|
897
|
+
}
|
|
898
|
+
if (activeProvider === provider) {
|
|
899
|
+
return {
|
|
900
|
+
write: provider.write,
|
|
901
|
+
read: provider.read ?? provider.write
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
if (getWriteInstance() || getReadInstance() || getWriteClient() || getReadClient() || activeProvider) {
|
|
905
|
+
throw new Error(
|
|
906
|
+
"Database already initialized. Call closeDatabase() before registering a different provider."
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
return registerDatabaseProvider(provider);
|
|
910
|
+
}
|
|
707
911
|
async function initDatabase(options) {
|
|
708
|
-
if (
|
|
912
|
+
if (getIsClosing()) {
|
|
709
913
|
throw new Error("Cannot initialize database while closing");
|
|
710
914
|
}
|
|
711
915
|
const writeInst = getWriteInstance();
|
|
712
916
|
if (writeInst) {
|
|
917
|
+
const activeProvider = getDatabaseProvider();
|
|
918
|
+
if (!activeProvider && !getWriteClient()) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
"Database was registered manually. Use getDatabase() directly or call closeDatabase() before initDatabase()."
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
if (options?.provider !== activeProvider) {
|
|
924
|
+
const detail = options?.provider ? "a different provider was supplied" : "the active external provider was not supplied";
|
|
925
|
+
throw new Error(`Database already initialized: ${detail}`);
|
|
926
|
+
}
|
|
713
927
|
dbLogger4.debug("Database already initialized");
|
|
714
|
-
return {
|
|
928
|
+
return {
|
|
929
|
+
write: writeInst,
|
|
930
|
+
read: getReadInstance()
|
|
931
|
+
};
|
|
715
932
|
}
|
|
716
|
-
|
|
933
|
+
const activeInitPromise = getInitPromise();
|
|
934
|
+
if (activeInitPromise) {
|
|
935
|
+
if (options?.provider !== getInitProvider()) {
|
|
936
|
+
throw new Error("Database initialization already in progress with a different provider");
|
|
937
|
+
}
|
|
717
938
|
dbLogger4.debug("Database initialization in progress, waiting...");
|
|
718
|
-
return await
|
|
939
|
+
return await activeInitPromise;
|
|
719
940
|
}
|
|
720
|
-
|
|
941
|
+
setInitProvider(options?.provider);
|
|
942
|
+
const currentInit = (async () => {
|
|
721
943
|
try {
|
|
944
|
+
if (options?.provider) {
|
|
945
|
+
const provider = options.provider;
|
|
946
|
+
try {
|
|
947
|
+
validateDatabaseProvider(provider);
|
|
948
|
+
await testDatabaseConnections(provider.write, provider.read);
|
|
949
|
+
if (getIsClosing()) {
|
|
950
|
+
throw new Error("Database closed during initialization");
|
|
951
|
+
}
|
|
952
|
+
const monConfig2 = buildMonitoringConfig(options.monitoring);
|
|
953
|
+
const result2 = registerDatabaseProvider(provider);
|
|
954
|
+
setInitOptions(options);
|
|
955
|
+
setMonitoringConfig(monConfig2);
|
|
956
|
+
dbLogger4.info("Database provider connected", {
|
|
957
|
+
kind: provider.kind,
|
|
958
|
+
hasReplica: !!(provider.read && provider.read !== provider.write)
|
|
959
|
+
});
|
|
960
|
+
return result2;
|
|
961
|
+
} catch (error) {
|
|
962
|
+
await closeDatabaseProvider(provider, "initialization_failed");
|
|
963
|
+
if (error instanceof Error && (error.message === "Database provider kind must be a non-empty string" || error.message === "Database closed during initialization")) {
|
|
964
|
+
throw error;
|
|
965
|
+
}
|
|
966
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
967
|
+
throw new Error(`Database connection test failed: ${message}`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
722
970
|
const result = await createDatabaseFromEnv(options);
|
|
723
971
|
try {
|
|
724
972
|
await testDatabaseConnections(result.write, result.read);
|
|
@@ -727,7 +975,7 @@ async function initDatabase(options) {
|
|
|
727
975
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
728
976
|
throw new Error(`Database connection test failed: ${message}`);
|
|
729
977
|
}
|
|
730
|
-
if (
|
|
978
|
+
if (getIsClosing()) {
|
|
731
979
|
dbLogger4.warn("Database closed during initialization, cleaning up...");
|
|
732
980
|
await cleanupDatabaseConnections(result.writeClient, result.readClient);
|
|
733
981
|
throw new Error("Database closed during initialization");
|
|
@@ -736,13 +984,14 @@ async function initDatabase(options) {
|
|
|
736
984
|
setReadInstance(result.read);
|
|
737
985
|
setWriteClient(result.writeClient);
|
|
738
986
|
setReadClient(result.readClient);
|
|
987
|
+
setInitOptions(options);
|
|
739
988
|
const hasReplica = result.read && result.read !== result.write;
|
|
740
989
|
dbLogger4.info(
|
|
741
990
|
hasReplica ? "Database connected (Primary + Replica)" : "Database connected"
|
|
742
991
|
);
|
|
743
992
|
const healthCheckConfig = buildHealthCheckConfig(options?.healthCheck);
|
|
744
993
|
if (healthCheckConfig.enabled) {
|
|
745
|
-
startHealthCheck(healthCheckConfig, options, getDatabase
|
|
994
|
+
startHealthCheck(healthCheckConfig, options, getDatabase);
|
|
746
995
|
}
|
|
747
996
|
const monConfig = buildMonitoringConfig(options?.monitoring);
|
|
748
997
|
setMonitoringConfig(monConfig);
|
|
@@ -754,53 +1003,79 @@ async function initDatabase(options) {
|
|
|
754
1003
|
}
|
|
755
1004
|
return { write: getWriteInstance(), read: getReadInstance() };
|
|
756
1005
|
} finally {
|
|
757
|
-
|
|
1006
|
+
setInitPromise(void 0);
|
|
1007
|
+
setInitProvider(void 0);
|
|
758
1008
|
}
|
|
759
1009
|
})();
|
|
760
|
-
|
|
1010
|
+
setInitPromise(currentInit);
|
|
1011
|
+
return await currentInit;
|
|
761
1012
|
}
|
|
762
1013
|
async function closeDatabase() {
|
|
763
|
-
|
|
1014
|
+
const activeClosePromise = getClosePromise();
|
|
1015
|
+
if (activeClosePromise) {
|
|
764
1016
|
dbLogger4.debug("Database close already in progress");
|
|
765
|
-
return;
|
|
1017
|
+
return await activeClosePromise;
|
|
766
1018
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1019
|
+
const currentClose = (async () => {
|
|
1020
|
+
setIsClosing(true);
|
|
1021
|
+
const activeInitPromise = getInitPromise();
|
|
1022
|
+
if (activeInitPromise) {
|
|
1023
|
+
dbLogger4.debug("Waiting for database initialization to complete before closing...");
|
|
1024
|
+
try {
|
|
1025
|
+
await activeInitPromise;
|
|
1026
|
+
} catch (_error) {
|
|
1027
|
+
dbLogger4.debug("Initialization failed during close, proceeding with cleanup");
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
const writeInst = getWriteInstance();
|
|
1031
|
+
const readInst = getReadInstance();
|
|
1032
|
+
const provider = getDatabaseProvider();
|
|
1033
|
+
if (!writeInst && !readInst && !provider) {
|
|
1034
|
+
dbLogger4.debug("No database connections to close");
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
770
1037
|
try {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1038
|
+
stopHealthCheck();
|
|
1039
|
+
const closePromises = [];
|
|
1040
|
+
if (provider) {
|
|
1041
|
+
closePromises.push(closeDatabaseProvider(provider, "shutdown"));
|
|
1042
|
+
}
|
|
1043
|
+
const writeC = getWriteClient();
|
|
1044
|
+
if (writeC) {
|
|
1045
|
+
closePromises.push(closeDatabaseClient(writeC, "write"));
|
|
1046
|
+
}
|
|
1047
|
+
const readC = getReadClient();
|
|
1048
|
+
if (readC && readC !== writeC) {
|
|
1049
|
+
closePromises.push(closeDatabaseClient(readC, "read"));
|
|
1050
|
+
}
|
|
1051
|
+
await Promise.allSettled(closePromises);
|
|
1052
|
+
dbLogger4.info("All database connections closed");
|
|
1053
|
+
} finally {
|
|
1054
|
+
setWriteInstance(void 0);
|
|
1055
|
+
setReadInstance(void 0);
|
|
1056
|
+
setWriteClient(void 0);
|
|
1057
|
+
setReadClient(void 0);
|
|
1058
|
+
setDatabaseProviderInstance(void 0);
|
|
1059
|
+
setMonitoringConfig(void 0);
|
|
1060
|
+
setInitOptions(void 0);
|
|
774
1061
|
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const readInst = getReadInstance();
|
|
778
|
-
if (!writeInst && !readInst) {
|
|
779
|
-
dbLogger4.debug("No database connections to close");
|
|
780
|
-
isClosing = false;
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
1062
|
+
})();
|
|
1063
|
+
setClosePromise(currentClose);
|
|
783
1064
|
try {
|
|
784
|
-
|
|
785
|
-
const closePromises = [];
|
|
786
|
-
const writeC = getWriteClient();
|
|
787
|
-
if (writeC) {
|
|
788
|
-
closePromises.push(closeDatabaseClient(writeC, "write"));
|
|
789
|
-
}
|
|
790
|
-
const readC = getReadClient();
|
|
791
|
-
if (readC && readC !== writeC) {
|
|
792
|
-
closePromises.push(closeDatabaseClient(readC, "read"));
|
|
793
|
-
}
|
|
794
|
-
await Promise.allSettled(closePromises);
|
|
795
|
-
dbLogger4.info("All database connections closed");
|
|
1065
|
+
await currentClose;
|
|
796
1066
|
} finally {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1067
|
+
if (getClosePromise() === currentClose) {
|
|
1068
|
+
setClosePromise(void 0);
|
|
1069
|
+
setIsClosing(false);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
async function forceReconnectDatabase(reason = "manual") {
|
|
1074
|
+
if (getDatabaseProvider()) {
|
|
1075
|
+
dbLogger4.debug("Force reconnect skipped: database is externally provided", { reason });
|
|
1076
|
+
return false;
|
|
803
1077
|
}
|
|
1078
|
+
return await triggerForceReconnect(reason);
|
|
804
1079
|
}
|
|
805
1080
|
function getDatabaseInfo() {
|
|
806
1081
|
const writeInst = getWriteInstance();
|
|
@@ -808,22 +1083,140 @@ function getDatabaseInfo() {
|
|
|
808
1083
|
return {
|
|
809
1084
|
hasWrite: !!writeInst,
|
|
810
1085
|
hasRead: !!readInst,
|
|
811
|
-
isReplica: !!(readInst && readInst !== writeInst)
|
|
1086
|
+
isReplica: !!(readInst && readInst !== writeInst),
|
|
1087
|
+
providerKind: getDatabaseProvider()?.kind
|
|
812
1088
|
};
|
|
813
1089
|
}
|
|
814
|
-
var
|
|
1090
|
+
var dbLogger5 = logger.child("@spfn/core:database");
|
|
1091
|
+
var POSTGRES_JS_CONNECTION_CODES = /* @__PURE__ */ new Set([
|
|
1092
|
+
"CONNECTION_ENDED",
|
|
1093
|
+
"CONNECTION_CLOSED",
|
|
1094
|
+
"CONNECTION_DESTROYED",
|
|
1095
|
+
"CONNECT_TIMEOUT",
|
|
1096
|
+
"CONNECTION_CONNECT_TIMEOUT"
|
|
1097
|
+
]);
|
|
1098
|
+
var NODE_NET_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1099
|
+
"ECONNRESET",
|
|
1100
|
+
"ECONNREFUSED",
|
|
1101
|
+
"EPIPE",
|
|
1102
|
+
"ETIMEDOUT",
|
|
1103
|
+
"EHOSTUNREACH",
|
|
1104
|
+
"ENETUNREACH",
|
|
1105
|
+
"ENOTFOUND"
|
|
1106
|
+
]);
|
|
1107
|
+
function isConnectionSqlState(code) {
|
|
1108
|
+
if (code.startsWith("08")) return true;
|
|
1109
|
+
if (code === "53300") return true;
|
|
1110
|
+
if (code === "57P01" || code === "57P02" || code === "57P03") return true;
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
function* unwrap(error) {
|
|
1114
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1115
|
+
const stack = [error];
|
|
1116
|
+
while (stack.length > 0) {
|
|
1117
|
+
const current = stack.pop();
|
|
1118
|
+
if (!current || typeof current !== "object" || seen.has(current)) {
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
seen.add(current);
|
|
1122
|
+
const obj = current;
|
|
1123
|
+
yield obj;
|
|
1124
|
+
for (const key of ["cause", "original", "error", "err", "inner"]) {
|
|
1125
|
+
const nested = obj[key];
|
|
1126
|
+
if (nested && typeof nested === "object" && !seen.has(nested)) {
|
|
1127
|
+
stack.push(nested);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function isConnectionLevelError(error) {
|
|
1133
|
+
if (!error) return false;
|
|
1134
|
+
if (error instanceof ConnectionError) return true;
|
|
1135
|
+
for (const candidate of unwrap(error)) {
|
|
1136
|
+
if (candidate instanceof ConnectionError) return true;
|
|
1137
|
+
const code = candidate.code;
|
|
1138
|
+
if (typeof code === "string") {
|
|
1139
|
+
if (POSTGRES_JS_CONNECTION_CODES.has(code)) return true;
|
|
1140
|
+
if (NODE_NET_ERROR_CODES.has(code)) return true;
|
|
1141
|
+
if (isConnectionSqlState(code)) return true;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
var DEFAULT_THRESHOLD = 3;
|
|
1147
|
+
var DEFAULT_WINDOW_MS = 1e4;
|
|
1148
|
+
var MIN_WINDOW_MS = 1e3;
|
|
1149
|
+
function readPositiveIntEnv(key, defaultValue, min) {
|
|
1150
|
+
const raw = process.env[key];
|
|
1151
|
+
if (raw === void 0) return defaultValue;
|
|
1152
|
+
try {
|
|
1153
|
+
return parseNumber(raw, { min, integer: true });
|
|
1154
|
+
} catch {
|
|
1155
|
+
return defaultValue;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
var ERROR_THRESHOLD = readPositiveIntEnv("DB_RECONNECT_ERROR_THRESHOLD", DEFAULT_THRESHOLD, 1);
|
|
1159
|
+
var ERROR_WINDOW_MS = readPositiveIntEnv("DB_RECONNECT_ERROR_WINDOW_MS", DEFAULT_WINDOW_MS, MIN_WINDOW_MS);
|
|
1160
|
+
var errorTimestamps = [];
|
|
1161
|
+
var reportedErrors = /* @__PURE__ */ new WeakSet();
|
|
1162
|
+
function resetConnectionErrorCounter() {
|
|
1163
|
+
errorTimestamps.length = 0;
|
|
1164
|
+
}
|
|
1165
|
+
function checkAndMarkReported(error) {
|
|
1166
|
+
let alreadySeen = false;
|
|
1167
|
+
const toMark = [];
|
|
1168
|
+
for (const candidate of unwrap(error)) {
|
|
1169
|
+
if (reportedErrors.has(candidate)) {
|
|
1170
|
+
alreadySeen = true;
|
|
1171
|
+
}
|
|
1172
|
+
toMark.push(candidate);
|
|
1173
|
+
}
|
|
1174
|
+
if (alreadySeen) return true;
|
|
1175
|
+
for (const obj of toMark) {
|
|
1176
|
+
reportedErrors.add(obj);
|
|
1177
|
+
}
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
function reportDatabaseError(error) {
|
|
1181
|
+
try {
|
|
1182
|
+
if (!isConnectionLevelError(error)) return;
|
|
1183
|
+
if (isReconnectingNow()) return;
|
|
1184
|
+
if (checkAndMarkReported(error)) return;
|
|
1185
|
+
const now = Date.now();
|
|
1186
|
+
errorTimestamps.push(now);
|
|
1187
|
+
const cutoff = now - ERROR_WINDOW_MS;
|
|
1188
|
+
while (errorTimestamps.length > 0 && errorTimestamps[0] < cutoff) {
|
|
1189
|
+
errorTimestamps.shift();
|
|
1190
|
+
}
|
|
1191
|
+
if (errorTimestamps.length < ERROR_THRESHOLD) return;
|
|
1192
|
+
errorTimestamps.length = 0;
|
|
1193
|
+
dbLogger5.error("Connection-error threshold crossed, forcing pool rebuild", {
|
|
1194
|
+
threshold: ERROR_THRESHOLD,
|
|
1195
|
+
windowMs: ERROR_WINDOW_MS
|
|
1196
|
+
});
|
|
1197
|
+
triggerForceReconnect("query_error_threshold").catch((err) => {
|
|
1198
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1199
|
+
dbLogger5.error("Forced reconnect after error threshold failed", { error: message });
|
|
1200
|
+
});
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1203
|
+
dbLogger5.debug("reportDatabaseError itself threw, ignoring", { error: message });
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
var BARREL_FILE_PATTERNS = [
|
|
815
1207
|
"/index",
|
|
816
1208
|
"/index.ts",
|
|
817
1209
|
"/index.js",
|
|
818
1210
|
"/index.mjs",
|
|
819
|
-
"
|
|
820
|
-
"
|
|
821
|
-
"
|
|
822
|
-
"
|
|
1211
|
+
"/config",
|
|
1212
|
+
"/config.ts",
|
|
1213
|
+
"/config.js",
|
|
1214
|
+
"/config.mjs"
|
|
823
1215
|
];
|
|
824
1216
|
var SUPPORTED_EXTENSIONS = [".ts", ".js", ".mjs"];
|
|
825
|
-
function
|
|
826
|
-
|
|
1217
|
+
function isBarrelFile(filePath) {
|
|
1218
|
+
const posixPath = toPosixPath(filePath);
|
|
1219
|
+
return BARREL_FILE_PATTERNS.some((pattern) => posixPath.endsWith(pattern));
|
|
827
1220
|
}
|
|
828
1221
|
function isAbsolutePath(path) {
|
|
829
1222
|
if (path.startsWith("/")) return true;
|
|
@@ -833,8 +1226,8 @@ function hasSupportedExtension(filePath) {
|
|
|
833
1226
|
if (filePath.endsWith(".d.ts")) return false;
|
|
834
1227
|
return SUPPORTED_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
835
1228
|
}
|
|
836
|
-
function
|
|
837
|
-
return files.filter((file) => !
|
|
1229
|
+
function filterBarrelFiles(files) {
|
|
1230
|
+
return files.filter((file) => !isBarrelFile(file));
|
|
838
1231
|
}
|
|
839
1232
|
function scanDirectoryRecursive(dir, extension) {
|
|
840
1233
|
const files = [];
|
|
@@ -943,7 +1336,7 @@ function discoverPackageSchemas(cwd) {
|
|
|
943
1336
|
for (const schema of packageSchemas) {
|
|
944
1337
|
const absolutePath = join(pkgPath, schema);
|
|
945
1338
|
const expandedFiles = expandGlobPattern(absolutePath);
|
|
946
|
-
const schemaFiles =
|
|
1339
|
+
const schemaFiles = filterBarrelFiles(expandedFiles);
|
|
947
1340
|
schemas.push(...schemaFiles);
|
|
948
1341
|
}
|
|
949
1342
|
}
|
|
@@ -993,7 +1386,7 @@ function getDrizzleConfig(options = {}) {
|
|
|
993
1386
|
if (options.packageFilter) {
|
|
994
1387
|
const packageSchemas2 = options.disablePackageDiscovery ? [] : discoverPackageSchemas(options.cwd ?? process.cwd());
|
|
995
1388
|
const filteredSchemas = packageSchemas2.filter(
|
|
996
|
-
(schemaPath) => schemaPath.includes(`node_modules/${options.packageFilter}/`)
|
|
1389
|
+
(schemaPath) => toPosixPath(schemaPath).includes(`node_modules/${options.packageFilter}/`)
|
|
997
1390
|
);
|
|
998
1391
|
if (filteredSchemas.length === 0) {
|
|
999
1392
|
throw new Error(
|
|
@@ -1005,7 +1398,10 @@ function getDrizzleConfig(options = {}) {
|
|
|
1005
1398
|
schema: schema2,
|
|
1006
1399
|
out,
|
|
1007
1400
|
dialect,
|
|
1008
|
-
dbCredentials: getDbCredentials(dialect, databaseUrl)
|
|
1401
|
+
dbCredentials: getDbCredentials(dialect, databaseUrl),
|
|
1402
|
+
migrations: {
|
|
1403
|
+
prefix: options.migrationPrefix ?? "timestamp"
|
|
1404
|
+
}
|
|
1009
1405
|
};
|
|
1010
1406
|
}
|
|
1011
1407
|
const userSchema = options.schema ?? "./src/server/entities/**/*.ts";
|
|
@@ -1018,7 +1414,7 @@ function getDrizzleConfig(options = {}) {
|
|
|
1018
1414
|
for (const schema2 of allSchemas) {
|
|
1019
1415
|
const absoluteSchema = isAbsolutePath(schema2) ? schema2 : join(cwd, schema2);
|
|
1020
1416
|
const expanded = expandGlobPattern(absoluteSchema);
|
|
1021
|
-
const filtered =
|
|
1417
|
+
const filtered = filterBarrelFiles(expanded);
|
|
1022
1418
|
expandedFiles.push(...filtered);
|
|
1023
1419
|
}
|
|
1024
1420
|
allSchemas = expandedFiles;
|
|
@@ -1037,7 +1433,10 @@ function getDrizzleConfig(options = {}) {
|
|
|
1037
1433
|
out,
|
|
1038
1434
|
dialect,
|
|
1039
1435
|
dbCredentials: getDbCredentials(dialect, databaseUrl),
|
|
1040
|
-
schemaFilter
|
|
1436
|
+
schemaFilter,
|
|
1437
|
+
migrations: {
|
|
1438
|
+
prefix: options.migrationPrefix ?? "timestamp"
|
|
1439
|
+
}
|
|
1041
1440
|
};
|
|
1042
1441
|
}
|
|
1043
1442
|
function getDbCredentials(dialect, url) {
|
|
@@ -1066,13 +1465,15 @@ function generateDrizzleConfigFile(options = {}) {
|
|
|
1066
1465
|
]` : `'${normalizeSchemaPath(config.schema)}'`;
|
|
1067
1466
|
const schemaFilterLine = config.schemaFilter && config.schemaFilter.length > 0 ? `
|
|
1068
1467
|
schemaFilter: ${JSON.stringify(config.schemaFilter)},` : "";
|
|
1468
|
+
const migrationsLine = config.migrations ? `
|
|
1469
|
+
migrations: ${JSON.stringify(config.migrations)},` : "";
|
|
1069
1470
|
return `import { defineConfig } from 'drizzle-kit';
|
|
1070
1471
|
|
|
1071
1472
|
export default defineConfig({
|
|
1072
1473
|
schema: ${schemaValue},
|
|
1073
1474
|
out: '${config.out}',
|
|
1074
1475
|
dialect: '${config.dialect}',
|
|
1075
|
-
dbCredentials: ${JSON.stringify(config.dbCredentials, null, 4)},${schemaFilterLine}
|
|
1476
|
+
dbCredentials: ${JSON.stringify(config.dbCredentials, null, 4)},${schemaFilterLine}${migrationsLine}
|
|
1076
1477
|
});
|
|
1077
1478
|
`;
|
|
1078
1479
|
}
|
|
@@ -1082,14 +1483,14 @@ function id() {
|
|
|
1082
1483
|
function timestamps() {
|
|
1083
1484
|
return {
|
|
1084
1485
|
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
|
|
1085
|
-
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
|
|
1486
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => sql`now()`)
|
|
1086
1487
|
};
|
|
1087
1488
|
}
|
|
1088
1489
|
function foreignKey(name, reference, options) {
|
|
1089
|
-
return
|
|
1490
|
+
return bigint(`${name}_id`, { mode: "number" }).notNull().references(reference, { onDelete: options?.onDelete ?? "cascade" });
|
|
1090
1491
|
}
|
|
1091
1492
|
function optionalForeignKey(name, reference, options) {
|
|
1092
|
-
return
|
|
1493
|
+
return bigint(`${name}_id`, { mode: "number" }).references(reference, { onDelete: options?.onDelete ?? "set null" });
|
|
1093
1494
|
}
|
|
1094
1495
|
function uuid() {
|
|
1095
1496
|
return uuid$1("id").defaultRandom().primaryKey();
|
|
@@ -1147,6 +1548,200 @@ function getSchemaInfo(packageName) {
|
|
|
1147
1548
|
scope
|
|
1148
1549
|
};
|
|
1149
1550
|
}
|
|
1551
|
+
var discoveryLogger = logger.child("@spfn/core:migrations");
|
|
1552
|
+
function functionMigrationsTable(packageName) {
|
|
1553
|
+
return `__spfn_fn_${packageName.replace("@spfn/", "")}_migrations`;
|
|
1554
|
+
}
|
|
1555
|
+
function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
1556
|
+
const spfnDir = join(cwd, "node_modules", "@spfn");
|
|
1557
|
+
if (!existsSync(spfnDir)) {
|
|
1558
|
+
return [];
|
|
1559
|
+
}
|
|
1560
|
+
const functions = [];
|
|
1561
|
+
for (const pkg of readdirSync(spfnDir)) {
|
|
1562
|
+
const packagePath = join(spfnDir, pkg);
|
|
1563
|
+
const packageJsonPath = join(packagePath, "package.json");
|
|
1564
|
+
if (!existsSync(packageJsonPath)) {
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
try {
|
|
1568
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
1569
|
+
const migrationsConfig = packageJson.spfn?.migrations;
|
|
1570
|
+
if (!migrationsConfig) {
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
const migrationsDir = join(packagePath, migrationsConfig.dir);
|
|
1574
|
+
if (!existsSync(migrationsDir)) {
|
|
1575
|
+
discoveryLogger.warn(
|
|
1576
|
+
`@spfn/${pkg} declares migrations but the directory is missing: ${migrationsDir}`
|
|
1577
|
+
);
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
functions.push({ packageName: `@spfn/${pkg}`, migrationsDir, packagePath });
|
|
1581
|
+
} catch {
|
|
1582
|
+
discoveryLogger.warn(`Failed to parse package.json for @spfn/${pkg}`);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return functions;
|
|
1586
|
+
}
|
|
1587
|
+
function readMigrationEntries(migrationsDir, packageName) {
|
|
1588
|
+
const journalPath = join(migrationsDir, "meta", "_journal.json");
|
|
1589
|
+
return existsSync(journalPath) ? readJournalEntries(migrationsDir, journalPath, packageName) : readFolderEntries(migrationsDir, packageName);
|
|
1590
|
+
}
|
|
1591
|
+
function readJournalEntries(migrationsDir, journalPath, packageName) {
|
|
1592
|
+
let journal;
|
|
1593
|
+
try {
|
|
1594
|
+
journal = JSON.parse(readFileSync(journalPath, "utf-8"));
|
|
1595
|
+
} catch {
|
|
1596
|
+
journal = {};
|
|
1597
|
+
}
|
|
1598
|
+
if (!Array.isArray(journal.entries)) {
|
|
1599
|
+
throw new Error(`${packageName}: invalid migration journal at ${journalPath}`);
|
|
1600
|
+
}
|
|
1601
|
+
const entries = [...journal.entries];
|
|
1602
|
+
entries.sort((a, b) => a.idx - b.idx);
|
|
1603
|
+
return entries.map((entry) => {
|
|
1604
|
+
if (typeof entry?.tag !== "string" || typeof entry?.when !== "number") {
|
|
1605
|
+
throw new Error(`${packageName}: invalid journal entry in ${journalPath}`);
|
|
1606
|
+
}
|
|
1607
|
+
const sqlPath = join(migrationsDir, `${entry.tag}.sql`);
|
|
1608
|
+
if (!existsSync(sqlPath)) {
|
|
1609
|
+
throw new Error(`${packageName}: migration file not found: ${entry.tag}.sql`);
|
|
1610
|
+
}
|
|
1611
|
+
return toEntry(entry.tag, readFileSync(sqlPath, "utf-8"), entry.when);
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
function readFolderEntries(migrationsDir, packageName) {
|
|
1615
|
+
const folders = readdirSync(migrationsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name).filter((name) => existsSync(join(migrationsDir, name, "migration.sql"))).sort((a, b) => a.localeCompare(b));
|
|
1616
|
+
return folders.map((name) => toEntry(
|
|
1617
|
+
name,
|
|
1618
|
+
readFileSync(join(migrationsDir, name, "migration.sql"), "utf-8"),
|
|
1619
|
+
folderTimestampMillis(name, packageName)
|
|
1620
|
+
));
|
|
1621
|
+
}
|
|
1622
|
+
function folderTimestampMillis(name, packageName) {
|
|
1623
|
+
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name);
|
|
1624
|
+
if (!match) {
|
|
1625
|
+
throw new Error(`${packageName}: migration folder name must start with a YYYYMMDDHHMMSS timestamp: ${name}`);
|
|
1626
|
+
}
|
|
1627
|
+
const [, year, month, day, hour, minute, second] = match;
|
|
1628
|
+
return Date.UTC(
|
|
1629
|
+
Number(year),
|
|
1630
|
+
Number(month) - 1,
|
|
1631
|
+
Number(day),
|
|
1632
|
+
Number(hour),
|
|
1633
|
+
Number(minute),
|
|
1634
|
+
Number(second)
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
function toEntry(name, content, millis) {
|
|
1638
|
+
return {
|
|
1639
|
+
name,
|
|
1640
|
+
millis,
|
|
1641
|
+
hash: createHash("sha256").update(content).digest("hex"),
|
|
1642
|
+
statements: content.split("--> statement-breakpoint").map((statement) => statement.trim()).filter((statement) => statement.length > 0)
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
var PROJECT_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
1646
|
+
var PROJECT_TARGET_NAME = "project (src/server/drizzle)";
|
|
1647
|
+
function filterPendingEntries(entries, lastAppliedMillis, appliedNames) {
|
|
1648
|
+
return entries.filter((entry) => entry.millis > lastAppliedMillis && !appliedNames.has(entry.name));
|
|
1649
|
+
}
|
|
1650
|
+
function migrationTargets(status) {
|
|
1651
|
+
return [...status.packages, ...status.project ? [status.project] : []];
|
|
1652
|
+
}
|
|
1653
|
+
function pendingMigrationTargets(status) {
|
|
1654
|
+
return migrationTargets(status).filter((target) => target.pending > 0);
|
|
1655
|
+
}
|
|
1656
|
+
function countPendingMigrations(status) {
|
|
1657
|
+
return migrationTargets(status).reduce((sum, target) => sum + target.pending, 0);
|
|
1658
|
+
}
|
|
1659
|
+
function toRows(result) {
|
|
1660
|
+
if (Array.isArray(result)) {
|
|
1661
|
+
return result;
|
|
1662
|
+
}
|
|
1663
|
+
const rows = result?.rows;
|
|
1664
|
+
return Array.isArray(rows) ? rows : [];
|
|
1665
|
+
}
|
|
1666
|
+
async function tableExists(db, tableName) {
|
|
1667
|
+
const rows = toRows(await db.execute(sql`
|
|
1668
|
+
SELECT EXISTS (
|
|
1669
|
+
SELECT 1 FROM information_schema.tables
|
|
1670
|
+
WHERE table_schema = 'drizzle' AND table_name = ${tableName}
|
|
1671
|
+
) AS "exists"`));
|
|
1672
|
+
return rows[0]?.exists === true;
|
|
1673
|
+
}
|
|
1674
|
+
async function readAppliedNames(db, tableName) {
|
|
1675
|
+
const columnRows = toRows(await db.execute(sql`
|
|
1676
|
+
SELECT EXISTS (
|
|
1677
|
+
SELECT 1 FROM information_schema.columns
|
|
1678
|
+
WHERE table_schema = 'drizzle' AND table_name = ${tableName} AND column_name = 'name'
|
|
1679
|
+
) AS "exists"`));
|
|
1680
|
+
if (columnRows[0]?.exists !== true) {
|
|
1681
|
+
return /* @__PURE__ */ new Set();
|
|
1682
|
+
}
|
|
1683
|
+
const rows = toRows(await db.execute(sql`
|
|
1684
|
+
SELECT name FROM drizzle.${sql.identifier(tableName)} WHERE name IS NOT NULL`));
|
|
1685
|
+
return new Set(rows.map((row) => String(row.name)));
|
|
1686
|
+
}
|
|
1687
|
+
async function collectTargetStatus(db, name, migrationsDir, tableName) {
|
|
1688
|
+
const entries = readMigrationEntries(migrationsDir, name);
|
|
1689
|
+
let lastApplied = 0;
|
|
1690
|
+
let appliedNames = /* @__PURE__ */ new Set();
|
|
1691
|
+
if (await tableExists(db, tableName)) {
|
|
1692
|
+
const rows = toRows(await db.execute(sql`
|
|
1693
|
+
SELECT created_at FROM drizzle.${sql.identifier(tableName)}
|
|
1694
|
+
ORDER BY created_at DESC LIMIT 1`));
|
|
1695
|
+
lastApplied = rows[0]?.created_at ? Number(rows[0].created_at) : 0;
|
|
1696
|
+
appliedNames = await readAppliedNames(db, tableName);
|
|
1697
|
+
}
|
|
1698
|
+
const pendingEntries = filterPendingEntries(entries, lastApplied, appliedNames);
|
|
1699
|
+
return {
|
|
1700
|
+
name,
|
|
1701
|
+
total: entries.length,
|
|
1702
|
+
applied: entries.length - pendingEntries.length,
|
|
1703
|
+
pending: pendingEntries.length,
|
|
1704
|
+
pendingTags: pendingEntries.map((entry) => entry.name)
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
function hasMigrationTargets(cwd = process.cwd()) {
|
|
1708
|
+
return discoverFunctionMigrations(cwd).length > 0 || existsSync(projectMigrationsDir(cwd));
|
|
1709
|
+
}
|
|
1710
|
+
function projectMigrationsDir(cwd = process.cwd()) {
|
|
1711
|
+
return join(cwd, "src", "server", "drizzle");
|
|
1712
|
+
}
|
|
1713
|
+
async function collectMigrationStatus(db, cwd = process.cwd()) {
|
|
1714
|
+
const packages = [];
|
|
1715
|
+
for (const func of discoverFunctionMigrations(cwd)) {
|
|
1716
|
+
packages.push(await collectTargetStatus(
|
|
1717
|
+
db,
|
|
1718
|
+
func.packageName,
|
|
1719
|
+
func.migrationsDir,
|
|
1720
|
+
functionMigrationsTable(func.packageName)
|
|
1721
|
+
));
|
|
1722
|
+
}
|
|
1723
|
+
const projectDir = projectMigrationsDir(cwd);
|
|
1724
|
+
const project = existsSync(projectDir) ? await collectTargetStatus(db, PROJECT_TARGET_NAME, projectDir, PROJECT_MIGRATIONS_TABLE) : null;
|
|
1725
|
+
return { packages, project: project && project.total > 0 ? project : null };
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// src/db/migrations/format.ts
|
|
1729
|
+
var RUN_MIGRATIONS_HINT = "Run: pnpm spfn db migrate";
|
|
1730
|
+
function formatPendingMigrations(targets) {
|
|
1731
|
+
const lines = [];
|
|
1732
|
+
for (const target of targets) {
|
|
1733
|
+
lines.push(`${target.name}: ${target.pending} pending migration(s) (${target.applied}/${target.total} applied)`);
|
|
1734
|
+
for (const tag of target.pendingTags) {
|
|
1735
|
+
lines.push(` - ${tag}`);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return lines;
|
|
1739
|
+
}
|
|
1740
|
+
function pendingMigrationsSummary(targets) {
|
|
1741
|
+
const pending = targets.reduce((sum, target) => sum + target.pending, 0);
|
|
1742
|
+
const names = targets.map((target) => target.name).join(", ");
|
|
1743
|
+
return `${pending} pending migration(s) in ${names}`;
|
|
1744
|
+
}
|
|
1150
1745
|
var txLogger = logger.child("@spfn/core:transaction");
|
|
1151
1746
|
var asyncContext = new AsyncLocalStorage();
|
|
1152
1747
|
function getTransactionContext() {
|
|
@@ -1168,7 +1763,23 @@ function runWithTransaction(tx, txId, callback) {
|
|
|
1168
1763
|
} else {
|
|
1169
1764
|
txLogger.debug("Root transaction context set", { txId, level: newLevel });
|
|
1170
1765
|
}
|
|
1171
|
-
|
|
1766
|
+
const afterCommitCallbacks = existingContext ? existingContext.afterCommitCallbacks : [];
|
|
1767
|
+
return asyncContext.run(
|
|
1768
|
+
{ tx, txId, level: newLevel, afterCommitCallbacks },
|
|
1769
|
+
callback
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
function onAfterCommit(callback) {
|
|
1773
|
+
const context = getTransactionContext();
|
|
1774
|
+
if (!context) {
|
|
1775
|
+
Promise.resolve().then(callback).catch((err) => {
|
|
1776
|
+
txLogger.error("afterCommit callback failed (no transaction)", {
|
|
1777
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1778
|
+
});
|
|
1779
|
+
});
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
context.afterCommitCallbacks.push(callback);
|
|
1172
1783
|
}
|
|
1173
1784
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
1174
1785
|
var txLogger2 = logger.child("@spfn/core:transaction");
|
|
@@ -1180,6 +1791,7 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1180
1791
|
context = "transaction"
|
|
1181
1792
|
} = options;
|
|
1182
1793
|
const timeout = options.timeout ?? defaultTimeout;
|
|
1794
|
+
const idleTimeout = options.idleTimeout ?? env.TRANSACTION_IDLE_TIMEOUT;
|
|
1183
1795
|
const txId = `tx_${randomUUID()}`;
|
|
1184
1796
|
const validateAndThrow = (condition, message, logMessage, metadata) => {
|
|
1185
1797
|
if (condition) {
|
|
@@ -1220,6 +1832,24 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1220
1832
|
"Timeout exceeds maximum",
|
|
1221
1833
|
{ txId, context, timeout, maxTimeout: MAX_TIMEOUT_MS }
|
|
1222
1834
|
);
|
|
1835
|
+
validateAndThrow(
|
|
1836
|
+
!Number.isInteger(idleTimeout),
|
|
1837
|
+
`Invalid idleTimeout value: ${idleTimeout}. Must be an integer.`,
|
|
1838
|
+
"Invalid idleTimeout type",
|
|
1839
|
+
{ txId, context, idleTimeout }
|
|
1840
|
+
);
|
|
1841
|
+
validateAndThrow(
|
|
1842
|
+
idleTimeout < 0,
|
|
1843
|
+
`Invalid idleTimeout value: ${idleTimeout}. Must be non-negative (0 to disable).`,
|
|
1844
|
+
"Invalid idleTimeout range",
|
|
1845
|
+
{ txId, context, idleTimeout }
|
|
1846
|
+
);
|
|
1847
|
+
validateAndThrow(
|
|
1848
|
+
idleTimeout > MAX_TIMEOUT_MS,
|
|
1849
|
+
`Invalid idleTimeout value: ${idleTimeout}. Maximum is ${MAX_TIMEOUT_MS}ms.`,
|
|
1850
|
+
"idleTimeout exceeds maximum",
|
|
1851
|
+
{ txId, context, idleTimeout, maxTimeout: MAX_TIMEOUT_MS }
|
|
1852
|
+
);
|
|
1223
1853
|
const writeDb = getDatabase("write");
|
|
1224
1854
|
if (!writeDb) {
|
|
1225
1855
|
const error = new TransactionError({
|
|
@@ -1251,13 +1881,25 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1251
1881
|
txLogger2.debug("Transaction started", { txId, context });
|
|
1252
1882
|
}
|
|
1253
1883
|
const startTime = Date.now();
|
|
1884
|
+
let afterCommitCallbacks = [];
|
|
1254
1885
|
try {
|
|
1255
1886
|
const result = await writeDb.transaction(async (tx) => {
|
|
1887
|
+
const transaction = tx;
|
|
1256
1888
|
if (timeout > 0 && !isNested) {
|
|
1257
|
-
await
|
|
1889
|
+
await transaction.execute(sql.raw(`SET LOCAL statement_timeout = ${timeout}`));
|
|
1890
|
+
}
|
|
1891
|
+
if (idleTimeout > 0 && !isNested) {
|
|
1892
|
+
await transaction.execute(sql.raw(`SET LOCAL idle_in_transaction_session_timeout = ${idleTimeout}`));
|
|
1258
1893
|
}
|
|
1259
|
-
return await runWithTransaction(
|
|
1260
|
-
|
|
1894
|
+
return await runWithTransaction(transaction, txId, async () => {
|
|
1895
|
+
const innerResult = await callback(transaction);
|
|
1896
|
+
if (!isNested) {
|
|
1897
|
+
const ctx = getTransactionContext();
|
|
1898
|
+
if (ctx) {
|
|
1899
|
+
afterCommitCallbacks = [...ctx.afterCommitCallbacks];
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
return innerResult;
|
|
1261
1903
|
});
|
|
1262
1904
|
});
|
|
1263
1905
|
const duration = Date.now() - startTime;
|
|
@@ -1267,7 +1909,8 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1267
1909
|
txId,
|
|
1268
1910
|
context,
|
|
1269
1911
|
duration: `${duration}ms`,
|
|
1270
|
-
threshold: `${slowThreshold}ms
|
|
1912
|
+
threshold: `${slowThreshold}ms`,
|
|
1913
|
+
hint: "A transaction holds a pooled connection (and row locks) for its whole duration. If this is slow because of non-DB work (external API, etc.) inside the transaction, move that work out \u2014 it starves the connection pool."
|
|
1271
1914
|
});
|
|
1272
1915
|
} else {
|
|
1273
1916
|
txLogger2.debug("Transaction committed", {
|
|
@@ -1277,6 +1920,24 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1277
1920
|
});
|
|
1278
1921
|
}
|
|
1279
1922
|
}
|
|
1923
|
+
if (!isNested && afterCommitCallbacks.length > 0) {
|
|
1924
|
+
if (enableLogging) {
|
|
1925
|
+
txLogger2.debug("Executing afterCommit callbacks", {
|
|
1926
|
+
txId,
|
|
1927
|
+
context,
|
|
1928
|
+
count: afterCommitCallbacks.length
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
for (const cb of afterCommitCallbacks) {
|
|
1932
|
+
Promise.resolve().then(cb).catch((err) => {
|
|
1933
|
+
txLogger2.error("afterCommit callback failed", {
|
|
1934
|
+
txId,
|
|
1935
|
+
context,
|
|
1936
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1937
|
+
});
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1280
1941
|
return result;
|
|
1281
1942
|
} catch (error) {
|
|
1282
1943
|
const duration = Date.now() - startTime;
|
|
@@ -1288,7 +1949,8 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1288
1949
|
duration: `${duration}ms`,
|
|
1289
1950
|
threshold: `${slowThreshold}ms`,
|
|
1290
1951
|
error: error instanceof Error ? error.message : String(error),
|
|
1291
|
-
errorType: error instanceof Error ? error.name : "Unknown"
|
|
1952
|
+
errorType: error instanceof Error ? error.name : "Unknown",
|
|
1953
|
+
hint: "If the error is an idle-in-transaction timeout, the transaction held a pooled connection while awaiting non-DB work (external API, etc.). Move that work out of the transaction."
|
|
1292
1954
|
});
|
|
1293
1955
|
} else {
|
|
1294
1956
|
txLogger2.error("Transaction rolled back", {
|
|
@@ -1323,6 +1985,7 @@ function Transactional(options = {}) {
|
|
|
1323
1985
|
}
|
|
1324
1986
|
);
|
|
1325
1987
|
} catch (error) {
|
|
1988
|
+
reportDatabaseError(error);
|
|
1326
1989
|
if (error instanceof DatabaseError) {
|
|
1327
1990
|
throw error;
|
|
1328
1991
|
}
|
|
@@ -1392,7 +2055,8 @@ async function create(table, data) {
|
|
|
1392
2055
|
if (!db) {
|
|
1393
2056
|
throw new Error("Database not initialized. Call initDatabase() first.");
|
|
1394
2057
|
}
|
|
1395
|
-
const
|
|
2058
|
+
const results = await db.insert(table).values(data).returning();
|
|
2059
|
+
const result = results[0];
|
|
1396
2060
|
return result;
|
|
1397
2061
|
}
|
|
1398
2062
|
async function createMany(table, data) {
|
|
@@ -1408,10 +2072,11 @@ async function upsert(table, data, options) {
|
|
|
1408
2072
|
if (!db) {
|
|
1409
2073
|
throw new Error("Database not initialized. Call initDatabase() first.");
|
|
1410
2074
|
}
|
|
1411
|
-
const
|
|
2075
|
+
const results = await db.insert(table).values(data).onConflictDoUpdate({
|
|
1412
2076
|
target: options.target,
|
|
1413
2077
|
set: options.set || data
|
|
1414
2078
|
}).returning();
|
|
2079
|
+
const result = results[0];
|
|
1415
2080
|
return result;
|
|
1416
2081
|
}
|
|
1417
2082
|
async function updateOne(table, where, data) {
|
|
@@ -1423,7 +2088,8 @@ async function updateOne(table, where, data) {
|
|
|
1423
2088
|
if (!whereClause) {
|
|
1424
2089
|
throw new Error("updateOne requires at least one where condition");
|
|
1425
2090
|
}
|
|
1426
|
-
const
|
|
2091
|
+
const results = await db.update(table).set(data).where(whereClause).returning();
|
|
2092
|
+
const result = results[0];
|
|
1427
2093
|
return result ?? null;
|
|
1428
2094
|
}
|
|
1429
2095
|
async function updateMany(table, where, data) {
|
|
@@ -1447,7 +2113,8 @@ async function deleteOne(table, where) {
|
|
|
1447
2113
|
if (!whereClause) {
|
|
1448
2114
|
throw new Error("deleteOne requires at least one where condition");
|
|
1449
2115
|
}
|
|
1450
|
-
const
|
|
2116
|
+
const results = await db.delete(table).where(whereClause).returning();
|
|
2117
|
+
const result = results[0];
|
|
1451
2118
|
return result ?? null;
|
|
1452
2119
|
}
|
|
1453
2120
|
async function deleteMany(table, where) {
|
|
@@ -1570,6 +2237,7 @@ var BaseRepository = class {
|
|
|
1570
2237
|
try {
|
|
1571
2238
|
return await queryFn();
|
|
1572
2239
|
} catch (error) {
|
|
2240
|
+
reportDatabaseError(error);
|
|
1573
2241
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1574
2242
|
const repositoryName = this.constructor.name;
|
|
1575
2243
|
throw new RepositoryError(
|
|
@@ -1636,14 +2304,51 @@ var BaseRepository = class {
|
|
|
1636
2304
|
const orderByArray = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
|
|
1637
2305
|
query = query.orderBy(...orderByArray);
|
|
1638
2306
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
2307
|
+
const maxRows = env.DB_MAX_ROWS;
|
|
2308
|
+
const requestedLimit = options?.limit && options.limit > 0 ? options.limit : void 0;
|
|
2309
|
+
const effectiveLimit = maxRows > 0 ? Math.min(requestedLimit ?? maxRows, maxRows) : requestedLimit;
|
|
2310
|
+
if (effectiveLimit) {
|
|
2311
|
+
query = query.limit(effectiveLimit);
|
|
1641
2312
|
}
|
|
1642
2313
|
if (options?.offset) {
|
|
1643
2314
|
query = query.offset(options.offset);
|
|
1644
2315
|
}
|
|
1645
2316
|
return query;
|
|
1646
2317
|
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Keyset (cursor) pagination — O(limit) instead of OFFSET's O(offset).
|
|
2320
|
+
*
|
|
2321
|
+
* Pages by a strictly-ordered, unique column instead of a numeric offset, so a
|
|
2322
|
+
* deep page doesn't scan and discard everything before it. Pass the cursor
|
|
2323
|
+
* column's value from the last row of the previous page as `after`; omit it for
|
|
2324
|
+
* the first page. The column must be unique and the sole sort key (e.g. an
|
|
2325
|
+
* auto-increment id or a ULID).
|
|
2326
|
+
*
|
|
2327
|
+
* @example
|
|
2328
|
+
* ```typescript
|
|
2329
|
+
* const page1 = await this._findManyKeyset(users, { cursorColumn: users.id, limit: 20 });
|
|
2330
|
+
* const page2 = await this._findManyKeyset(users, {
|
|
2331
|
+
* cursorColumn: users.id,
|
|
2332
|
+
* after: page1.at(-1)?.id,
|
|
2333
|
+
* limit: 20,
|
|
2334
|
+
* });
|
|
2335
|
+
* ```
|
|
2336
|
+
*/
|
|
2337
|
+
async _findManyKeyset(table, options) {
|
|
2338
|
+
const { cursorColumn, after, order = "asc", where } = options;
|
|
2339
|
+
const maxRows = env.DB_MAX_ROWS;
|
|
2340
|
+
const limit = maxRows > 0 ? Math.min(options.limit, maxRows) : options.limit;
|
|
2341
|
+
const baseWhere = where ? isSQLWrapper(where) ? where : buildWhereFromObject(table, where) : void 0;
|
|
2342
|
+
const cursorPredicate = after !== void 0 ? order === "desc" ? lt(cursorColumn, after) : gt(cursorColumn, after) : void 0;
|
|
2343
|
+
const whereClause = baseWhere && cursorPredicate ? and(baseWhere, cursorPredicate) : cursorPredicate ?? baseWhere;
|
|
2344
|
+
let query = this.readDb.select().from(table);
|
|
2345
|
+
if (whereClause) {
|
|
2346
|
+
query = query.where(whereClause);
|
|
2347
|
+
}
|
|
2348
|
+
query = query.orderBy(order === "desc" ? desc(cursorColumn) : asc(cursorColumn));
|
|
2349
|
+
query = query.limit(limit);
|
|
2350
|
+
return query;
|
|
2351
|
+
}
|
|
1647
2352
|
/**
|
|
1648
2353
|
* Create a new record
|
|
1649
2354
|
*
|
|
@@ -1660,7 +2365,8 @@ var BaseRepository = class {
|
|
|
1660
2365
|
* ```
|
|
1661
2366
|
*/
|
|
1662
2367
|
async _create(table, data) {
|
|
1663
|
-
const
|
|
2368
|
+
const results = await this.db.insert(table).values(data).returning();
|
|
2369
|
+
const result = results[0];
|
|
1664
2370
|
return result;
|
|
1665
2371
|
}
|
|
1666
2372
|
/**
|
|
@@ -1697,15 +2403,16 @@ var BaseRepository = class {
|
|
|
1697
2403
|
* value: {...}
|
|
1698
2404
|
* }, {
|
|
1699
2405
|
* target: [cache.key],
|
|
1700
|
-
* set: { value: data.value
|
|
2406
|
+
* set: { value: data.value } // updatedAt is stamped by the database
|
|
1701
2407
|
* });
|
|
1702
2408
|
* ```
|
|
1703
2409
|
*/
|
|
1704
2410
|
async _upsert(table, data, options) {
|
|
1705
|
-
const
|
|
2411
|
+
const results = await this.db.insert(table).values(data).onConflictDoUpdate({
|
|
1706
2412
|
target: options.target,
|
|
1707
2413
|
set: options.set || data
|
|
1708
2414
|
}).returning();
|
|
2415
|
+
const result = results[0];
|
|
1709
2416
|
return result;
|
|
1710
2417
|
}
|
|
1711
2418
|
/**
|
|
@@ -1729,7 +2436,8 @@ var BaseRepository = class {
|
|
|
1729
2436
|
if (!whereClause) {
|
|
1730
2437
|
throw new Error("updateOne requires at least one where condition");
|
|
1731
2438
|
}
|
|
1732
|
-
const
|
|
2439
|
+
const results = await this.db.update(table).set(data).where(whereClause).returning();
|
|
2440
|
+
const result = results[0];
|
|
1733
2441
|
return result ?? null;
|
|
1734
2442
|
}
|
|
1735
2443
|
/**
|
|
@@ -1773,7 +2481,8 @@ var BaseRepository = class {
|
|
|
1773
2481
|
if (!whereClause) {
|
|
1774
2482
|
throw new Error("deleteOne requires at least one where condition");
|
|
1775
2483
|
}
|
|
1776
|
-
const
|
|
2484
|
+
const results = await this.db.delete(table).where(whereClause).returning();
|
|
2485
|
+
const result = results[0];
|
|
1777
2486
|
return result ?? null;
|
|
1778
2487
|
}
|
|
1779
2488
|
/**
|
|
@@ -1822,6 +2531,6 @@ var BaseRepository = class {
|
|
|
1822
2531
|
}
|
|
1823
2532
|
};
|
|
1824
2533
|
|
|
1825
|
-
export { BaseRepository, RepositoryError, Transactional, auditFields, checkConnection, closeDatabase, count, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, enumText, findMany, findOne, foreignKey, fromPostgresError, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, id, initDatabase, optionalForeignKey, packageNameToSchema, publishingFields, runWithTransaction, setDatabase, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
|
|
2534
|
+
export { BaseRepository, PROJECT_MIGRATIONS_TABLE, PROJECT_TARGET_NAME, RUN_MIGRATIONS_HINT, RepositoryError, Transactional, auditFields, checkConnection, closeDatabase, collectMigrationStatus, count, countPendingMigrations, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, discoverFunctionMigrations, enumText, filterPendingEntries, findMany, findOne, forceReconnectDatabase, foreignKey, formatPendingMigrations, fromPostgresError, functionMigrationsTable, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, getTransactionContext, hasMigrationTargets, id, initDatabase, isConnectionLevelError, migrationTargets, onAfterCommit, optionalForeignKey, packageNameToSchema, pendingMigrationTargets, pendingMigrationsSummary, projectMigrationsDir, publishingFields, readMigrationEntries, reportDatabaseError, resetConnectionErrorCounter, runInTransaction, runWithTransaction, setDatabase, setDatabaseProvider, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
|
|
1826
2535
|
//# sourceMappingURL=index.js.map
|
|
1827
2536
|
//# sourceMappingURL=index.js.map
|