@spfn/core 0.2.0-beta.6 → 0.2.0-beta.64
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 +181 -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 +270 -4
- package/dist/db/index.js +404 -60
- 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 +450 -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 +235 -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,8 +437,9 @@ 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
444
|
write: drizzle(writeClient),
|
|
404
445
|
read: drizzle(readClient),
|
|
@@ -502,7 +543,20 @@ var setHealthCheckInterval = (interval) => {
|
|
|
502
543
|
var setMonitoringConfig = (config) => {
|
|
503
544
|
globalThis.__SPFN_DB_MONITORING__ = config;
|
|
504
545
|
};
|
|
546
|
+
var getInitOptions = () => globalThis.__SPFN_DB_INIT_OPTIONS__;
|
|
547
|
+
var setInitOptions = (options) => {
|
|
548
|
+
globalThis.__SPFN_DB_INIT_OPTIONS__ = options;
|
|
549
|
+
};
|
|
550
|
+
var getIsClosing = () => globalThis.__SPFN_DB_CLOSING__ === true;
|
|
551
|
+
var setIsClosing = (closing) => {
|
|
552
|
+
globalThis.__SPFN_DB_CLOSING__ = closing;
|
|
553
|
+
};
|
|
505
554
|
var dbLogger3 = logger.child("@spfn/core:database");
|
|
555
|
+
var CLIENT_CLOSE_TIMEOUT = 5;
|
|
556
|
+
var isReconnecting = false;
|
|
557
|
+
function isReconnectingNow() {
|
|
558
|
+
return isReconnecting;
|
|
559
|
+
}
|
|
506
560
|
async function testDatabaseConnection(db) {
|
|
507
561
|
await db.execute("SELECT 1");
|
|
508
562
|
}
|
|
@@ -514,8 +568,17 @@ async function performHealthCheck(getDatabase2) {
|
|
|
514
568
|
await testDatabaseConnection(read);
|
|
515
569
|
}
|
|
516
570
|
}
|
|
517
|
-
async function
|
|
518
|
-
|
|
571
|
+
async function closeClient(client) {
|
|
572
|
+
try {
|
|
573
|
+
await client.end({ timeout: CLIENT_CLOSE_TIMEOUT });
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async function reconnectAndRestore(options) {
|
|
578
|
+
if (getIsClosing()) {
|
|
579
|
+
dbLogger3.debug("reconnectAndRestore aborted: database is closing");
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
519
582
|
const result = await createDatabaseFromEnv(options);
|
|
520
583
|
if (!result.write) {
|
|
521
584
|
return false;
|
|
@@ -524,15 +587,33 @@ async function reconnectAndRestore(options, closeDatabase2) {
|
|
|
524
587
|
if (result.read && result.read !== result.write) {
|
|
525
588
|
await testDatabaseConnection(result.read);
|
|
526
589
|
}
|
|
590
|
+
if (getIsClosing()) {
|
|
591
|
+
dbLogger3.warn("reconnectAndRestore: close started mid-rebuild, discarding new pool");
|
|
592
|
+
if (result.writeClient) {
|
|
593
|
+
await closeClient(result.writeClient);
|
|
594
|
+
}
|
|
595
|
+
if (result.readClient && result.readClient !== result.writeClient) {
|
|
596
|
+
await closeClient(result.readClient);
|
|
597
|
+
}
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
const oldWriteClient = getWriteClient();
|
|
601
|
+
const oldReadClient = getReadClient();
|
|
527
602
|
setWriteInstance(result.write);
|
|
528
603
|
setReadInstance(result.read);
|
|
529
604
|
setWriteClient(result.writeClient);
|
|
530
605
|
setReadClient(result.readClient);
|
|
531
606
|
const monConfig = buildMonitoringConfig(options?.monitoring);
|
|
532
607
|
setMonitoringConfig(monConfig);
|
|
608
|
+
if (oldWriteClient) {
|
|
609
|
+
closeClient(oldWriteClient);
|
|
610
|
+
}
|
|
611
|
+
if (oldReadClient && oldReadClient !== oldWriteClient) {
|
|
612
|
+
closeClient(oldReadClient);
|
|
613
|
+
}
|
|
533
614
|
return true;
|
|
534
615
|
}
|
|
535
|
-
function startHealthCheck(config, options, getDatabase2
|
|
616
|
+
function startHealthCheck(config, options, getDatabase2) {
|
|
536
617
|
const healthCheck = getHealthCheckInterval();
|
|
537
618
|
if (healthCheck) {
|
|
538
619
|
dbLogger3.debug("Health check already running");
|
|
@@ -543,48 +624,77 @@ function startHealthCheck(config, options, getDatabase2, closeDatabase2) {
|
|
|
543
624
|
reconnect: config.reconnect
|
|
544
625
|
});
|
|
545
626
|
const interval = setInterval(async () => {
|
|
627
|
+
if (isReconnecting) {
|
|
628
|
+
dbLogger3.debug("Health check skipped: reconnection in progress");
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
546
631
|
try {
|
|
547
632
|
await performHealthCheck(getDatabase2);
|
|
548
633
|
} catch (error) {
|
|
549
634
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
550
635
|
dbLogger3.error("Database health check failed", { error: message });
|
|
551
636
|
if (config.reconnect) {
|
|
552
|
-
await attemptReconnection(config, options,
|
|
637
|
+
await attemptReconnection(config, options, "health_check_failed");
|
|
553
638
|
}
|
|
554
639
|
}
|
|
555
640
|
}, config.interval);
|
|
556
641
|
setHealthCheckInterval(interval);
|
|
557
642
|
}
|
|
558
|
-
async function
|
|
643
|
+
async function triggerForceReconnect(reason) {
|
|
644
|
+
if (!getWriteInstance()) {
|
|
645
|
+
dbLogger3.warn("Force reconnect skipped: database not initialized", { reason });
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
if (getIsClosing()) {
|
|
649
|
+
dbLogger3.debug("Force reconnect skipped: database is closing", { reason });
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
const options = getInitOptions();
|
|
653
|
+
const config = buildHealthCheckConfig(options?.healthCheck);
|
|
654
|
+
dbLogger3.warn("Force reconnect triggered", { reason });
|
|
655
|
+
return await attemptReconnection(config, options, reason);
|
|
656
|
+
}
|
|
657
|
+
async function attemptReconnection(config, options, reason) {
|
|
658
|
+
if (isReconnecting) {
|
|
659
|
+
dbLogger3.debug("Reconnection coalesced: attempt already in progress", { reason });
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
isReconnecting = true;
|
|
559
663
|
dbLogger3.warn("Attempting database reconnection", {
|
|
664
|
+
reason,
|
|
560
665
|
maxRetries: config.maxRetries,
|
|
561
666
|
retryInterval: `${config.retryInterval}ms`
|
|
562
667
|
});
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
668
|
+
try {
|
|
669
|
+
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
|
|
670
|
+
try {
|
|
671
|
+
dbLogger3.debug(`Reconnection attempt ${attempt}/${config.maxRetries}`);
|
|
672
|
+
if (attempt > 1) {
|
|
673
|
+
await new Promise((resolve) => setTimeout(resolve, config.retryInterval));
|
|
674
|
+
}
|
|
675
|
+
const success = await reconnectAndRestore(options);
|
|
676
|
+
if (success) {
|
|
677
|
+
dbLogger3.info("Database reconnection successful", { attempt });
|
|
678
|
+
return true;
|
|
679
|
+
} else {
|
|
680
|
+
dbLogger3.error(`Reconnection attempt ${attempt} failed: No write database instance created`);
|
|
681
|
+
}
|
|
682
|
+
} catch (error) {
|
|
683
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
684
|
+
dbLogger3.error(`Reconnection attempt ${attempt} failed`, {
|
|
685
|
+
error: message,
|
|
686
|
+
attempt,
|
|
687
|
+
maxRetries: config.maxRetries
|
|
688
|
+
});
|
|
568
689
|
}
|
|
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`);
|
|
690
|
+
if (attempt === config.maxRetries) {
|
|
691
|
+
dbLogger3.error("Max reconnection attempts reached, will retry on next health check");
|
|
575
692
|
}
|
|
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
693
|
}
|
|
694
|
+
} finally {
|
|
695
|
+
isReconnecting = false;
|
|
587
696
|
}
|
|
697
|
+
return true;
|
|
588
698
|
}
|
|
589
699
|
function stopHealthCheck() {
|
|
590
700
|
const healthCheck = getHealthCheckInterval();
|
|
@@ -593,6 +703,7 @@ function stopHealthCheck() {
|
|
|
593
703
|
setHealthCheckInterval(void 0);
|
|
594
704
|
dbLogger3.info("Database health check stopped");
|
|
595
705
|
}
|
|
706
|
+
isReconnecting = false;
|
|
596
707
|
}
|
|
597
708
|
|
|
598
709
|
// src/db/manager/manager.ts
|
|
@@ -604,7 +715,6 @@ var STACK_TRACE_PATTERNS = {
|
|
|
604
715
|
withoutParens: /at (.+):(\d+):(\d+)/
|
|
605
716
|
};
|
|
606
717
|
var initPromise = null;
|
|
607
|
-
var isClosing = false;
|
|
608
718
|
async function cleanupDatabaseConnections(writeClient, readClient) {
|
|
609
719
|
const cleanupPromises = [];
|
|
610
720
|
if (writeClient) {
|
|
@@ -705,7 +815,7 @@ function setDatabase(write, read) {
|
|
|
705
815
|
setReadInstance(read ?? write);
|
|
706
816
|
}
|
|
707
817
|
async function initDatabase(options) {
|
|
708
|
-
if (
|
|
818
|
+
if (getIsClosing()) {
|
|
709
819
|
throw new Error("Cannot initialize database while closing");
|
|
710
820
|
}
|
|
711
821
|
const writeInst = getWriteInstance();
|
|
@@ -727,7 +837,7 @@ async function initDatabase(options) {
|
|
|
727
837
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
728
838
|
throw new Error(`Database connection test failed: ${message}`);
|
|
729
839
|
}
|
|
730
|
-
if (
|
|
840
|
+
if (getIsClosing()) {
|
|
731
841
|
dbLogger4.warn("Database closed during initialization, cleaning up...");
|
|
732
842
|
await cleanupDatabaseConnections(result.writeClient, result.readClient);
|
|
733
843
|
throw new Error("Database closed during initialization");
|
|
@@ -736,13 +846,14 @@ async function initDatabase(options) {
|
|
|
736
846
|
setReadInstance(result.read);
|
|
737
847
|
setWriteClient(result.writeClient);
|
|
738
848
|
setReadClient(result.readClient);
|
|
849
|
+
setInitOptions(options);
|
|
739
850
|
const hasReplica = result.read && result.read !== result.write;
|
|
740
851
|
dbLogger4.info(
|
|
741
852
|
hasReplica ? "Database connected (Primary + Replica)" : "Database connected"
|
|
742
853
|
);
|
|
743
854
|
const healthCheckConfig = buildHealthCheckConfig(options?.healthCheck);
|
|
744
855
|
if (healthCheckConfig.enabled) {
|
|
745
|
-
startHealthCheck(healthCheckConfig, options, getDatabase
|
|
856
|
+
startHealthCheck(healthCheckConfig, options, getDatabase);
|
|
746
857
|
}
|
|
747
858
|
const monConfig = buildMonitoringConfig(options?.monitoring);
|
|
748
859
|
setMonitoringConfig(monConfig);
|
|
@@ -760,11 +871,11 @@ async function initDatabase(options) {
|
|
|
760
871
|
return await initPromise;
|
|
761
872
|
}
|
|
762
873
|
async function closeDatabase() {
|
|
763
|
-
if (
|
|
874
|
+
if (getIsClosing()) {
|
|
764
875
|
dbLogger4.debug("Database close already in progress");
|
|
765
876
|
return;
|
|
766
877
|
}
|
|
767
|
-
|
|
878
|
+
setIsClosing(true);
|
|
768
879
|
if (initPromise) {
|
|
769
880
|
dbLogger4.debug("Waiting for database initialization to complete before closing...");
|
|
770
881
|
try {
|
|
@@ -777,7 +888,7 @@ async function closeDatabase() {
|
|
|
777
888
|
const readInst = getReadInstance();
|
|
778
889
|
if (!writeInst && !readInst) {
|
|
779
890
|
dbLogger4.debug("No database connections to close");
|
|
780
|
-
|
|
891
|
+
setIsClosing(false);
|
|
781
892
|
return;
|
|
782
893
|
}
|
|
783
894
|
try {
|
|
@@ -799,9 +910,13 @@ async function closeDatabase() {
|
|
|
799
910
|
setWriteClient(void 0);
|
|
800
911
|
setReadClient(void 0);
|
|
801
912
|
setMonitoringConfig(void 0);
|
|
802
|
-
|
|
913
|
+
setInitOptions(void 0);
|
|
914
|
+
setIsClosing(false);
|
|
803
915
|
}
|
|
804
916
|
}
|
|
917
|
+
async function forceReconnectDatabase(reason = "manual") {
|
|
918
|
+
return await triggerForceReconnect(reason);
|
|
919
|
+
}
|
|
805
920
|
function getDatabaseInfo() {
|
|
806
921
|
const writeInst = getWriteInstance();
|
|
807
922
|
const readInst = getReadInstance();
|
|
@@ -811,7 +926,123 @@ function getDatabaseInfo() {
|
|
|
811
926
|
isReplica: !!(readInst && readInst !== writeInst)
|
|
812
927
|
};
|
|
813
928
|
}
|
|
814
|
-
var
|
|
929
|
+
var dbLogger5 = logger.child("@spfn/core:database");
|
|
930
|
+
var POSTGRES_JS_CONNECTION_CODES = /* @__PURE__ */ new Set([
|
|
931
|
+
"CONNECTION_ENDED",
|
|
932
|
+
"CONNECTION_CLOSED",
|
|
933
|
+
"CONNECTION_DESTROYED",
|
|
934
|
+
"CONNECT_TIMEOUT",
|
|
935
|
+
"CONNECTION_CONNECT_TIMEOUT"
|
|
936
|
+
]);
|
|
937
|
+
var NODE_NET_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
938
|
+
"ECONNRESET",
|
|
939
|
+
"ECONNREFUSED",
|
|
940
|
+
"EPIPE",
|
|
941
|
+
"ETIMEDOUT",
|
|
942
|
+
"EHOSTUNREACH",
|
|
943
|
+
"ENETUNREACH",
|
|
944
|
+
"ENOTFOUND"
|
|
945
|
+
]);
|
|
946
|
+
function isConnectionSqlState(code) {
|
|
947
|
+
if (code.startsWith("08")) return true;
|
|
948
|
+
if (code === "53300") return true;
|
|
949
|
+
if (code === "57P01" || code === "57P02" || code === "57P03") return true;
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
function* unwrap(error) {
|
|
953
|
+
const seen = /* @__PURE__ */ new Set();
|
|
954
|
+
const stack = [error];
|
|
955
|
+
while (stack.length > 0) {
|
|
956
|
+
const current = stack.pop();
|
|
957
|
+
if (!current || typeof current !== "object" || seen.has(current)) {
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
seen.add(current);
|
|
961
|
+
const obj = current;
|
|
962
|
+
yield obj;
|
|
963
|
+
for (const key of ["cause", "original", "error", "err", "inner"]) {
|
|
964
|
+
const nested = obj[key];
|
|
965
|
+
if (nested && typeof nested === "object" && !seen.has(nested)) {
|
|
966
|
+
stack.push(nested);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function isConnectionLevelError(error) {
|
|
972
|
+
if (!error) return false;
|
|
973
|
+
if (error instanceof ConnectionError) return true;
|
|
974
|
+
for (const candidate of unwrap(error)) {
|
|
975
|
+
if (candidate instanceof ConnectionError) return true;
|
|
976
|
+
const code = candidate.code;
|
|
977
|
+
if (typeof code === "string") {
|
|
978
|
+
if (POSTGRES_JS_CONNECTION_CODES.has(code)) return true;
|
|
979
|
+
if (NODE_NET_ERROR_CODES.has(code)) return true;
|
|
980
|
+
if (isConnectionSqlState(code)) return true;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
var DEFAULT_THRESHOLD = 3;
|
|
986
|
+
var DEFAULT_WINDOW_MS = 1e4;
|
|
987
|
+
var MIN_WINDOW_MS = 1e3;
|
|
988
|
+
function readPositiveIntEnv(key, defaultValue, min) {
|
|
989
|
+
const raw = process.env[key];
|
|
990
|
+
if (raw === void 0) return defaultValue;
|
|
991
|
+
try {
|
|
992
|
+
return parseNumber(raw, { min, integer: true });
|
|
993
|
+
} catch {
|
|
994
|
+
return defaultValue;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
var ERROR_THRESHOLD = readPositiveIntEnv("DB_RECONNECT_ERROR_THRESHOLD", DEFAULT_THRESHOLD, 1);
|
|
998
|
+
var ERROR_WINDOW_MS = readPositiveIntEnv("DB_RECONNECT_ERROR_WINDOW_MS", DEFAULT_WINDOW_MS, MIN_WINDOW_MS);
|
|
999
|
+
var errorTimestamps = [];
|
|
1000
|
+
var reportedErrors = /* @__PURE__ */ new WeakSet();
|
|
1001
|
+
function resetConnectionErrorCounter() {
|
|
1002
|
+
errorTimestamps.length = 0;
|
|
1003
|
+
}
|
|
1004
|
+
function checkAndMarkReported(error) {
|
|
1005
|
+
let alreadySeen = false;
|
|
1006
|
+
const toMark = [];
|
|
1007
|
+
for (const candidate of unwrap(error)) {
|
|
1008
|
+
if (reportedErrors.has(candidate)) {
|
|
1009
|
+
alreadySeen = true;
|
|
1010
|
+
}
|
|
1011
|
+
toMark.push(candidate);
|
|
1012
|
+
}
|
|
1013
|
+
if (alreadySeen) return true;
|
|
1014
|
+
for (const obj of toMark) {
|
|
1015
|
+
reportedErrors.add(obj);
|
|
1016
|
+
}
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
function reportDatabaseError(error) {
|
|
1020
|
+
try {
|
|
1021
|
+
if (!isConnectionLevelError(error)) return;
|
|
1022
|
+
if (isReconnectingNow()) return;
|
|
1023
|
+
if (checkAndMarkReported(error)) return;
|
|
1024
|
+
const now = Date.now();
|
|
1025
|
+
errorTimestamps.push(now);
|
|
1026
|
+
const cutoff = now - ERROR_WINDOW_MS;
|
|
1027
|
+
while (errorTimestamps.length > 0 && errorTimestamps[0] < cutoff) {
|
|
1028
|
+
errorTimestamps.shift();
|
|
1029
|
+
}
|
|
1030
|
+
if (errorTimestamps.length < ERROR_THRESHOLD) return;
|
|
1031
|
+
errorTimestamps.length = 0;
|
|
1032
|
+
dbLogger5.error("Connection-error threshold crossed, forcing pool rebuild", {
|
|
1033
|
+
threshold: ERROR_THRESHOLD,
|
|
1034
|
+
windowMs: ERROR_WINDOW_MS
|
|
1035
|
+
});
|
|
1036
|
+
triggerForceReconnect("query_error_threshold").catch((err) => {
|
|
1037
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1038
|
+
dbLogger5.error("Forced reconnect after error threshold failed", { error: message });
|
|
1039
|
+
});
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1042
|
+
dbLogger5.debug("reportDatabaseError itself threw, ignoring", { error: message });
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
var BARREL_FILE_PATTERNS = [
|
|
815
1046
|
"/index",
|
|
816
1047
|
"/index.ts",
|
|
817
1048
|
"/index.js",
|
|
@@ -819,11 +1050,14 @@ var INDEX_FILE_PATTERNS = [
|
|
|
819
1050
|
"\\index",
|
|
820
1051
|
"\\index.ts",
|
|
821
1052
|
"\\index.js",
|
|
822
|
-
"\\index.mjs"
|
|
1053
|
+
"\\index.mjs",
|
|
1054
|
+
"\\config.ts",
|
|
1055
|
+
"\\config.js",
|
|
1056
|
+
"\\config.mjs"
|
|
823
1057
|
];
|
|
824
1058
|
var SUPPORTED_EXTENSIONS = [".ts", ".js", ".mjs"];
|
|
825
|
-
function
|
|
826
|
-
return
|
|
1059
|
+
function isBarrelFile(filePath) {
|
|
1060
|
+
return BARREL_FILE_PATTERNS.some((pattern) => filePath.endsWith(pattern));
|
|
827
1061
|
}
|
|
828
1062
|
function isAbsolutePath(path) {
|
|
829
1063
|
if (path.startsWith("/")) return true;
|
|
@@ -833,8 +1067,8 @@ function hasSupportedExtension(filePath) {
|
|
|
833
1067
|
if (filePath.endsWith(".d.ts")) return false;
|
|
834
1068
|
return SUPPORTED_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
835
1069
|
}
|
|
836
|
-
function
|
|
837
|
-
return files.filter((file) => !
|
|
1070
|
+
function filterBarrelFiles(files) {
|
|
1071
|
+
return files.filter((file) => !isBarrelFile(file));
|
|
838
1072
|
}
|
|
839
1073
|
function scanDirectoryRecursive(dir, extension) {
|
|
840
1074
|
const files = [];
|
|
@@ -943,7 +1177,7 @@ function discoverPackageSchemas(cwd) {
|
|
|
943
1177
|
for (const schema of packageSchemas) {
|
|
944
1178
|
const absolutePath = join(pkgPath, schema);
|
|
945
1179
|
const expandedFiles = expandGlobPattern(absolutePath);
|
|
946
|
-
const schemaFiles =
|
|
1180
|
+
const schemaFiles = filterBarrelFiles(expandedFiles);
|
|
947
1181
|
schemas.push(...schemaFiles);
|
|
948
1182
|
}
|
|
949
1183
|
}
|
|
@@ -1005,7 +1239,10 @@ function getDrizzleConfig(options = {}) {
|
|
|
1005
1239
|
schema: schema2,
|
|
1006
1240
|
out,
|
|
1007
1241
|
dialect,
|
|
1008
|
-
dbCredentials: getDbCredentials(dialect, databaseUrl)
|
|
1242
|
+
dbCredentials: getDbCredentials(dialect, databaseUrl),
|
|
1243
|
+
migrations: {
|
|
1244
|
+
prefix: options.migrationPrefix ?? "timestamp"
|
|
1245
|
+
}
|
|
1009
1246
|
};
|
|
1010
1247
|
}
|
|
1011
1248
|
const userSchema = options.schema ?? "./src/server/entities/**/*.ts";
|
|
@@ -1018,7 +1255,7 @@ function getDrizzleConfig(options = {}) {
|
|
|
1018
1255
|
for (const schema2 of allSchemas) {
|
|
1019
1256
|
const absoluteSchema = isAbsolutePath(schema2) ? schema2 : join(cwd, schema2);
|
|
1020
1257
|
const expanded = expandGlobPattern(absoluteSchema);
|
|
1021
|
-
const filtered =
|
|
1258
|
+
const filtered = filterBarrelFiles(expanded);
|
|
1022
1259
|
expandedFiles.push(...filtered);
|
|
1023
1260
|
}
|
|
1024
1261
|
allSchemas = expandedFiles;
|
|
@@ -1037,7 +1274,10 @@ function getDrizzleConfig(options = {}) {
|
|
|
1037
1274
|
out,
|
|
1038
1275
|
dialect,
|
|
1039
1276
|
dbCredentials: getDbCredentials(dialect, databaseUrl),
|
|
1040
|
-
schemaFilter
|
|
1277
|
+
schemaFilter,
|
|
1278
|
+
migrations: {
|
|
1279
|
+
prefix: options.migrationPrefix ?? "timestamp"
|
|
1280
|
+
}
|
|
1041
1281
|
};
|
|
1042
1282
|
}
|
|
1043
1283
|
function getDbCredentials(dialect, url) {
|
|
@@ -1066,13 +1306,15 @@ function generateDrizzleConfigFile(options = {}) {
|
|
|
1066
1306
|
]` : `'${normalizeSchemaPath(config.schema)}'`;
|
|
1067
1307
|
const schemaFilterLine = config.schemaFilter && config.schemaFilter.length > 0 ? `
|
|
1068
1308
|
schemaFilter: ${JSON.stringify(config.schemaFilter)},` : "";
|
|
1309
|
+
const migrationsLine = config.migrations ? `
|
|
1310
|
+
migrations: ${JSON.stringify(config.migrations)},` : "";
|
|
1069
1311
|
return `import { defineConfig } from 'drizzle-kit';
|
|
1070
1312
|
|
|
1071
1313
|
export default defineConfig({
|
|
1072
1314
|
schema: ${schemaValue},
|
|
1073
1315
|
out: '${config.out}',
|
|
1074
1316
|
dialect: '${config.dialect}',
|
|
1075
|
-
dbCredentials: ${JSON.stringify(config.dbCredentials, null, 4)},${schemaFilterLine}
|
|
1317
|
+
dbCredentials: ${JSON.stringify(config.dbCredentials, null, 4)},${schemaFilterLine}${migrationsLine}
|
|
1076
1318
|
});
|
|
1077
1319
|
`;
|
|
1078
1320
|
}
|
|
@@ -1086,10 +1328,10 @@ function timestamps() {
|
|
|
1086
1328
|
};
|
|
1087
1329
|
}
|
|
1088
1330
|
function foreignKey(name, reference, options) {
|
|
1089
|
-
return
|
|
1331
|
+
return bigint(`${name}_id`, { mode: "number" }).notNull().references(reference, { onDelete: options?.onDelete ?? "cascade" });
|
|
1090
1332
|
}
|
|
1091
1333
|
function optionalForeignKey(name, reference, options) {
|
|
1092
|
-
return
|
|
1334
|
+
return bigint(`${name}_id`, { mode: "number" }).references(reference, { onDelete: options?.onDelete ?? "set null" });
|
|
1093
1335
|
}
|
|
1094
1336
|
function uuid() {
|
|
1095
1337
|
return uuid$1("id").defaultRandom().primaryKey();
|
|
@@ -1168,7 +1410,20 @@ function runWithTransaction(tx, txId, callback) {
|
|
|
1168
1410
|
} else {
|
|
1169
1411
|
txLogger.debug("Root transaction context set", { txId, level: newLevel });
|
|
1170
1412
|
}
|
|
1171
|
-
|
|
1413
|
+
const afterCommitCallbacks = existingContext ? existingContext.afterCommitCallbacks : [];
|
|
1414
|
+
return asyncContext.run({ tx, txId, level: newLevel, afterCommitCallbacks }, callback);
|
|
1415
|
+
}
|
|
1416
|
+
function onAfterCommit(callback) {
|
|
1417
|
+
const context = getTransactionContext();
|
|
1418
|
+
if (!context) {
|
|
1419
|
+
Promise.resolve().then(callback).catch((err) => {
|
|
1420
|
+
txLogger.error("afterCommit callback failed (no transaction)", {
|
|
1421
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1422
|
+
});
|
|
1423
|
+
});
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
context.afterCommitCallbacks.push(callback);
|
|
1172
1427
|
}
|
|
1173
1428
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
1174
1429
|
var txLogger2 = logger.child("@spfn/core:transaction");
|
|
@@ -1180,6 +1435,7 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1180
1435
|
context = "transaction"
|
|
1181
1436
|
} = options;
|
|
1182
1437
|
const timeout = options.timeout ?? defaultTimeout;
|
|
1438
|
+
const idleTimeout = options.idleTimeout ?? env.TRANSACTION_IDLE_TIMEOUT;
|
|
1183
1439
|
const txId = `tx_${randomUUID()}`;
|
|
1184
1440
|
const validateAndThrow = (condition, message, logMessage, metadata) => {
|
|
1185
1441
|
if (condition) {
|
|
@@ -1220,6 +1476,24 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1220
1476
|
"Timeout exceeds maximum",
|
|
1221
1477
|
{ txId, context, timeout, maxTimeout: MAX_TIMEOUT_MS }
|
|
1222
1478
|
);
|
|
1479
|
+
validateAndThrow(
|
|
1480
|
+
!Number.isInteger(idleTimeout),
|
|
1481
|
+
`Invalid idleTimeout value: ${idleTimeout}. Must be an integer.`,
|
|
1482
|
+
"Invalid idleTimeout type",
|
|
1483
|
+
{ txId, context, idleTimeout }
|
|
1484
|
+
);
|
|
1485
|
+
validateAndThrow(
|
|
1486
|
+
idleTimeout < 0,
|
|
1487
|
+
`Invalid idleTimeout value: ${idleTimeout}. Must be non-negative (0 to disable).`,
|
|
1488
|
+
"Invalid idleTimeout range",
|
|
1489
|
+
{ txId, context, idleTimeout }
|
|
1490
|
+
);
|
|
1491
|
+
validateAndThrow(
|
|
1492
|
+
idleTimeout > MAX_TIMEOUT_MS,
|
|
1493
|
+
`Invalid idleTimeout value: ${idleTimeout}. Maximum is ${MAX_TIMEOUT_MS}ms.`,
|
|
1494
|
+
"idleTimeout exceeds maximum",
|
|
1495
|
+
{ txId, context, idleTimeout, maxTimeout: MAX_TIMEOUT_MS }
|
|
1496
|
+
);
|
|
1223
1497
|
const writeDb = getDatabase("write");
|
|
1224
1498
|
if (!writeDb) {
|
|
1225
1499
|
const error = new TransactionError({
|
|
@@ -1251,13 +1525,24 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1251
1525
|
txLogger2.debug("Transaction started", { txId, context });
|
|
1252
1526
|
}
|
|
1253
1527
|
const startTime = Date.now();
|
|
1528
|
+
let afterCommitCallbacks = [];
|
|
1254
1529
|
try {
|
|
1255
1530
|
const result = await writeDb.transaction(async (tx) => {
|
|
1256
1531
|
if (timeout > 0 && !isNested) {
|
|
1257
1532
|
await tx.execute(sql.raw(`SET LOCAL statement_timeout = ${timeout}`));
|
|
1258
1533
|
}
|
|
1534
|
+
if (idleTimeout > 0 && !isNested) {
|
|
1535
|
+
await tx.execute(sql.raw(`SET LOCAL idle_in_transaction_session_timeout = ${idleTimeout}`));
|
|
1536
|
+
}
|
|
1259
1537
|
return await runWithTransaction(tx, txId, async () => {
|
|
1260
|
-
|
|
1538
|
+
const innerResult = await callback(tx);
|
|
1539
|
+
if (!isNested) {
|
|
1540
|
+
const ctx = getTransactionContext();
|
|
1541
|
+
if (ctx) {
|
|
1542
|
+
afterCommitCallbacks = [...ctx.afterCommitCallbacks];
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return innerResult;
|
|
1261
1546
|
});
|
|
1262
1547
|
});
|
|
1263
1548
|
const duration = Date.now() - startTime;
|
|
@@ -1267,7 +1552,8 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1267
1552
|
txId,
|
|
1268
1553
|
context,
|
|
1269
1554
|
duration: `${duration}ms`,
|
|
1270
|
-
threshold: `${slowThreshold}ms
|
|
1555
|
+
threshold: `${slowThreshold}ms`,
|
|
1556
|
+
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
1557
|
});
|
|
1272
1558
|
} else {
|
|
1273
1559
|
txLogger2.debug("Transaction committed", {
|
|
@@ -1277,6 +1563,24 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1277
1563
|
});
|
|
1278
1564
|
}
|
|
1279
1565
|
}
|
|
1566
|
+
if (!isNested && afterCommitCallbacks.length > 0) {
|
|
1567
|
+
if (enableLogging) {
|
|
1568
|
+
txLogger2.debug("Executing afterCommit callbacks", {
|
|
1569
|
+
txId,
|
|
1570
|
+
context,
|
|
1571
|
+
count: afterCommitCallbacks.length
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
for (const cb of afterCommitCallbacks) {
|
|
1575
|
+
Promise.resolve().then(cb).catch((err) => {
|
|
1576
|
+
txLogger2.error("afterCommit callback failed", {
|
|
1577
|
+
txId,
|
|
1578
|
+
context,
|
|
1579
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1280
1584
|
return result;
|
|
1281
1585
|
} catch (error) {
|
|
1282
1586
|
const duration = Date.now() - startTime;
|
|
@@ -1288,7 +1592,8 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1288
1592
|
duration: `${duration}ms`,
|
|
1289
1593
|
threshold: `${slowThreshold}ms`,
|
|
1290
1594
|
error: error instanceof Error ? error.message : String(error),
|
|
1291
|
-
errorType: error instanceof Error ? error.name : "Unknown"
|
|
1595
|
+
errorType: error instanceof Error ? error.name : "Unknown",
|
|
1596
|
+
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
1597
|
});
|
|
1293
1598
|
} else {
|
|
1294
1599
|
txLogger2.error("Transaction rolled back", {
|
|
@@ -1323,6 +1628,7 @@ function Transactional(options = {}) {
|
|
|
1323
1628
|
}
|
|
1324
1629
|
);
|
|
1325
1630
|
} catch (error) {
|
|
1631
|
+
reportDatabaseError(error);
|
|
1326
1632
|
if (error instanceof DatabaseError) {
|
|
1327
1633
|
throw error;
|
|
1328
1634
|
}
|
|
@@ -1570,6 +1876,7 @@ var BaseRepository = class {
|
|
|
1570
1876
|
try {
|
|
1571
1877
|
return await queryFn();
|
|
1572
1878
|
} catch (error) {
|
|
1879
|
+
reportDatabaseError(error);
|
|
1573
1880
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1574
1881
|
const repositoryName = this.constructor.name;
|
|
1575
1882
|
throw new RepositoryError(
|
|
@@ -1636,14 +1943,51 @@ var BaseRepository = class {
|
|
|
1636
1943
|
const orderByArray = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
|
|
1637
1944
|
query = query.orderBy(...orderByArray);
|
|
1638
1945
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1946
|
+
const maxRows = env.DB_MAX_ROWS;
|
|
1947
|
+
const requestedLimit = options?.limit && options.limit > 0 ? options.limit : void 0;
|
|
1948
|
+
const effectiveLimit = maxRows > 0 ? Math.min(requestedLimit ?? maxRows, maxRows) : requestedLimit;
|
|
1949
|
+
if (effectiveLimit) {
|
|
1950
|
+
query = query.limit(effectiveLimit);
|
|
1641
1951
|
}
|
|
1642
1952
|
if (options?.offset) {
|
|
1643
1953
|
query = query.offset(options.offset);
|
|
1644
1954
|
}
|
|
1645
1955
|
return query;
|
|
1646
1956
|
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Keyset (cursor) pagination — O(limit) instead of OFFSET's O(offset).
|
|
1959
|
+
*
|
|
1960
|
+
* Pages by a strictly-ordered, unique column instead of a numeric offset, so a
|
|
1961
|
+
* deep page doesn't scan and discard everything before it. Pass the cursor
|
|
1962
|
+
* column's value from the last row of the previous page as `after`; omit it for
|
|
1963
|
+
* the first page. The column must be unique and the sole sort key (e.g. an
|
|
1964
|
+
* auto-increment id or a ULID).
|
|
1965
|
+
*
|
|
1966
|
+
* @example
|
|
1967
|
+
* ```typescript
|
|
1968
|
+
* const page1 = await this._findManyKeyset(users, { cursorColumn: users.id, limit: 20 });
|
|
1969
|
+
* const page2 = await this._findManyKeyset(users, {
|
|
1970
|
+
* cursorColumn: users.id,
|
|
1971
|
+
* after: page1.at(-1)?.id,
|
|
1972
|
+
* limit: 20,
|
|
1973
|
+
* });
|
|
1974
|
+
* ```
|
|
1975
|
+
*/
|
|
1976
|
+
async _findManyKeyset(table, options) {
|
|
1977
|
+
const { cursorColumn, after, order = "asc", where } = options;
|
|
1978
|
+
const maxRows = env.DB_MAX_ROWS;
|
|
1979
|
+
const limit = maxRows > 0 ? Math.min(options.limit, maxRows) : options.limit;
|
|
1980
|
+
const baseWhere = where ? isSQLWrapper(where) ? where : buildWhereFromObject(table, where) : void 0;
|
|
1981
|
+
const cursorPredicate = after !== void 0 ? order === "desc" ? lt(cursorColumn, after) : gt(cursorColumn, after) : void 0;
|
|
1982
|
+
const whereClause = baseWhere && cursorPredicate ? and(baseWhere, cursorPredicate) : cursorPredicate ?? baseWhere;
|
|
1983
|
+
let query = this.readDb.select().from(table);
|
|
1984
|
+
if (whereClause) {
|
|
1985
|
+
query = query.where(whereClause);
|
|
1986
|
+
}
|
|
1987
|
+
query = query.orderBy(order === "desc" ? desc(cursorColumn) : asc(cursorColumn));
|
|
1988
|
+
query = query.limit(limit);
|
|
1989
|
+
return query;
|
|
1990
|
+
}
|
|
1647
1991
|
/**
|
|
1648
1992
|
* Create a new record
|
|
1649
1993
|
*
|
|
@@ -1822,6 +2166,6 @@ var BaseRepository = class {
|
|
|
1822
2166
|
}
|
|
1823
2167
|
};
|
|
1824
2168
|
|
|
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 };
|
|
2169
|
+
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, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
|
|
1826
2170
|
//# sourceMappingURL=index.js.map
|
|
1827
2171
|
//# sourceMappingURL=index.js.map
|