@spfn/core 0.2.0-beta.9 → 0.3.0-beta.10
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 +693 -279
- package/dist/app-config/index.d.ts +96 -0
- package/dist/app-config/index.js +58 -0
- package/dist/app-config/index.js.map +1 -0
- package/dist/authz/index.d.ts +34 -0
- package/dist/authz/index.js +430 -0
- package/dist/authz/index.js.map +1 -0
- package/dist/boss-BiDCbbAQ.d.ts +337 -0
- 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 +576 -30
- package/dist/config/index.js +136 -17
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +221 -0
- package/dist/contract/index.js +558 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/db/index.d.ts +704 -94
- package/dist/db/index.js +1105 -260
- package/dist/db/index.js.map +1 -1
- package/dist/define-middleware-CVKgqo8S.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 +55 -25
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.js +40 -7
- 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 +55 -3
- package/dist/event/sse/client.js +164 -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 +48 -3
- package/dist/job/index.js +265 -74
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.js +14 -12
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +352 -10
- package/dist/middleware/index.js +1079 -13
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +39 -3
- package/dist/nextjs/index.js +82 -29
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +69 -52
- package/dist/nextjs/server.js +140 -60
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +208 -0
- package/dist/ops/index.js +800 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/route/index.d.ts +10 -694
- package/dist/route/index.js +112 -23
- package/dist/route/index.js.map +1 -1
- package/dist/route-builder-2ani2jEI.d.ts +553 -0
- package/dist/router-DJdpwuB6.d.ts +127 -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 +619 -31
- package/dist/server/index.js +2583 -360
- 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-ClQVomgV.d.ts +115 -0
- package/dist/types-ZQODsBft.d.ts +282 -0
- package/docs/file-upload.md +195 -333
- package/package.json +250 -208
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +516 -0
- package/src/config/README.md +326 -0
- package/src/contract/README.md +326 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +354 -0
- package/src/db/transaction/README.md +822 -0
- package/src/env/README.md +651 -0
- package/src/errors/README.md +429 -0
- package/src/event/README.md +741 -0
- package/src/job/README.md +613 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +608 -0
- package/src/route/README.md +738 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
- package/dist/boss-DI1r4kTS.d.ts +0 -244
- 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,18 @@
|
|
|
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
|
-
import { QueryError, ConnectionError, DeadlockError, TransactionError, ConstraintViolationError, DuplicateEntryError,
|
|
6
|
+
import { QueryError, ConnectionError, DeadlockError, TransactionError, ConstraintViolationError, DuplicateEntryError, SerializableError } from '@spfn/core/errors';
|
|
6
7
|
import { parseNumber, parseBoolean } from '@spfn/core/env';
|
|
7
|
-
import { existsSync,
|
|
8
|
-
import { join,
|
|
9
|
-
import
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync, realpathSync, lstatSync } from 'fs';
|
|
9
|
+
import { join, relative } from 'path';
|
|
10
|
+
import mm from 'micromatch';
|
|
11
|
+
import { sql, count as count$1, lt, gt, and, desc, asc, eq } from 'drizzle-orm';
|
|
12
|
+
import { bigserial, timestamp, bigint, uuid as uuid$1, text, jsonb, pgSchema } from 'drizzle-orm/pg-core';
|
|
13
|
+
import { createHash, randomUUID } from 'crypto';
|
|
10
14
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
11
15
|
import { createMiddleware } from 'hono/factory';
|
|
12
|
-
import { randomUUID } from 'crypto';
|
|
13
|
-
import { count as count$1, sql, eq, and } from 'drizzle-orm';
|
|
14
16
|
|
|
15
17
|
// src/db/manager/factory.ts
|
|
16
18
|
function parseUniqueViolation(message) {
|
|
@@ -129,7 +131,16 @@ function fromPostgresError(error) {
|
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
// src/db/manager/connection.ts
|
|
134
|
+
function getSocketFamily() {
|
|
135
|
+
const family = process.env.DATABASE_SOCKET_FAMILY;
|
|
136
|
+
if (family === "4") return 4;
|
|
137
|
+
if (family === "6") return 6;
|
|
138
|
+
return void 0;
|
|
139
|
+
}
|
|
132
140
|
var dbLogger = logger.child("@spfn/core:database");
|
|
141
|
+
function isTransactionPooler(connectionString) {
|
|
142
|
+
return /:6543(?:[/?]|$)|[?&]pgbouncer=true\b|pooler\.supabase\.com/i.test(connectionString);
|
|
143
|
+
}
|
|
133
144
|
var DEFAULT_CONNECT_TIMEOUT = 10;
|
|
134
145
|
function delay(ms) {
|
|
135
146
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -189,10 +200,23 @@ async function createDatabaseConnection(connectionString, poolConfig, retryConfi
|
|
|
189
200
|
let client;
|
|
190
201
|
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
|
|
191
202
|
try {
|
|
203
|
+
const socketFamily = getSocketFamily();
|
|
204
|
+
const prepare = poolConfig.prepare ?? !isTransactionPooler(connectionString);
|
|
192
205
|
client = postgres(connectionString, {
|
|
193
206
|
max: poolConfig.max,
|
|
194
207
|
idle_timeout: poolConfig.idleTimeout,
|
|
195
|
-
connect_timeout: DEFAULT_CONNECT_TIMEOUT
|
|
208
|
+
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
|
|
209
|
+
prepare,
|
|
210
|
+
...socketFamily && {
|
|
211
|
+
socket: ({ host, port }) => new Promise((resolve, reject) => {
|
|
212
|
+
const socket = new net.Socket();
|
|
213
|
+
socket.on("error", reject);
|
|
214
|
+
socket.connect(
|
|
215
|
+
{ port: port[0], host: host[0], family: socketFamily },
|
|
216
|
+
() => resolve(socket)
|
|
217
|
+
);
|
|
218
|
+
})
|
|
219
|
+
}
|
|
196
220
|
});
|
|
197
221
|
await client`SELECT 1 as test`;
|
|
198
222
|
if (attempt > 0) {
|
|
@@ -325,10 +349,27 @@ function parseEnvBoolean(key, defaultValue) {
|
|
|
325
349
|
throw new Error(`${key}: ${message}`);
|
|
326
350
|
}
|
|
327
351
|
}
|
|
352
|
+
function parseEnvBooleanOptional(key) {
|
|
353
|
+
const value = process.env[key];
|
|
354
|
+
if (value === void 0) {
|
|
355
|
+
return void 0;
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
return parseBoolean(value);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
361
|
+
throw new Error(`${key}: ${message}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
328
364
|
function getPoolConfig(options) {
|
|
365
|
+
const max = options?.max ?? parseEnvNumber("DB_POOL_MAX", 20, 10);
|
|
329
366
|
return {
|
|
330
|
-
max
|
|
331
|
-
idleTimeout: options?.idleTimeout ?? parseEnvNumber("DB_POOL_IDLE_TIMEOUT", 30, 20)
|
|
367
|
+
max,
|
|
368
|
+
idleTimeout: options?.idleTimeout ?? parseEnvNumber("DB_POOL_IDLE_TIMEOUT", 30, 20),
|
|
369
|
+
// Defaults to the write `max`; DB_POOL_READ_MAX lets ops size the replica pool separately
|
|
370
|
+
readMax: options?.readMax ?? parseEnvNumber("DB_POOL_READ_MAX", max, max),
|
|
371
|
+
// Left undefined for connection-layer auto-detection unless explicitly overridden.
|
|
372
|
+
prepare: options?.prepare ?? parseEnvBooleanOptional("SPFN_DB_PREPARE")
|
|
332
373
|
};
|
|
333
374
|
}
|
|
334
375
|
function getRetryConfig() {
|
|
@@ -397,11 +438,12 @@ async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig
|
|
|
397
438
|
dbLogger2.error("Failed to connect to write database", errorObj);
|
|
398
439
|
throw new Error(`Write database connection failed: ${errorObj.message}`, { cause: error });
|
|
399
440
|
}
|
|
441
|
+
const readPoolConfig = { ...poolConfig, max: poolConfig.readMax ?? poolConfig.max };
|
|
400
442
|
try {
|
|
401
|
-
readClient = await createDatabaseConnection(readUrl,
|
|
443
|
+
readClient = await createDatabaseConnection(readUrl, readPoolConfig, retryConfig);
|
|
402
444
|
return {
|
|
403
|
-
write: drizzle(writeClient),
|
|
404
|
-
read: drizzle(readClient),
|
|
445
|
+
write: drizzle({ client: writeClient }),
|
|
446
|
+
read: drizzle({ client: readClient }),
|
|
405
447
|
writeClient,
|
|
406
448
|
readClient
|
|
407
449
|
};
|
|
@@ -417,8 +459,8 @@ async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig
|
|
|
417
459
|
}
|
|
418
460
|
);
|
|
419
461
|
return {
|
|
420
|
-
write: drizzle(writeClient),
|
|
421
|
-
read: drizzle(writeClient),
|
|
462
|
+
write: drizzle({ client: writeClient }),
|
|
463
|
+
read: drizzle({ client: writeClient }),
|
|
422
464
|
writeClient,
|
|
423
465
|
readClient: writeClient
|
|
424
466
|
};
|
|
@@ -426,7 +468,7 @@ async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig
|
|
|
426
468
|
}
|
|
427
469
|
async function createSingleClient(url, poolConfig, retryConfig) {
|
|
428
470
|
const client = await createDatabaseConnection(url, poolConfig, retryConfig);
|
|
429
|
-
const db = drizzle(client);
|
|
471
|
+
const db = drizzle({ client });
|
|
430
472
|
return {
|
|
431
473
|
write: db,
|
|
432
474
|
read: db,
|
|
@@ -478,6 +520,11 @@ async function createDatabaseFromEnv(options) {
|
|
|
478
520
|
throw new Error("No database pattern detected despite passing config check");
|
|
479
521
|
}
|
|
480
522
|
|
|
523
|
+
// src/db/manager/path-utils.ts
|
|
524
|
+
function toPosixPath(path) {
|
|
525
|
+
return path.split("\\").join("/");
|
|
526
|
+
}
|
|
527
|
+
|
|
481
528
|
// src/db/manager/global-state.ts
|
|
482
529
|
var getWriteInstance = () => globalThis.__SPFN_DB_WRITE__;
|
|
483
530
|
var setWriteInstance = (instance) => {
|
|
@@ -487,6 +534,10 @@ var getReadInstance = () => globalThis.__SPFN_DB_READ__;
|
|
|
487
534
|
var setReadInstance = (instance) => {
|
|
488
535
|
globalThis.__SPFN_DB_READ__ = instance;
|
|
489
536
|
};
|
|
537
|
+
var getDatabaseProvider = () => globalThis.__SPFN_DB_PROVIDER__;
|
|
538
|
+
var setDatabaseProviderInstance = (provider) => {
|
|
539
|
+
globalThis.__SPFN_DB_PROVIDER__ = provider;
|
|
540
|
+
};
|
|
490
541
|
var getWriteClient = () => globalThis.__SPFN_DB_WRITE_CLIENT__;
|
|
491
542
|
var setWriteClient = (client) => {
|
|
492
543
|
globalThis.__SPFN_DB_WRITE_CLIENT__ = client;
|
|
@@ -502,7 +553,32 @@ var setHealthCheckInterval = (interval) => {
|
|
|
502
553
|
var setMonitoringConfig = (config) => {
|
|
503
554
|
globalThis.__SPFN_DB_MONITORING__ = config;
|
|
504
555
|
};
|
|
556
|
+
var getInitOptions = () => globalThis.__SPFN_DB_INIT_OPTIONS__;
|
|
557
|
+
var setInitOptions = (options) => {
|
|
558
|
+
globalThis.__SPFN_DB_INIT_OPTIONS__ = options;
|
|
559
|
+
};
|
|
560
|
+
var getInitPromise = () => globalThis.__SPFN_DB_INIT_PROMISE__;
|
|
561
|
+
var setInitPromise = (promise) => {
|
|
562
|
+
globalThis.__SPFN_DB_INIT_PROMISE__ = promise;
|
|
563
|
+
};
|
|
564
|
+
var getInitProvider = () => globalThis.__SPFN_DB_INIT_PROVIDER__;
|
|
565
|
+
var setInitProvider = (provider) => {
|
|
566
|
+
globalThis.__SPFN_DB_INIT_PROVIDER__ = provider;
|
|
567
|
+
};
|
|
568
|
+
var getClosePromise = () => globalThis.__SPFN_DB_CLOSE_PROMISE__;
|
|
569
|
+
var setClosePromise = (promise) => {
|
|
570
|
+
globalThis.__SPFN_DB_CLOSE_PROMISE__ = promise;
|
|
571
|
+
};
|
|
572
|
+
var getIsClosing = () => globalThis.__SPFN_DB_CLOSING__ === true;
|
|
573
|
+
var setIsClosing = (closing) => {
|
|
574
|
+
globalThis.__SPFN_DB_CLOSING__ = closing;
|
|
575
|
+
};
|
|
505
576
|
var dbLogger3 = logger.child("@spfn/core:database");
|
|
577
|
+
var CLIENT_CLOSE_TIMEOUT = 5;
|
|
578
|
+
var isReconnecting = false;
|
|
579
|
+
function isReconnectingNow() {
|
|
580
|
+
return isReconnecting;
|
|
581
|
+
}
|
|
506
582
|
async function testDatabaseConnection(db) {
|
|
507
583
|
await db.execute("SELECT 1");
|
|
508
584
|
}
|
|
@@ -514,8 +590,17 @@ async function performHealthCheck(getDatabase2) {
|
|
|
514
590
|
await testDatabaseConnection(read);
|
|
515
591
|
}
|
|
516
592
|
}
|
|
517
|
-
async function
|
|
518
|
-
|
|
593
|
+
async function closeClient(client) {
|
|
594
|
+
try {
|
|
595
|
+
await client.end({ timeout: CLIENT_CLOSE_TIMEOUT });
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async function reconnectAndRestore(options) {
|
|
600
|
+
if (getIsClosing()) {
|
|
601
|
+
dbLogger3.debug("reconnectAndRestore aborted: database is closing");
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
519
604
|
const result = await createDatabaseFromEnv(options);
|
|
520
605
|
if (!result.write) {
|
|
521
606
|
return false;
|
|
@@ -524,15 +609,33 @@ async function reconnectAndRestore(options, closeDatabase2) {
|
|
|
524
609
|
if (result.read && result.read !== result.write) {
|
|
525
610
|
await testDatabaseConnection(result.read);
|
|
526
611
|
}
|
|
612
|
+
if (getIsClosing()) {
|
|
613
|
+
dbLogger3.warn("reconnectAndRestore: close started mid-rebuild, discarding new pool");
|
|
614
|
+
if (result.writeClient) {
|
|
615
|
+
await closeClient(result.writeClient);
|
|
616
|
+
}
|
|
617
|
+
if (result.readClient && result.readClient !== result.writeClient) {
|
|
618
|
+
await closeClient(result.readClient);
|
|
619
|
+
}
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
const oldWriteClient = getWriteClient();
|
|
623
|
+
const oldReadClient = getReadClient();
|
|
527
624
|
setWriteInstance(result.write);
|
|
528
625
|
setReadInstance(result.read);
|
|
529
626
|
setWriteClient(result.writeClient);
|
|
530
627
|
setReadClient(result.readClient);
|
|
531
628
|
const monConfig = buildMonitoringConfig(options?.monitoring);
|
|
532
629
|
setMonitoringConfig(monConfig);
|
|
630
|
+
if (oldWriteClient) {
|
|
631
|
+
closeClient(oldWriteClient);
|
|
632
|
+
}
|
|
633
|
+
if (oldReadClient && oldReadClient !== oldWriteClient) {
|
|
634
|
+
closeClient(oldReadClient);
|
|
635
|
+
}
|
|
533
636
|
return true;
|
|
534
637
|
}
|
|
535
|
-
function startHealthCheck(config, options, getDatabase2
|
|
638
|
+
function startHealthCheck(config, options, getDatabase2) {
|
|
536
639
|
const healthCheck = getHealthCheckInterval();
|
|
537
640
|
if (healthCheck) {
|
|
538
641
|
dbLogger3.debug("Health check already running");
|
|
@@ -543,48 +646,85 @@ function startHealthCheck(config, options, getDatabase2, closeDatabase2) {
|
|
|
543
646
|
reconnect: config.reconnect
|
|
544
647
|
});
|
|
545
648
|
const interval = setInterval(async () => {
|
|
649
|
+
if (isReconnecting) {
|
|
650
|
+
dbLogger3.debug("Health check skipped: reconnection in progress");
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
546
653
|
try {
|
|
547
654
|
await performHealthCheck(getDatabase2);
|
|
548
655
|
} catch (error) {
|
|
549
656
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
550
657
|
dbLogger3.error("Database health check failed", { error: message });
|
|
551
658
|
if (config.reconnect) {
|
|
552
|
-
await attemptReconnection(config, options,
|
|
659
|
+
await attemptReconnection(config, options, "health_check_failed");
|
|
553
660
|
}
|
|
554
661
|
}
|
|
555
662
|
}, config.interval);
|
|
556
663
|
setHealthCheckInterval(interval);
|
|
557
664
|
}
|
|
558
|
-
async function
|
|
665
|
+
async function triggerForceReconnect(reason) {
|
|
666
|
+
if (getDatabaseProvider()) {
|
|
667
|
+
dbLogger3.debug("Force reconnect skipped: database is externally provided", { reason });
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
if (!getWriteInstance()) {
|
|
671
|
+
dbLogger3.warn("Force reconnect skipped: database not initialized", { reason });
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
if (getIsClosing()) {
|
|
675
|
+
dbLogger3.debug("Force reconnect skipped: database is closing", { reason });
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
const options = getInitOptions();
|
|
679
|
+
const config = buildHealthCheckConfig(options?.healthCheck);
|
|
680
|
+
dbLogger3.warn("Force reconnect triggered", { reason });
|
|
681
|
+
return await attemptReconnection(config, options, reason);
|
|
682
|
+
}
|
|
683
|
+
async function attemptReconnection(config, options, reason) {
|
|
684
|
+
if (getDatabaseProvider()) {
|
|
685
|
+
dbLogger3.debug("Reconnection skipped: database is externally provided", { reason });
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
if (isReconnecting) {
|
|
689
|
+
dbLogger3.debug("Reconnection coalesced: attempt already in progress", { reason });
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
isReconnecting = true;
|
|
559
693
|
dbLogger3.warn("Attempting database reconnection", {
|
|
694
|
+
reason,
|
|
560
695
|
maxRetries: config.maxRetries,
|
|
561
696
|
retryInterval: `${config.retryInterval}ms`
|
|
562
697
|
});
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
698
|
+
try {
|
|
699
|
+
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
|
|
700
|
+
try {
|
|
701
|
+
dbLogger3.debug(`Reconnection attempt ${attempt}/${config.maxRetries}`);
|
|
702
|
+
if (attempt > 1) {
|
|
703
|
+
await new Promise((resolve) => setTimeout(resolve, config.retryInterval));
|
|
704
|
+
}
|
|
705
|
+
const success = await reconnectAndRestore(options);
|
|
706
|
+
if (success) {
|
|
707
|
+
dbLogger3.info("Database reconnection successful", { attempt });
|
|
708
|
+
return true;
|
|
709
|
+
} else {
|
|
710
|
+
dbLogger3.error(`Reconnection attempt ${attempt} failed: No write database instance created`);
|
|
711
|
+
}
|
|
712
|
+
} catch (error) {
|
|
713
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
714
|
+
dbLogger3.error(`Reconnection attempt ${attempt} failed`, {
|
|
715
|
+
error: message,
|
|
716
|
+
attempt,
|
|
717
|
+
maxRetries: config.maxRetries
|
|
718
|
+
});
|
|
568
719
|
}
|
|
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`);
|
|
720
|
+
if (attempt === config.maxRetries) {
|
|
721
|
+
dbLogger3.error("Max reconnection attempts reached, will retry on next health check");
|
|
575
722
|
}
|
|
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
723
|
}
|
|
724
|
+
} finally {
|
|
725
|
+
isReconnecting = false;
|
|
587
726
|
}
|
|
727
|
+
return true;
|
|
588
728
|
}
|
|
589
729
|
function stopHealthCheck() {
|
|
590
730
|
const healthCheck = getHealthCheckInterval();
|
|
@@ -593,6 +733,7 @@ function stopHealthCheck() {
|
|
|
593
733
|
setHealthCheckInterval(void 0);
|
|
594
734
|
dbLogger3.info("Database health check stopped");
|
|
595
735
|
}
|
|
736
|
+
isReconnecting = false;
|
|
596
737
|
}
|
|
597
738
|
|
|
598
739
|
// src/db/manager/manager.ts
|
|
@@ -603,8 +744,6 @@ var STACK_TRACE_PATTERNS = {
|
|
|
603
744
|
withParens: /\((.+):(\d+):(\d+)\)/,
|
|
604
745
|
withoutParens: /at (.+):(\d+):(\d+)/
|
|
605
746
|
};
|
|
606
|
-
var initPromise = null;
|
|
607
|
-
var isClosing = false;
|
|
608
747
|
async function cleanupDatabaseConnections(writeClient, readClient) {
|
|
609
748
|
const cleanupPromises = [];
|
|
610
749
|
if (writeClient) {
|
|
@@ -642,13 +781,40 @@ async function testDatabaseConnections(write, read) {
|
|
|
642
781
|
}
|
|
643
782
|
}
|
|
644
783
|
}
|
|
784
|
+
function validateDatabaseProvider(provider) {
|
|
785
|
+
if (typeof provider.kind !== "string" || !provider.kind.trim()) {
|
|
786
|
+
throw new Error("Database provider kind must be a non-empty string");
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function registerDatabaseProvider(provider) {
|
|
790
|
+
setDatabaseProviderInstance(provider);
|
|
791
|
+
setWriteInstance(provider.write);
|
|
792
|
+
setReadInstance(provider.read ?? provider.write);
|
|
793
|
+
return {
|
|
794
|
+
write: provider.write,
|
|
795
|
+
read: provider.read ?? provider.write
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
async function closeDatabaseProvider(provider, context) {
|
|
799
|
+
if (!provider.close) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
try {
|
|
803
|
+
await provider.close();
|
|
804
|
+
} catch (error) {
|
|
805
|
+
const closeError = error instanceof Error ? error : new Error(String(error));
|
|
806
|
+
dbLogger4.error(`Error closing ${provider.kind || "unknown"} database provider`, closeError, {
|
|
807
|
+
context
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
645
811
|
function getCallerInfo() {
|
|
646
812
|
try {
|
|
647
813
|
const stack = new Error().stack;
|
|
648
814
|
if (!stack) return void 0;
|
|
649
815
|
const lines = stack.split("\n");
|
|
650
816
|
for (let i = STACK_TRACE_SKIP_LINES; i < lines.length; i++) {
|
|
651
|
-
const line = lines[i];
|
|
817
|
+
const line = toPosixPath(lines[i]);
|
|
652
818
|
if (!line.includes("node_modules") && !line.includes("/db/manager/")) {
|
|
653
819
|
const match = line.match(STACK_TRACE_PATTERNS.withParens) || line.match(STACK_TRACE_PATTERNS.withoutParens);
|
|
654
820
|
if (match) {
|
|
@@ -701,24 +867,107 @@ function getDatabase(type) {
|
|
|
701
867
|
return writeInst;
|
|
702
868
|
}
|
|
703
869
|
function setDatabase(write, read) {
|
|
870
|
+
if (getIsClosing()) {
|
|
871
|
+
throw new Error("Cannot set database while closing");
|
|
872
|
+
}
|
|
873
|
+
if (getInitPromise()) {
|
|
874
|
+
throw new Error("Cannot set database while initialization is in progress");
|
|
875
|
+
}
|
|
876
|
+
if (getDatabaseProvider()) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
"An external database provider is active. Call closeDatabase() before setting a database manually."
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (getWriteClient() || getReadClient()) {
|
|
882
|
+
throw new Error(
|
|
883
|
+
"Managed database connections are active. Call closeDatabase() before setting a database manually."
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
setDatabaseProviderInstance(void 0);
|
|
704
887
|
setWriteInstance(write);
|
|
705
888
|
setReadInstance(read ?? write);
|
|
706
889
|
}
|
|
890
|
+
function setDatabaseProvider(provider) {
|
|
891
|
+
validateDatabaseProvider(provider);
|
|
892
|
+
const activeProvider = getDatabaseProvider();
|
|
893
|
+
if (getIsClosing()) {
|
|
894
|
+
throw new Error("Cannot set database provider while closing");
|
|
895
|
+
}
|
|
896
|
+
if (getInitPromise()) {
|
|
897
|
+
throw new Error("Cannot set database provider while initialization is in progress");
|
|
898
|
+
}
|
|
899
|
+
if (activeProvider === provider) {
|
|
900
|
+
return {
|
|
901
|
+
write: provider.write,
|
|
902
|
+
read: provider.read ?? provider.write
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
if (getWriteInstance() || getReadInstance() || getWriteClient() || getReadClient() || activeProvider) {
|
|
906
|
+
throw new Error(
|
|
907
|
+
"Database already initialized. Call closeDatabase() before registering a different provider."
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
return registerDatabaseProvider(provider);
|
|
911
|
+
}
|
|
707
912
|
async function initDatabase(options) {
|
|
708
|
-
if (
|
|
913
|
+
if (getIsClosing()) {
|
|
709
914
|
throw new Error("Cannot initialize database while closing");
|
|
710
915
|
}
|
|
711
916
|
const writeInst = getWriteInstance();
|
|
712
917
|
if (writeInst) {
|
|
918
|
+
const activeProvider = getDatabaseProvider();
|
|
919
|
+
if (!activeProvider && !getWriteClient()) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
"Database was registered manually. Use getDatabase() directly or call closeDatabase() before initDatabase()."
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
if (options?.provider !== activeProvider) {
|
|
925
|
+
const detail = options?.provider ? "a different provider was supplied" : "the active external provider was not supplied";
|
|
926
|
+
throw new Error(`Database already initialized: ${detail}`);
|
|
927
|
+
}
|
|
713
928
|
dbLogger4.debug("Database already initialized");
|
|
714
|
-
return {
|
|
929
|
+
return {
|
|
930
|
+
write: writeInst,
|
|
931
|
+
read: getReadInstance()
|
|
932
|
+
};
|
|
715
933
|
}
|
|
716
|
-
|
|
934
|
+
const activeInitPromise = getInitPromise();
|
|
935
|
+
if (activeInitPromise) {
|
|
936
|
+
if (options?.provider !== getInitProvider()) {
|
|
937
|
+
throw new Error("Database initialization already in progress with a different provider");
|
|
938
|
+
}
|
|
717
939
|
dbLogger4.debug("Database initialization in progress, waiting...");
|
|
718
|
-
return await
|
|
940
|
+
return await activeInitPromise;
|
|
719
941
|
}
|
|
720
|
-
|
|
942
|
+
setInitProvider(options?.provider);
|
|
943
|
+
const currentInit = (async () => {
|
|
721
944
|
try {
|
|
945
|
+
if (options?.provider) {
|
|
946
|
+
const provider = options.provider;
|
|
947
|
+
try {
|
|
948
|
+
validateDatabaseProvider(provider);
|
|
949
|
+
await testDatabaseConnections(provider.write, provider.read);
|
|
950
|
+
if (getIsClosing()) {
|
|
951
|
+
throw new Error("Database closed during initialization");
|
|
952
|
+
}
|
|
953
|
+
const monConfig2 = buildMonitoringConfig(options.monitoring);
|
|
954
|
+
const result2 = registerDatabaseProvider(provider);
|
|
955
|
+
setInitOptions(options);
|
|
956
|
+
setMonitoringConfig(monConfig2);
|
|
957
|
+
dbLogger4.info("Database provider connected", {
|
|
958
|
+
kind: provider.kind,
|
|
959
|
+
hasReplica: !!(provider.read && provider.read !== provider.write)
|
|
960
|
+
});
|
|
961
|
+
return result2;
|
|
962
|
+
} catch (error) {
|
|
963
|
+
await closeDatabaseProvider(provider, "initialization_failed");
|
|
964
|
+
if (error instanceof Error && (error.message === "Database provider kind must be a non-empty string" || error.message === "Database closed during initialization")) {
|
|
965
|
+
throw error;
|
|
966
|
+
}
|
|
967
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
968
|
+
throw new Error(`Database connection test failed: ${message}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
722
971
|
const result = await createDatabaseFromEnv(options);
|
|
723
972
|
try {
|
|
724
973
|
await testDatabaseConnections(result.write, result.read);
|
|
@@ -727,7 +976,7 @@ async function initDatabase(options) {
|
|
|
727
976
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
728
977
|
throw new Error(`Database connection test failed: ${message}`);
|
|
729
978
|
}
|
|
730
|
-
if (
|
|
979
|
+
if (getIsClosing()) {
|
|
731
980
|
dbLogger4.warn("Database closed during initialization, cleaning up...");
|
|
732
981
|
await cleanupDatabaseConnections(result.writeClient, result.readClient);
|
|
733
982
|
throw new Error("Database closed during initialization");
|
|
@@ -736,13 +985,14 @@ async function initDatabase(options) {
|
|
|
736
985
|
setReadInstance(result.read);
|
|
737
986
|
setWriteClient(result.writeClient);
|
|
738
987
|
setReadClient(result.readClient);
|
|
988
|
+
setInitOptions(options);
|
|
739
989
|
const hasReplica = result.read && result.read !== result.write;
|
|
740
990
|
dbLogger4.info(
|
|
741
991
|
hasReplica ? "Database connected (Primary + Replica)" : "Database connected"
|
|
742
992
|
);
|
|
743
993
|
const healthCheckConfig = buildHealthCheckConfig(options?.healthCheck);
|
|
744
994
|
if (healthCheckConfig.enabled) {
|
|
745
|
-
startHealthCheck(healthCheckConfig, options, getDatabase
|
|
995
|
+
startHealthCheck(healthCheckConfig, options, getDatabase);
|
|
746
996
|
}
|
|
747
997
|
const monConfig = buildMonitoringConfig(options?.monitoring);
|
|
748
998
|
setMonitoringConfig(monConfig);
|
|
@@ -754,166 +1004,323 @@ async function initDatabase(options) {
|
|
|
754
1004
|
}
|
|
755
1005
|
return { write: getWriteInstance(), read: getReadInstance() };
|
|
756
1006
|
} finally {
|
|
757
|
-
|
|
1007
|
+
setInitPromise(void 0);
|
|
1008
|
+
setInitProvider(void 0);
|
|
758
1009
|
}
|
|
759
1010
|
})();
|
|
760
|
-
|
|
1011
|
+
setInitPromise(currentInit);
|
|
1012
|
+
return await currentInit;
|
|
761
1013
|
}
|
|
762
1014
|
async function closeDatabase() {
|
|
763
|
-
|
|
1015
|
+
const activeClosePromise = getClosePromise();
|
|
1016
|
+
if (activeClosePromise) {
|
|
764
1017
|
dbLogger4.debug("Database close already in progress");
|
|
765
|
-
return;
|
|
1018
|
+
return await activeClosePromise;
|
|
766
1019
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1020
|
+
const currentClose = (async () => {
|
|
1021
|
+
setIsClosing(true);
|
|
1022
|
+
const activeInitPromise = getInitPromise();
|
|
1023
|
+
if (activeInitPromise) {
|
|
1024
|
+
dbLogger4.debug("Waiting for database initialization to complete before closing...");
|
|
1025
|
+
try {
|
|
1026
|
+
await activeInitPromise;
|
|
1027
|
+
} catch (_error) {
|
|
1028
|
+
dbLogger4.debug("Initialization failed during close, proceeding with cleanup");
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const writeInst = getWriteInstance();
|
|
1032
|
+
const readInst = getReadInstance();
|
|
1033
|
+
const provider = getDatabaseProvider();
|
|
1034
|
+
if (!writeInst && !readInst && !provider) {
|
|
1035
|
+
dbLogger4.debug("No database connections to close");
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
770
1038
|
try {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1039
|
+
stopHealthCheck();
|
|
1040
|
+
const closePromises = [];
|
|
1041
|
+
if (provider) {
|
|
1042
|
+
closePromises.push(closeDatabaseProvider(provider, "shutdown"));
|
|
1043
|
+
}
|
|
1044
|
+
const writeC = getWriteClient();
|
|
1045
|
+
if (writeC) {
|
|
1046
|
+
closePromises.push(closeDatabaseClient(writeC, "write"));
|
|
1047
|
+
}
|
|
1048
|
+
const readC = getReadClient();
|
|
1049
|
+
if (readC && readC !== writeC) {
|
|
1050
|
+
closePromises.push(closeDatabaseClient(readC, "read"));
|
|
1051
|
+
}
|
|
1052
|
+
await Promise.allSettled(closePromises);
|
|
1053
|
+
dbLogger4.info("All database connections closed");
|
|
1054
|
+
} finally {
|
|
1055
|
+
setWriteInstance(void 0);
|
|
1056
|
+
setReadInstance(void 0);
|
|
1057
|
+
setWriteClient(void 0);
|
|
1058
|
+
setReadClient(void 0);
|
|
1059
|
+
setDatabaseProviderInstance(void 0);
|
|
1060
|
+
setMonitoringConfig(void 0);
|
|
1061
|
+
setInitOptions(void 0);
|
|
774
1062
|
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const readInst = getReadInstance();
|
|
778
|
-
if (!writeInst && !readInst) {
|
|
779
|
-
dbLogger4.debug("No database connections to close");
|
|
780
|
-
isClosing = false;
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
1063
|
+
})();
|
|
1064
|
+
setClosePromise(currentClose);
|
|
783
1065
|
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");
|
|
1066
|
+
await currentClose;
|
|
796
1067
|
} finally {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
setMonitoringConfig(void 0);
|
|
802
|
-
isClosing = false;
|
|
1068
|
+
if (getClosePromise() === currentClose) {
|
|
1069
|
+
setClosePromise(void 0);
|
|
1070
|
+
setIsClosing(false);
|
|
1071
|
+
}
|
|
803
1072
|
}
|
|
804
1073
|
}
|
|
1074
|
+
async function forceReconnectDatabase(reason = "manual") {
|
|
1075
|
+
if (getDatabaseProvider()) {
|
|
1076
|
+
dbLogger4.debug("Force reconnect skipped: database is externally provided", { reason });
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
return await triggerForceReconnect(reason);
|
|
1080
|
+
}
|
|
805
1081
|
function getDatabaseInfo() {
|
|
806
1082
|
const writeInst = getWriteInstance();
|
|
807
1083
|
const readInst = getReadInstance();
|
|
808
1084
|
return {
|
|
809
1085
|
hasWrite: !!writeInst,
|
|
810
1086
|
hasRead: !!readInst,
|
|
811
|
-
isReplica: !!(readInst && readInst !== writeInst)
|
|
1087
|
+
isReplica: !!(readInst && readInst !== writeInst),
|
|
1088
|
+
providerKind: getDatabaseProvider()?.kind
|
|
812
1089
|
};
|
|
813
1090
|
}
|
|
814
|
-
var
|
|
1091
|
+
var dbLogger5 = logger.child("@spfn/core:database");
|
|
1092
|
+
var POSTGRES_JS_CONNECTION_CODES = /* @__PURE__ */ new Set([
|
|
1093
|
+
"CONNECTION_ENDED",
|
|
1094
|
+
"CONNECTION_CLOSED",
|
|
1095
|
+
"CONNECTION_DESTROYED",
|
|
1096
|
+
"CONNECT_TIMEOUT",
|
|
1097
|
+
"CONNECTION_CONNECT_TIMEOUT"
|
|
1098
|
+
]);
|
|
1099
|
+
var NODE_NET_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1100
|
+
"ECONNRESET",
|
|
1101
|
+
"ECONNREFUSED",
|
|
1102
|
+
"EPIPE",
|
|
1103
|
+
"ETIMEDOUT",
|
|
1104
|
+
"EHOSTUNREACH",
|
|
1105
|
+
"ENETUNREACH",
|
|
1106
|
+
"ENOTFOUND"
|
|
1107
|
+
]);
|
|
1108
|
+
function isConnectionSqlState(code) {
|
|
1109
|
+
if (code.startsWith("08")) return true;
|
|
1110
|
+
if (code === "53300") return true;
|
|
1111
|
+
if (code === "57P01" || code === "57P02" || code === "57P03") return true;
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
function* unwrap(error) {
|
|
1115
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1116
|
+
const stack = [error];
|
|
1117
|
+
while (stack.length > 0) {
|
|
1118
|
+
const current = stack.pop();
|
|
1119
|
+
if (!current || typeof current !== "object" || seen.has(current)) {
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
seen.add(current);
|
|
1123
|
+
const obj = current;
|
|
1124
|
+
yield obj;
|
|
1125
|
+
for (const key of ["cause", "original", "error", "err", "inner"]) {
|
|
1126
|
+
const nested = obj[key];
|
|
1127
|
+
if (nested && typeof nested === "object" && !seen.has(nested)) {
|
|
1128
|
+
stack.push(nested);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function isConnectionLevelError(error) {
|
|
1134
|
+
if (!error) return false;
|
|
1135
|
+
if (error instanceof ConnectionError) return true;
|
|
1136
|
+
for (const candidate of unwrap(error)) {
|
|
1137
|
+
if (candidate instanceof ConnectionError) return true;
|
|
1138
|
+
const code = candidate.code;
|
|
1139
|
+
if (typeof code === "string") {
|
|
1140
|
+
if (POSTGRES_JS_CONNECTION_CODES.has(code)) return true;
|
|
1141
|
+
if (NODE_NET_ERROR_CODES.has(code)) return true;
|
|
1142
|
+
if (isConnectionSqlState(code)) return true;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return false;
|
|
1146
|
+
}
|
|
1147
|
+
var DEFAULT_THRESHOLD = 3;
|
|
1148
|
+
var DEFAULT_WINDOW_MS = 1e4;
|
|
1149
|
+
var MIN_WINDOW_MS = 1e3;
|
|
1150
|
+
function readPositiveIntEnv(key, defaultValue, min) {
|
|
1151
|
+
const raw = process.env[key];
|
|
1152
|
+
if (raw === void 0) return defaultValue;
|
|
1153
|
+
try {
|
|
1154
|
+
return parseNumber(raw, { min, integer: true });
|
|
1155
|
+
} catch {
|
|
1156
|
+
return defaultValue;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
var ERROR_THRESHOLD = readPositiveIntEnv("DB_RECONNECT_ERROR_THRESHOLD", DEFAULT_THRESHOLD, 1);
|
|
1160
|
+
var ERROR_WINDOW_MS = readPositiveIntEnv("DB_RECONNECT_ERROR_WINDOW_MS", DEFAULT_WINDOW_MS, MIN_WINDOW_MS);
|
|
1161
|
+
var errorTimestamps = [];
|
|
1162
|
+
var reportedErrors = /* @__PURE__ */ new WeakSet();
|
|
1163
|
+
function resetConnectionErrorCounter() {
|
|
1164
|
+
errorTimestamps.length = 0;
|
|
1165
|
+
}
|
|
1166
|
+
function checkAndMarkReported(error) {
|
|
1167
|
+
let alreadySeen = false;
|
|
1168
|
+
const toMark = [];
|
|
1169
|
+
for (const candidate of unwrap(error)) {
|
|
1170
|
+
if (reportedErrors.has(candidate)) {
|
|
1171
|
+
alreadySeen = true;
|
|
1172
|
+
}
|
|
1173
|
+
toMark.push(candidate);
|
|
1174
|
+
}
|
|
1175
|
+
if (alreadySeen) return true;
|
|
1176
|
+
for (const obj of toMark) {
|
|
1177
|
+
reportedErrors.add(obj);
|
|
1178
|
+
}
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
function reportDatabaseError(error) {
|
|
1182
|
+
try {
|
|
1183
|
+
if (!isConnectionLevelError(error)) return;
|
|
1184
|
+
if (isReconnectingNow()) return;
|
|
1185
|
+
if (checkAndMarkReported(error)) return;
|
|
1186
|
+
const now = Date.now();
|
|
1187
|
+
errorTimestamps.push(now);
|
|
1188
|
+
const cutoff = now - ERROR_WINDOW_MS;
|
|
1189
|
+
while (errorTimestamps.length > 0 && errorTimestamps[0] < cutoff) {
|
|
1190
|
+
errorTimestamps.shift();
|
|
1191
|
+
}
|
|
1192
|
+
if (errorTimestamps.length < ERROR_THRESHOLD) return;
|
|
1193
|
+
errorTimestamps.length = 0;
|
|
1194
|
+
dbLogger5.error("Connection-error threshold crossed, forcing pool rebuild", {
|
|
1195
|
+
threshold: ERROR_THRESHOLD,
|
|
1196
|
+
windowMs: ERROR_WINDOW_MS
|
|
1197
|
+
});
|
|
1198
|
+
triggerForceReconnect("query_error_threshold").catch((err) => {
|
|
1199
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1200
|
+
dbLogger5.error("Forced reconnect after error threshold failed", { error: message });
|
|
1201
|
+
});
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1204
|
+
dbLogger5.debug("reportDatabaseError itself threw, ignoring", { error: message });
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
var BARREL_FILE_PATTERNS = [
|
|
815
1208
|
"/index",
|
|
816
1209
|
"/index.ts",
|
|
817
1210
|
"/index.js",
|
|
818
1211
|
"/index.mjs",
|
|
819
|
-
"
|
|
820
|
-
"
|
|
821
|
-
"
|
|
822
|
-
"
|
|
1212
|
+
"/config",
|
|
1213
|
+
"/config.ts",
|
|
1214
|
+
"/config.js",
|
|
1215
|
+
"/config.mjs"
|
|
823
1216
|
];
|
|
824
|
-
var
|
|
825
|
-
|
|
826
|
-
|
|
1217
|
+
var DEFAULT_ENTITY_GLOB = "./src/server/entities/**/*.ts";
|
|
1218
|
+
var SUPPORTED_EXTENSIONS = [".ts", ".mts", ".cts", ".tsx", ".js", ".mjs", ".cjs", ".jsx"];
|
|
1219
|
+
var DECLARATION_PATTERN = /\.d\.[mc]?ts$/;
|
|
1220
|
+
var SKIPPED_DIRECTORIES = /^(\.|node_modules$)/;
|
|
1221
|
+
function isBarrelFile(filePath) {
|
|
1222
|
+
const posixPath = toPosixPath(filePath);
|
|
1223
|
+
return BARREL_FILE_PATTERNS.some((pattern) => posixPath.endsWith(pattern));
|
|
827
1224
|
}
|
|
828
1225
|
function isAbsolutePath(path) {
|
|
829
1226
|
if (path.startsWith("/")) return true;
|
|
830
1227
|
return !!path.match(/^[A-Za-z]:[\/\\]/);
|
|
831
1228
|
}
|
|
832
1229
|
function hasSupportedExtension(filePath) {
|
|
833
|
-
if (
|
|
1230
|
+
if (DECLARATION_PATTERN.test(filePath)) return false;
|
|
834
1231
|
return SUPPORTED_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
835
1232
|
}
|
|
836
|
-
function
|
|
837
|
-
return files.filter((file) => !
|
|
1233
|
+
function filterBarrelFiles(files) {
|
|
1234
|
+
return files.filter((file) => !isBarrelFile(file));
|
|
838
1235
|
}
|
|
839
|
-
function
|
|
840
|
-
const files = [];
|
|
841
|
-
if (!existsSync(dir)) return files;
|
|
1236
|
+
function safeStat(path) {
|
|
842
1237
|
try {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
try {
|
|
847
|
-
const stat = statSync(fullPath);
|
|
848
|
-
if (stat.isDirectory()) {
|
|
849
|
-
files.push(...scanDirectoryRecursive(fullPath, extension));
|
|
850
|
-
} else if (stat.isFile()) {
|
|
851
|
-
if ((!extension || fullPath.endsWith(extension)) && hasSupportedExtension(fullPath)) {
|
|
852
|
-
files.push(fullPath);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
} catch (error) {
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
} catch (error) {
|
|
1238
|
+
return statSync(path);
|
|
1239
|
+
} catch {
|
|
1240
|
+
return void 0;
|
|
859
1241
|
}
|
|
860
|
-
return files;
|
|
861
1242
|
}
|
|
862
|
-
function
|
|
863
|
-
const files = [];
|
|
864
|
-
if (!existsSync(dir)) return files;
|
|
1243
|
+
function safeReaddir(dir) {
|
|
865
1244
|
try {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1245
|
+
return readdirSync(dir);
|
|
1246
|
+
} catch {
|
|
1247
|
+
return [];
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function walkDirectory(dir, maxDepth, ancestors = /* @__PURE__ */ new Set()) {
|
|
1251
|
+
const walk = { files: [], directories: [] };
|
|
1252
|
+
if (maxDepth < 1 || !safeStat(dir)?.isDirectory()) return walk;
|
|
1253
|
+
const realDir = realpathSafe(dir);
|
|
1254
|
+
if (ancestors.has(realDir)) return walk;
|
|
1255
|
+
const chain = new Set(ancestors).add(realDir);
|
|
1256
|
+
for (const entry of safeReaddir(dir)) {
|
|
1257
|
+
const path = join(dir, entry);
|
|
1258
|
+
const stat = safeStat(path);
|
|
1259
|
+
if (stat?.isFile() && hasSupportedExtension(path)) {
|
|
1260
|
+
walk.files.push(path);
|
|
1261
|
+
} else if (stat?.isDirectory() && !SKIPPED_DIRECTORIES.test(entry)) {
|
|
1262
|
+
walk.directories.push(path);
|
|
1263
|
+
const isLink = isSymlink(path);
|
|
1264
|
+
const child = walkDirectory(path, isLink ? Math.min(maxDepth - 1, 1) : maxDepth - 1, chain);
|
|
1265
|
+
for (const file of child.files) walk.files.push(file);
|
|
1266
|
+
for (const sub of child.directories) walk.directories.push(sub);
|
|
878
1267
|
}
|
|
879
|
-
} catch (error) {
|
|
880
1268
|
}
|
|
881
|
-
return
|
|
1269
|
+
return walk;
|
|
882
1270
|
}
|
|
883
|
-
function
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
try {
|
|
889
|
-
const content = readFileSync(filePath, "utf-8");
|
|
890
|
-
let match;
|
|
891
|
-
while ((match = pgSchemaPattern.exec(content)) !== null) {
|
|
892
|
-
schemas.add(match[1]);
|
|
893
|
-
}
|
|
894
|
-
while ((match = createSchemaPattern.exec(content)) !== null) {
|
|
895
|
-
const packageName = match[1];
|
|
896
|
-
const schemaName = packageName.replace(/@/g, "").replace(/\//g, "_").replace(/-/g, "_");
|
|
897
|
-
schemas.add(schemaName);
|
|
898
|
-
}
|
|
899
|
-
} catch {
|
|
900
|
-
}
|
|
1271
|
+
function isSymlink(path) {
|
|
1272
|
+
try {
|
|
1273
|
+
return lstatSync(path).isSymbolicLink();
|
|
1274
|
+
} catch {
|
|
1275
|
+
return false;
|
|
901
1276
|
}
|
|
902
|
-
return Array.from(schemas);
|
|
903
1277
|
}
|
|
904
|
-
function
|
|
905
|
-
|
|
906
|
-
return
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1278
|
+
function realpathSafe(path) {
|
|
1279
|
+
try {
|
|
1280
|
+
return realpathSync(path);
|
|
1281
|
+
} catch {
|
|
1282
|
+
return path;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
function scanDirectory(dir, maxDepth) {
|
|
1286
|
+
return walkDirectory(dir, maxDepth).files;
|
|
1287
|
+
}
|
|
1288
|
+
function toRelativePattern(entry, cwd) {
|
|
1289
|
+
return toPosixPath(isAbsolutePath(entry) ? relative(cwd, entry) : entry).replace(/^\.\//, "").replace(/[()]/g, "\\$&");
|
|
1290
|
+
}
|
|
1291
|
+
function expandGlob(pattern, cwd) {
|
|
1292
|
+
const scan = mm.scan(pattern);
|
|
1293
|
+
const depth = scan.isGlobstar || scan.glob.includes("**") ? Infinity : scan.glob.split("/").length;
|
|
1294
|
+
const base = join(cwd, scan.base.replace(/\\([()])/g, "$1"));
|
|
1295
|
+
const matches = mm.matcher(pattern);
|
|
1296
|
+
const matched = (path) => matches(toPosixPath(relative(cwd, path)));
|
|
1297
|
+
const walk = walkDirectory(base, depth);
|
|
1298
|
+
return Array.from(/* @__PURE__ */ new Set([...walk.files.filter(matched), ...walk.directories.filter(matched).flatMap((dir) => scanDirectory(dir, 1))]));
|
|
1299
|
+
}
|
|
1300
|
+
function expandSchemaEntry(entry, cwd) {
|
|
1301
|
+
const pattern = toRelativePattern(entry, cwd);
|
|
1302
|
+
if (mm.scan(pattern).isGlob) {
|
|
1303
|
+
return expandGlob(pattern, cwd);
|
|
1304
|
+
}
|
|
1305
|
+
const path = isAbsolutePath(entry) ? entry : join(cwd, entry);
|
|
1306
|
+
const stat = safeStat(path);
|
|
1307
|
+
if (!stat) return [];
|
|
1308
|
+
return stat.isDirectory() ? scanDirectory(path, 1) : [path];
|
|
1309
|
+
}
|
|
1310
|
+
function selectDefaultSchema(cwd) {
|
|
1311
|
+
const registry = env.DRIZZLE_SCHEMA_PATH;
|
|
1312
|
+
const registryFile = isAbsolutePath(registry) ? registry : join(cwd, registry);
|
|
1313
|
+
const registryExists = safeStat(registryFile)?.isFile() === true;
|
|
1314
|
+
const scanned = filterBarrelFiles(expandSchemaEntry(DEFAULT_ENTITY_GLOB, cwd));
|
|
1315
|
+
if (scanned.length === 0 && registryExists) {
|
|
1316
|
+
return { schemas: [registry], source: `entity registry ${registry}` };
|
|
913
1317
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1318
|
+
return {
|
|
1319
|
+
schemas: [DEFAULT_ENTITY_GLOB],
|
|
1320
|
+
source: `${DEFAULT_ENTITY_GLOB} scan`,
|
|
1321
|
+
files: scanned,
|
|
1322
|
+
registry: registryExists ? registry : void 0
|
|
1323
|
+
};
|
|
917
1324
|
}
|
|
918
1325
|
function discoverPackageSchemas(cwd) {
|
|
919
1326
|
const schemas = [];
|
|
@@ -942,9 +1349,7 @@ function discoverPackageSchemas(cwd) {
|
|
|
942
1349
|
const packageSchemas = Array.isArray(pkgJson.spfn.schemas) ? pkgJson.spfn.schemas : [pkgJson.spfn.schemas];
|
|
943
1350
|
for (const schema of packageSchemas) {
|
|
944
1351
|
const absolutePath = join(pkgPath, schema);
|
|
945
|
-
|
|
946
|
-
const schemaFiles = filterIndexFiles(expandedFiles);
|
|
947
|
-
schemas.push(...schemaFiles);
|
|
1352
|
+
schemas.push(...filterBarrelFiles(expandSchemaEntry(absolutePath, pkgPath)));
|
|
948
1353
|
}
|
|
949
1354
|
}
|
|
950
1355
|
} catch (error) {
|
|
@@ -993,7 +1398,7 @@ function getDrizzleConfig(options = {}) {
|
|
|
993
1398
|
if (options.packageFilter) {
|
|
994
1399
|
const packageSchemas2 = options.disablePackageDiscovery ? [] : discoverPackageSchemas(options.cwd ?? process.cwd());
|
|
995
1400
|
const filteredSchemas = packageSchemas2.filter(
|
|
996
|
-
(schemaPath) => schemaPath.includes(`node_modules/${options.packageFilter}/`)
|
|
1401
|
+
(schemaPath) => toPosixPath(schemaPath).includes(`node_modules/${options.packageFilter}/`)
|
|
997
1402
|
);
|
|
998
1403
|
if (filteredSchemas.length === 0) {
|
|
999
1404
|
throw new Error(
|
|
@@ -1003,41 +1408,35 @@ function getDrizzleConfig(options = {}) {
|
|
|
1003
1408
|
const schema2 = filteredSchemas.length === 1 ? filteredSchemas[0] : filteredSchemas;
|
|
1004
1409
|
return {
|
|
1005
1410
|
schema: schema2,
|
|
1411
|
+
schemaSource: `package ${options.packageFilter}`,
|
|
1412
|
+
schemaRegistry: void 0,
|
|
1006
1413
|
out,
|
|
1007
1414
|
dialect,
|
|
1008
|
-
dbCredentials: getDbCredentials(dialect, databaseUrl)
|
|
1415
|
+
dbCredentials: getDbCredentials(dialect, databaseUrl),
|
|
1416
|
+
migrations: {
|
|
1417
|
+
prefix: options.migrationPrefix ?? "timestamp"
|
|
1418
|
+
}
|
|
1009
1419
|
};
|
|
1010
1420
|
}
|
|
1011
|
-
const userSchema = options.schema ?? "./src/server/entities/**/*.ts";
|
|
1012
|
-
const userSchemas = Array.isArray(userSchema) ? userSchema : [userSchema];
|
|
1013
|
-
const packageSchemas = options.disablePackageDiscovery ? [] : discoverPackageSchemas(options.cwd ?? process.cwd());
|
|
1014
|
-
let allSchemas = [...userSchemas, ...packageSchemas];
|
|
1015
1421
|
const cwd = options.cwd ?? process.cwd();
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
const filtered = filterIndexFiles(expanded);
|
|
1022
|
-
expandedFiles.push(...filtered);
|
|
1023
|
-
}
|
|
1024
|
-
allSchemas = expandedFiles;
|
|
1025
|
-
}
|
|
1422
|
+
const selection = options.schema ? { schemas: Array.isArray(options.schema) ? options.schema : [options.schema], source: "schema option" } : selectDefaultSchema(cwd);
|
|
1423
|
+
const schemaSource = selection.source;
|
|
1424
|
+
const schemaRegistry = selection.registry;
|
|
1425
|
+
const packageSchemas = options.disablePackageDiscovery ? [] : discoverPackageSchemas(cwd);
|
|
1426
|
+
const allSchemas = options.expandGlobs ? [...selection.files ?? selection.schemas.flatMap((entry) => expandSchemaEntry(entry, cwd)), ...packageSchemas] : [...selection.schemas, ...packageSchemas];
|
|
1026
1427
|
const schema = allSchemas.length === 1 ? allSchemas[0] : allSchemas;
|
|
1027
|
-
|
|
1028
|
-
if (dialect === "postgresql") {
|
|
1029
|
-
if (options.schemaFilter) {
|
|
1030
|
-
schemaFilter = options.schemaFilter;
|
|
1031
|
-
} else if (options.autoDetectSchemas && expandedFiles.length > 0) {
|
|
1032
|
-
schemaFilter = detectSchemasFromFiles(expandedFiles);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1428
|
+
const schemaFilter = dialect === "postgresql" ? options.schemaFilter : void 0;
|
|
1035
1429
|
return {
|
|
1036
1430
|
schema,
|
|
1431
|
+
schemaSource,
|
|
1432
|
+
schemaRegistry,
|
|
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) {
|
|
@@ -1052,9 +1451,7 @@ function getDbCredentials(dialect, url) {
|
|
|
1052
1451
|
throw new Error(`Unsupported dialect: ${dialect}`);
|
|
1053
1452
|
}
|
|
1054
1453
|
}
|
|
1055
|
-
function
|
|
1056
|
-
const config = getDrizzleConfig(options);
|
|
1057
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1454
|
+
function renderDrizzleConfig(config, cwd = process.cwd()) {
|
|
1058
1455
|
const normalizeSchemaPath = (schemaPath) => {
|
|
1059
1456
|
if (isAbsolutePath(schemaPath)) {
|
|
1060
1457
|
return schemaPath;
|
|
@@ -1066,30 +1463,35 @@ function generateDrizzleConfigFile(options = {}) {
|
|
|
1066
1463
|
]` : `'${normalizeSchemaPath(config.schema)}'`;
|
|
1067
1464
|
const schemaFilterLine = config.schemaFilter && config.schemaFilter.length > 0 ? `
|
|
1068
1465
|
schemaFilter: ${JSON.stringify(config.schemaFilter)},` : "";
|
|
1466
|
+
const migrationsLine = config.migrations ? `
|
|
1467
|
+
migrations: ${JSON.stringify(config.migrations)},` : "";
|
|
1069
1468
|
return `import { defineConfig } from 'drizzle-kit';
|
|
1070
1469
|
|
|
1071
1470
|
export default defineConfig({
|
|
1072
1471
|
schema: ${schemaValue},
|
|
1073
1472
|
out: '${config.out}',
|
|
1074
1473
|
dialect: '${config.dialect}',
|
|
1075
|
-
dbCredentials: ${JSON.stringify(config.dbCredentials, null, 4)},${schemaFilterLine}
|
|
1474
|
+
dbCredentials: ${JSON.stringify(config.dbCredentials, null, 4)},${schemaFilterLine}${migrationsLine}
|
|
1076
1475
|
});
|
|
1077
1476
|
`;
|
|
1078
1477
|
}
|
|
1478
|
+
function generateDrizzleConfigFile(options = {}) {
|
|
1479
|
+
return renderDrizzleConfig(getDrizzleConfig(options), options.cwd ?? process.cwd());
|
|
1480
|
+
}
|
|
1079
1481
|
function id() {
|
|
1080
1482
|
return bigserial("id", { mode: "number" }).primaryKey();
|
|
1081
1483
|
}
|
|
1082
1484
|
function timestamps() {
|
|
1083
1485
|
return {
|
|
1084
1486
|
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
|
|
1085
|
-
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
|
|
1487
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => sql`now()`)
|
|
1086
1488
|
};
|
|
1087
1489
|
}
|
|
1088
1490
|
function foreignKey(name, reference, options) {
|
|
1089
|
-
return
|
|
1491
|
+
return bigint(`${name}_id`, { mode: "number" }).notNull().references(reference, { onDelete: options?.onDelete ?? "cascade" });
|
|
1090
1492
|
}
|
|
1091
1493
|
function optionalForeignKey(name, reference, options) {
|
|
1092
|
-
return
|
|
1494
|
+
return bigint(`${name}_id`, { mode: "number" }).references(reference, { onDelete: options?.onDelete ?? "set null" });
|
|
1093
1495
|
}
|
|
1094
1496
|
function uuid() {
|
|
1095
1497
|
return uuid$1("id").defaultRandom().primaryKey();
|
|
@@ -1147,7 +1549,227 @@ function getSchemaInfo(packageName) {
|
|
|
1147
1549
|
scope
|
|
1148
1550
|
};
|
|
1149
1551
|
}
|
|
1552
|
+
var discoveryLogger = logger.child("@spfn/core:migrations");
|
|
1553
|
+
function functionMigrationsTable(packageName) {
|
|
1554
|
+
return `__spfn_fn_${packageName.replace("@spfn/", "")}_migrations`;
|
|
1555
|
+
}
|
|
1556
|
+
function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
1557
|
+
const spfnDir = join(cwd, "node_modules", "@spfn");
|
|
1558
|
+
if (!existsSync(spfnDir)) {
|
|
1559
|
+
return [];
|
|
1560
|
+
}
|
|
1561
|
+
const functions = [];
|
|
1562
|
+
for (const pkg of readdirSync(spfnDir)) {
|
|
1563
|
+
const packagePath = join(spfnDir, pkg);
|
|
1564
|
+
const packageJsonPath = join(packagePath, "package.json");
|
|
1565
|
+
if (!existsSync(packageJsonPath)) {
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
try {
|
|
1569
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
1570
|
+
const migrationsConfig = packageJson.spfn?.migrations;
|
|
1571
|
+
if (!migrationsConfig) {
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
const migrationsDir = join(packagePath, migrationsConfig.dir);
|
|
1575
|
+
if (!existsSync(migrationsDir)) {
|
|
1576
|
+
discoveryLogger.warn(
|
|
1577
|
+
`@spfn/${pkg} declares migrations but the directory is missing: ${migrationsDir}`
|
|
1578
|
+
);
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
functions.push({ packageName: `@spfn/${pkg}`, migrationsDir, packagePath });
|
|
1582
|
+
} catch {
|
|
1583
|
+
discoveryLogger.warn(`Failed to parse package.json for @spfn/${pkg}`);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return functions;
|
|
1587
|
+
}
|
|
1588
|
+
function readMigrationEntries(migrationsDir, packageName) {
|
|
1589
|
+
const journalPath = join(migrationsDir, "meta", "_journal.json");
|
|
1590
|
+
return existsSync(journalPath) ? readJournalEntries(migrationsDir, journalPath, packageName) : readFolderEntries(migrationsDir, packageName);
|
|
1591
|
+
}
|
|
1592
|
+
function readJournalEntries(migrationsDir, journalPath, packageName) {
|
|
1593
|
+
let journal;
|
|
1594
|
+
try {
|
|
1595
|
+
journal = JSON.parse(readFileSync(journalPath, "utf-8"));
|
|
1596
|
+
} catch {
|
|
1597
|
+
journal = {};
|
|
1598
|
+
}
|
|
1599
|
+
if (!Array.isArray(journal.entries)) {
|
|
1600
|
+
throw new Error(`${packageName}: invalid migration journal at ${journalPath}`);
|
|
1601
|
+
}
|
|
1602
|
+
const entries = [...journal.entries];
|
|
1603
|
+
entries.sort((a, b) => a.idx - b.idx);
|
|
1604
|
+
return entries.map((entry) => {
|
|
1605
|
+
if (typeof entry?.tag !== "string" || typeof entry?.when !== "number") {
|
|
1606
|
+
throw new Error(`${packageName}: invalid journal entry in ${journalPath}`);
|
|
1607
|
+
}
|
|
1608
|
+
const sqlPath = join(migrationsDir, `${entry.tag}.sql`);
|
|
1609
|
+
if (!existsSync(sqlPath)) {
|
|
1610
|
+
throw new Error(`${packageName}: migration file not found: ${entry.tag}.sql`);
|
|
1611
|
+
}
|
|
1612
|
+
return toEntry(entry.tag, readFileSync(sqlPath, "utf-8"), entry.when);
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
function readFolderEntries(migrationsDir, packageName) {
|
|
1616
|
+
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));
|
|
1617
|
+
return folders.map((name) => toEntry(
|
|
1618
|
+
name,
|
|
1619
|
+
readFileSync(join(migrationsDir, name, "migration.sql"), "utf-8"),
|
|
1620
|
+
folderTimestampMillis(name, packageName)
|
|
1621
|
+
));
|
|
1622
|
+
}
|
|
1623
|
+
function folderTimestampMillis(name, packageName) {
|
|
1624
|
+
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name);
|
|
1625
|
+
if (!match) {
|
|
1626
|
+
throw new Error(`${packageName}: migration folder name must start with a YYYYMMDDHHMMSS timestamp: ${name}`);
|
|
1627
|
+
}
|
|
1628
|
+
const [, year, month, day, hour, minute, second] = match;
|
|
1629
|
+
return Date.UTC(
|
|
1630
|
+
Number(year),
|
|
1631
|
+
Number(month) - 1,
|
|
1632
|
+
Number(day),
|
|
1633
|
+
Number(hour),
|
|
1634
|
+
Number(minute),
|
|
1635
|
+
Number(second)
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
function toEntry(name, content, millis) {
|
|
1639
|
+
return {
|
|
1640
|
+
name,
|
|
1641
|
+
millis,
|
|
1642
|
+
hash: createHash("sha256").update(content).digest("hex"),
|
|
1643
|
+
statements: content.split("--> statement-breakpoint").map((statement) => statement.trim()).filter((statement) => statement.length > 0)
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
var PROJECT_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
1647
|
+
var PROJECT_TARGET_NAME = "project (src/server/drizzle)";
|
|
1648
|
+
function filterPendingEntries(entries, lastAppliedMillis, appliedNames) {
|
|
1649
|
+
return entries.filter((entry) => entry.millis > lastAppliedMillis && !appliedNames.has(entry.name));
|
|
1650
|
+
}
|
|
1651
|
+
function migrationTargets(status) {
|
|
1652
|
+
return [...status.packages, ...status.project ? [status.project] : []];
|
|
1653
|
+
}
|
|
1654
|
+
function pendingMigrationTargets(status) {
|
|
1655
|
+
return migrationTargets(status).filter((target) => target.pending > 0);
|
|
1656
|
+
}
|
|
1657
|
+
function countPendingMigrations(status) {
|
|
1658
|
+
return migrationTargets(status).reduce((sum, target) => sum + target.pending, 0);
|
|
1659
|
+
}
|
|
1660
|
+
function toRows(result) {
|
|
1661
|
+
if (Array.isArray(result)) {
|
|
1662
|
+
return result;
|
|
1663
|
+
}
|
|
1664
|
+
const rows = result?.rows;
|
|
1665
|
+
return Array.isArray(rows) ? rows : [];
|
|
1666
|
+
}
|
|
1667
|
+
async function tableExists(db, tableName) {
|
|
1668
|
+
const rows = toRows(await db.execute(sql`
|
|
1669
|
+
SELECT EXISTS (
|
|
1670
|
+
SELECT 1 FROM information_schema.tables
|
|
1671
|
+
WHERE table_schema = 'drizzle' AND table_name = ${tableName}
|
|
1672
|
+
) AS "exists"`));
|
|
1673
|
+
return rows[0]?.exists === true;
|
|
1674
|
+
}
|
|
1675
|
+
async function readAppliedNames(db, tableName) {
|
|
1676
|
+
const columnRows = toRows(await db.execute(sql`
|
|
1677
|
+
SELECT EXISTS (
|
|
1678
|
+
SELECT 1 FROM information_schema.columns
|
|
1679
|
+
WHERE table_schema = 'drizzle' AND table_name = ${tableName} AND column_name = 'name'
|
|
1680
|
+
) AS "exists"`));
|
|
1681
|
+
if (columnRows[0]?.exists !== true) {
|
|
1682
|
+
return /* @__PURE__ */ new Set();
|
|
1683
|
+
}
|
|
1684
|
+
const rows = toRows(await db.execute(sql`
|
|
1685
|
+
SELECT name FROM drizzle.${sql.identifier(tableName)} WHERE name IS NOT NULL`));
|
|
1686
|
+
return new Set(rows.map((row) => String(row.name)));
|
|
1687
|
+
}
|
|
1688
|
+
async function collectTargetStatus(db, name, migrationsDir, tableName) {
|
|
1689
|
+
const entries = readMigrationEntries(migrationsDir, name);
|
|
1690
|
+
let lastApplied = 0;
|
|
1691
|
+
let appliedNames = /* @__PURE__ */ new Set();
|
|
1692
|
+
if (await tableExists(db, tableName)) {
|
|
1693
|
+
const rows = toRows(await db.execute(sql`
|
|
1694
|
+
SELECT created_at FROM drizzle.${sql.identifier(tableName)}
|
|
1695
|
+
ORDER BY created_at DESC LIMIT 1`));
|
|
1696
|
+
lastApplied = rows[0]?.created_at ? Number(rows[0].created_at) : 0;
|
|
1697
|
+
appliedNames = await readAppliedNames(db, tableName);
|
|
1698
|
+
}
|
|
1699
|
+
const pendingEntries = filterPendingEntries(entries, lastApplied, appliedNames);
|
|
1700
|
+
return {
|
|
1701
|
+
name,
|
|
1702
|
+
total: entries.length,
|
|
1703
|
+
applied: entries.length - pendingEntries.length,
|
|
1704
|
+
pending: pendingEntries.length,
|
|
1705
|
+
pendingTags: pendingEntries.map((entry) => entry.name)
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
function hasMigrationTargets(cwd = process.cwd()) {
|
|
1709
|
+
return discoverFunctionMigrations(cwd).length > 0 || existsSync(projectMigrationsDir(cwd));
|
|
1710
|
+
}
|
|
1711
|
+
function projectMigrationsDir(cwd = process.cwd()) {
|
|
1712
|
+
return join(cwd, "src", "server", "drizzle");
|
|
1713
|
+
}
|
|
1714
|
+
async function collectMigrationStatus(db, cwd = process.cwd()) {
|
|
1715
|
+
const packages = [];
|
|
1716
|
+
for (const func of discoverFunctionMigrations(cwd)) {
|
|
1717
|
+
packages.push(await collectTargetStatus(
|
|
1718
|
+
db,
|
|
1719
|
+
func.packageName,
|
|
1720
|
+
func.migrationsDir,
|
|
1721
|
+
functionMigrationsTable(func.packageName)
|
|
1722
|
+
));
|
|
1723
|
+
}
|
|
1724
|
+
const projectDir = projectMigrationsDir(cwd);
|
|
1725
|
+
const project = existsSync(projectDir) ? await collectTargetStatus(db, PROJECT_TARGET_NAME, projectDir, PROJECT_MIGRATIONS_TABLE) : null;
|
|
1726
|
+
return { packages, project: project && project.total > 0 ? project : null };
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// src/db/migrations/format.ts
|
|
1730
|
+
var RUN_MIGRATIONS_HINT = "Run: pnpm spfn db migrate";
|
|
1731
|
+
function formatPendingMigrations(targets) {
|
|
1732
|
+
const lines = [];
|
|
1733
|
+
for (const target of targets) {
|
|
1734
|
+
lines.push(`${target.name}: ${target.pending} pending migration(s) (${target.applied}/${target.total} applied)`);
|
|
1735
|
+
for (const tag of target.pendingTags) {
|
|
1736
|
+
lines.push(` - ${tag}`);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return lines;
|
|
1740
|
+
}
|
|
1741
|
+
function pendingMigrationsSummary(targets) {
|
|
1742
|
+
const pending = targets.reduce((sum, target) => sum + target.pending, 0);
|
|
1743
|
+
const names = targets.map((target) => target.name).join(", ");
|
|
1744
|
+
return `${pending} pending migration(s) in ${names}`;
|
|
1745
|
+
}
|
|
1150
1746
|
var txLogger = logger.child("@spfn/core:transaction");
|
|
1747
|
+
var concurrentNestingReported = false;
|
|
1748
|
+
function reportConcurrentNesting() {
|
|
1749
|
+
if (concurrentNestingReported) {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
concurrentNestingReported = true;
|
|
1753
|
+
txLogger.warn("Concurrent nested transactions are serialized", {
|
|
1754
|
+
reason: "sibling SAVEPOINTs share the outer transaction's connection, where one sibling's ROLLBACK TO would discard the other's writes",
|
|
1755
|
+
hint: "await nested calls one at a time, or pass requiresNew: true to give a branch its own connection. A nested call whose callback awaits a sibling started after it deadlocks."
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
function createNestedFrameGate() {
|
|
1759
|
+
let tail = Promise.resolve();
|
|
1760
|
+
let pending = 0;
|
|
1761
|
+
return {
|
|
1762
|
+
run(frame) {
|
|
1763
|
+
if (pending > 0) {
|
|
1764
|
+
reportConcurrentNesting();
|
|
1765
|
+
}
|
|
1766
|
+
pending++;
|
|
1767
|
+
const result = tail.then(frame);
|
|
1768
|
+
tail = result.then(() => void pending--, () => void pending--);
|
|
1769
|
+
return result;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1151
1773
|
var asyncContext = new AsyncLocalStorage();
|
|
1152
1774
|
function getTransactionContext() {
|
|
1153
1775
|
return asyncContext.getStore() ?? null;
|
|
@@ -1160,7 +1782,7 @@ function runWithTransaction(tx, txId, callback) {
|
|
|
1160
1782
|
const existingContext = getTransactionContext();
|
|
1161
1783
|
const newLevel = existingContext ? existingContext.level + 1 : 1;
|
|
1162
1784
|
if (existingContext) {
|
|
1163
|
-
txLogger.
|
|
1785
|
+
txLogger.debug("Nested transaction started (SAVEPOINT)", {
|
|
1164
1786
|
outerTxId: existingContext.txId,
|
|
1165
1787
|
innerTxId: txId,
|
|
1166
1788
|
level: newLevel
|
|
@@ -1168,18 +1790,98 @@ function runWithTransaction(tx, txId, callback) {
|
|
|
1168
1790
|
} else {
|
|
1169
1791
|
txLogger.debug("Root transaction context set", { txId, level: newLevel });
|
|
1170
1792
|
}
|
|
1171
|
-
|
|
1793
|
+
const beforeCommitCallbacks = existingContext?.beforeCommitCallbacks ?? [];
|
|
1794
|
+
const afterCommitCallbacks = existingContext?.afterCommitCallbacks ?? [];
|
|
1795
|
+
const afterRollbackCallbacks = existingContext?.afterRollbackCallbacks ?? [];
|
|
1796
|
+
return asyncContext.run(
|
|
1797
|
+
{
|
|
1798
|
+
tx,
|
|
1799
|
+
txId,
|
|
1800
|
+
level: newLevel,
|
|
1801
|
+
beforeCommitCallbacks,
|
|
1802
|
+
afterCommitCallbacks,
|
|
1803
|
+
afterRollbackCallbacks,
|
|
1804
|
+
nestedFrames: createNestedFrameGate()
|
|
1805
|
+
},
|
|
1806
|
+
callback
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
function onAfterCommit(callback) {
|
|
1810
|
+
const context = getTransactionContext();
|
|
1811
|
+
if (!context) {
|
|
1812
|
+
Promise.resolve().then(callback).catch((err) => {
|
|
1813
|
+
txLogger.error("afterCommit callback failed (no transaction)", {
|
|
1814
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1815
|
+
});
|
|
1816
|
+
});
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
context.afterCommitCallbacks.push(callback);
|
|
1820
|
+
}
|
|
1821
|
+
function onBeforeCommit(callback) {
|
|
1822
|
+
const context = getTransactionContext();
|
|
1823
|
+
if (!context) {
|
|
1824
|
+
txLogger.warn(
|
|
1825
|
+
"beforeCommit callback ran immediately (no transaction): a throw cannot abort anything"
|
|
1826
|
+
);
|
|
1827
|
+
Promise.resolve().then(callback).catch((err) => {
|
|
1828
|
+
txLogger.error("beforeCommit callback failed (no transaction)", {
|
|
1829
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1830
|
+
});
|
|
1831
|
+
});
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
context.beforeCommitCallbacks.push(callback);
|
|
1835
|
+
}
|
|
1836
|
+
function onAfterRollback(callback) {
|
|
1837
|
+
const context = getTransactionContext();
|
|
1838
|
+
if (!context) {
|
|
1839
|
+
txLogger.warn("afterRollback callback ignored (no transaction)");
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
context.afterRollbackCallbacks.push(callback);
|
|
1172
1843
|
}
|
|
1173
1844
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
1174
1845
|
var txLogger2 = logger.child("@spfn/core:transaction");
|
|
1846
|
+
function logSafely(emit) {
|
|
1847
|
+
try {
|
|
1848
|
+
emit();
|
|
1849
|
+
} catch {
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
async function runBeforeCommitCallbacks(callbacks) {
|
|
1853
|
+
for (const cb of [...callbacks]) {
|
|
1854
|
+
await cb();
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
async function runAfterRollbackCallbacks(callbacks, txId, context, enableLogging) {
|
|
1858
|
+
if (enableLogging) {
|
|
1859
|
+
logSafely(() => txLogger2.debug("Executing afterRollback callbacks", {
|
|
1860
|
+
txId,
|
|
1861
|
+
context,
|
|
1862
|
+
count: callbacks.length
|
|
1863
|
+
}));
|
|
1864
|
+
}
|
|
1865
|
+
for (const cb of callbacks) {
|
|
1866
|
+
await Promise.resolve().then(cb).catch((err) => logSafely(() => {
|
|
1867
|
+
txLogger2.error("afterRollback callback failed", {
|
|
1868
|
+
txId,
|
|
1869
|
+
context,
|
|
1870
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1871
|
+
});
|
|
1872
|
+
}));
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1175
1875
|
async function runInTransaction(callback, options = {}) {
|
|
1176
1876
|
const defaultTimeout = env.TRANSACTION_TIMEOUT;
|
|
1177
1877
|
const {
|
|
1178
1878
|
slowThreshold = 1e3,
|
|
1179
1879
|
enableLogging = true,
|
|
1180
|
-
context = "transaction"
|
|
1880
|
+
context = "transaction",
|
|
1881
|
+
requiresNew = false
|
|
1181
1882
|
} = options;
|
|
1182
1883
|
const timeout = options.timeout ?? defaultTimeout;
|
|
1884
|
+
const idleTimeout = options.idleTimeout ?? env.TRANSACTION_IDLE_TIMEOUT;
|
|
1183
1885
|
const txId = `tx_${randomUUID()}`;
|
|
1184
1886
|
const validateAndThrow = (condition, message, logMessage, metadata) => {
|
|
1185
1887
|
if (condition) {
|
|
@@ -1220,6 +1922,24 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1220
1922
|
"Timeout exceeds maximum",
|
|
1221
1923
|
{ txId, context, timeout, maxTimeout: MAX_TIMEOUT_MS }
|
|
1222
1924
|
);
|
|
1925
|
+
validateAndThrow(
|
|
1926
|
+
!Number.isInteger(idleTimeout),
|
|
1927
|
+
`Invalid idleTimeout value: ${idleTimeout}. Must be an integer.`,
|
|
1928
|
+
"Invalid idleTimeout type",
|
|
1929
|
+
{ txId, context, idleTimeout }
|
|
1930
|
+
);
|
|
1931
|
+
validateAndThrow(
|
|
1932
|
+
idleTimeout < 0,
|
|
1933
|
+
`Invalid idleTimeout value: ${idleTimeout}. Must be non-negative (0 to disable).`,
|
|
1934
|
+
"Invalid idleTimeout range",
|
|
1935
|
+
{ txId, context, idleTimeout }
|
|
1936
|
+
);
|
|
1937
|
+
validateAndThrow(
|
|
1938
|
+
idleTimeout > MAX_TIMEOUT_MS,
|
|
1939
|
+
`Invalid idleTimeout value: ${idleTimeout}. Maximum is ${MAX_TIMEOUT_MS}ms.`,
|
|
1940
|
+
"idleTimeout exceeds maximum",
|
|
1941
|
+
{ txId, context, idleTimeout, maxTimeout: MAX_TIMEOUT_MS }
|
|
1942
|
+
);
|
|
1223
1943
|
const writeDb = getDatabase("write");
|
|
1224
1944
|
if (!writeDb) {
|
|
1225
1945
|
const error = new TransactionError({
|
|
@@ -1236,75 +1956,156 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1236
1956
|
}
|
|
1237
1957
|
throw error;
|
|
1238
1958
|
}
|
|
1239
|
-
const
|
|
1240
|
-
const isNested =
|
|
1241
|
-
if (isNested && timeout > 0 && enableLogging) {
|
|
1959
|
+
const savepointOwner = requiresNew ? null : getTransactionContext();
|
|
1960
|
+
const isNested = savepointOwner !== null;
|
|
1961
|
+
if (isNested && options.timeout !== void 0 && options.timeout > 0 && enableLogging) {
|
|
1242
1962
|
txLogger2.warn("Timeout ignored in nested transaction", {
|
|
1243
1963
|
txId,
|
|
1244
1964
|
context,
|
|
1245
|
-
outerTxId:
|
|
1965
|
+
outerTxId: savepointOwner.txId,
|
|
1246
1966
|
requestedTimeout: `${timeout}ms`,
|
|
1247
|
-
reason: "SET LOCAL
|
|
1967
|
+
reason: "the SAVEPOINT runs under the outer transaction's statement_timeout; SET LOCAL here would re-scope the whole outer transaction"
|
|
1248
1968
|
});
|
|
1249
1969
|
}
|
|
1250
1970
|
if (enableLogging) {
|
|
1251
1971
|
txLogger2.debug("Transaction started", { txId, context });
|
|
1252
1972
|
}
|
|
1253
1973
|
const startTime = Date.now();
|
|
1974
|
+
let beforeCommitCallbacks = [];
|
|
1975
|
+
let afterCommitCallbacks = [];
|
|
1976
|
+
let afterRollbackCallbacks = [];
|
|
1977
|
+
const openTransaction = (body) => {
|
|
1978
|
+
if (savepointOwner) {
|
|
1979
|
+
return savepointOwner.nestedFrames.run(
|
|
1980
|
+
() => savepointOwner.tx.transaction(body)
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
if (requiresNew) {
|
|
1984
|
+
return asyncContext.exit(() => writeDb.transaction(body));
|
|
1985
|
+
}
|
|
1986
|
+
return writeDb.transaction(body);
|
|
1987
|
+
};
|
|
1254
1988
|
try {
|
|
1255
|
-
const result = await
|
|
1989
|
+
const result = await openTransaction(async (tx) => {
|
|
1990
|
+
const transaction = tx;
|
|
1256
1991
|
if (timeout > 0 && !isNested) {
|
|
1257
|
-
await
|
|
1992
|
+
await transaction.execute(sql.raw(`SET LOCAL statement_timeout = ${timeout}`));
|
|
1258
1993
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1994
|
+
if (idleTimeout > 0 && !isNested) {
|
|
1995
|
+
await transaction.execute(sql.raw(`SET LOCAL idle_in_transaction_session_timeout = ${idleTimeout}`));
|
|
1996
|
+
}
|
|
1997
|
+
return await runWithTransaction(transaction, txId, async () => {
|
|
1998
|
+
if (!isNested) {
|
|
1999
|
+
const ctx = getTransactionContext();
|
|
2000
|
+
if (ctx) {
|
|
2001
|
+
beforeCommitCallbacks = ctx.beforeCommitCallbacks;
|
|
2002
|
+
afterCommitCallbacks = ctx.afterCommitCallbacks;
|
|
2003
|
+
afterRollbackCallbacks = ctx.afterRollbackCallbacks;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
const innerResult = await callback(transaction);
|
|
2007
|
+
if (!isNested && beforeCommitCallbacks.length > 0) {
|
|
2008
|
+
if (enableLogging) {
|
|
2009
|
+
logSafely(() => txLogger2.debug("Executing beforeCommit callbacks", {
|
|
2010
|
+
txId,
|
|
2011
|
+
context,
|
|
2012
|
+
count: beforeCommitCallbacks.length
|
|
2013
|
+
}));
|
|
2014
|
+
}
|
|
2015
|
+
await runBeforeCommitCallbacks(beforeCommitCallbacks);
|
|
2016
|
+
}
|
|
2017
|
+
return innerResult;
|
|
1261
2018
|
});
|
|
1262
2019
|
});
|
|
1263
2020
|
const duration = Date.now() - startTime;
|
|
1264
2021
|
if (enableLogging) {
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
2022
|
+
logSafely(() => {
|
|
2023
|
+
if (duration >= slowThreshold) {
|
|
2024
|
+
txLogger2.warn("Slow transaction committed", {
|
|
2025
|
+
txId,
|
|
2026
|
+
context,
|
|
2027
|
+
duration: `${duration}ms`,
|
|
2028
|
+
threshold: `${slowThreshold}ms`,
|
|
2029
|
+
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."
|
|
2030
|
+
});
|
|
2031
|
+
} else {
|
|
2032
|
+
txLogger2.debug("Transaction committed", {
|
|
2033
|
+
txId,
|
|
2034
|
+
context,
|
|
2035
|
+
duration: `${duration}ms`
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
if (!isNested && afterCommitCallbacks.length > 0) {
|
|
2041
|
+
if (enableLogging) {
|
|
2042
|
+
logSafely(() => txLogger2.debug("Executing afterCommit callbacks", {
|
|
1274
2043
|
txId,
|
|
1275
2044
|
context,
|
|
1276
|
-
|
|
1277
|
-
});
|
|
2045
|
+
count: afterCommitCallbacks.length
|
|
2046
|
+
}));
|
|
2047
|
+
}
|
|
2048
|
+
for (const cb of afterCommitCallbacks) {
|
|
2049
|
+
Promise.resolve().then(cb).catch((err) => logSafely(() => {
|
|
2050
|
+
txLogger2.error("afterCommit callback failed", {
|
|
2051
|
+
txId,
|
|
2052
|
+
context,
|
|
2053
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2054
|
+
});
|
|
2055
|
+
}));
|
|
1278
2056
|
}
|
|
1279
2057
|
}
|
|
1280
2058
|
return result;
|
|
1281
2059
|
} catch (error) {
|
|
1282
2060
|
const duration = Date.now() - startTime;
|
|
1283
2061
|
if (enableLogging) {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
2062
|
+
logSafely(() => {
|
|
2063
|
+
if (duration >= slowThreshold) {
|
|
2064
|
+
txLogger2.warn("Slow transaction rolled back", {
|
|
2065
|
+
txId,
|
|
2066
|
+
context,
|
|
2067
|
+
duration: `${duration}ms`,
|
|
2068
|
+
threshold: `${slowThreshold}ms`,
|
|
2069
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2070
|
+
errorType: error instanceof Error ? error.name : "Unknown",
|
|
2071
|
+
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."
|
|
2072
|
+
});
|
|
2073
|
+
} else {
|
|
2074
|
+
txLogger2.error("Transaction rolled back", {
|
|
2075
|
+
txId,
|
|
2076
|
+
context,
|
|
2077
|
+
duration: `${duration}ms`,
|
|
2078
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2079
|
+
errorType: error instanceof Error ? error.name : "Unknown"
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
if (!isNested && afterRollbackCallbacks.length > 0) {
|
|
2085
|
+
await runAfterRollbackCallbacks(afterRollbackCallbacks, txId, context, enableLogging).catch(() => void 0);
|
|
1302
2086
|
}
|
|
1303
2087
|
throw error;
|
|
1304
2088
|
}
|
|
1305
2089
|
}
|
|
1306
2090
|
|
|
1307
2091
|
// src/db/transaction/middleware.ts
|
|
2092
|
+
var SQLSTATE_PATTERN = /^[0-9A-Z]{5}$/;
|
|
2093
|
+
function isDriverOriginError(error) {
|
|
2094
|
+
if (!error || typeof error !== "object") {
|
|
2095
|
+
return false;
|
|
2096
|
+
}
|
|
2097
|
+
const candidate = error;
|
|
2098
|
+
if (typeof candidate.code !== "string") {
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
if (POSTGRES_JS_CONNECTION_CODES.has(candidate.code)) {
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
2104
|
+
if (!SQLSTATE_PATTERN.test(candidate.code)) {
|
|
2105
|
+
return false;
|
|
2106
|
+
}
|
|
2107
|
+
return typeof candidate.severity === "string" || typeof candidate.severity_local === "string";
|
|
2108
|
+
}
|
|
1308
2109
|
function Transactional(options = {}) {
|
|
1309
2110
|
return createMiddleware(async (c, next) => {
|
|
1310
2111
|
const route = `${c.req.method} ${c.req.path}`;
|
|
@@ -1323,13 +2124,11 @@ function Transactional(options = {}) {
|
|
|
1323
2124
|
}
|
|
1324
2125
|
);
|
|
1325
2126
|
} catch (error) {
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
}
|
|
1329
|
-
if (error instanceof TransactionError) {
|
|
2127
|
+
reportDatabaseError(error);
|
|
2128
|
+
if (error instanceof SerializableError) {
|
|
1330
2129
|
throw error;
|
|
1331
2130
|
}
|
|
1332
|
-
if (error
|
|
2131
|
+
if (isDriverOriginError(error)) {
|
|
1333
2132
|
throw fromPostgresError(error);
|
|
1334
2133
|
}
|
|
1335
2134
|
throw error;
|
|
@@ -1392,7 +2191,8 @@ async function create(table, data) {
|
|
|
1392
2191
|
if (!db) {
|
|
1393
2192
|
throw new Error("Database not initialized. Call initDatabase() first.");
|
|
1394
2193
|
}
|
|
1395
|
-
const
|
|
2194
|
+
const results = await db.insert(table).values(data).returning();
|
|
2195
|
+
const result = results[0];
|
|
1396
2196
|
return result;
|
|
1397
2197
|
}
|
|
1398
2198
|
async function createMany(table, data) {
|
|
@@ -1408,10 +2208,11 @@ async function upsert(table, data, options) {
|
|
|
1408
2208
|
if (!db) {
|
|
1409
2209
|
throw new Error("Database not initialized. Call initDatabase() first.");
|
|
1410
2210
|
}
|
|
1411
|
-
const
|
|
2211
|
+
const results = await db.insert(table).values(data).onConflictDoUpdate({
|
|
1412
2212
|
target: options.target,
|
|
1413
2213
|
set: options.set || data
|
|
1414
2214
|
}).returning();
|
|
2215
|
+
const result = results[0];
|
|
1415
2216
|
return result;
|
|
1416
2217
|
}
|
|
1417
2218
|
async function updateOne(table, where, data) {
|
|
@@ -1423,7 +2224,8 @@ async function updateOne(table, where, data) {
|
|
|
1423
2224
|
if (!whereClause) {
|
|
1424
2225
|
throw new Error("updateOne requires at least one where condition");
|
|
1425
2226
|
}
|
|
1426
|
-
const
|
|
2227
|
+
const results = await db.update(table).set(data).where(whereClause).returning();
|
|
2228
|
+
const result = results[0];
|
|
1427
2229
|
return result ?? null;
|
|
1428
2230
|
}
|
|
1429
2231
|
async function updateMany(table, where, data) {
|
|
@@ -1447,7 +2249,8 @@ async function deleteOne(table, where) {
|
|
|
1447
2249
|
if (!whereClause) {
|
|
1448
2250
|
throw new Error("deleteOne requires at least one where condition");
|
|
1449
2251
|
}
|
|
1450
|
-
const
|
|
2252
|
+
const results = await db.delete(table).where(whereClause).returning();
|
|
2253
|
+
const result = results[0];
|
|
1451
2254
|
return result ?? null;
|
|
1452
2255
|
}
|
|
1453
2256
|
async function deleteMany(table, where) {
|
|
@@ -1570,6 +2373,7 @@ var BaseRepository = class {
|
|
|
1570
2373
|
try {
|
|
1571
2374
|
return await queryFn();
|
|
1572
2375
|
} catch (error) {
|
|
2376
|
+
reportDatabaseError(error);
|
|
1573
2377
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1574
2378
|
const repositoryName = this.constructor.name;
|
|
1575
2379
|
throw new RepositoryError(
|
|
@@ -1636,14 +2440,51 @@ var BaseRepository = class {
|
|
|
1636
2440
|
const orderByArray = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
|
|
1637
2441
|
query = query.orderBy(...orderByArray);
|
|
1638
2442
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
2443
|
+
const maxRows = env.DB_MAX_ROWS;
|
|
2444
|
+
const requestedLimit = options?.limit && options.limit > 0 ? options.limit : void 0;
|
|
2445
|
+
const effectiveLimit = maxRows > 0 ? Math.min(requestedLimit ?? maxRows, maxRows) : requestedLimit;
|
|
2446
|
+
if (effectiveLimit) {
|
|
2447
|
+
query = query.limit(effectiveLimit);
|
|
1641
2448
|
}
|
|
1642
2449
|
if (options?.offset) {
|
|
1643
2450
|
query = query.offset(options.offset);
|
|
1644
2451
|
}
|
|
1645
2452
|
return query;
|
|
1646
2453
|
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Keyset (cursor) pagination — O(limit) instead of OFFSET's O(offset).
|
|
2456
|
+
*
|
|
2457
|
+
* Pages by a strictly-ordered, unique column instead of a numeric offset, so a
|
|
2458
|
+
* deep page doesn't scan and discard everything before it. Pass the cursor
|
|
2459
|
+
* column's value from the last row of the previous page as `after`; omit it for
|
|
2460
|
+
* the first page. The column must be unique and the sole sort key (e.g. an
|
|
2461
|
+
* auto-increment id or a ULID).
|
|
2462
|
+
*
|
|
2463
|
+
* @example
|
|
2464
|
+
* ```typescript
|
|
2465
|
+
* const page1 = await this._findManyKeyset(users, { cursorColumn: users.id, limit: 20 });
|
|
2466
|
+
* const page2 = await this._findManyKeyset(users, {
|
|
2467
|
+
* cursorColumn: users.id,
|
|
2468
|
+
* after: page1.at(-1)?.id,
|
|
2469
|
+
* limit: 20,
|
|
2470
|
+
* });
|
|
2471
|
+
* ```
|
|
2472
|
+
*/
|
|
2473
|
+
async _findManyKeyset(table, options) {
|
|
2474
|
+
const { cursorColumn, after, order = "asc", where } = options;
|
|
2475
|
+
const maxRows = env.DB_MAX_ROWS;
|
|
2476
|
+
const limit = maxRows > 0 ? Math.min(options.limit, maxRows) : options.limit;
|
|
2477
|
+
const baseWhere = where ? isSQLWrapper(where) ? where : buildWhereFromObject(table, where) : void 0;
|
|
2478
|
+
const cursorPredicate = after !== void 0 ? order === "desc" ? lt(cursorColumn, after) : gt(cursorColumn, after) : void 0;
|
|
2479
|
+
const whereClause = baseWhere && cursorPredicate ? and(baseWhere, cursorPredicate) : cursorPredicate ?? baseWhere;
|
|
2480
|
+
let query = this.readDb.select().from(table);
|
|
2481
|
+
if (whereClause) {
|
|
2482
|
+
query = query.where(whereClause);
|
|
2483
|
+
}
|
|
2484
|
+
query = query.orderBy(order === "desc" ? desc(cursorColumn) : asc(cursorColumn));
|
|
2485
|
+
query = query.limit(limit);
|
|
2486
|
+
return query;
|
|
2487
|
+
}
|
|
1647
2488
|
/**
|
|
1648
2489
|
* Create a new record
|
|
1649
2490
|
*
|
|
@@ -1660,7 +2501,8 @@ var BaseRepository = class {
|
|
|
1660
2501
|
* ```
|
|
1661
2502
|
*/
|
|
1662
2503
|
async _create(table, data) {
|
|
1663
|
-
const
|
|
2504
|
+
const results = await this.db.insert(table).values(data).returning();
|
|
2505
|
+
const result = results[0];
|
|
1664
2506
|
return result;
|
|
1665
2507
|
}
|
|
1666
2508
|
/**
|
|
@@ -1697,15 +2539,16 @@ var BaseRepository = class {
|
|
|
1697
2539
|
* value: {...}
|
|
1698
2540
|
* }, {
|
|
1699
2541
|
* target: [cache.key],
|
|
1700
|
-
* set: { value: data.value
|
|
2542
|
+
* set: { value: data.value } // updatedAt is stamped by the database
|
|
1701
2543
|
* });
|
|
1702
2544
|
* ```
|
|
1703
2545
|
*/
|
|
1704
2546
|
async _upsert(table, data, options) {
|
|
1705
|
-
const
|
|
2547
|
+
const results = await this.db.insert(table).values(data).onConflictDoUpdate({
|
|
1706
2548
|
target: options.target,
|
|
1707
2549
|
set: options.set || data
|
|
1708
2550
|
}).returning();
|
|
2551
|
+
const result = results[0];
|
|
1709
2552
|
return result;
|
|
1710
2553
|
}
|
|
1711
2554
|
/**
|
|
@@ -1729,7 +2572,8 @@ var BaseRepository = class {
|
|
|
1729
2572
|
if (!whereClause) {
|
|
1730
2573
|
throw new Error("updateOne requires at least one where condition");
|
|
1731
2574
|
}
|
|
1732
|
-
const
|
|
2575
|
+
const results = await this.db.update(table).set(data).where(whereClause).returning();
|
|
2576
|
+
const result = results[0];
|
|
1733
2577
|
return result ?? null;
|
|
1734
2578
|
}
|
|
1735
2579
|
/**
|
|
@@ -1773,7 +2617,8 @@ var BaseRepository = class {
|
|
|
1773
2617
|
if (!whereClause) {
|
|
1774
2618
|
throw new Error("deleteOne requires at least one where condition");
|
|
1775
2619
|
}
|
|
1776
|
-
const
|
|
2620
|
+
const results = await this.db.delete(table).where(whereClause).returning();
|
|
2621
|
+
const result = results[0];
|
|
1777
2622
|
return result ?? null;
|
|
1778
2623
|
}
|
|
1779
2624
|
/**
|
|
@@ -1822,6 +2667,6 @@ var BaseRepository = class {
|
|
|
1822
2667
|
}
|
|
1823
2668
|
};
|
|
1824
2669
|
|
|
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 };
|
|
2670
|
+
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, onAfterRollback, onBeforeCommit, optionalForeignKey, packageNameToSchema, pendingMigrationTargets, pendingMigrationsSummary, projectMigrationsDir, publishingFields, readMigrationEntries, renderDrizzleConfig, reportDatabaseError, resetConnectionErrorCounter, runInTransaction, runWithTransaction, setDatabase, setDatabaseProvider, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
|
|
1826
2671
|
//# sourceMappingURL=index.js.map
|
|
1827
2672
|
//# sourceMappingURL=index.js.map
|