@spfn/core 0.2.0-beta.6 → 0.2.0-beta.66
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 +183 -1281
- package/dist/authz/index.d.ts +34 -0
- package/dist/authz/index.js +415 -0
- package/dist/authz/index.js.map +1 -0
- package/dist/{boss-DI1r4kTS.d.ts → boss-gXhgctn6.d.ts} +40 -0
- package/dist/cache/index.js +42 -30
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +55 -8
- package/dist/codegen/index.js +183 -7
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +585 -6
- package/dist/config/index.js +116 -5
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +379 -75
- package/dist/db/index.js +625 -113
- package/dist/db/index.js.map +1 -1
- package/dist/define-middleware-DuXD8Hvu.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 +20 -2
- 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 +271 -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 +243 -2
- package/dist/middleware/index.js +1323 -13
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +77 -31
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +53 -23
- package/dist/nextjs/server.js +197 -66
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.d.ts +138 -146
- package/dist/route/index.js +238 -22
- package/dist/route/index.js.map +1 -1
- 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 +458 -17
- package/dist/server/index.js +1756 -277
- package/dist/server/index.js.map +1 -1
- package/dist/{router-Di7ENoah.d.ts → token-manager-jKD_EsSE.d.ts} +121 -1
- package/dist/{types-D_N_U-Py.d.ts → types-7Mhoxnnt.d.ts} +21 -1
- package/dist/types-BFB72jbM.d.ts +282 -0
- package/dist/types-DVjf37yO.d.ts +205 -0
- package/docs/file-upload.md +717 -0
- package/package.json +237 -208
- package/dist/types-B-e_f2dQ.d.ts +0 -121
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
8
|
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
8
9
|
import { join, dirname, basename } from 'path';
|
|
9
|
-
import { bigserial, timestamp, uuid as uuid$1, text, jsonb, pgSchema } from 'drizzle-orm/pg-core';
|
|
10
|
+
import { bigserial, timestamp, bigint, uuid as uuid$1, text, jsonb, pgSchema } from 'drizzle-orm/pg-core';
|
|
10
11
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
11
12
|
import { createMiddleware } from 'hono/factory';
|
|
12
13
|
import { randomUUID } from 'crypto';
|
|
13
|
-
import { count as count$1,
|
|
14
|
+
import { sql, count as count$1, lt, gt, and, desc, asc, eq } 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,
|
|
@@ -487,6 +528,10 @@ var getReadInstance = () => globalThis.__SPFN_DB_READ__;
|
|
|
487
528
|
var setReadInstance = (instance) => {
|
|
488
529
|
globalThis.__SPFN_DB_READ__ = instance;
|
|
489
530
|
};
|
|
531
|
+
var getDatabaseProvider = () => globalThis.__SPFN_DB_PROVIDER__;
|
|
532
|
+
var setDatabaseProviderInstance = (provider) => {
|
|
533
|
+
globalThis.__SPFN_DB_PROVIDER__ = provider;
|
|
534
|
+
};
|
|
490
535
|
var getWriteClient = () => globalThis.__SPFN_DB_WRITE_CLIENT__;
|
|
491
536
|
var setWriteClient = (client) => {
|
|
492
537
|
globalThis.__SPFN_DB_WRITE_CLIENT__ = client;
|
|
@@ -502,7 +547,32 @@ var setHealthCheckInterval = (interval) => {
|
|
|
502
547
|
var setMonitoringConfig = (config) => {
|
|
503
548
|
globalThis.__SPFN_DB_MONITORING__ = config;
|
|
504
549
|
};
|
|
550
|
+
var getInitOptions = () => globalThis.__SPFN_DB_INIT_OPTIONS__;
|
|
551
|
+
var setInitOptions = (options) => {
|
|
552
|
+
globalThis.__SPFN_DB_INIT_OPTIONS__ = options;
|
|
553
|
+
};
|
|
554
|
+
var getInitPromise = () => globalThis.__SPFN_DB_INIT_PROMISE__;
|
|
555
|
+
var setInitPromise = (promise) => {
|
|
556
|
+
globalThis.__SPFN_DB_INIT_PROMISE__ = promise;
|
|
557
|
+
};
|
|
558
|
+
var getInitProvider = () => globalThis.__SPFN_DB_INIT_PROVIDER__;
|
|
559
|
+
var setInitProvider = (provider) => {
|
|
560
|
+
globalThis.__SPFN_DB_INIT_PROVIDER__ = provider;
|
|
561
|
+
};
|
|
562
|
+
var getClosePromise = () => globalThis.__SPFN_DB_CLOSE_PROMISE__;
|
|
563
|
+
var setClosePromise = (promise) => {
|
|
564
|
+
globalThis.__SPFN_DB_CLOSE_PROMISE__ = promise;
|
|
565
|
+
};
|
|
566
|
+
var getIsClosing = () => globalThis.__SPFN_DB_CLOSING__ === true;
|
|
567
|
+
var setIsClosing = (closing) => {
|
|
568
|
+
globalThis.__SPFN_DB_CLOSING__ = closing;
|
|
569
|
+
};
|
|
505
570
|
var dbLogger3 = logger.child("@spfn/core:database");
|
|
571
|
+
var CLIENT_CLOSE_TIMEOUT = 5;
|
|
572
|
+
var isReconnecting = false;
|
|
573
|
+
function isReconnectingNow() {
|
|
574
|
+
return isReconnecting;
|
|
575
|
+
}
|
|
506
576
|
async function testDatabaseConnection(db) {
|
|
507
577
|
await db.execute("SELECT 1");
|
|
508
578
|
}
|
|
@@ -514,8 +584,17 @@ async function performHealthCheck(getDatabase2) {
|
|
|
514
584
|
await testDatabaseConnection(read);
|
|
515
585
|
}
|
|
516
586
|
}
|
|
517
|
-
async function
|
|
518
|
-
|
|
587
|
+
async function closeClient(client) {
|
|
588
|
+
try {
|
|
589
|
+
await client.end({ timeout: CLIENT_CLOSE_TIMEOUT });
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async function reconnectAndRestore(options) {
|
|
594
|
+
if (getIsClosing()) {
|
|
595
|
+
dbLogger3.debug("reconnectAndRestore aborted: database is closing");
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
519
598
|
const result = await createDatabaseFromEnv(options);
|
|
520
599
|
if (!result.write) {
|
|
521
600
|
return false;
|
|
@@ -524,15 +603,33 @@ async function reconnectAndRestore(options, closeDatabase2) {
|
|
|
524
603
|
if (result.read && result.read !== result.write) {
|
|
525
604
|
await testDatabaseConnection(result.read);
|
|
526
605
|
}
|
|
606
|
+
if (getIsClosing()) {
|
|
607
|
+
dbLogger3.warn("reconnectAndRestore: close started mid-rebuild, discarding new pool");
|
|
608
|
+
if (result.writeClient) {
|
|
609
|
+
await closeClient(result.writeClient);
|
|
610
|
+
}
|
|
611
|
+
if (result.readClient && result.readClient !== result.writeClient) {
|
|
612
|
+
await closeClient(result.readClient);
|
|
613
|
+
}
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
const oldWriteClient = getWriteClient();
|
|
617
|
+
const oldReadClient = getReadClient();
|
|
527
618
|
setWriteInstance(result.write);
|
|
528
619
|
setReadInstance(result.read);
|
|
529
620
|
setWriteClient(result.writeClient);
|
|
530
621
|
setReadClient(result.readClient);
|
|
531
622
|
const monConfig = buildMonitoringConfig(options?.monitoring);
|
|
532
623
|
setMonitoringConfig(monConfig);
|
|
624
|
+
if (oldWriteClient) {
|
|
625
|
+
closeClient(oldWriteClient);
|
|
626
|
+
}
|
|
627
|
+
if (oldReadClient && oldReadClient !== oldWriteClient) {
|
|
628
|
+
closeClient(oldReadClient);
|
|
629
|
+
}
|
|
533
630
|
return true;
|
|
534
631
|
}
|
|
535
|
-
function startHealthCheck(config, options, getDatabase2
|
|
632
|
+
function startHealthCheck(config, options, getDatabase2) {
|
|
536
633
|
const healthCheck = getHealthCheckInterval();
|
|
537
634
|
if (healthCheck) {
|
|
538
635
|
dbLogger3.debug("Health check already running");
|
|
@@ -543,48 +640,85 @@ function startHealthCheck(config, options, getDatabase2, closeDatabase2) {
|
|
|
543
640
|
reconnect: config.reconnect
|
|
544
641
|
});
|
|
545
642
|
const interval = setInterval(async () => {
|
|
643
|
+
if (isReconnecting) {
|
|
644
|
+
dbLogger3.debug("Health check skipped: reconnection in progress");
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
546
647
|
try {
|
|
547
648
|
await performHealthCheck(getDatabase2);
|
|
548
649
|
} catch (error) {
|
|
549
650
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
550
651
|
dbLogger3.error("Database health check failed", { error: message });
|
|
551
652
|
if (config.reconnect) {
|
|
552
|
-
await attemptReconnection(config, options,
|
|
653
|
+
await attemptReconnection(config, options, "health_check_failed");
|
|
553
654
|
}
|
|
554
655
|
}
|
|
555
656
|
}, config.interval);
|
|
556
657
|
setHealthCheckInterval(interval);
|
|
557
658
|
}
|
|
558
|
-
async function
|
|
659
|
+
async function triggerForceReconnect(reason) {
|
|
660
|
+
if (getDatabaseProvider()) {
|
|
661
|
+
dbLogger3.debug("Force reconnect skipped: database is externally provided", { reason });
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
if (!getWriteInstance()) {
|
|
665
|
+
dbLogger3.warn("Force reconnect skipped: database not initialized", { reason });
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
if (getIsClosing()) {
|
|
669
|
+
dbLogger3.debug("Force reconnect skipped: database is closing", { reason });
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
const options = getInitOptions();
|
|
673
|
+
const config = buildHealthCheckConfig(options?.healthCheck);
|
|
674
|
+
dbLogger3.warn("Force reconnect triggered", { reason });
|
|
675
|
+
return await attemptReconnection(config, options, reason);
|
|
676
|
+
}
|
|
677
|
+
async function attemptReconnection(config, options, reason) {
|
|
678
|
+
if (getDatabaseProvider()) {
|
|
679
|
+
dbLogger3.debug("Reconnection skipped: database is externally provided", { reason });
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
if (isReconnecting) {
|
|
683
|
+
dbLogger3.debug("Reconnection coalesced: attempt already in progress", { reason });
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
isReconnecting = true;
|
|
559
687
|
dbLogger3.warn("Attempting database reconnection", {
|
|
688
|
+
reason,
|
|
560
689
|
maxRetries: config.maxRetries,
|
|
561
690
|
retryInterval: `${config.retryInterval}ms`
|
|
562
691
|
});
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
692
|
+
try {
|
|
693
|
+
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
|
|
694
|
+
try {
|
|
695
|
+
dbLogger3.debug(`Reconnection attempt ${attempt}/${config.maxRetries}`);
|
|
696
|
+
if (attempt > 1) {
|
|
697
|
+
await new Promise((resolve) => setTimeout(resolve, config.retryInterval));
|
|
698
|
+
}
|
|
699
|
+
const success = await reconnectAndRestore(options);
|
|
700
|
+
if (success) {
|
|
701
|
+
dbLogger3.info("Database reconnection successful", { attempt });
|
|
702
|
+
return true;
|
|
703
|
+
} else {
|
|
704
|
+
dbLogger3.error(`Reconnection attempt ${attempt} failed: No write database instance created`);
|
|
705
|
+
}
|
|
706
|
+
} catch (error) {
|
|
707
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
708
|
+
dbLogger3.error(`Reconnection attempt ${attempt} failed`, {
|
|
709
|
+
error: message,
|
|
710
|
+
attempt,
|
|
711
|
+
maxRetries: config.maxRetries
|
|
712
|
+
});
|
|
568
713
|
}
|
|
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`);
|
|
714
|
+
if (attempt === config.maxRetries) {
|
|
715
|
+
dbLogger3.error("Max reconnection attempts reached, will retry on next health check");
|
|
575
716
|
}
|
|
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
717
|
}
|
|
718
|
+
} finally {
|
|
719
|
+
isReconnecting = false;
|
|
587
720
|
}
|
|
721
|
+
return true;
|
|
588
722
|
}
|
|
589
723
|
function stopHealthCheck() {
|
|
590
724
|
const healthCheck = getHealthCheckInterval();
|
|
@@ -593,6 +727,7 @@ function stopHealthCheck() {
|
|
|
593
727
|
setHealthCheckInterval(void 0);
|
|
594
728
|
dbLogger3.info("Database health check stopped");
|
|
595
729
|
}
|
|
730
|
+
isReconnecting = false;
|
|
596
731
|
}
|
|
597
732
|
|
|
598
733
|
// src/db/manager/manager.ts
|
|
@@ -603,8 +738,6 @@ var STACK_TRACE_PATTERNS = {
|
|
|
603
738
|
withParens: /\((.+):(\d+):(\d+)\)/,
|
|
604
739
|
withoutParens: /at (.+):(\d+):(\d+)/
|
|
605
740
|
};
|
|
606
|
-
var initPromise = null;
|
|
607
|
-
var isClosing = false;
|
|
608
741
|
async function cleanupDatabaseConnections(writeClient, readClient) {
|
|
609
742
|
const cleanupPromises = [];
|
|
610
743
|
if (writeClient) {
|
|
@@ -642,6 +775,33 @@ async function testDatabaseConnections(write, read) {
|
|
|
642
775
|
}
|
|
643
776
|
}
|
|
644
777
|
}
|
|
778
|
+
function validateDatabaseProvider(provider) {
|
|
779
|
+
if (typeof provider.kind !== "string" || !provider.kind.trim()) {
|
|
780
|
+
throw new Error("Database provider kind must be a non-empty string");
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function registerDatabaseProvider(provider) {
|
|
784
|
+
setDatabaseProviderInstance(provider);
|
|
785
|
+
setWriteInstance(provider.write);
|
|
786
|
+
setReadInstance(provider.read ?? provider.write);
|
|
787
|
+
return {
|
|
788
|
+
write: provider.write,
|
|
789
|
+
read: provider.read ?? provider.write
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
async function closeDatabaseProvider(provider, context) {
|
|
793
|
+
if (!provider.close) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
try {
|
|
797
|
+
await provider.close();
|
|
798
|
+
} catch (error) {
|
|
799
|
+
const closeError = error instanceof Error ? error : new Error(String(error));
|
|
800
|
+
dbLogger4.error(`Error closing ${provider.kind || "unknown"} database provider`, closeError, {
|
|
801
|
+
context
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
645
805
|
function getCallerInfo() {
|
|
646
806
|
try {
|
|
647
807
|
const stack = new Error().stack;
|
|
@@ -701,24 +861,107 @@ function getDatabase(type) {
|
|
|
701
861
|
return writeInst;
|
|
702
862
|
}
|
|
703
863
|
function setDatabase(write, read) {
|
|
864
|
+
if (getIsClosing()) {
|
|
865
|
+
throw new Error("Cannot set database while closing");
|
|
866
|
+
}
|
|
867
|
+
if (getInitPromise()) {
|
|
868
|
+
throw new Error("Cannot set database while initialization is in progress");
|
|
869
|
+
}
|
|
870
|
+
if (getDatabaseProvider()) {
|
|
871
|
+
throw new Error(
|
|
872
|
+
"An external database provider is active. Call closeDatabase() before setting a database manually."
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (getWriteClient() || getReadClient()) {
|
|
876
|
+
throw new Error(
|
|
877
|
+
"Managed database connections are active. Call closeDatabase() before setting a database manually."
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
setDatabaseProviderInstance(void 0);
|
|
704
881
|
setWriteInstance(write);
|
|
705
882
|
setReadInstance(read ?? write);
|
|
706
883
|
}
|
|
884
|
+
function setDatabaseProvider(provider) {
|
|
885
|
+
validateDatabaseProvider(provider);
|
|
886
|
+
const activeProvider = getDatabaseProvider();
|
|
887
|
+
if (getIsClosing()) {
|
|
888
|
+
throw new Error("Cannot set database provider while closing");
|
|
889
|
+
}
|
|
890
|
+
if (getInitPromise()) {
|
|
891
|
+
throw new Error("Cannot set database provider while initialization is in progress");
|
|
892
|
+
}
|
|
893
|
+
if (activeProvider === provider) {
|
|
894
|
+
return {
|
|
895
|
+
write: provider.write,
|
|
896
|
+
read: provider.read ?? provider.write
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
if (getWriteInstance() || getReadInstance() || getWriteClient() || getReadClient() || activeProvider) {
|
|
900
|
+
throw new Error(
|
|
901
|
+
"Database already initialized. Call closeDatabase() before registering a different provider."
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
return registerDatabaseProvider(provider);
|
|
905
|
+
}
|
|
707
906
|
async function initDatabase(options) {
|
|
708
|
-
if (
|
|
907
|
+
if (getIsClosing()) {
|
|
709
908
|
throw new Error("Cannot initialize database while closing");
|
|
710
909
|
}
|
|
711
910
|
const writeInst = getWriteInstance();
|
|
712
911
|
if (writeInst) {
|
|
912
|
+
const activeProvider = getDatabaseProvider();
|
|
913
|
+
if (!activeProvider && !getWriteClient()) {
|
|
914
|
+
throw new Error(
|
|
915
|
+
"Database was registered manually. Use getDatabase() directly or call closeDatabase() before initDatabase()."
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
if (options?.provider !== activeProvider) {
|
|
919
|
+
const detail = options?.provider ? "a different provider was supplied" : "the active external provider was not supplied";
|
|
920
|
+
throw new Error(`Database already initialized: ${detail}`);
|
|
921
|
+
}
|
|
713
922
|
dbLogger4.debug("Database already initialized");
|
|
714
|
-
return {
|
|
923
|
+
return {
|
|
924
|
+
write: writeInst,
|
|
925
|
+
read: getReadInstance()
|
|
926
|
+
};
|
|
715
927
|
}
|
|
716
|
-
|
|
928
|
+
const activeInitPromise = getInitPromise();
|
|
929
|
+
if (activeInitPromise) {
|
|
930
|
+
if (options?.provider !== getInitProvider()) {
|
|
931
|
+
throw new Error("Database initialization already in progress with a different provider");
|
|
932
|
+
}
|
|
717
933
|
dbLogger4.debug("Database initialization in progress, waiting...");
|
|
718
|
-
return await
|
|
934
|
+
return await activeInitPromise;
|
|
719
935
|
}
|
|
720
|
-
|
|
936
|
+
setInitProvider(options?.provider);
|
|
937
|
+
const currentInit = (async () => {
|
|
721
938
|
try {
|
|
939
|
+
if (options?.provider) {
|
|
940
|
+
const provider = options.provider;
|
|
941
|
+
try {
|
|
942
|
+
validateDatabaseProvider(provider);
|
|
943
|
+
await testDatabaseConnections(provider.write, provider.read);
|
|
944
|
+
if (getIsClosing()) {
|
|
945
|
+
throw new Error("Database closed during initialization");
|
|
946
|
+
}
|
|
947
|
+
const monConfig2 = buildMonitoringConfig(options.monitoring);
|
|
948
|
+
const result2 = registerDatabaseProvider(provider);
|
|
949
|
+
setInitOptions(options);
|
|
950
|
+
setMonitoringConfig(monConfig2);
|
|
951
|
+
dbLogger4.info("Database provider connected", {
|
|
952
|
+
kind: provider.kind,
|
|
953
|
+
hasReplica: !!(provider.read && provider.read !== provider.write)
|
|
954
|
+
});
|
|
955
|
+
return result2;
|
|
956
|
+
} catch (error) {
|
|
957
|
+
await closeDatabaseProvider(provider, "initialization_failed");
|
|
958
|
+
if (error instanceof Error && (error.message === "Database provider kind must be a non-empty string" || error.message === "Database closed during initialization")) {
|
|
959
|
+
throw error;
|
|
960
|
+
}
|
|
961
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
962
|
+
throw new Error(`Database connection test failed: ${message}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
722
965
|
const result = await createDatabaseFromEnv(options);
|
|
723
966
|
try {
|
|
724
967
|
await testDatabaseConnections(result.write, result.read);
|
|
@@ -727,7 +970,7 @@ async function initDatabase(options) {
|
|
|
727
970
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
728
971
|
throw new Error(`Database connection test failed: ${message}`);
|
|
729
972
|
}
|
|
730
|
-
if (
|
|
973
|
+
if (getIsClosing()) {
|
|
731
974
|
dbLogger4.warn("Database closed during initialization, cleaning up...");
|
|
732
975
|
await cleanupDatabaseConnections(result.writeClient, result.readClient);
|
|
733
976
|
throw new Error("Database closed during initialization");
|
|
@@ -736,13 +979,14 @@ async function initDatabase(options) {
|
|
|
736
979
|
setReadInstance(result.read);
|
|
737
980
|
setWriteClient(result.writeClient);
|
|
738
981
|
setReadClient(result.readClient);
|
|
982
|
+
setInitOptions(options);
|
|
739
983
|
const hasReplica = result.read && result.read !== result.write;
|
|
740
984
|
dbLogger4.info(
|
|
741
985
|
hasReplica ? "Database connected (Primary + Replica)" : "Database connected"
|
|
742
986
|
);
|
|
743
987
|
const healthCheckConfig = buildHealthCheckConfig(options?.healthCheck);
|
|
744
988
|
if (healthCheckConfig.enabled) {
|
|
745
|
-
startHealthCheck(healthCheckConfig, options, getDatabase
|
|
989
|
+
startHealthCheck(healthCheckConfig, options, getDatabase);
|
|
746
990
|
}
|
|
747
991
|
const monConfig = buildMonitoringConfig(options?.monitoring);
|
|
748
992
|
setMonitoringConfig(monConfig);
|
|
@@ -754,53 +998,79 @@ async function initDatabase(options) {
|
|
|
754
998
|
}
|
|
755
999
|
return { write: getWriteInstance(), read: getReadInstance() };
|
|
756
1000
|
} finally {
|
|
757
|
-
|
|
1001
|
+
setInitPromise(void 0);
|
|
1002
|
+
setInitProvider(void 0);
|
|
758
1003
|
}
|
|
759
1004
|
})();
|
|
760
|
-
|
|
1005
|
+
setInitPromise(currentInit);
|
|
1006
|
+
return await currentInit;
|
|
761
1007
|
}
|
|
762
1008
|
async function closeDatabase() {
|
|
763
|
-
|
|
1009
|
+
const activeClosePromise = getClosePromise();
|
|
1010
|
+
if (activeClosePromise) {
|
|
764
1011
|
dbLogger4.debug("Database close already in progress");
|
|
765
|
-
return;
|
|
1012
|
+
return await activeClosePromise;
|
|
766
1013
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1014
|
+
const currentClose = (async () => {
|
|
1015
|
+
setIsClosing(true);
|
|
1016
|
+
const activeInitPromise = getInitPromise();
|
|
1017
|
+
if (activeInitPromise) {
|
|
1018
|
+
dbLogger4.debug("Waiting for database initialization to complete before closing...");
|
|
1019
|
+
try {
|
|
1020
|
+
await activeInitPromise;
|
|
1021
|
+
} catch (_error) {
|
|
1022
|
+
dbLogger4.debug("Initialization failed during close, proceeding with cleanup");
|
|
1023
|
+
}
|
|
774
1024
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
try {
|
|
784
|
-
stopHealthCheck();
|
|
785
|
-
const closePromises = [];
|
|
786
|
-
const writeC = getWriteClient();
|
|
787
|
-
if (writeC) {
|
|
788
|
-
closePromises.push(closeDatabaseClient(writeC, "write"));
|
|
1025
|
+
const writeInst = getWriteInstance();
|
|
1026
|
+
const readInst = getReadInstance();
|
|
1027
|
+
const provider = getDatabaseProvider();
|
|
1028
|
+
if (!writeInst && !readInst && !provider) {
|
|
1029
|
+
dbLogger4.debug("No database connections to close");
|
|
1030
|
+
return;
|
|
789
1031
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
closePromises
|
|
1032
|
+
try {
|
|
1033
|
+
stopHealthCheck();
|
|
1034
|
+
const closePromises = [];
|
|
1035
|
+
if (provider) {
|
|
1036
|
+
closePromises.push(closeDatabaseProvider(provider, "shutdown"));
|
|
1037
|
+
}
|
|
1038
|
+
const writeC = getWriteClient();
|
|
1039
|
+
if (writeC) {
|
|
1040
|
+
closePromises.push(closeDatabaseClient(writeC, "write"));
|
|
1041
|
+
}
|
|
1042
|
+
const readC = getReadClient();
|
|
1043
|
+
if (readC && readC !== writeC) {
|
|
1044
|
+
closePromises.push(closeDatabaseClient(readC, "read"));
|
|
1045
|
+
}
|
|
1046
|
+
await Promise.allSettled(closePromises);
|
|
1047
|
+
dbLogger4.info("All database connections closed");
|
|
1048
|
+
} finally {
|
|
1049
|
+
setWriteInstance(void 0);
|
|
1050
|
+
setReadInstance(void 0);
|
|
1051
|
+
setWriteClient(void 0);
|
|
1052
|
+
setReadClient(void 0);
|
|
1053
|
+
setDatabaseProviderInstance(void 0);
|
|
1054
|
+
setMonitoringConfig(void 0);
|
|
1055
|
+
setInitOptions(void 0);
|
|
793
1056
|
}
|
|
794
|
-
|
|
795
|
-
|
|
1057
|
+
})();
|
|
1058
|
+
setClosePromise(currentClose);
|
|
1059
|
+
try {
|
|
1060
|
+
await currentClose;
|
|
796
1061
|
} finally {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1062
|
+
if (getClosePromise() === currentClose) {
|
|
1063
|
+
setClosePromise(void 0);
|
|
1064
|
+
setIsClosing(false);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async function forceReconnectDatabase(reason = "manual") {
|
|
1069
|
+
if (getDatabaseProvider()) {
|
|
1070
|
+
dbLogger4.debug("Force reconnect skipped: database is externally provided", { reason });
|
|
1071
|
+
return false;
|
|
803
1072
|
}
|
|
1073
|
+
return await triggerForceReconnect(reason);
|
|
804
1074
|
}
|
|
805
1075
|
function getDatabaseInfo() {
|
|
806
1076
|
const writeInst = getWriteInstance();
|
|
@@ -808,10 +1078,127 @@ function getDatabaseInfo() {
|
|
|
808
1078
|
return {
|
|
809
1079
|
hasWrite: !!writeInst,
|
|
810
1080
|
hasRead: !!readInst,
|
|
811
|
-
isReplica: !!(readInst && readInst !== writeInst)
|
|
1081
|
+
isReplica: !!(readInst && readInst !== writeInst),
|
|
1082
|
+
providerKind: getDatabaseProvider()?.kind
|
|
812
1083
|
};
|
|
813
1084
|
}
|
|
814
|
-
var
|
|
1085
|
+
var dbLogger5 = logger.child("@spfn/core:database");
|
|
1086
|
+
var POSTGRES_JS_CONNECTION_CODES = /* @__PURE__ */ new Set([
|
|
1087
|
+
"CONNECTION_ENDED",
|
|
1088
|
+
"CONNECTION_CLOSED",
|
|
1089
|
+
"CONNECTION_DESTROYED",
|
|
1090
|
+
"CONNECT_TIMEOUT",
|
|
1091
|
+
"CONNECTION_CONNECT_TIMEOUT"
|
|
1092
|
+
]);
|
|
1093
|
+
var NODE_NET_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1094
|
+
"ECONNRESET",
|
|
1095
|
+
"ECONNREFUSED",
|
|
1096
|
+
"EPIPE",
|
|
1097
|
+
"ETIMEDOUT",
|
|
1098
|
+
"EHOSTUNREACH",
|
|
1099
|
+
"ENETUNREACH",
|
|
1100
|
+
"ENOTFOUND"
|
|
1101
|
+
]);
|
|
1102
|
+
function isConnectionSqlState(code) {
|
|
1103
|
+
if (code.startsWith("08")) return true;
|
|
1104
|
+
if (code === "53300") return true;
|
|
1105
|
+
if (code === "57P01" || code === "57P02" || code === "57P03") return true;
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
function* unwrap(error) {
|
|
1109
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1110
|
+
const stack = [error];
|
|
1111
|
+
while (stack.length > 0) {
|
|
1112
|
+
const current = stack.pop();
|
|
1113
|
+
if (!current || typeof current !== "object" || seen.has(current)) {
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
seen.add(current);
|
|
1117
|
+
const obj = current;
|
|
1118
|
+
yield obj;
|
|
1119
|
+
for (const key of ["cause", "original", "error", "err", "inner"]) {
|
|
1120
|
+
const nested = obj[key];
|
|
1121
|
+
if (nested && typeof nested === "object" && !seen.has(nested)) {
|
|
1122
|
+
stack.push(nested);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function isConnectionLevelError(error) {
|
|
1128
|
+
if (!error) return false;
|
|
1129
|
+
if (error instanceof ConnectionError) return true;
|
|
1130
|
+
for (const candidate of unwrap(error)) {
|
|
1131
|
+
if (candidate instanceof ConnectionError) return true;
|
|
1132
|
+
const code = candidate.code;
|
|
1133
|
+
if (typeof code === "string") {
|
|
1134
|
+
if (POSTGRES_JS_CONNECTION_CODES.has(code)) return true;
|
|
1135
|
+
if (NODE_NET_ERROR_CODES.has(code)) return true;
|
|
1136
|
+
if (isConnectionSqlState(code)) return true;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
var DEFAULT_THRESHOLD = 3;
|
|
1142
|
+
var DEFAULT_WINDOW_MS = 1e4;
|
|
1143
|
+
var MIN_WINDOW_MS = 1e3;
|
|
1144
|
+
function readPositiveIntEnv(key, defaultValue, min) {
|
|
1145
|
+
const raw = process.env[key];
|
|
1146
|
+
if (raw === void 0) return defaultValue;
|
|
1147
|
+
try {
|
|
1148
|
+
return parseNumber(raw, { min, integer: true });
|
|
1149
|
+
} catch {
|
|
1150
|
+
return defaultValue;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
var ERROR_THRESHOLD = readPositiveIntEnv("DB_RECONNECT_ERROR_THRESHOLD", DEFAULT_THRESHOLD, 1);
|
|
1154
|
+
var ERROR_WINDOW_MS = readPositiveIntEnv("DB_RECONNECT_ERROR_WINDOW_MS", DEFAULT_WINDOW_MS, MIN_WINDOW_MS);
|
|
1155
|
+
var errorTimestamps = [];
|
|
1156
|
+
var reportedErrors = /* @__PURE__ */ new WeakSet();
|
|
1157
|
+
function resetConnectionErrorCounter() {
|
|
1158
|
+
errorTimestamps.length = 0;
|
|
1159
|
+
}
|
|
1160
|
+
function checkAndMarkReported(error) {
|
|
1161
|
+
let alreadySeen = false;
|
|
1162
|
+
const toMark = [];
|
|
1163
|
+
for (const candidate of unwrap(error)) {
|
|
1164
|
+
if (reportedErrors.has(candidate)) {
|
|
1165
|
+
alreadySeen = true;
|
|
1166
|
+
}
|
|
1167
|
+
toMark.push(candidate);
|
|
1168
|
+
}
|
|
1169
|
+
if (alreadySeen) return true;
|
|
1170
|
+
for (const obj of toMark) {
|
|
1171
|
+
reportedErrors.add(obj);
|
|
1172
|
+
}
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1175
|
+
function reportDatabaseError(error) {
|
|
1176
|
+
try {
|
|
1177
|
+
if (!isConnectionLevelError(error)) return;
|
|
1178
|
+
if (isReconnectingNow()) return;
|
|
1179
|
+
if (checkAndMarkReported(error)) return;
|
|
1180
|
+
const now = Date.now();
|
|
1181
|
+
errorTimestamps.push(now);
|
|
1182
|
+
const cutoff = now - ERROR_WINDOW_MS;
|
|
1183
|
+
while (errorTimestamps.length > 0 && errorTimestamps[0] < cutoff) {
|
|
1184
|
+
errorTimestamps.shift();
|
|
1185
|
+
}
|
|
1186
|
+
if (errorTimestamps.length < ERROR_THRESHOLD) return;
|
|
1187
|
+
errorTimestamps.length = 0;
|
|
1188
|
+
dbLogger5.error("Connection-error threshold crossed, forcing pool rebuild", {
|
|
1189
|
+
threshold: ERROR_THRESHOLD,
|
|
1190
|
+
windowMs: ERROR_WINDOW_MS
|
|
1191
|
+
});
|
|
1192
|
+
triggerForceReconnect("query_error_threshold").catch((err) => {
|
|
1193
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1194
|
+
dbLogger5.error("Forced reconnect after error threshold failed", { error: message });
|
|
1195
|
+
});
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1198
|
+
dbLogger5.debug("reportDatabaseError itself threw, ignoring", { error: message });
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
var BARREL_FILE_PATTERNS = [
|
|
815
1202
|
"/index",
|
|
816
1203
|
"/index.ts",
|
|
817
1204
|
"/index.js",
|
|
@@ -819,11 +1206,14 @@ var INDEX_FILE_PATTERNS = [
|
|
|
819
1206
|
"\\index",
|
|
820
1207
|
"\\index.ts",
|
|
821
1208
|
"\\index.js",
|
|
822
|
-
"\\index.mjs"
|
|
1209
|
+
"\\index.mjs",
|
|
1210
|
+
"\\config.ts",
|
|
1211
|
+
"\\config.js",
|
|
1212
|
+
"\\config.mjs"
|
|
823
1213
|
];
|
|
824
1214
|
var SUPPORTED_EXTENSIONS = [".ts", ".js", ".mjs"];
|
|
825
|
-
function
|
|
826
|
-
return
|
|
1215
|
+
function isBarrelFile(filePath) {
|
|
1216
|
+
return BARREL_FILE_PATTERNS.some((pattern) => filePath.endsWith(pattern));
|
|
827
1217
|
}
|
|
828
1218
|
function isAbsolutePath(path) {
|
|
829
1219
|
if (path.startsWith("/")) return true;
|
|
@@ -833,8 +1223,8 @@ function hasSupportedExtension(filePath) {
|
|
|
833
1223
|
if (filePath.endsWith(".d.ts")) return false;
|
|
834
1224
|
return SUPPORTED_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
835
1225
|
}
|
|
836
|
-
function
|
|
837
|
-
return files.filter((file) => !
|
|
1226
|
+
function filterBarrelFiles(files) {
|
|
1227
|
+
return files.filter((file) => !isBarrelFile(file));
|
|
838
1228
|
}
|
|
839
1229
|
function scanDirectoryRecursive(dir, extension) {
|
|
840
1230
|
const files = [];
|
|
@@ -943,7 +1333,7 @@ function discoverPackageSchemas(cwd) {
|
|
|
943
1333
|
for (const schema of packageSchemas) {
|
|
944
1334
|
const absolutePath = join(pkgPath, schema);
|
|
945
1335
|
const expandedFiles = expandGlobPattern(absolutePath);
|
|
946
|
-
const schemaFiles =
|
|
1336
|
+
const schemaFiles = filterBarrelFiles(expandedFiles);
|
|
947
1337
|
schemas.push(...schemaFiles);
|
|
948
1338
|
}
|
|
949
1339
|
}
|
|
@@ -1005,7 +1395,10 @@ function getDrizzleConfig(options = {}) {
|
|
|
1005
1395
|
schema: schema2,
|
|
1006
1396
|
out,
|
|
1007
1397
|
dialect,
|
|
1008
|
-
dbCredentials: getDbCredentials(dialect, databaseUrl)
|
|
1398
|
+
dbCredentials: getDbCredentials(dialect, databaseUrl),
|
|
1399
|
+
migrations: {
|
|
1400
|
+
prefix: options.migrationPrefix ?? "timestamp"
|
|
1401
|
+
}
|
|
1009
1402
|
};
|
|
1010
1403
|
}
|
|
1011
1404
|
const userSchema = options.schema ?? "./src/server/entities/**/*.ts";
|
|
@@ -1018,7 +1411,7 @@ function getDrizzleConfig(options = {}) {
|
|
|
1018
1411
|
for (const schema2 of allSchemas) {
|
|
1019
1412
|
const absoluteSchema = isAbsolutePath(schema2) ? schema2 : join(cwd, schema2);
|
|
1020
1413
|
const expanded = expandGlobPattern(absoluteSchema);
|
|
1021
|
-
const filtered =
|
|
1414
|
+
const filtered = filterBarrelFiles(expanded);
|
|
1022
1415
|
expandedFiles.push(...filtered);
|
|
1023
1416
|
}
|
|
1024
1417
|
allSchemas = expandedFiles;
|
|
@@ -1037,7 +1430,10 @@ function getDrizzleConfig(options = {}) {
|
|
|
1037
1430
|
out,
|
|
1038
1431
|
dialect,
|
|
1039
1432
|
dbCredentials: getDbCredentials(dialect, databaseUrl),
|
|
1040
|
-
schemaFilter
|
|
1433
|
+
schemaFilter,
|
|
1434
|
+
migrations: {
|
|
1435
|
+
prefix: options.migrationPrefix ?? "timestamp"
|
|
1436
|
+
}
|
|
1041
1437
|
};
|
|
1042
1438
|
}
|
|
1043
1439
|
function getDbCredentials(dialect, url) {
|
|
@@ -1066,13 +1462,15 @@ function generateDrizzleConfigFile(options = {}) {
|
|
|
1066
1462
|
]` : `'${normalizeSchemaPath(config.schema)}'`;
|
|
1067
1463
|
const schemaFilterLine = config.schemaFilter && config.schemaFilter.length > 0 ? `
|
|
1068
1464
|
schemaFilter: ${JSON.stringify(config.schemaFilter)},` : "";
|
|
1465
|
+
const migrationsLine = config.migrations ? `
|
|
1466
|
+
migrations: ${JSON.stringify(config.migrations)},` : "";
|
|
1069
1467
|
return `import { defineConfig } from 'drizzle-kit';
|
|
1070
1468
|
|
|
1071
1469
|
export default defineConfig({
|
|
1072
1470
|
schema: ${schemaValue},
|
|
1073
1471
|
out: '${config.out}',
|
|
1074
1472
|
dialect: '${config.dialect}',
|
|
1075
|
-
dbCredentials: ${JSON.stringify(config.dbCredentials, null, 4)},${schemaFilterLine}
|
|
1473
|
+
dbCredentials: ${JSON.stringify(config.dbCredentials, null, 4)},${schemaFilterLine}${migrationsLine}
|
|
1076
1474
|
});
|
|
1077
1475
|
`;
|
|
1078
1476
|
}
|
|
@@ -1086,10 +1484,10 @@ function timestamps() {
|
|
|
1086
1484
|
};
|
|
1087
1485
|
}
|
|
1088
1486
|
function foreignKey(name, reference, options) {
|
|
1089
|
-
return
|
|
1487
|
+
return bigint(`${name}_id`, { mode: "number" }).notNull().references(reference, { onDelete: options?.onDelete ?? "cascade" });
|
|
1090
1488
|
}
|
|
1091
1489
|
function optionalForeignKey(name, reference, options) {
|
|
1092
|
-
return
|
|
1490
|
+
return bigint(`${name}_id`, { mode: "number" }).references(reference, { onDelete: options?.onDelete ?? "set null" });
|
|
1093
1491
|
}
|
|
1094
1492
|
function uuid() {
|
|
1095
1493
|
return uuid$1("id").defaultRandom().primaryKey();
|
|
@@ -1168,7 +1566,23 @@ function runWithTransaction(tx, txId, callback) {
|
|
|
1168
1566
|
} else {
|
|
1169
1567
|
txLogger.debug("Root transaction context set", { txId, level: newLevel });
|
|
1170
1568
|
}
|
|
1171
|
-
|
|
1569
|
+
const afterCommitCallbacks = existingContext ? existingContext.afterCommitCallbacks : [];
|
|
1570
|
+
return asyncContext.run(
|
|
1571
|
+
{ tx, txId, level: newLevel, afterCommitCallbacks },
|
|
1572
|
+
callback
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
function onAfterCommit(callback) {
|
|
1576
|
+
const context = getTransactionContext();
|
|
1577
|
+
if (!context) {
|
|
1578
|
+
Promise.resolve().then(callback).catch((err) => {
|
|
1579
|
+
txLogger.error("afterCommit callback failed (no transaction)", {
|
|
1580
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1581
|
+
});
|
|
1582
|
+
});
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
context.afterCommitCallbacks.push(callback);
|
|
1172
1586
|
}
|
|
1173
1587
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
1174
1588
|
var txLogger2 = logger.child("@spfn/core:transaction");
|
|
@@ -1180,6 +1594,7 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1180
1594
|
context = "transaction"
|
|
1181
1595
|
} = options;
|
|
1182
1596
|
const timeout = options.timeout ?? defaultTimeout;
|
|
1597
|
+
const idleTimeout = options.idleTimeout ?? env.TRANSACTION_IDLE_TIMEOUT;
|
|
1183
1598
|
const txId = `tx_${randomUUID()}`;
|
|
1184
1599
|
const validateAndThrow = (condition, message, logMessage, metadata) => {
|
|
1185
1600
|
if (condition) {
|
|
@@ -1220,6 +1635,24 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1220
1635
|
"Timeout exceeds maximum",
|
|
1221
1636
|
{ txId, context, timeout, maxTimeout: MAX_TIMEOUT_MS }
|
|
1222
1637
|
);
|
|
1638
|
+
validateAndThrow(
|
|
1639
|
+
!Number.isInteger(idleTimeout),
|
|
1640
|
+
`Invalid idleTimeout value: ${idleTimeout}. Must be an integer.`,
|
|
1641
|
+
"Invalid idleTimeout type",
|
|
1642
|
+
{ txId, context, idleTimeout }
|
|
1643
|
+
);
|
|
1644
|
+
validateAndThrow(
|
|
1645
|
+
idleTimeout < 0,
|
|
1646
|
+
`Invalid idleTimeout value: ${idleTimeout}. Must be non-negative (0 to disable).`,
|
|
1647
|
+
"Invalid idleTimeout range",
|
|
1648
|
+
{ txId, context, idleTimeout }
|
|
1649
|
+
);
|
|
1650
|
+
validateAndThrow(
|
|
1651
|
+
idleTimeout > MAX_TIMEOUT_MS,
|
|
1652
|
+
`Invalid idleTimeout value: ${idleTimeout}. Maximum is ${MAX_TIMEOUT_MS}ms.`,
|
|
1653
|
+
"idleTimeout exceeds maximum",
|
|
1654
|
+
{ txId, context, idleTimeout, maxTimeout: MAX_TIMEOUT_MS }
|
|
1655
|
+
);
|
|
1223
1656
|
const writeDb = getDatabase("write");
|
|
1224
1657
|
if (!writeDb) {
|
|
1225
1658
|
const error = new TransactionError({
|
|
@@ -1251,13 +1684,25 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1251
1684
|
txLogger2.debug("Transaction started", { txId, context });
|
|
1252
1685
|
}
|
|
1253
1686
|
const startTime = Date.now();
|
|
1687
|
+
let afterCommitCallbacks = [];
|
|
1254
1688
|
try {
|
|
1255
1689
|
const result = await writeDb.transaction(async (tx) => {
|
|
1690
|
+
const transaction = tx;
|
|
1256
1691
|
if (timeout > 0 && !isNested) {
|
|
1257
|
-
await
|
|
1692
|
+
await transaction.execute(sql.raw(`SET LOCAL statement_timeout = ${timeout}`));
|
|
1258
1693
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1694
|
+
if (idleTimeout > 0 && !isNested) {
|
|
1695
|
+
await transaction.execute(sql.raw(`SET LOCAL idle_in_transaction_session_timeout = ${idleTimeout}`));
|
|
1696
|
+
}
|
|
1697
|
+
return await runWithTransaction(transaction, txId, async () => {
|
|
1698
|
+
const innerResult = await callback(transaction);
|
|
1699
|
+
if (!isNested) {
|
|
1700
|
+
const ctx = getTransactionContext();
|
|
1701
|
+
if (ctx) {
|
|
1702
|
+
afterCommitCallbacks = [...ctx.afterCommitCallbacks];
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
return innerResult;
|
|
1261
1706
|
});
|
|
1262
1707
|
});
|
|
1263
1708
|
const duration = Date.now() - startTime;
|
|
@@ -1267,7 +1712,8 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1267
1712
|
txId,
|
|
1268
1713
|
context,
|
|
1269
1714
|
duration: `${duration}ms`,
|
|
1270
|
-
threshold: `${slowThreshold}ms
|
|
1715
|
+
threshold: `${slowThreshold}ms`,
|
|
1716
|
+
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
1717
|
});
|
|
1272
1718
|
} else {
|
|
1273
1719
|
txLogger2.debug("Transaction committed", {
|
|
@@ -1277,6 +1723,24 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1277
1723
|
});
|
|
1278
1724
|
}
|
|
1279
1725
|
}
|
|
1726
|
+
if (!isNested && afterCommitCallbacks.length > 0) {
|
|
1727
|
+
if (enableLogging) {
|
|
1728
|
+
txLogger2.debug("Executing afterCommit callbacks", {
|
|
1729
|
+
txId,
|
|
1730
|
+
context,
|
|
1731
|
+
count: afterCommitCallbacks.length
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
for (const cb of afterCommitCallbacks) {
|
|
1735
|
+
Promise.resolve().then(cb).catch((err) => {
|
|
1736
|
+
txLogger2.error("afterCommit callback failed", {
|
|
1737
|
+
txId,
|
|
1738
|
+
context,
|
|
1739
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1740
|
+
});
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1280
1744
|
return result;
|
|
1281
1745
|
} catch (error) {
|
|
1282
1746
|
const duration = Date.now() - startTime;
|
|
@@ -1288,7 +1752,8 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1288
1752
|
duration: `${duration}ms`,
|
|
1289
1753
|
threshold: `${slowThreshold}ms`,
|
|
1290
1754
|
error: error instanceof Error ? error.message : String(error),
|
|
1291
|
-
errorType: error instanceof Error ? error.name : "Unknown"
|
|
1755
|
+
errorType: error instanceof Error ? error.name : "Unknown",
|
|
1756
|
+
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
1757
|
});
|
|
1293
1758
|
} else {
|
|
1294
1759
|
txLogger2.error("Transaction rolled back", {
|
|
@@ -1323,6 +1788,7 @@ function Transactional(options = {}) {
|
|
|
1323
1788
|
}
|
|
1324
1789
|
);
|
|
1325
1790
|
} catch (error) {
|
|
1791
|
+
reportDatabaseError(error);
|
|
1326
1792
|
if (error instanceof DatabaseError) {
|
|
1327
1793
|
throw error;
|
|
1328
1794
|
}
|
|
@@ -1392,7 +1858,8 @@ async function create(table, data) {
|
|
|
1392
1858
|
if (!db) {
|
|
1393
1859
|
throw new Error("Database not initialized. Call initDatabase() first.");
|
|
1394
1860
|
}
|
|
1395
|
-
const
|
|
1861
|
+
const results = await db.insert(table).values(data).returning();
|
|
1862
|
+
const result = results[0];
|
|
1396
1863
|
return result;
|
|
1397
1864
|
}
|
|
1398
1865
|
async function createMany(table, data) {
|
|
@@ -1408,10 +1875,11 @@ async function upsert(table, data, options) {
|
|
|
1408
1875
|
if (!db) {
|
|
1409
1876
|
throw new Error("Database not initialized. Call initDatabase() first.");
|
|
1410
1877
|
}
|
|
1411
|
-
const
|
|
1878
|
+
const results = await db.insert(table).values(data).onConflictDoUpdate({
|
|
1412
1879
|
target: options.target,
|
|
1413
1880
|
set: options.set || data
|
|
1414
1881
|
}).returning();
|
|
1882
|
+
const result = results[0];
|
|
1415
1883
|
return result;
|
|
1416
1884
|
}
|
|
1417
1885
|
async function updateOne(table, where, data) {
|
|
@@ -1423,7 +1891,8 @@ async function updateOne(table, where, data) {
|
|
|
1423
1891
|
if (!whereClause) {
|
|
1424
1892
|
throw new Error("updateOne requires at least one where condition");
|
|
1425
1893
|
}
|
|
1426
|
-
const
|
|
1894
|
+
const results = await db.update(table).set(data).where(whereClause).returning();
|
|
1895
|
+
const result = results[0];
|
|
1427
1896
|
return result ?? null;
|
|
1428
1897
|
}
|
|
1429
1898
|
async function updateMany(table, where, data) {
|
|
@@ -1447,7 +1916,8 @@ async function deleteOne(table, where) {
|
|
|
1447
1916
|
if (!whereClause) {
|
|
1448
1917
|
throw new Error("deleteOne requires at least one where condition");
|
|
1449
1918
|
}
|
|
1450
|
-
const
|
|
1919
|
+
const results = await db.delete(table).where(whereClause).returning();
|
|
1920
|
+
const result = results[0];
|
|
1451
1921
|
return result ?? null;
|
|
1452
1922
|
}
|
|
1453
1923
|
async function deleteMany(table, where) {
|
|
@@ -1570,6 +2040,7 @@ var BaseRepository = class {
|
|
|
1570
2040
|
try {
|
|
1571
2041
|
return await queryFn();
|
|
1572
2042
|
} catch (error) {
|
|
2043
|
+
reportDatabaseError(error);
|
|
1573
2044
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1574
2045
|
const repositoryName = this.constructor.name;
|
|
1575
2046
|
throw new RepositoryError(
|
|
@@ -1636,14 +2107,51 @@ var BaseRepository = class {
|
|
|
1636
2107
|
const orderByArray = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
|
|
1637
2108
|
query = query.orderBy(...orderByArray);
|
|
1638
2109
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
2110
|
+
const maxRows = env.DB_MAX_ROWS;
|
|
2111
|
+
const requestedLimit = options?.limit && options.limit > 0 ? options.limit : void 0;
|
|
2112
|
+
const effectiveLimit = maxRows > 0 ? Math.min(requestedLimit ?? maxRows, maxRows) : requestedLimit;
|
|
2113
|
+
if (effectiveLimit) {
|
|
2114
|
+
query = query.limit(effectiveLimit);
|
|
1641
2115
|
}
|
|
1642
2116
|
if (options?.offset) {
|
|
1643
2117
|
query = query.offset(options.offset);
|
|
1644
2118
|
}
|
|
1645
2119
|
return query;
|
|
1646
2120
|
}
|
|
2121
|
+
/**
|
|
2122
|
+
* Keyset (cursor) pagination — O(limit) instead of OFFSET's O(offset).
|
|
2123
|
+
*
|
|
2124
|
+
* Pages by a strictly-ordered, unique column instead of a numeric offset, so a
|
|
2125
|
+
* deep page doesn't scan and discard everything before it. Pass the cursor
|
|
2126
|
+
* column's value from the last row of the previous page as `after`; omit it for
|
|
2127
|
+
* the first page. The column must be unique and the sole sort key (e.g. an
|
|
2128
|
+
* auto-increment id or a ULID).
|
|
2129
|
+
*
|
|
2130
|
+
* @example
|
|
2131
|
+
* ```typescript
|
|
2132
|
+
* const page1 = await this._findManyKeyset(users, { cursorColumn: users.id, limit: 20 });
|
|
2133
|
+
* const page2 = await this._findManyKeyset(users, {
|
|
2134
|
+
* cursorColumn: users.id,
|
|
2135
|
+
* after: page1.at(-1)?.id,
|
|
2136
|
+
* limit: 20,
|
|
2137
|
+
* });
|
|
2138
|
+
* ```
|
|
2139
|
+
*/
|
|
2140
|
+
async _findManyKeyset(table, options) {
|
|
2141
|
+
const { cursorColumn, after, order = "asc", where } = options;
|
|
2142
|
+
const maxRows = env.DB_MAX_ROWS;
|
|
2143
|
+
const limit = maxRows > 0 ? Math.min(options.limit, maxRows) : options.limit;
|
|
2144
|
+
const baseWhere = where ? isSQLWrapper(where) ? where : buildWhereFromObject(table, where) : void 0;
|
|
2145
|
+
const cursorPredicate = after !== void 0 ? order === "desc" ? lt(cursorColumn, after) : gt(cursorColumn, after) : void 0;
|
|
2146
|
+
const whereClause = baseWhere && cursorPredicate ? and(baseWhere, cursorPredicate) : cursorPredicate ?? baseWhere;
|
|
2147
|
+
let query = this.readDb.select().from(table);
|
|
2148
|
+
if (whereClause) {
|
|
2149
|
+
query = query.where(whereClause);
|
|
2150
|
+
}
|
|
2151
|
+
query = query.orderBy(order === "desc" ? desc(cursorColumn) : asc(cursorColumn));
|
|
2152
|
+
query = query.limit(limit);
|
|
2153
|
+
return query;
|
|
2154
|
+
}
|
|
1647
2155
|
/**
|
|
1648
2156
|
* Create a new record
|
|
1649
2157
|
*
|
|
@@ -1660,7 +2168,8 @@ var BaseRepository = class {
|
|
|
1660
2168
|
* ```
|
|
1661
2169
|
*/
|
|
1662
2170
|
async _create(table, data) {
|
|
1663
|
-
const
|
|
2171
|
+
const results = await this.db.insert(table).values(data).returning();
|
|
2172
|
+
const result = results[0];
|
|
1664
2173
|
return result;
|
|
1665
2174
|
}
|
|
1666
2175
|
/**
|
|
@@ -1702,10 +2211,11 @@ var BaseRepository = class {
|
|
|
1702
2211
|
* ```
|
|
1703
2212
|
*/
|
|
1704
2213
|
async _upsert(table, data, options) {
|
|
1705
|
-
const
|
|
2214
|
+
const results = await this.db.insert(table).values(data).onConflictDoUpdate({
|
|
1706
2215
|
target: options.target,
|
|
1707
2216
|
set: options.set || data
|
|
1708
2217
|
}).returning();
|
|
2218
|
+
const result = results[0];
|
|
1709
2219
|
return result;
|
|
1710
2220
|
}
|
|
1711
2221
|
/**
|
|
@@ -1729,7 +2239,8 @@ var BaseRepository = class {
|
|
|
1729
2239
|
if (!whereClause) {
|
|
1730
2240
|
throw new Error("updateOne requires at least one where condition");
|
|
1731
2241
|
}
|
|
1732
|
-
const
|
|
2242
|
+
const results = await this.db.update(table).set(data).where(whereClause).returning();
|
|
2243
|
+
const result = results[0];
|
|
1733
2244
|
return result ?? null;
|
|
1734
2245
|
}
|
|
1735
2246
|
/**
|
|
@@ -1773,7 +2284,8 @@ var BaseRepository = class {
|
|
|
1773
2284
|
if (!whereClause) {
|
|
1774
2285
|
throw new Error("deleteOne requires at least one where condition");
|
|
1775
2286
|
}
|
|
1776
|
-
const
|
|
2287
|
+
const results = await this.db.delete(table).where(whereClause).returning();
|
|
2288
|
+
const result = results[0];
|
|
1777
2289
|
return result ?? null;
|
|
1778
2290
|
}
|
|
1779
2291
|
/**
|
|
@@ -1822,6 +2334,6 @@ var BaseRepository = class {
|
|
|
1822
2334
|
}
|
|
1823
2335
|
};
|
|
1824
2336
|
|
|
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 };
|
|
2337
|
+
export { BaseRepository, RepositoryError, Transactional, auditFields, checkConnection, closeDatabase, count, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, enumText, findMany, findOne, forceReconnectDatabase, foreignKey, fromPostgresError, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, getTransactionContext, id, initDatabase, isConnectionLevelError, onAfterCommit, optionalForeignKey, packageNameToSchema, publishingFields, reportDatabaseError, resetConnectionErrorCounter, runInTransaction, runWithTransaction, setDatabase, setDatabaseProvider, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
|
|
1826
2338
|
//# sourceMappingURL=index.js.map
|
|
1827
2339
|
//# sourceMappingURL=index.js.map
|