@plyaz/db 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -134
- package/dist/adapters/drizzle/DrizzleAdapter.d.ts +109 -9
- package/dist/adapters/drizzle/DrizzleAdapter.d.ts.map +1 -1
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/mock/MockAdapter.d.ts +88 -0
- package/dist/adapters/mock/MockAdapter.d.ts.map +1 -0
- package/dist/adapters/sql/SQLAdapter.d.ts +29 -6
- package/dist/adapters/sql/SQLAdapter.d.ts.map +1 -1
- package/dist/adapters/supabase/SupabaseAdapter.d.ts +9 -2
- package/dist/adapters/supabase/SupabaseAdapter.d.ts.map +1 -1
- package/dist/advanced/multi-tenancy/TenantRepository.d.ts +1 -7
- package/dist/advanced/multi-tenancy/TenantRepository.d.ts.map +1 -1
- package/dist/advanced/read-replica/ReadReplicaAdapter.d.ts +3 -2
- package/dist/advanced/read-replica/ReadReplicaAdapter.d.ts.map +1 -1
- package/dist/cli/index.d.ts +27 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +9201 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/extensions/AuditExtension.d.ts +56 -9
- package/dist/extensions/AuditExtension.d.ts.map +1 -1
- package/dist/extensions/CachingAdapter.d.ts +5 -4
- package/dist/extensions/CachingAdapter.d.ts.map +1 -1
- package/dist/extensions/EncryptionExtension.d.ts +5 -4
- package/dist/extensions/EncryptionExtension.d.ts.map +1 -1
- package/dist/extensions/MultiReadExtension.d.ts +95 -0
- package/dist/extensions/MultiReadExtension.d.ts.map +1 -0
- package/dist/extensions/MultiWriteExtension.d.ts +67 -0
- package/dist/extensions/MultiWriteExtension.d.ts.map +1 -0
- package/dist/extensions/ReadReplicaAdapter.d.ts +4 -3
- package/dist/extensions/ReadReplicaAdapter.d.ts.map +1 -1
- package/dist/extensions/SoftDeleteExtension.d.ts +5 -4
- package/dist/extensions/SoftDeleteExtension.d.ts.map +1 -1
- package/dist/extensions/index.d.ts +4 -0
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/factory/AdapterFactory.d.ts.map +1 -1
- package/dist/factory/createDatabaseService.d.ts.map +1 -1
- package/dist/index.cjs +3298 -396
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +4441 -1562
- package/dist/index.mjs.map +1 -1
- package/dist/migrations/MigrationManager.d.ts +128 -0
- package/dist/migrations/MigrationManager.d.ts.map +1 -0
- package/dist/migrations/generateDownMigration.d.ts +25 -0
- package/dist/migrations/generateDownMigration.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +10 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/repository/BaseRepository.d.ts +109 -23
- package/dist/repository/BaseRepository.d.ts.map +1 -1
- package/dist/seeds/SeedManager.d.ts +120 -0
- package/dist/seeds/SeedManager.d.ts.map +1 -0
- package/dist/seeds/index.d.ts +10 -0
- package/dist/seeds/index.d.ts.map +1 -0
- package/dist/service/DatabaseService.d.ts +89 -13
- package/dist/service/DatabaseService.d.ts.map +1 -1
- package/dist/service/EventEmitter.d.ts +3 -14
- package/dist/service/EventEmitter.d.ts.map +1 -1
- package/dist/service/HealthManager.d.ts +42 -3
- package/dist/service/HealthManager.d.ts.map +1 -1
- package/package.json +9 -5
package/dist/index.cjs
CHANGED
|
@@ -25,6 +25,26 @@ var sanitizeHtml = require('sanitize-html');
|
|
|
25
25
|
|
|
26
26
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
27
27
|
|
|
28
|
+
function _interopNamespace(e) {
|
|
29
|
+
if (e && e.__esModule) return e;
|
|
30
|
+
var n = Object.create(null);
|
|
31
|
+
if (e) {
|
|
32
|
+
Object.keys(e).forEach(function (k) {
|
|
33
|
+
if (k !== 'default') {
|
|
34
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
35
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
get: function () { return e[k]; }
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
n.default = e;
|
|
43
|
+
return Object.freeze(n);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
47
|
+
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
28
48
|
var sanitizeHtml__default = /*#__PURE__*/_interopDefault(sanitizeHtml);
|
|
29
49
|
|
|
30
50
|
// @plyaz package - Built with tsup
|
|
@@ -294,15 +314,37 @@ function normalizeDetails(details) {
|
|
|
294
314
|
}
|
|
295
315
|
}
|
|
296
316
|
__name(normalizeDetails, "normalizeDetails");
|
|
317
|
+
var DEFAULT_FAILOVER_THRESHOLD = 3;
|
|
297
318
|
var HealthManager = class {
|
|
298
|
-
constructor(adapter) {
|
|
299
|
-
this.adapter = adapter;
|
|
300
|
-
}
|
|
301
319
|
static {
|
|
302
320
|
__name(this, "HealthManager");
|
|
303
321
|
}
|
|
304
322
|
lastHealthStatus = null;
|
|
305
323
|
initialized = false;
|
|
324
|
+
currentAdapter;
|
|
325
|
+
primaryAdapter;
|
|
326
|
+
backupAdapters;
|
|
327
|
+
consecutiveFailures = 0;
|
|
328
|
+
healthCheckInterval;
|
|
329
|
+
failoverThreshold;
|
|
330
|
+
autoFailover;
|
|
331
|
+
healthCheckTimer;
|
|
332
|
+
constructor(config) {
|
|
333
|
+
if ("primary" in config) {
|
|
334
|
+
this.primaryAdapter = config.primary;
|
|
335
|
+
this.currentAdapter = config.primary;
|
|
336
|
+
this.backupAdapters = config.backups ?? [];
|
|
337
|
+
this.healthCheckInterval = config.healthCheckInterval;
|
|
338
|
+
this.failoverThreshold = config.failoverThreshold ?? DEFAULT_FAILOVER_THRESHOLD;
|
|
339
|
+
this.autoFailover = config.autoFailover ?? false;
|
|
340
|
+
} else {
|
|
341
|
+
this.primaryAdapter = config;
|
|
342
|
+
this.currentAdapter = config;
|
|
343
|
+
this.backupAdapters = [];
|
|
344
|
+
this.failoverThreshold = DEFAULT_FAILOVER_THRESHOLD;
|
|
345
|
+
this.autoFailover = false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
306
348
|
/**
|
|
307
349
|
* Initializes the health manager by establishing database connection and performing initial health check
|
|
308
350
|
*
|
|
@@ -333,11 +375,14 @@ var HealthManager = class {
|
|
|
333
375
|
*/
|
|
334
376
|
async init() {
|
|
335
377
|
if (this.initialized) return;
|
|
336
|
-
if (typeof this.
|
|
337
|
-
await this.
|
|
378
|
+
if (typeof this.currentAdapter.initialize === "function") {
|
|
379
|
+
await this.currentAdapter.initialize();
|
|
338
380
|
}
|
|
339
381
|
await this.checkHealth();
|
|
340
382
|
this.initialized = true;
|
|
383
|
+
if (this.healthCheckInterval && this.healthCheckInterval > 0) {
|
|
384
|
+
this.startPeriodicHealthChecks();
|
|
385
|
+
}
|
|
341
386
|
}
|
|
342
387
|
/**
|
|
343
388
|
* Performs a comprehensive health check on the database connection
|
|
@@ -396,16 +441,37 @@ var HealthManager = class {
|
|
|
396
441
|
* ```
|
|
397
442
|
*
|
|
398
443
|
*/
|
|
444
|
+
/**
|
|
445
|
+
* Handle health check result and update internal state
|
|
446
|
+
*/
|
|
447
|
+
async handleHealthResult(isSuccess) {
|
|
448
|
+
if (isSuccess) {
|
|
449
|
+
this.consecutiveFailures = 0;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
this.consecutiveFailures++;
|
|
453
|
+
if (this.autoFailover && this.consecutiveFailures >= this.failoverThreshold) {
|
|
454
|
+
await this.performFailover();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Create health status from check result
|
|
459
|
+
*/
|
|
460
|
+
createHealthStatus(result, responseTime) {
|
|
461
|
+
return {
|
|
462
|
+
isHealthy: result.success,
|
|
463
|
+
responseTime,
|
|
464
|
+
details: result.success ? normalizeDetails(result.value) : { error: result.error?.message ?? "Unknown error" }
|
|
465
|
+
};
|
|
466
|
+
}
|
|
399
467
|
async checkHealth() {
|
|
400
468
|
const startTime = Date.now();
|
|
401
469
|
try {
|
|
402
|
-
const result = await this.
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
responseTime: Date.now() - startTime,
|
|
406
|
-
details: result.success ? normalizeDetails(result.value) : { error: result.error?.message ?? "Unknown error" }
|
|
407
|
-
};
|
|
470
|
+
const result = await this.currentAdapter.healthCheck();
|
|
471
|
+
const responseTime = Date.now() - startTime;
|
|
472
|
+
const status = this.createHealthStatus(result, responseTime);
|
|
408
473
|
this.lastHealthStatus = status;
|
|
474
|
+
await this.handleHealthResult(result.success);
|
|
409
475
|
return success(status);
|
|
410
476
|
} catch (error) {
|
|
411
477
|
const status = {
|
|
@@ -414,6 +480,7 @@ var HealthManager = class {
|
|
|
414
480
|
details: { error: error.message }
|
|
415
481
|
};
|
|
416
482
|
this.lastHealthStatus = status;
|
|
483
|
+
await this.handleHealthResult(false);
|
|
417
484
|
return failure(
|
|
418
485
|
new errors.DatabaseError(
|
|
419
486
|
`Health check failed: ${error.message}`,
|
|
@@ -438,13 +505,82 @@ var HealthManager = class {
|
|
|
438
505
|
isHealthy() {
|
|
439
506
|
return this.lastHealthStatus?.isHealthy ?? false;
|
|
440
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* Get the current active adapter
|
|
510
|
+
*/
|
|
511
|
+
getCurrentAdapter() {
|
|
512
|
+
return this.currentAdapter;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Try to initialize and verify health of a backup adapter
|
|
516
|
+
*/
|
|
517
|
+
async tryBackupAdapter(backup) {
|
|
518
|
+
if (typeof backup.initialize === "function") {
|
|
519
|
+
const initResult = await backup.initialize();
|
|
520
|
+
if (!initResult.success) return false;
|
|
521
|
+
}
|
|
522
|
+
const healthResult = await backup.healthCheck();
|
|
523
|
+
return healthResult.success && (healthResult.value?.isHealthy ?? false);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Switch to a healthy backup adapter
|
|
527
|
+
*/
|
|
528
|
+
async switchToBackup(backup) {
|
|
529
|
+
await this.currentAdapter.close();
|
|
530
|
+
this.currentAdapter = backup;
|
|
531
|
+
this.consecutiveFailures = 0;
|
|
532
|
+
console.log("[HealthManager] Failover successful to backup adapter");
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Perform failover to next available backup adapter
|
|
536
|
+
*/
|
|
537
|
+
async performFailover() {
|
|
538
|
+
if (this.backupAdapters.length === 0) {
|
|
539
|
+
console.warn("[HealthManager] No backup adapters available for failover");
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
for (const backup of this.backupAdapters) {
|
|
543
|
+
try {
|
|
544
|
+
const isHealthy = await this.tryBackupAdapter(backup);
|
|
545
|
+
if (isHealthy) {
|
|
546
|
+
await this.switchToBackup(backup);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
} catch (error) {
|
|
550
|
+
console.error(
|
|
551
|
+
"[HealthManager] Backup adapter health check failed:",
|
|
552
|
+
error
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
console.error(
|
|
557
|
+
"[HealthManager] All backup adapters failed, staying with current adapter"
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Start periodic health checks
|
|
562
|
+
*/
|
|
563
|
+
startPeriodicHealthChecks() {
|
|
564
|
+
if (this.healthCheckTimer) return;
|
|
565
|
+
this.healthCheckTimer = setInterval(async () => {
|
|
566
|
+
await this.checkHealth();
|
|
567
|
+
}, this.healthCheckInterval);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Stop periodic health checks
|
|
571
|
+
*/
|
|
572
|
+
stopPeriodicHealthChecks() {
|
|
573
|
+
if (this.healthCheckTimer) {
|
|
574
|
+
clearInterval(this.healthCheckTimer);
|
|
575
|
+
this.healthCheckTimer = void 0;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
441
578
|
/**
|
|
442
579
|
* Gracefully shut down the health manager
|
|
443
580
|
*/
|
|
444
581
|
async shutdown() {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
582
|
+
this.stopPeriodicHealthChecks();
|
|
583
|
+
await this.currentAdapter.close();
|
|
448
584
|
this.initialized = false;
|
|
449
585
|
this.lastHealthStatus = null;
|
|
450
586
|
}
|
|
@@ -719,6 +855,52 @@ var DatabaseService = class {
|
|
|
719
855
|
this.healthManager = new HealthManager(config.adapter);
|
|
720
856
|
console.log(`DatabaseService initialized with ${adapterType} adapter`);
|
|
721
857
|
}
|
|
858
|
+
/**
|
|
859
|
+
* 🔧 Prepare table name and configuration for operation
|
|
860
|
+
*
|
|
861
|
+
* Handles per-operation overrides for:
|
|
862
|
+
* 1. Schema - prepends schema to table name (e.g., "audit.logs")
|
|
863
|
+
* 2. ID Column - temporarily registers table with custom ID column
|
|
864
|
+
*
|
|
865
|
+
* Priority order:
|
|
866
|
+
* - Operation config (highest) - per-query override
|
|
867
|
+
* - TABLE_REGISTRY - global registration
|
|
868
|
+
* - Default (lowest) - 'id' column, 'public' schema
|
|
869
|
+
*
|
|
870
|
+
* @param table Base table name
|
|
871
|
+
* @param operationConfig Optional operation configuration with idColumn/schema
|
|
872
|
+
* @returns Final table name to use (with schema prefix if applicable)
|
|
873
|
+
*
|
|
874
|
+
* @example
|
|
875
|
+
* ```typescript
|
|
876
|
+
* // With schema override
|
|
877
|
+
* const tableName = this.prepareTable('logs', { schema: 'audit' });
|
|
878
|
+
* // Returns: 'audit.logs'
|
|
879
|
+
*
|
|
880
|
+
* // With ID column override
|
|
881
|
+
* const tableName = this.prepareTable('feature_flags', { idColumn: 'key' });
|
|
882
|
+
* // Registers table with 'key' as ID column, returns: 'feature_flags'
|
|
883
|
+
*
|
|
884
|
+
* // With both
|
|
885
|
+
* const tableName = this.prepareTable('users', { schema: 'backoffice', idColumn: 'user_id' });
|
|
886
|
+
* // Returns: 'backoffice.users' with 'user_id' as ID column
|
|
887
|
+
* ```
|
|
888
|
+
*/
|
|
889
|
+
prepareTable(table, operationConfig) {
|
|
890
|
+
let finalTableName = table;
|
|
891
|
+
if (operationConfig?.schema) {
|
|
892
|
+
const tableWithoutSchema = table.includes(".") ? table.split(".")[1] : table;
|
|
893
|
+
finalTableName = `${operationConfig.schema}.${tableWithoutSchema}`;
|
|
894
|
+
}
|
|
895
|
+
if (operationConfig?.idColumn) {
|
|
896
|
+
this.adapter.registerTable(
|
|
897
|
+
finalTableName,
|
|
898
|
+
finalTableName,
|
|
899
|
+
operationConfig.idColumn
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
return finalTableName;
|
|
903
|
+
}
|
|
722
904
|
/**
|
|
723
905
|
* 🔍 Get a single record by ID
|
|
724
906
|
*
|
|
@@ -749,10 +931,28 @@ var DatabaseService = class {
|
|
|
749
931
|
* forceAdapter: 'primary', // Force primary DB
|
|
750
932
|
* cache: { enabled: false } // Skip cache
|
|
751
933
|
* });
|
|
934
|
+
*
|
|
935
|
+
* // With custom ID column (per-query override)
|
|
936
|
+
* const flag = await this.db.get('feature_flags', 'my-flag-key', {
|
|
937
|
+
* idColumn: 'key' // Use 'key' instead of 'id' as primary key
|
|
938
|
+
* });
|
|
939
|
+
*
|
|
940
|
+
* // With custom schema
|
|
941
|
+
* const auditLog = await this.db.get('logs', '123', {
|
|
942
|
+
* schema: 'audit' // Query from audit.logs table
|
|
943
|
+
* });
|
|
944
|
+
*
|
|
945
|
+
* // Combining multiple overrides
|
|
946
|
+
* const backofficeUser = await this.db.get('users', 'admin-456', {
|
|
947
|
+
* schema: 'backoffice',
|
|
948
|
+
* idColumn: 'user_id',
|
|
949
|
+
* forceAdapter: 'primary'
|
|
950
|
+
* });
|
|
752
951
|
* ```
|
|
753
952
|
*/
|
|
754
953
|
async get(table, id, operationConfig) {
|
|
755
954
|
ConfigMerger.mergeConfigs(this.globalConfig, operationConfig);
|
|
955
|
+
const finalTable = this.prepareTable(table, operationConfig);
|
|
756
956
|
try {
|
|
757
957
|
if (this.eventHandlers?.onBeforeRead) {
|
|
758
958
|
await this.eventHandlers.onBeforeRead({
|
|
@@ -762,7 +962,7 @@ var DatabaseService = class {
|
|
|
762
962
|
timestamp: /* @__PURE__ */ new Date()
|
|
763
963
|
});
|
|
764
964
|
}
|
|
765
|
-
const result = await this.adapter.findById(
|
|
965
|
+
const result = await this.adapter.findById(finalTable, id);
|
|
766
966
|
if (result.success && this.eventHandlers?.onAfterRead) {
|
|
767
967
|
await this.eventHandlers.onAfterRead({
|
|
768
968
|
type: "afterRead",
|
|
@@ -821,6 +1021,7 @@ var DatabaseService = class {
|
|
|
821
1021
|
*/
|
|
822
1022
|
async create(table, input, operationConfig) {
|
|
823
1023
|
ConfigMerger.mergeConfigs(this.globalConfig, operationConfig);
|
|
1024
|
+
const finalTable = this.prepareTable(table, operationConfig);
|
|
824
1025
|
if (this.eventHandlers?.onBeforeWrite) {
|
|
825
1026
|
await this.eventHandlers.onBeforeWrite({
|
|
826
1027
|
type: "beforeWrite",
|
|
@@ -830,7 +1031,7 @@ var DatabaseService = class {
|
|
|
830
1031
|
timestamp: /* @__PURE__ */ new Date()
|
|
831
1032
|
});
|
|
832
1033
|
}
|
|
833
|
-
const result = await this.adapter.create(
|
|
1034
|
+
const result = await this.adapter.create(finalTable, input);
|
|
834
1035
|
if (result.success && this.eventHandlers?.onAfterWrite) {
|
|
835
1036
|
await this.eventHandlers.onAfterWrite({
|
|
836
1037
|
type: "afterWrite",
|
|
@@ -849,6 +1050,7 @@ var DatabaseService = class {
|
|
|
849
1050
|
*/
|
|
850
1051
|
async update(table, id, input, operationConfig) {
|
|
851
1052
|
ConfigMerger.mergeConfigs(this.globalConfig, operationConfig);
|
|
1053
|
+
const finalTable = this.prepareTable(table, operationConfig);
|
|
852
1054
|
if (this.eventHandlers?.onBeforeWrite) {
|
|
853
1055
|
await this.eventHandlers.onBeforeWrite({
|
|
854
1056
|
type: "beforeWrite",
|
|
@@ -858,7 +1060,11 @@ var DatabaseService = class {
|
|
|
858
1060
|
timestamp: /* @__PURE__ */ new Date()
|
|
859
1061
|
});
|
|
860
1062
|
}
|
|
861
|
-
const result = await this.adapter.update(
|
|
1063
|
+
const result = await this.adapter.update(
|
|
1064
|
+
finalTable,
|
|
1065
|
+
id,
|
|
1066
|
+
input
|
|
1067
|
+
);
|
|
862
1068
|
if (result.success && this.eventHandlers?.onAfterWrite) {
|
|
863
1069
|
await this.eventHandlers.onAfterWrite({
|
|
864
1070
|
type: "afterWrite",
|
|
@@ -877,6 +1083,7 @@ var DatabaseService = class {
|
|
|
877
1083
|
*/
|
|
878
1084
|
async delete(table, id, operationConfig) {
|
|
879
1085
|
ConfigMerger.mergeConfigs(this.globalConfig, operationConfig);
|
|
1086
|
+
const finalTable = this.prepareTable(table, operationConfig);
|
|
880
1087
|
if (this.eventHandlers?.onBeforeWrite) {
|
|
881
1088
|
await this.eventHandlers.onBeforeWrite({
|
|
882
1089
|
type: "beforeWrite",
|
|
@@ -886,7 +1093,7 @@ var DatabaseService = class {
|
|
|
886
1093
|
timestamp: /* @__PURE__ */ new Date()
|
|
887
1094
|
});
|
|
888
1095
|
}
|
|
889
|
-
const result = await this.adapter.delete(
|
|
1096
|
+
const result = await this.adapter.delete(finalTable, id);
|
|
890
1097
|
if (result.success && this.eventHandlers?.onAfterWrite) {
|
|
891
1098
|
await this.eventHandlers.onAfterWrite({
|
|
892
1099
|
type: "afterWrite",
|
|
@@ -1013,7 +1220,29 @@ var DatabaseService = class {
|
|
|
1013
1220
|
async initialize() {
|
|
1014
1221
|
return success();
|
|
1015
1222
|
}
|
|
1016
|
-
|
|
1223
|
+
/**
|
|
1224
|
+
* Registers a table with the underlying adapter.
|
|
1225
|
+
*
|
|
1226
|
+
* @param name - Logical table name (e.g., 'users')
|
|
1227
|
+
* @param table - Physical table name or Drizzle table object
|
|
1228
|
+
* @param idColumn - ID column name (defaults to 'id')
|
|
1229
|
+
*
|
|
1230
|
+
* @example SQL Adapter
|
|
1231
|
+
* ```typescript
|
|
1232
|
+
* db.registerTable('users', 'users', 'id');
|
|
1233
|
+
* db.registerTable('feature_flags', 'feature_flags', 'key');
|
|
1234
|
+
* ```
|
|
1235
|
+
*
|
|
1236
|
+
* @example Drizzle Adapter (requires PgTable objects)
|
|
1237
|
+
* ```typescript
|
|
1238
|
+
* db.registerTable('users', usersTable, usersTable.id);
|
|
1239
|
+
* ```
|
|
1240
|
+
*/
|
|
1241
|
+
registerTable(name, table, idColumn) {
|
|
1242
|
+
if (this.adapter && typeof this.adapter.registerTable === "function") {
|
|
1243
|
+
const tableValue = table ?? name;
|
|
1244
|
+
this.adapter.registerTable(name, tableValue, idColumn);
|
|
1245
|
+
}
|
|
1017
1246
|
}
|
|
1018
1247
|
async exists(table, id) {
|
|
1019
1248
|
const result = await this.get(table, id);
|
|
@@ -1111,6 +1340,33 @@ var DatabaseService = class {
|
|
|
1111
1340
|
get events() {
|
|
1112
1341
|
return this.eventEmitter;
|
|
1113
1342
|
}
|
|
1343
|
+
/**
|
|
1344
|
+
* 🔌 Close the database connection
|
|
1345
|
+
*
|
|
1346
|
+
* Gracefully shuts down the database connection and releases all resources.
|
|
1347
|
+
* Should be called when the service is no longer needed.
|
|
1348
|
+
*
|
|
1349
|
+
* @returns Promise resolving to DatabaseResult indicating success or failure
|
|
1350
|
+
*/
|
|
1351
|
+
async close() {
|
|
1352
|
+
try {
|
|
1353
|
+
if (this.adapter.close) {
|
|
1354
|
+
return await this.adapter.close();
|
|
1355
|
+
}
|
|
1356
|
+
return success();
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
return failure(
|
|
1359
|
+
new errors.DatabaseError(
|
|
1360
|
+
`Failed to close database connection: ${error.message}`,
|
|
1361
|
+
errors$1.DATABASE_ERROR_CODES.DISCONNECT_FAILED,
|
|
1362
|
+
{
|
|
1363
|
+
context: { source: "close" },
|
|
1364
|
+
cause: error
|
|
1365
|
+
}
|
|
1366
|
+
)
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1114
1370
|
};
|
|
1115
1371
|
function validatePaginationInputs(total, limit, offset) {
|
|
1116
1372
|
if (!isNumber(total) || total < 0) {
|
|
@@ -1348,6 +1604,7 @@ var DB_REGEX = {
|
|
|
1348
1604
|
};
|
|
1349
1605
|
|
|
1350
1606
|
// src/adapters/drizzle/DrizzleAdapter.ts
|
|
1607
|
+
var BETWEEN_MIN_ELEMENTS = 2;
|
|
1351
1608
|
var DrizzleAdapter = class {
|
|
1352
1609
|
static {
|
|
1353
1610
|
__name(this, "DrizzleAdapter");
|
|
@@ -1357,6 +1614,10 @@ var DrizzleAdapter = class {
|
|
|
1357
1614
|
config;
|
|
1358
1615
|
tableMap = /* @__PURE__ */ new Map();
|
|
1359
1616
|
idColumnMap = /* @__PURE__ */ new Map();
|
|
1617
|
+
// String-based table registry for auto-registration (fallback when no PgTable schema)
|
|
1618
|
+
stringTableMap = /* @__PURE__ */ new Map();
|
|
1619
|
+
stringIdColumnMap = /* @__PURE__ */ new Map();
|
|
1620
|
+
configIdColumns;
|
|
1360
1621
|
/**
|
|
1361
1622
|
* Creates a new DrizzleAdapter instance.
|
|
1362
1623
|
* @param {DrizzleAdapterConfig} config - Configuration object for Drizzle and PostgreSQL connection.
|
|
@@ -1372,6 +1633,7 @@ var DrizzleAdapter = class {
|
|
|
1372
1633
|
...config.pool
|
|
1373
1634
|
});
|
|
1374
1635
|
this.db = nodePostgres.drizzle(this.pool);
|
|
1636
|
+
this.configIdColumns = config.tableIdColumns ?? {};
|
|
1375
1637
|
}
|
|
1376
1638
|
/**
|
|
1377
1639
|
* Initializes the adapter by verifying database connectivity.
|
|
@@ -1456,6 +1718,23 @@ var DrizzleAdapter = class {
|
|
|
1456
1718
|
);
|
|
1457
1719
|
}
|
|
1458
1720
|
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Closes the database connection and cleanup resources.
|
|
1723
|
+
* @returns Promise resolving to DatabaseResult indicating success or failure.
|
|
1724
|
+
*/
|
|
1725
|
+
async close() {
|
|
1726
|
+
try {
|
|
1727
|
+
await this.disconnect();
|
|
1728
|
+
return success();
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
return failure(
|
|
1731
|
+
new errors.DatabaseError(
|
|
1732
|
+
`Failed to close connection: ${error.message}`,
|
|
1733
|
+
errors$1.DATABASE_ERROR_CODES.DISCONNECT_FAILED
|
|
1734
|
+
)
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1459
1738
|
/**
|
|
1460
1739
|
* Gets the underlying Drizzle client instance.
|
|
1461
1740
|
* @returns {TClient} The internal Drizzle client.
|
|
@@ -1505,15 +1784,20 @@ var DrizzleAdapter = class {
|
|
|
1505
1784
|
}
|
|
1506
1785
|
/**
|
|
1507
1786
|
* Registers a table and optional ID column for ORM operations.
|
|
1508
|
-
* @template TTable - Type representing the table structure.
|
|
1509
|
-
* @template TIdColumn - Type representing the ID column.
|
|
1787
|
+
* @template TTable - Type representing the table structure (PgTable or string).
|
|
1788
|
+
* @template TIdColumn - Type representing the ID column (PgColumn or string).
|
|
1510
1789
|
* @param {string} name - Logical name of the table.
|
|
1511
|
-
* @param {TTable} table - Drizzle table object.
|
|
1512
|
-
* @param {TIdColumn} [idColumn] - Optional primary key column.
|
|
1790
|
+
* @param {TTable} table - Drizzle table object (PgTable) or string table name.
|
|
1791
|
+
* @param {TIdColumn} [idColumn] - Optional primary key column (PgColumn or string).
|
|
1513
1792
|
* @description
|
|
1514
1793
|
* Registers a table with the adapter, allowing it to be referenced by a logical name
|
|
1515
1794
|
* in subsequent operations. This is necessary for the adapter to perform ORM operations
|
|
1516
1795
|
* on the table. The ID column can also be specified if it differs from the default 'id'.
|
|
1796
|
+
*
|
|
1797
|
+
* **Supports two modes:**
|
|
1798
|
+
* 1. **PgTable mode**: Pass Drizzle schema objects for type-safe ORM operations
|
|
1799
|
+
* 2. **String mode**: Pass string table names for raw SQL fallback (auto-registration)
|
|
1800
|
+
*
|
|
1517
1801
|
* This registration enables the adapter to map logical table names to actual table objects
|
|
1518
1802
|
* and ID columns, providing a layer of abstraction between the application and the database schema.
|
|
1519
1803
|
*/
|
|
@@ -1525,9 +1809,16 @@ var DrizzleAdapter = class {
|
|
|
1525
1809
|
errors$1.DATABASE_ERROR_CODES.INVALID_TABLE_NAME
|
|
1526
1810
|
);
|
|
1527
1811
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1812
|
+
if (typeof table === "string") {
|
|
1813
|
+
this.stringTableMap.set(name, table);
|
|
1814
|
+
if (idColumn && typeof idColumn === "string") {
|
|
1815
|
+
this.stringIdColumnMap.set(name, idColumn);
|
|
1816
|
+
}
|
|
1817
|
+
} else {
|
|
1818
|
+
this.tableMap.set(name, table);
|
|
1819
|
+
if (idColumn) {
|
|
1820
|
+
this.idColumnMap.set(name, idColumn);
|
|
1821
|
+
}
|
|
1531
1822
|
}
|
|
1532
1823
|
} catch (error) {
|
|
1533
1824
|
throw new errors.DatabaseError(
|
|
@@ -1542,6 +1833,16 @@ var DrizzleAdapter = class {
|
|
|
1542
1833
|
);
|
|
1543
1834
|
}
|
|
1544
1835
|
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Execute raw SQL findById query
|
|
1838
|
+
*/
|
|
1839
|
+
async rawSqlFindById(table, id) {
|
|
1840
|
+
const tableName = this.getStringTableName(table);
|
|
1841
|
+
const idColumn = this.getStringIdColumn(table);
|
|
1842
|
+
const sqlQuery = `SELECT * FROM "${tableName}" WHERE "${idColumn}" = $1 LIMIT 1`;
|
|
1843
|
+
const result = await this.pool.query(sqlQuery, [id]);
|
|
1844
|
+
return result.rows[0] || null;
|
|
1845
|
+
}
|
|
1545
1846
|
/**
|
|
1546
1847
|
* Finds a record by its primary ID.
|
|
1547
1848
|
* @template T - The expected type of the record.
|
|
@@ -1554,28 +1855,59 @@ var DrizzleAdapter = class {
|
|
|
1554
1855
|
* If the record is found, it is returned in a success result. If no record is found,
|
|
1555
1856
|
* null is returned in a success result. If an error occurs during the operation,
|
|
1556
1857
|
* a failure result with an error message is returned.
|
|
1858
|
+
*
|
|
1859
|
+
* **Auto-registers tables**: If table is not registered with PgTable schema,
|
|
1860
|
+
* falls back to raw SQL mode (same behavior as SQLAdapter).
|
|
1557
1861
|
*/
|
|
1558
1862
|
async findById(table, id) {
|
|
1559
1863
|
try {
|
|
1864
|
+
if (this.isStringMode(table)) {
|
|
1865
|
+
return success(await this.rawSqlFindById(table, id));
|
|
1866
|
+
}
|
|
1560
1867
|
const tableObj = this.getTable(table);
|
|
1561
1868
|
const idColumn = this.getIdColumn(table);
|
|
1562
1869
|
const result = await this.db.select().from(tableObj).where(drizzleOrm.eq(idColumn, id)).limit(1);
|
|
1563
1870
|
return success(result[0] || null);
|
|
1564
1871
|
} catch (error) {
|
|
1872
|
+
if (error instanceof errors.DatabaseError && error.message === "USE_STRING_MODE") {
|
|
1873
|
+
return this.handleStringModeFallback(
|
|
1874
|
+
() => this.rawSqlFindById(table, id),
|
|
1875
|
+
table,
|
|
1876
|
+
"DrizzleAdapter.findById",
|
|
1877
|
+
errors$1.DATABASE_ERROR_CODES.FIND_BY_ID_FAILED
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1565
1880
|
return failure(
|
|
1566
1881
|
new errors.DatabaseError(
|
|
1567
1882
|
`Failed to find by id in table ${table}: ${error.message}`,
|
|
1568
1883
|
errors$1.DATABASE_ERROR_CODES.FIND_BY_ID_FAILED,
|
|
1569
1884
|
{
|
|
1570
|
-
context: {
|
|
1571
|
-
source: "DrizzleAdapter.findById"
|
|
1572
|
-
},
|
|
1885
|
+
context: { source: "DrizzleAdapter.findById" },
|
|
1573
1886
|
cause: error
|
|
1574
1887
|
}
|
|
1575
1888
|
)
|
|
1576
1889
|
);
|
|
1577
1890
|
}
|
|
1578
1891
|
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Handle USE_STRING_MODE fallback with error wrapping
|
|
1894
|
+
*/
|
|
1895
|
+
async handleStringModeFallback(operation, table, source, errorCode) {
|
|
1896
|
+
try {
|
|
1897
|
+
return success(await operation());
|
|
1898
|
+
} catch (sqlError) {
|
|
1899
|
+
return failure(
|
|
1900
|
+
new errors.DatabaseError(
|
|
1901
|
+
`Failed operation on table ${table}: ${sqlError.message}`,
|
|
1902
|
+
errorCode,
|
|
1903
|
+
{
|
|
1904
|
+
context: { source },
|
|
1905
|
+
cause: sqlError
|
|
1906
|
+
}
|
|
1907
|
+
)
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1579
1911
|
/**
|
|
1580
1912
|
* Retrieves multiple records with optional filtering, sorting, and pagination.
|
|
1581
1913
|
* @template T - The expected type of the records.
|
|
@@ -1589,10 +1921,16 @@ var DrizzleAdapter = class {
|
|
|
1589
1921
|
* of records returned. The result includes the data array, total count of matching records,
|
|
1590
1922
|
* and pagination metadata such as current page, total pages, and next/previous cursors.
|
|
1591
1923
|
* If an error occurs during the operation, a failure result with an error message is returned.
|
|
1924
|
+
*
|
|
1925
|
+
* **Auto-registers tables**: If table is not registered with PgTable schema,
|
|
1926
|
+
* falls back to raw SQL mode (same behavior as SQLAdapter).
|
|
1592
1927
|
*/
|
|
1593
1928
|
// eslint-disable-next-line complexity
|
|
1594
1929
|
async findMany(table, options) {
|
|
1595
1930
|
try {
|
|
1931
|
+
if (this.isStringMode(table)) {
|
|
1932
|
+
return this.findManyRawSql(table, options);
|
|
1933
|
+
}
|
|
1596
1934
|
const tableObj = this.getTable(table);
|
|
1597
1935
|
let query = this.db.select().from(tableObj);
|
|
1598
1936
|
if (options?.sort) {
|
|
@@ -1627,6 +1965,9 @@ var DrizzleAdapter = class {
|
|
|
1627
1965
|
pagination: calculatePagination(total, options?.pagination)
|
|
1628
1966
|
});
|
|
1629
1967
|
} catch (error) {
|
|
1968
|
+
if (error instanceof errors.DatabaseError && error.message === "USE_STRING_MODE") {
|
|
1969
|
+
return this.findManyRawSql(table, options);
|
|
1970
|
+
}
|
|
1630
1971
|
return failure(
|
|
1631
1972
|
new errors.DatabaseError(
|
|
1632
1973
|
`Failed to find many in table ${table}: ${error.message}`,
|
|
@@ -1641,6 +1982,126 @@ var DrizzleAdapter = class {
|
|
|
1641
1982
|
);
|
|
1642
1983
|
}
|
|
1643
1984
|
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Raw SQL fallback for findMany when no PgTable schema is registered.
|
|
1987
|
+
* @private
|
|
1988
|
+
*/
|
|
1989
|
+
// eslint-disable-next-line complexity
|
|
1990
|
+
async findManyRawSql(table, options) {
|
|
1991
|
+
try {
|
|
1992
|
+
const tableName = this.getStringTableName(table);
|
|
1993
|
+
const params = [];
|
|
1994
|
+
let whereClause = "";
|
|
1995
|
+
let paramIndex = 1;
|
|
1996
|
+
if (options?.filter) {
|
|
1997
|
+
const { field, operator, value } = options.filter;
|
|
1998
|
+
whereClause = this.buildSqlWhereClause({
|
|
1999
|
+
field,
|
|
2000
|
+
operator,
|
|
2001
|
+
value,
|
|
2002
|
+
params,
|
|
2003
|
+
startIndex: paramIndex
|
|
2004
|
+
});
|
|
2005
|
+
paramIndex = params.length + 1;
|
|
2006
|
+
}
|
|
2007
|
+
const countSql = `SELECT COUNT(*) as total FROM "${tableName}"${whereClause}`;
|
|
2008
|
+
const countResult = await this.pool.query(countSql, params);
|
|
2009
|
+
const total = Number.parseInt(countResult.rows[0].total);
|
|
2010
|
+
let orderClause = "";
|
|
2011
|
+
if (options?.sort?.length) {
|
|
2012
|
+
orderClause = " ORDER BY " + options.sort.map((s) => `"${s.field}" ${s.direction.toUpperCase()}`).join(", ");
|
|
2013
|
+
}
|
|
2014
|
+
const queryParams = [...params];
|
|
2015
|
+
let limitClause = "";
|
|
2016
|
+
if (options?.pagination?.limit) {
|
|
2017
|
+
limitClause += ` LIMIT $${paramIndex++}`;
|
|
2018
|
+
queryParams.push(options.pagination.limit);
|
|
2019
|
+
}
|
|
2020
|
+
if (options?.pagination?.offset) {
|
|
2021
|
+
limitClause += ` OFFSET $${paramIndex++}`;
|
|
2022
|
+
queryParams.push(options.pagination.offset);
|
|
2023
|
+
}
|
|
2024
|
+
const sqlQuery = `SELECT * FROM "${tableName}"${whereClause}${orderClause}${limitClause}`;
|
|
2025
|
+
const result = await this.pool.query(sqlQuery, queryParams);
|
|
2026
|
+
return success({
|
|
2027
|
+
data: result.rows,
|
|
2028
|
+
total,
|
|
2029
|
+
pagination: calculatePagination(total, options?.pagination)
|
|
2030
|
+
});
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
return failure(
|
|
2033
|
+
new errors.DatabaseError(
|
|
2034
|
+
`Failed to find many in table ${table}: ${error.message}`,
|
|
2035
|
+
errors$1.DATABASE_ERROR_CODES.FIND_MANY_FAILED,
|
|
2036
|
+
{
|
|
2037
|
+
context: {
|
|
2038
|
+
source: "DrizzleAdapter.findManyRawSql"
|
|
2039
|
+
},
|
|
2040
|
+
cause: error
|
|
2041
|
+
}
|
|
2042
|
+
)
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Builds a SQL WHERE clause string for raw SQL queries.
|
|
2048
|
+
* @private
|
|
2049
|
+
*/
|
|
2050
|
+
// eslint-disable-next-line complexity
|
|
2051
|
+
buildSqlWhereClause(options) {
|
|
2052
|
+
const { field, operator, value, params, startIndex } = options;
|
|
2053
|
+
let clause = "";
|
|
2054
|
+
switch (operator) {
|
|
2055
|
+
case "eq":
|
|
2056
|
+
clause = ` WHERE "${field}" = $${startIndex}`;
|
|
2057
|
+
params.push(value);
|
|
2058
|
+
break;
|
|
2059
|
+
case "ne":
|
|
2060
|
+
clause = ` WHERE "${field}" != $${startIndex}`;
|
|
2061
|
+
params.push(value);
|
|
2062
|
+
break;
|
|
2063
|
+
case "gt":
|
|
2064
|
+
clause = ` WHERE "${field}" > $${startIndex}`;
|
|
2065
|
+
params.push(value);
|
|
2066
|
+
break;
|
|
2067
|
+
case "gte":
|
|
2068
|
+
clause = ` WHERE "${field}" >= $${startIndex}`;
|
|
2069
|
+
params.push(value);
|
|
2070
|
+
break;
|
|
2071
|
+
case "lt":
|
|
2072
|
+
clause = ` WHERE "${field}" < $${startIndex}`;
|
|
2073
|
+
params.push(value);
|
|
2074
|
+
break;
|
|
2075
|
+
case "lte":
|
|
2076
|
+
clause = ` WHERE "${field}" <= $${startIndex}`;
|
|
2077
|
+
params.push(value);
|
|
2078
|
+
break;
|
|
2079
|
+
case "in":
|
|
2080
|
+
if (Array.isArray(value)) {
|
|
2081
|
+
const placeholders = value.map((_, i) => `$${startIndex + i}`).join(", ");
|
|
2082
|
+
clause = ` WHERE "${field}" IN (${placeholders})`;
|
|
2083
|
+
params.push(...value);
|
|
2084
|
+
}
|
|
2085
|
+
break;
|
|
2086
|
+
case "like":
|
|
2087
|
+
clause = ` WHERE "${field}" LIKE $${startIndex}`;
|
|
2088
|
+
params.push(value);
|
|
2089
|
+
break;
|
|
2090
|
+
case "between":
|
|
2091
|
+
if (Array.isArray(value) && value.length >= BETWEEN_MIN_ELEMENTS) {
|
|
2092
|
+
clause = ` WHERE "${field}" BETWEEN $${startIndex} AND $${startIndex + 1}`;
|
|
2093
|
+
params.push(value[0], value[1]);
|
|
2094
|
+
}
|
|
2095
|
+
break;
|
|
2096
|
+
case "isNull":
|
|
2097
|
+
clause = ` WHERE "${field}" IS NULL`;
|
|
2098
|
+
break;
|
|
2099
|
+
case "isNotNull":
|
|
2100
|
+
clause = ` WHERE "${field}" IS NOT NULL`;
|
|
2101
|
+
break;
|
|
2102
|
+
}
|
|
2103
|
+
return clause;
|
|
2104
|
+
}
|
|
1644
2105
|
/**
|
|
1645
2106
|
* Inserts a new record into the specified table.
|
|
1646
2107
|
* @template T - The expected type of the record.
|
|
@@ -1653,13 +2114,22 @@ var DrizzleAdapter = class {
|
|
|
1653
2114
|
* After insertion, it returns the inserted record with any auto-generated fields
|
|
1654
2115
|
* (like IDs) populated. If an error occurs during the operation,
|
|
1655
2116
|
* a failure result with an error message is returned.
|
|
2117
|
+
*
|
|
2118
|
+
* **Auto-registers tables**: If table is not registered with PgTable schema,
|
|
2119
|
+
* falls back to raw SQL mode (same behavior as SQLAdapter).
|
|
1656
2120
|
*/
|
|
1657
2121
|
async create(table, data) {
|
|
1658
2122
|
try {
|
|
2123
|
+
if (this.isStringMode(table)) {
|
|
2124
|
+
return this.createRawSql(table, data);
|
|
2125
|
+
}
|
|
1659
2126
|
const tableObj = this.getTable(table);
|
|
1660
2127
|
const result = await this.db.insert(tableObj).values(data).returning();
|
|
1661
2128
|
return success(result[0]);
|
|
1662
2129
|
} catch (error) {
|
|
2130
|
+
if (error instanceof errors.DatabaseError && error.message === "USE_STRING_MODE") {
|
|
2131
|
+
return this.createRawSql(table, data);
|
|
2132
|
+
}
|
|
1663
2133
|
return failure(
|
|
1664
2134
|
new errors.DatabaseError(
|
|
1665
2135
|
`Failed to create in table ${table}: ${error.message}`,
|
|
@@ -1675,29 +2145,67 @@ var DrizzleAdapter = class {
|
|
|
1675
2145
|
}
|
|
1676
2146
|
}
|
|
1677
2147
|
/**
|
|
1678
|
-
*
|
|
1679
|
-
* @
|
|
1680
|
-
* @param {string} table - Table name.
|
|
1681
|
-
* @param {string} id - Record ID.
|
|
1682
|
-
* @param {Partial<T>} data - Partial object containing fields to update.
|
|
1683
|
-
* @returns {Promise<DatabaseResult<T>>} Updated record.
|
|
1684
|
-
* @description
|
|
1685
|
-
* Updates an existing record in the specified table using its primary ID.
|
|
1686
|
-
* Only the fields provided in the data object are updated, allowing for partial updates.
|
|
1687
|
-
* The method uses the registered table and ID column to construct the update query.
|
|
1688
|
-
* After updating, it returns the updated record. If an error occurs during the operation,
|
|
1689
|
-
* a failure result with an error message is returned.
|
|
2148
|
+
* Raw SQL fallback for create when no PgTable schema is registered.
|
|
2149
|
+
* @private
|
|
1690
2150
|
*/
|
|
1691
|
-
async
|
|
2151
|
+
async createRawSql(table, data) {
|
|
1692
2152
|
try {
|
|
1693
|
-
const
|
|
1694
|
-
const
|
|
1695
|
-
const
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2153
|
+
const tableName = this.getStringTableName(table);
|
|
2154
|
+
const keys = Object.keys(data);
|
|
2155
|
+
const values = Object.values(data);
|
|
2156
|
+
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
|
2157
|
+
const escapedKeys = keys.map((k) => `"${k}"`).join(", ");
|
|
2158
|
+
const sqlQuery = `INSERT INTO "${tableName}" (${escapedKeys}) VALUES (${placeholders}) RETURNING *`;
|
|
2159
|
+
const result = await this.pool.query(sqlQuery, values);
|
|
2160
|
+
return success(result.rows[0]);
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
return failure(
|
|
2163
|
+
new errors.DatabaseError(
|
|
2164
|
+
`Failed to create in table ${table}: ${error.message}`,
|
|
2165
|
+
errors$1.DATABASE_ERROR_CODES.CREATE_FAILED,
|
|
2166
|
+
{
|
|
2167
|
+
context: {
|
|
2168
|
+
source: "DrizzleAdapter.createRawSql"
|
|
2169
|
+
},
|
|
2170
|
+
cause: error
|
|
2171
|
+
}
|
|
2172
|
+
)
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Updates an existing record by ID.
|
|
2178
|
+
* @template T - The expected type of the record.
|
|
2179
|
+
* @param {string} table - Table name.
|
|
2180
|
+
* @param {string} id - Record ID.
|
|
2181
|
+
* @param {Partial<T>} data - Partial object containing fields to update.
|
|
2182
|
+
* @returns {Promise<DatabaseResult<T>>} Updated record.
|
|
2183
|
+
* @description
|
|
2184
|
+
* Updates an existing record in the specified table using its primary ID.
|
|
2185
|
+
* Only the fields provided in the data object are updated, allowing for partial updates.
|
|
2186
|
+
* The method uses the registered table and ID column to construct the update query.
|
|
2187
|
+
* After updating, it returns the updated record. If an error occurs during the operation,
|
|
2188
|
+
* a failure result with an error message is returned.
|
|
2189
|
+
*
|
|
2190
|
+
* **Auto-registers tables**: If table is not registered with PgTable schema,
|
|
2191
|
+
* falls back to raw SQL mode (same behavior as SQLAdapter).
|
|
2192
|
+
*/
|
|
2193
|
+
async update(table, id, data) {
|
|
2194
|
+
try {
|
|
2195
|
+
if (this.isStringMode(table)) {
|
|
2196
|
+
return this.updateRawSql(table, id, data);
|
|
2197
|
+
}
|
|
2198
|
+
const tableObj = this.getTable(table);
|
|
2199
|
+
const idColumn = this.getIdColumn(table);
|
|
2200
|
+
const result = await this.db.update(tableObj).set(data).where(drizzleOrm.eq(idColumn, id)).returning();
|
|
2201
|
+
return success(result[0]);
|
|
2202
|
+
} catch (error) {
|
|
2203
|
+
if (error instanceof errors.DatabaseError && error.message === "USE_STRING_MODE") {
|
|
2204
|
+
return this.updateRawSql(table, id, data);
|
|
2205
|
+
}
|
|
2206
|
+
return failure(
|
|
2207
|
+
new errors.DatabaseError(
|
|
2208
|
+
`Failed to update in table ${table}: ${error.message}`,
|
|
1701
2209
|
errors$1.DATABASE_ERROR_CODES.UPDATE_FAILED,
|
|
1702
2210
|
{
|
|
1703
2211
|
context: {
|
|
@@ -1709,6 +2217,35 @@ var DrizzleAdapter = class {
|
|
|
1709
2217
|
);
|
|
1710
2218
|
}
|
|
1711
2219
|
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Raw SQL fallback for update when no PgTable schema is registered.
|
|
2222
|
+
* @private
|
|
2223
|
+
*/
|
|
2224
|
+
async updateRawSql(table, id, data) {
|
|
2225
|
+
try {
|
|
2226
|
+
const tableName = this.getStringTableName(table);
|
|
2227
|
+
const idColumn = this.getStringIdColumn(table);
|
|
2228
|
+
const keys = Object.keys(data);
|
|
2229
|
+
const values = Object.values(data);
|
|
2230
|
+
const setClause = keys.map((key, i) => `"${key}" = $${i + 1}`).join(", ");
|
|
2231
|
+
const sqlQuery = `UPDATE "${tableName}" SET ${setClause} WHERE "${idColumn}" = $${keys.length + 1} RETURNING *`;
|
|
2232
|
+
const result = await this.pool.query(sqlQuery, [...values, id]);
|
|
2233
|
+
return success(result.rows[0]);
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
return failure(
|
|
2236
|
+
new errors.DatabaseError(
|
|
2237
|
+
`Failed to update in table ${table}: ${error.message}`,
|
|
2238
|
+
errors$1.DATABASE_ERROR_CODES.UPDATE_FAILED,
|
|
2239
|
+
{
|
|
2240
|
+
context: {
|
|
2241
|
+
source: "DrizzleAdapter.updateRawSql"
|
|
2242
|
+
},
|
|
2243
|
+
cause: error
|
|
2244
|
+
}
|
|
2245
|
+
)
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
1712
2249
|
/**
|
|
1713
2250
|
* Deletes a record by ID.
|
|
1714
2251
|
* @param {string} table - Table name.
|
|
@@ -1719,14 +2256,23 @@ var DrizzleAdapter = class {
|
|
|
1719
2256
|
* The method uses the registered table and ID column to construct the delete query.
|
|
1720
2257
|
* If the operation is successful, it returns a success result with no value.
|
|
1721
2258
|
* If an error occurs during the operation, a failure result with an error message is returned.
|
|
2259
|
+
*
|
|
2260
|
+
* **Auto-registers tables**: If table is not registered with PgTable schema,
|
|
2261
|
+
* falls back to raw SQL mode (same behavior as SQLAdapter).
|
|
1722
2262
|
*/
|
|
1723
2263
|
async delete(table, id) {
|
|
1724
2264
|
try {
|
|
2265
|
+
if (this.isStringMode(table)) {
|
|
2266
|
+
return this.deleteRawSql(table, id);
|
|
2267
|
+
}
|
|
1725
2268
|
const tableObj = this.getTable(table);
|
|
1726
2269
|
const idColumn = this.getIdColumn(table);
|
|
1727
2270
|
await this.db.delete(tableObj).where(drizzleOrm.eq(idColumn, id));
|
|
1728
2271
|
return success();
|
|
1729
2272
|
} catch (error) {
|
|
2273
|
+
if (error instanceof errors.DatabaseError && error.message === "USE_STRING_MODE") {
|
|
2274
|
+
return this.deleteRawSql(table, id);
|
|
2275
|
+
}
|
|
1730
2276
|
return failure(
|
|
1731
2277
|
new errors.DatabaseError(
|
|
1732
2278
|
`Failed to delete from table ${table}: ${error.message}`,
|
|
@@ -1741,6 +2287,32 @@ var DrizzleAdapter = class {
|
|
|
1741
2287
|
);
|
|
1742
2288
|
}
|
|
1743
2289
|
}
|
|
2290
|
+
/**
|
|
2291
|
+
* Raw SQL fallback for delete when no PgTable schema is registered.
|
|
2292
|
+
* @private
|
|
2293
|
+
*/
|
|
2294
|
+
async deleteRawSql(table, id) {
|
|
2295
|
+
try {
|
|
2296
|
+
const tableName = this.getStringTableName(table);
|
|
2297
|
+
const idColumn = this.getStringIdColumn(table);
|
|
2298
|
+
const sqlQuery = `DELETE FROM "${tableName}" WHERE "${idColumn}" = $1`;
|
|
2299
|
+
await this.pool.query(sqlQuery, [id]);
|
|
2300
|
+
return success();
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
return failure(
|
|
2303
|
+
new errors.DatabaseError(
|
|
2304
|
+
`Failed to delete from table ${table}: ${error.message}`,
|
|
2305
|
+
errors$1.DATABASE_ERROR_CODES.DELETE_FAILED,
|
|
2306
|
+
{
|
|
2307
|
+
context: {
|
|
2308
|
+
source: "DrizzleAdapter.deleteRawSql"
|
|
2309
|
+
},
|
|
2310
|
+
cause: error
|
|
2311
|
+
}
|
|
2312
|
+
)
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
1744
2316
|
/**
|
|
1745
2317
|
* Executes a transactional operation with rollback on failure.
|
|
1746
2318
|
* @template T - The expected type of the transaction result.
|
|
@@ -1753,6 +2325,8 @@ var DrizzleAdapter = class {
|
|
|
1753
2325
|
* is rolled back, ensuring that no partial changes are applied to the database.
|
|
1754
2326
|
* The method returns the result of the callback function if successful,
|
|
1755
2327
|
* or a failure result with an error message if an error occurs.
|
|
2328
|
+
*
|
|
2329
|
+
* **Auto-registers tables**: Transaction operations use raw SQL mode for unregistered tables.
|
|
1756
2330
|
*/
|
|
1757
2331
|
async transaction(callback) {
|
|
1758
2332
|
const client = await this.pool.connect();
|
|
@@ -1761,23 +2335,57 @@ var DrizzleAdapter = class {
|
|
|
1761
2335
|
const trxDb = nodePostgres.drizzle(client);
|
|
1762
2336
|
const trx = {
|
|
1763
2337
|
findById: /* @__PURE__ */ __name(async (table, id) => {
|
|
2338
|
+
if (this.isStringMode(table)) {
|
|
2339
|
+
const tableName = this.getStringTableName(table);
|
|
2340
|
+
const idColumn2 = this.getStringIdColumn(table);
|
|
2341
|
+
const sqlQuery = `SELECT * FROM "${tableName}" WHERE "${idColumn2}" = $1 LIMIT 1`;
|
|
2342
|
+
const result3 = await client.query(sqlQuery, [id]);
|
|
2343
|
+
return success(result3.rows[0] || null);
|
|
2344
|
+
}
|
|
1764
2345
|
const tableObj = this.getTable(table);
|
|
1765
2346
|
const idColumn = this.getIdColumn(table);
|
|
1766
2347
|
const result2 = await trxDb.select().from(tableObj).where(drizzleOrm.eq(idColumn, id)).limit(1);
|
|
1767
2348
|
return success(result2[0] || null);
|
|
1768
2349
|
}, "findById"),
|
|
1769
2350
|
create: /* @__PURE__ */ __name(async (table, data) => {
|
|
2351
|
+
if (this.isStringMode(table)) {
|
|
2352
|
+
const tableName = this.getStringTableName(table);
|
|
2353
|
+
const keys = Object.keys(data);
|
|
2354
|
+
const values = Object.values(data);
|
|
2355
|
+
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
|
2356
|
+
const escapedKeys = keys.map((k) => `"${k}"`).join(", ");
|
|
2357
|
+
const sqlQuery = `INSERT INTO "${tableName}" (${escapedKeys}) VALUES (${placeholders}) RETURNING *`;
|
|
2358
|
+
const result3 = await client.query(sqlQuery, values);
|
|
2359
|
+
return success(result3.rows[0]);
|
|
2360
|
+
}
|
|
1770
2361
|
const tableObj = this.getTable(table);
|
|
1771
2362
|
const result2 = await trxDb.insert(tableObj).values(data).returning();
|
|
1772
2363
|
return success(result2[0]);
|
|
1773
2364
|
}, "create"),
|
|
1774
2365
|
update: /* @__PURE__ */ __name(async (table, id, data) => {
|
|
2366
|
+
if (this.isStringMode(table)) {
|
|
2367
|
+
const tableName = this.getStringTableName(table);
|
|
2368
|
+
const idColumn2 = this.getStringIdColumn(table);
|
|
2369
|
+
const keys = Object.keys(data);
|
|
2370
|
+
const values = Object.values(data);
|
|
2371
|
+
const setClause = keys.map((key, i) => `"${key}" = $${i + 1}`).join(", ");
|
|
2372
|
+
const sqlQuery = `UPDATE "${tableName}" SET ${setClause} WHERE "${idColumn2}" = $${keys.length + 1} RETURNING *`;
|
|
2373
|
+
const result3 = await client.query(sqlQuery, [...values, id]);
|
|
2374
|
+
return success(result3.rows[0]);
|
|
2375
|
+
}
|
|
1775
2376
|
const tableObj = this.getTable(table);
|
|
1776
2377
|
const idColumn = this.getIdColumn(table);
|
|
1777
2378
|
const result2 = await trxDb.update(tableObj).set(data).where(drizzleOrm.eq(idColumn, id)).returning();
|
|
1778
2379
|
return success(result2[0]);
|
|
1779
2380
|
}, "update"),
|
|
1780
2381
|
delete: /* @__PURE__ */ __name(async (table, id) => {
|
|
2382
|
+
if (this.isStringMode(table)) {
|
|
2383
|
+
const tableName = this.getStringTableName(table);
|
|
2384
|
+
const idColumn2 = this.getStringIdColumn(table);
|
|
2385
|
+
const sqlQuery = `DELETE FROM "${tableName}" WHERE "${idColumn2}" = $1`;
|
|
2386
|
+
await client.query(sqlQuery, [id]);
|
|
2387
|
+
return success();
|
|
2388
|
+
}
|
|
1781
2389
|
const tableObj = this.getTable(table);
|
|
1782
2390
|
const idColumn = this.getIdColumn(table);
|
|
1783
2391
|
await trxDb.delete(tableObj).where(drizzleOrm.eq(idColumn, id));
|
|
@@ -1821,14 +2429,31 @@ var DrizzleAdapter = class {
|
|
|
1821
2429
|
* The method uses the registered table and ID column to construct the query.
|
|
1822
2430
|
* It returns a success result with a boolean value indicating whether the record exists.
|
|
1823
2431
|
* If an error occurs during the operation, a failure result with an error message is returned.
|
|
2432
|
+
*
|
|
2433
|
+
* **Auto-registers tables**: If table is not registered with PgTable schema,
|
|
2434
|
+
* falls back to raw SQL mode (same behavior as SQLAdapter).
|
|
1824
2435
|
*/
|
|
1825
2436
|
async exists(table, id) {
|
|
1826
2437
|
try {
|
|
2438
|
+
if (this.isStringMode(table)) {
|
|
2439
|
+
const tableName = this.getStringTableName(table);
|
|
2440
|
+
const idColumn2 = this.getStringIdColumn(table);
|
|
2441
|
+
const sqlQuery = `SELECT 1 FROM "${tableName}" WHERE "${idColumn2}" = $1 LIMIT 1`;
|
|
2442
|
+
const result2 = await this.pool.query(sqlQuery, [id]);
|
|
2443
|
+
return success(result2.rows.length > 0);
|
|
2444
|
+
}
|
|
1827
2445
|
const tableObj = this.getTable(table);
|
|
1828
2446
|
const idColumn = this.getIdColumn(table);
|
|
1829
2447
|
const result = await this.db.select({ exists: drizzleOrm.sql`1` }).from(tableObj).where(drizzleOrm.eq(idColumn, id)).limit(1);
|
|
1830
2448
|
return success(!!result.length);
|
|
1831
2449
|
} catch (error) {
|
|
2450
|
+
if (error instanceof errors.DatabaseError && error.message === "USE_STRING_MODE") {
|
|
2451
|
+
const tableName = this.getStringTableName(table);
|
|
2452
|
+
const idColumn = this.getStringIdColumn(table);
|
|
2453
|
+
const sqlQuery = `SELECT 1 FROM "${tableName}" WHERE "${idColumn}" = $1 LIMIT 1`;
|
|
2454
|
+
const result = await this.pool.query(sqlQuery, [id]);
|
|
2455
|
+
return success(result.rows.length > 0);
|
|
2456
|
+
}
|
|
1832
2457
|
return failure(
|
|
1833
2458
|
new errors.DatabaseError(
|
|
1834
2459
|
`Failed to check existence in table ${table}: ${error.message}`,
|
|
@@ -1854,9 +2479,15 @@ var DrizzleAdapter = class {
|
|
|
1854
2479
|
* If a filter is provided, it is applied to narrow down the count to matching records.
|
|
1855
2480
|
* It returns a success result with the count of matching records.
|
|
1856
2481
|
* If an error occurs during the operation, a failure result with an error message is returned.
|
|
2482
|
+
*
|
|
2483
|
+
* **Auto-registers tables**: If table is not registered with PgTable schema,
|
|
2484
|
+
* falls back to raw SQL mode (same behavior as SQLAdapter).
|
|
1857
2485
|
*/
|
|
1858
2486
|
async count(table, filter) {
|
|
1859
2487
|
try {
|
|
2488
|
+
if (this.isStringMode(table)) {
|
|
2489
|
+
return this.countRawSql(table, filter);
|
|
2490
|
+
}
|
|
1860
2491
|
const tableObj = this.getTable(table);
|
|
1861
2492
|
const baseQuery = this.db.select({ count: drizzleOrm.sql`count(*)::int` }).from(tableObj);
|
|
1862
2493
|
const query = filter && this.buildWhereClause(filter, tableObj) ? baseQuery.where(this.buildWhereClause(filter, tableObj)) : baseQuery;
|
|
@@ -1864,6 +2495,9 @@ var DrizzleAdapter = class {
|
|
|
1864
2495
|
const countValue = result.length > 0 ? result[0].count : 0;
|
|
1865
2496
|
return success(Number(countValue));
|
|
1866
2497
|
} catch (error) {
|
|
2498
|
+
if (error instanceof errors.DatabaseError && error.message === "USE_STRING_MODE") {
|
|
2499
|
+
return this.countRawSql(table, filter);
|
|
2500
|
+
}
|
|
1867
2501
|
return failure(
|
|
1868
2502
|
new errors.DatabaseError(
|
|
1869
2503
|
`Failed to count in table ${table}: ${error.message}`,
|
|
@@ -1878,6 +2512,44 @@ var DrizzleAdapter = class {
|
|
|
1878
2512
|
);
|
|
1879
2513
|
}
|
|
1880
2514
|
}
|
|
2515
|
+
/**
|
|
2516
|
+
* Raw SQL fallback for count when no PgTable schema is registered.
|
|
2517
|
+
* @private
|
|
2518
|
+
*/
|
|
2519
|
+
async countRawSql(table, filter) {
|
|
2520
|
+
try {
|
|
2521
|
+
const tableName = this.getStringTableName(table);
|
|
2522
|
+
const params = [];
|
|
2523
|
+
let whereClause = "";
|
|
2524
|
+
if (filter) {
|
|
2525
|
+
const { field, operator, value } = filter;
|
|
2526
|
+
whereClause = this.buildSqlWhereClause({
|
|
2527
|
+
field,
|
|
2528
|
+
operator,
|
|
2529
|
+
value,
|
|
2530
|
+
params,
|
|
2531
|
+
startIndex: 1
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
const sqlQuery = `SELECT COUNT(*) as count FROM "${tableName}"${whereClause}`;
|
|
2535
|
+
const result = await this.pool.query(sqlQuery, params);
|
|
2536
|
+
const rowCount = Number.parseInt(result.rows[0].count);
|
|
2537
|
+
return success(Number(rowCount));
|
|
2538
|
+
} catch (error) {
|
|
2539
|
+
return failure(
|
|
2540
|
+
new errors.DatabaseError(
|
|
2541
|
+
`Failed to count in table ${table}: ${error.message}`,
|
|
2542
|
+
errors$1.DATABASE_ERROR_CODES.COUNT_FAILED,
|
|
2543
|
+
{
|
|
2544
|
+
context: {
|
|
2545
|
+
source: "DrizzleAdapter.countRawSql"
|
|
2546
|
+
},
|
|
2547
|
+
cause: error
|
|
2548
|
+
}
|
|
2549
|
+
)
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
1881
2553
|
/**
|
|
1882
2554
|
* Performs a health check on the database connection.
|
|
1883
2555
|
* @returns {Promise<DatabaseResult<DatabaseHealthStatus>>} Health status with response time.
|
|
@@ -1911,16 +2583,25 @@ var DrizzleAdapter = class {
|
|
|
1911
2583
|
}
|
|
1912
2584
|
}
|
|
1913
2585
|
// --- Private Helpers ---
|
|
2586
|
+
/**
|
|
2587
|
+
* Checks if a table is registered in string mode (raw SQL fallback).
|
|
2588
|
+
* @private
|
|
2589
|
+
* @param {string} name - Table name.
|
|
2590
|
+
* @returns {boolean} True if table is registered in string mode.
|
|
2591
|
+
*/
|
|
2592
|
+
isStringMode(name) {
|
|
2593
|
+
return this.stringTableMap.has(name) || !this.tableMap.has(name);
|
|
2594
|
+
}
|
|
1914
2595
|
/**
|
|
1915
2596
|
* Retrieves a registered table by name.
|
|
1916
2597
|
* @private
|
|
1917
2598
|
* @param {string} name - Table name.
|
|
1918
2599
|
* @returns {PgTable} Table object.
|
|
1919
|
-
* @throws {DatabaseError} If table is not registered.
|
|
2600
|
+
* @throws {DatabaseError} If table is not registered and cannot be auto-registered.
|
|
1920
2601
|
* @description
|
|
1921
2602
|
* Retrieves a table object that has been previously registered with the adapter.
|
|
1922
|
-
*
|
|
1923
|
-
*
|
|
2603
|
+
* If the table is not found in PgTable mode, it auto-registers in string mode
|
|
2604
|
+
* for raw SQL operations (same behavior as SQLAdapter).
|
|
1924
2605
|
*/
|
|
1925
2606
|
getTable(name) {
|
|
1926
2607
|
try {
|
|
@@ -1932,8 +2613,11 @@ var DrizzleAdapter = class {
|
|
|
1932
2613
|
}
|
|
1933
2614
|
const table = this.tableMap.get(name);
|
|
1934
2615
|
if (!table) {
|
|
2616
|
+
if (!this.stringTableMap.has(name)) {
|
|
2617
|
+
this.stringTableMap.set(name, name);
|
|
2618
|
+
}
|
|
1935
2619
|
throw new errors.DatabaseError(
|
|
1936
|
-
"
|
|
2620
|
+
"USE_STRING_MODE",
|
|
1937
2621
|
errors$1.DATABASE_ERROR_CODES.TABLE_NOT_REGISTERED
|
|
1938
2622
|
);
|
|
1939
2623
|
}
|
|
@@ -1945,6 +2629,46 @@ var DrizzleAdapter = class {
|
|
|
1945
2629
|
);
|
|
1946
2630
|
}
|
|
1947
2631
|
}
|
|
2632
|
+
/**
|
|
2633
|
+
* Gets the string table name for raw SQL operations.
|
|
2634
|
+
* @private
|
|
2635
|
+
* @param {string} name - Logical table name.
|
|
2636
|
+
* @returns {string} Physical table name.
|
|
2637
|
+
*/
|
|
2638
|
+
getStringTableName(name) {
|
|
2639
|
+
let tableName = this.stringTableMap.get(name);
|
|
2640
|
+
if (!tableName) {
|
|
2641
|
+
const customIdColumn = this.configIdColumns[name];
|
|
2642
|
+
if (customIdColumn) {
|
|
2643
|
+
this.stringIdColumnMap.set(name, customIdColumn);
|
|
2644
|
+
}
|
|
2645
|
+
this.stringTableMap.set(name, name);
|
|
2646
|
+
tableName = name;
|
|
2647
|
+
}
|
|
2648
|
+
return tableName;
|
|
2649
|
+
}
|
|
2650
|
+
/**
|
|
2651
|
+
* Gets the string ID column for raw SQL operations.
|
|
2652
|
+
* @private
|
|
2653
|
+
* @param {string} name - Logical table name.
|
|
2654
|
+
* @returns {string} ID column name (defaults to 'id').
|
|
2655
|
+
* @description
|
|
2656
|
+
* Retrieves the ID column for a table. Checks in this order:
|
|
2657
|
+
* 1. Runtime registered ID column (from registerTable calls)
|
|
2658
|
+
* 2. Config-provided ID column (from tableIdColumns in config)
|
|
2659
|
+
* 3. Default 'id' column
|
|
2660
|
+
*/
|
|
2661
|
+
getStringIdColumn(name) {
|
|
2662
|
+
const runtimeIdColumn = this.stringIdColumnMap.get(name);
|
|
2663
|
+
if (runtimeIdColumn) {
|
|
2664
|
+
return runtimeIdColumn;
|
|
2665
|
+
}
|
|
2666
|
+
const configIdColumn = this.configIdColumns[name];
|
|
2667
|
+
if (configIdColumn) {
|
|
2668
|
+
return configIdColumn;
|
|
2669
|
+
}
|
|
2670
|
+
return "id";
|
|
2671
|
+
}
|
|
1948
2672
|
/**
|
|
1949
2673
|
* Retrieves the registered ID column for a table.
|
|
1950
2674
|
* @private
|
|
@@ -2033,7 +2757,11 @@ var DrizzleAdapter = class {
|
|
|
2033
2757
|
case "like":
|
|
2034
2758
|
return drizzleOrm.like(column, value);
|
|
2035
2759
|
case "between":
|
|
2036
|
-
return drizzleOrm.between(
|
|
2760
|
+
return drizzleOrm.between(
|
|
2761
|
+
column,
|
|
2762
|
+
value[0],
|
|
2763
|
+
value[1]
|
|
2764
|
+
);
|
|
2037
2765
|
case "isNull":
|
|
2038
2766
|
return drizzleOrm.isNull(column);
|
|
2039
2767
|
case "isNotNull":
|
|
@@ -2101,6 +2829,7 @@ var SupabaseAdapter = class {
|
|
|
2101
2829
|
config;
|
|
2102
2830
|
tableMap = /* @__PURE__ */ new Map();
|
|
2103
2831
|
idColumnMap = /* @__PURE__ */ new Map();
|
|
2832
|
+
configIdColumns;
|
|
2104
2833
|
/**
|
|
2105
2834
|
* Creates a new SupabaseAdapter instance.
|
|
2106
2835
|
* @param {SupabaseAdapterConfig} config - Configuration for the Supabase adapter.
|
|
@@ -2114,6 +2843,7 @@ var SupabaseAdapter = class {
|
|
|
2114
2843
|
*/
|
|
2115
2844
|
constructor(config) {
|
|
2116
2845
|
this.config = config;
|
|
2846
|
+
this.configIdColumns = config.tableIdColumns ?? {};
|
|
2117
2847
|
if (!config.supabaseUrl) {
|
|
2118
2848
|
throw new errors.DatabaseError(
|
|
2119
2849
|
"Supabase URL is required for Supabase adapter",
|
|
@@ -2131,6 +2861,9 @@ var SupabaseAdapter = class {
|
|
|
2131
2861
|
auth: {
|
|
2132
2862
|
persistSession: false,
|
|
2133
2863
|
autoRefreshToken: false
|
|
2864
|
+
},
|
|
2865
|
+
db: {
|
|
2866
|
+
schema: config.schema ?? "public"
|
|
2134
2867
|
}
|
|
2135
2868
|
});
|
|
2136
2869
|
}
|
|
@@ -2202,6 +2935,15 @@ var SupabaseAdapter = class {
|
|
|
2202
2935
|
*/
|
|
2203
2936
|
async disconnect() {
|
|
2204
2937
|
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Closes the database connection and cleanup resources.
|
|
2940
|
+
* Supabase handles connections automatically, so this just returns success.
|
|
2941
|
+
* @returns Promise resolving to DatabaseResult indicating success.
|
|
2942
|
+
*/
|
|
2943
|
+
async close() {
|
|
2944
|
+
await this.disconnect();
|
|
2945
|
+
return success();
|
|
2946
|
+
}
|
|
2205
2947
|
/**
|
|
2206
2948
|
* Gets the underlying Supabase client instance.
|
|
2207
2949
|
* @template TClient - The type of the Supabase client to return.
|
|
@@ -2769,7 +3511,9 @@ var SupabaseAdapter = class {
|
|
|
2769
3511
|
getTableName(name) {
|
|
2770
3512
|
let tableName = this.tableMap.get(name);
|
|
2771
3513
|
if (!tableName) {
|
|
2772
|
-
this.
|
|
3514
|
+
const hasRuntimeIdColumn = this.idColumnMap.has(name);
|
|
3515
|
+
const customIdColumn = hasRuntimeIdColumn ? void 0 : this.configIdColumns[name];
|
|
3516
|
+
this.registerTable(name, name, customIdColumn);
|
|
2773
3517
|
tableName = name;
|
|
2774
3518
|
}
|
|
2775
3519
|
return tableName;
|
|
@@ -2877,6 +3621,7 @@ var SupabaseAdapter = class {
|
|
|
2877
3621
|
return ops[operator]();
|
|
2878
3622
|
}
|
|
2879
3623
|
};
|
|
3624
|
+
var SQL_ERROR_TRUNCATE_LENGTH = 500;
|
|
2880
3625
|
var SQLAdapter = class {
|
|
2881
3626
|
static {
|
|
2882
3627
|
__name(this, "SQLAdapter");
|
|
@@ -2885,6 +3630,9 @@ var SQLAdapter = class {
|
|
|
2885
3630
|
config;
|
|
2886
3631
|
tableMap = /* @__PURE__ */ new Map();
|
|
2887
3632
|
idColumnMap = /* @__PURE__ */ new Map();
|
|
3633
|
+
configIdColumns;
|
|
3634
|
+
defaultSchema;
|
|
3635
|
+
showSqlInErrors;
|
|
2888
3636
|
/**
|
|
2889
3637
|
* Creates a new SQLAdapter instance.
|
|
2890
3638
|
* @param {SQLAdapterConfig} config - Configuration for the SQL adapter.
|
|
@@ -2897,10 +3645,23 @@ var SQLAdapter = class {
|
|
|
2897
3645
|
*/
|
|
2898
3646
|
constructor(config) {
|
|
2899
3647
|
this.config = config;
|
|
3648
|
+
this.defaultSchema = config.schema ?? "public";
|
|
3649
|
+
this.showSqlInErrors = config.showSqlInErrors ?? true;
|
|
2900
3650
|
this.pool = new pg.Pool({
|
|
2901
3651
|
connectionString: config.connectionString,
|
|
2902
3652
|
...config.pool
|
|
2903
3653
|
});
|
|
3654
|
+
this.configIdColumns = config.tableIdColumns ?? {};
|
|
3655
|
+
}
|
|
3656
|
+
/**
|
|
3657
|
+
* Get fully-qualified table name with schema
|
|
3658
|
+
*/
|
|
3659
|
+
getQualifiedTableName(table, schema) {
|
|
3660
|
+
const targetSchema = schema ?? this.defaultSchema;
|
|
3661
|
+
if (table.includes(".")) {
|
|
3662
|
+
return table;
|
|
3663
|
+
}
|
|
3664
|
+
return `${targetSchema}.${table}`;
|
|
2904
3665
|
}
|
|
2905
3666
|
/**
|
|
2906
3667
|
* Initialize the adapter.
|
|
@@ -2914,7 +3675,11 @@ var SQLAdapter = class {
|
|
|
2914
3675
|
*/
|
|
2915
3676
|
async initialize() {
|
|
2916
3677
|
try {
|
|
2917
|
-
await this.pool.connect();
|
|
3678
|
+
const client = await this.pool.connect();
|
|
3679
|
+
if (this.defaultSchema && this.defaultSchema !== "public") {
|
|
3680
|
+
await client.query(`SET search_path TO ${this.defaultSchema}, public`);
|
|
3681
|
+
}
|
|
3682
|
+
client.release();
|
|
2918
3683
|
return success();
|
|
2919
3684
|
} catch (error) {
|
|
2920
3685
|
return failure(
|
|
@@ -2953,6 +3718,23 @@ var SQLAdapter = class {
|
|
|
2953
3718
|
async disconnect() {
|
|
2954
3719
|
await this.pool.end();
|
|
2955
3720
|
}
|
|
3721
|
+
/**
|
|
3722
|
+
* Closes the database connection and cleanup resources.
|
|
3723
|
+
* @returns Promise resolving to DatabaseResult indicating success or failure.
|
|
3724
|
+
*/
|
|
3725
|
+
async close() {
|
|
3726
|
+
try {
|
|
3727
|
+
await this.disconnect();
|
|
3728
|
+
return success();
|
|
3729
|
+
} catch (error) {
|
|
3730
|
+
return failure(
|
|
3731
|
+
new errors.DatabaseError(
|
|
3732
|
+
`Failed to close connection: ${error.message}`,
|
|
3733
|
+
errors$1.DATABASE_ERROR_CODES.DISCONNECT_FAILED
|
|
3734
|
+
)
|
|
3735
|
+
);
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
2956
3738
|
/**
|
|
2957
3739
|
* Gets the underlying PostgreSQL client instance.
|
|
2958
3740
|
* @template TClient - The type of the database client to return.
|
|
@@ -2983,16 +3765,16 @@ var SQLAdapter = class {
|
|
|
2983
3765
|
const result = await this.pool.query(sql2, params);
|
|
2984
3766
|
return result.rows;
|
|
2985
3767
|
} catch (error) {
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
);
|
|
3768
|
+
const truncatedSql = sql2.slice(0, SQL_ERROR_TRUNCATE_LENGTH);
|
|
3769
|
+
const sqlSuffix = sql2.length > SQL_ERROR_TRUNCATE_LENGTH ? "..." : "";
|
|
3770
|
+
const errorMessage = this.showSqlInErrors ? `SQL Error: ${error.message}
|
|
3771
|
+
Query: ${truncatedSql}${sqlSuffix}` : `SQL Error: ${error.message}`;
|
|
3772
|
+
throw new errors.DatabaseError(errorMessage, errors$1.DATABASE_ERROR_CODES.QUERY_FAILED, {
|
|
3773
|
+
context: {
|
|
3774
|
+
source: "SQLAdapter.query"
|
|
3775
|
+
},
|
|
3776
|
+
cause: error
|
|
3777
|
+
});
|
|
2996
3778
|
}
|
|
2997
3779
|
}
|
|
2998
3780
|
/**
|
|
@@ -3033,8 +3815,9 @@ var SQLAdapter = class {
|
|
|
3033
3815
|
const validationError = this.validateBasicParams(table, id);
|
|
3034
3816
|
if (validationError) return failure(validationError);
|
|
3035
3817
|
const tableName = this.getTableName(table);
|
|
3036
|
-
const
|
|
3037
|
-
const
|
|
3818
|
+
const qualifiedTable = this.getQualifiedTableName(tableName);
|
|
3819
|
+
const idColumn = this.getIdColumn(table);
|
|
3820
|
+
const sql2 = `SELECT * FROM ${qualifiedTable} WHERE "${idColumn}" = $1`;
|
|
3038
3821
|
const result = await this.pool.query(sql2, [id]);
|
|
3039
3822
|
if (!result?.rows) {
|
|
3040
3823
|
return failure(
|
|
@@ -3097,6 +3880,7 @@ var SQLAdapter = class {
|
|
|
3097
3880
|
async findMany(table, options) {
|
|
3098
3881
|
try {
|
|
3099
3882
|
const tableName = this.getTableName(table);
|
|
3883
|
+
const qualifiedTable = this.getQualifiedTableName(tableName);
|
|
3100
3884
|
const params = [];
|
|
3101
3885
|
let whereClause = "";
|
|
3102
3886
|
let paramIndex = 1;
|
|
@@ -3104,7 +3888,7 @@ var SQLAdapter = class {
|
|
|
3104
3888
|
whereClause = this.buildWhereClause(options.filter, params, paramIndex);
|
|
3105
3889
|
paramIndex += params.length;
|
|
3106
3890
|
}
|
|
3107
|
-
const countSql = `SELECT COUNT(*) as total FROM ${
|
|
3891
|
+
const countSql = `SELECT COUNT(*) as total FROM ${qualifiedTable}${whereClause}`;
|
|
3108
3892
|
const countResult = await this.pool.query(countSql, params);
|
|
3109
3893
|
if (!countResult.rows || countResult.rows.length === 0) {
|
|
3110
3894
|
throw new errors.DatabaseError(
|
|
@@ -3132,7 +3916,7 @@ var SQLAdapter = class {
|
|
|
3132
3916
|
limitClause += ` OFFSET $${paramIndex++}`;
|
|
3133
3917
|
params.push(options.pagination.offset);
|
|
3134
3918
|
}
|
|
3135
|
-
const sql2 = `SELECT * FROM ${
|
|
3919
|
+
const sql2 = `SELECT * FROM ${qualifiedTable}${whereClause}${orderClause}${limitClause}`;
|
|
3136
3920
|
const result = await this.pool.query(sql2, params);
|
|
3137
3921
|
return success({
|
|
3138
3922
|
data: result.rows,
|
|
@@ -3172,11 +3956,12 @@ var SQLAdapter = class {
|
|
|
3172
3956
|
const validationError = this.validateCreateParams(table, data);
|
|
3173
3957
|
if (validationError) return failure(validationError);
|
|
3174
3958
|
const tableName = this.getTableName(table);
|
|
3959
|
+
const qualifiedTable = this.getQualifiedTableName(tableName);
|
|
3175
3960
|
const keys = Object.keys(data);
|
|
3176
3961
|
const values = Object.values(data);
|
|
3177
3962
|
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
|
3178
3963
|
const escapedKeys = keys.map((k) => `"${k}"`).join(", ");
|
|
3179
|
-
const sql2 = `INSERT INTO
|
|
3964
|
+
const sql2 = `INSERT INTO ${qualifiedTable} (${escapedKeys}) VALUES (${placeholders}) RETURNING *`;
|
|
3180
3965
|
const result = await this.pool.query(sql2, values);
|
|
3181
3966
|
if (!result?.rows?.length) {
|
|
3182
3967
|
return failure(
|
|
@@ -3222,11 +4007,12 @@ var SQLAdapter = class {
|
|
|
3222
4007
|
const validationError = this.validateUpdateParams(table, id, data);
|
|
3223
4008
|
if (validationError) return failure(validationError);
|
|
3224
4009
|
const tableName = this.getTableName(table);
|
|
4010
|
+
const qualifiedTable = this.getQualifiedTableName(tableName);
|
|
3225
4011
|
const keys = Object.keys(data);
|
|
3226
4012
|
const values = Object.values(data);
|
|
3227
4013
|
const setClause = keys.map((key, i) => `"${key}" = $${i + 1}`).join(", ");
|
|
3228
|
-
const idColumn = this.
|
|
3229
|
-
const sql2 = `UPDATE
|
|
4014
|
+
const idColumn = this.getIdColumn(table);
|
|
4015
|
+
const sql2 = `UPDATE ${qualifiedTable} SET ${setClause} WHERE "${idColumn}" = $${keys.length + 1} RETURNING *`;
|
|
3230
4016
|
const result = await this.pool.query(sql2, [...values, id]);
|
|
3231
4017
|
if (!result.rows?.length) {
|
|
3232
4018
|
return failure(
|
|
@@ -3274,8 +4060,9 @@ var SQLAdapter = class {
|
|
|
3274
4060
|
);
|
|
3275
4061
|
}
|
|
3276
4062
|
const tableName = this.getTableName(table);
|
|
3277
|
-
const
|
|
3278
|
-
const
|
|
4063
|
+
const qualifiedTable = this.getQualifiedTableName(tableName);
|
|
4064
|
+
const idColumn = this.getIdColumn(table);
|
|
4065
|
+
const sql2 = `DELETE FROM ${qualifiedTable} WHERE "${idColumn}" = $1`;
|
|
3279
4066
|
const result = await this.pool.query(sql2, [id]);
|
|
3280
4067
|
if (!result) {
|
|
3281
4068
|
return failure(
|
|
@@ -3323,34 +4110,39 @@ var SQLAdapter = class {
|
|
|
3323
4110
|
const trx = {
|
|
3324
4111
|
findById: /* @__PURE__ */ __name(async (table, id) => {
|
|
3325
4112
|
const tableName = this.getTableName(table);
|
|
3326
|
-
const
|
|
3327
|
-
const
|
|
4113
|
+
const qualifiedTable = this.getQualifiedTableName(tableName);
|
|
4114
|
+
const idColumn = this.getIdColumn(table);
|
|
4115
|
+
const sql2 = `SELECT * FROM ${qualifiedTable} WHERE "${idColumn}" = $1`;
|
|
3328
4116
|
const result2 = await client.query(sql2, [id]);
|
|
3329
4117
|
return success(result2.rows[0] ?? null);
|
|
3330
4118
|
}, "findById"),
|
|
3331
4119
|
create: /* @__PURE__ */ __name(async (table, data) => {
|
|
3332
4120
|
const tableName = this.getTableName(table);
|
|
4121
|
+
const qualifiedTable = this.getQualifiedTableName(tableName);
|
|
3333
4122
|
const keys = Object.keys(data);
|
|
3334
4123
|
const values = Object.values(data);
|
|
3335
4124
|
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
|
3336
|
-
const
|
|
4125
|
+
const escapedKeys = keys.map((k) => `"${k}"`).join(", ");
|
|
4126
|
+
const sql2 = `INSERT INTO ${qualifiedTable} (${escapedKeys}) VALUES (${placeholders}) RETURNING *`;
|
|
3337
4127
|
const result2 = await client.query(sql2, values);
|
|
3338
4128
|
return success(result2.rows[0]);
|
|
3339
4129
|
}, "create"),
|
|
3340
4130
|
update: /* @__PURE__ */ __name(async (table, id, data) => {
|
|
3341
4131
|
const tableName = this.getTableName(table);
|
|
4132
|
+
const qualifiedTable = this.getQualifiedTableName(tableName);
|
|
3342
4133
|
const keys = Object.keys(data);
|
|
3343
4134
|
const values = Object.values(data);
|
|
3344
|
-
const setClause = keys.map((key, i) =>
|
|
3345
|
-
const idColumn = this.
|
|
3346
|
-
const sql2 = `UPDATE ${
|
|
4135
|
+
const setClause = keys.map((key, i) => `"${key}" = $${i + 1}`).join(", ");
|
|
4136
|
+
const idColumn = this.getIdColumn(table);
|
|
4137
|
+
const sql2 = `UPDATE ${qualifiedTable} SET ${setClause} WHERE "${idColumn}" = $${keys.length + 1} RETURNING *`;
|
|
3347
4138
|
const result2 = await client.query(sql2, [...values, id]);
|
|
3348
4139
|
return success(result2.rows[0]);
|
|
3349
4140
|
}, "update"),
|
|
3350
4141
|
delete: /* @__PURE__ */ __name(async (table, id) => {
|
|
3351
4142
|
const tableName = this.getTableName(table);
|
|
3352
|
-
const
|
|
3353
|
-
const
|
|
4143
|
+
const qualifiedTable = this.getQualifiedTableName(tableName);
|
|
4144
|
+
const idColumn = this.getIdColumn(table);
|
|
4145
|
+
const sql2 = `DELETE FROM ${qualifiedTable} WHERE "${idColumn}" = $1`;
|
|
3354
4146
|
await client.query(sql2, [id]);
|
|
3355
4147
|
return success();
|
|
3356
4148
|
}, "delete"),
|
|
@@ -3397,8 +4189,9 @@ var SQLAdapter = class {
|
|
|
3397
4189
|
async exists(table, id) {
|
|
3398
4190
|
try {
|
|
3399
4191
|
const tableName = this.getTableName(table);
|
|
3400
|
-
const
|
|
3401
|
-
const
|
|
4192
|
+
const qualifiedTable = this.getQualifiedTableName(tableName);
|
|
4193
|
+
const idColumn = this.getIdColumn(table);
|
|
4194
|
+
const sql2 = `SELECT 1 FROM ${qualifiedTable} WHERE "${idColumn}" = $1 LIMIT 1`;
|
|
3402
4195
|
const result = await this.pool.query(sql2, [id]);
|
|
3403
4196
|
return success(result.rows.length > 0);
|
|
3404
4197
|
} catch (error) {
|
|
@@ -3430,12 +4223,13 @@ var SQLAdapter = class {
|
|
|
3430
4223
|
async count(table, filter) {
|
|
3431
4224
|
try {
|
|
3432
4225
|
const tableName = this.getTableName(table);
|
|
4226
|
+
const qualifiedTable = this.getQualifiedTableName(tableName);
|
|
3433
4227
|
let whereClause = "";
|
|
3434
4228
|
let params = [];
|
|
3435
4229
|
if (filter) {
|
|
3436
4230
|
whereClause = this.buildWhereClause(filter, params, 1);
|
|
3437
4231
|
}
|
|
3438
|
-
const sql2 = `SELECT COUNT(*) as count FROM ${
|
|
4232
|
+
const sql2 = `SELECT COUNT(*) as count FROM ${qualifiedTable}${whereClause}`;
|
|
3439
4233
|
const result = await this.pool.query(sql2, params);
|
|
3440
4234
|
const rowCount = Number.parseInt(result.rows[0].count);
|
|
3441
4235
|
return success(Number(rowCount));
|
|
@@ -3488,22 +4282,43 @@ var SQLAdapter = class {
|
|
|
3488
4282
|
* @private
|
|
3489
4283
|
* @param {string} name - Logical table name.
|
|
3490
4284
|
* @returns {string} Actual table name.
|
|
3491
|
-
* @throws {DatabaseError} If table is not registered.
|
|
3492
4285
|
* @description
|
|
3493
|
-
* Retrieves the actual table name
|
|
3494
|
-
*
|
|
3495
|
-
*
|
|
4286
|
+
* Retrieves the actual table name. If not registered, auto-registers it with
|
|
4287
|
+
* the same logical name as the physical table name. This enables seamless
|
|
4288
|
+
* table operations without manual registration (matching Supabase adapter behavior).
|
|
3496
4289
|
*/
|
|
3497
4290
|
getTableName(name) {
|
|
3498
|
-
|
|
4291
|
+
let tableName = this.tableMap.get(name);
|
|
3499
4292
|
if (!tableName) {
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
4293
|
+
const hasRuntimeIdColumn = this.idColumnMap.has(name);
|
|
4294
|
+
const customIdColumn = hasRuntimeIdColumn ? void 0 : this.configIdColumns[name];
|
|
4295
|
+
this.registerTable(name, name, customIdColumn);
|
|
4296
|
+
tableName = name;
|
|
3504
4297
|
}
|
|
3505
4298
|
return tableName;
|
|
3506
4299
|
}
|
|
4300
|
+
/**
|
|
4301
|
+
* Get the ID column for a table.
|
|
4302
|
+
* @private
|
|
4303
|
+
* @param {string} table - Logical table name.
|
|
4304
|
+
* @returns {string} ID column name (defaults to 'id').
|
|
4305
|
+
* @description
|
|
4306
|
+
* Retrieves the ID column for a table. Checks in this order:
|
|
4307
|
+
* 1. Runtime registered ID column (from registerTable calls)
|
|
4308
|
+
* 2. Config-provided ID column (from tableIdColumns in config)
|
|
4309
|
+
* 3. Default 'id' column
|
|
4310
|
+
*/
|
|
4311
|
+
getIdColumn(table) {
|
|
4312
|
+
const runtimeIdColumn = this.idColumnMap.get(table);
|
|
4313
|
+
if (runtimeIdColumn) {
|
|
4314
|
+
return runtimeIdColumn;
|
|
4315
|
+
}
|
|
4316
|
+
const configIdColumn = this.configIdColumns[table];
|
|
4317
|
+
if (configIdColumn) {
|
|
4318
|
+
return configIdColumn;
|
|
4319
|
+
}
|
|
4320
|
+
return "id";
|
|
4321
|
+
}
|
|
3507
4322
|
validateBasicParams(table, id) {
|
|
3508
4323
|
if (!table || !id) {
|
|
3509
4324
|
return new errors.DatabaseError(
|
|
@@ -3661,22 +4476,435 @@ var SQLAdapter = class {
|
|
|
3661
4476
|
return clause;
|
|
3662
4477
|
}
|
|
3663
4478
|
};
|
|
3664
|
-
var
|
|
4479
|
+
var DEFAULT_PAGINATION_LIMIT = 50;
|
|
4480
|
+
var RANDOM_STRING_BASE = 36;
|
|
4481
|
+
var ID_SUBSTRING_START = 2;
|
|
4482
|
+
var ID_SUBSTRING_END = 9;
|
|
4483
|
+
var BETWEEN_VALUES_LENGTH = 2;
|
|
4484
|
+
var FILTER_OPERATORS = {
|
|
4485
|
+
eq: /* @__PURE__ */ __name((fieldValue, value) => fieldValue === value, "eq"),
|
|
4486
|
+
ne: /* @__PURE__ */ __name((fieldValue, value) => fieldValue !== value, "ne"),
|
|
4487
|
+
gt: /* @__PURE__ */ __name((fieldValue, value) => fieldValue > value, "gt"),
|
|
4488
|
+
gte: /* @__PURE__ */ __name((fieldValue, value) => fieldValue >= value, "gte"),
|
|
4489
|
+
lt: /* @__PURE__ */ __name((fieldValue, value) => fieldValue < value, "lt"),
|
|
4490
|
+
lte: /* @__PURE__ */ __name((fieldValue, value) => fieldValue <= value, "lte"),
|
|
4491
|
+
in: /* @__PURE__ */ __name((fieldValue, value) => Array.isArray(value) && value.includes(fieldValue), "in"),
|
|
4492
|
+
like: /* @__PURE__ */ __name((fieldValue, value) => String(fieldValue).toLowerCase().includes(String(value).toLowerCase()), "like"),
|
|
4493
|
+
between: /* @__PURE__ */ __name((fieldValue, value) => {
|
|
4494
|
+
const betweenValues = value;
|
|
4495
|
+
return Array.isArray(betweenValues) && betweenValues.length === BETWEEN_VALUES_LENGTH && fieldValue >= betweenValues[0] && fieldValue <= betweenValues[1];
|
|
4496
|
+
}, "between"),
|
|
4497
|
+
isNull: /* @__PURE__ */ __name((fieldValue) => fieldValue === null || fieldValue === void 0, "isNull"),
|
|
4498
|
+
isNotNull: /* @__PURE__ */ __name((fieldValue) => fieldValue !== null && fieldValue !== void 0, "isNotNull")
|
|
4499
|
+
};
|
|
4500
|
+
var MockAdapter = class {
|
|
3665
4501
|
static {
|
|
3666
|
-
__name(this, "
|
|
4502
|
+
__name(this, "MockAdapter");
|
|
4503
|
+
}
|
|
4504
|
+
data = /* @__PURE__ */ new Map();
|
|
4505
|
+
config;
|
|
4506
|
+
tableIdColumns = /* @__PURE__ */ new Map();
|
|
4507
|
+
defaultSchema;
|
|
4508
|
+
isInitialized = false;
|
|
4509
|
+
transactionDepth = 0;
|
|
4510
|
+
transactionData = null;
|
|
4511
|
+
constructor(config = {}) {
|
|
4512
|
+
this.config = {
|
|
4513
|
+
autoGenerateIds: true,
|
|
4514
|
+
latency: 0,
|
|
4515
|
+
failRate: 0,
|
|
4516
|
+
...config
|
|
4517
|
+
};
|
|
4518
|
+
this.defaultSchema = config.schema ?? "public";
|
|
4519
|
+
if (config.initialData) {
|
|
4520
|
+
this.initializeTableData(config.initialData);
|
|
4521
|
+
}
|
|
4522
|
+
if (config.tableIdColumns) {
|
|
4523
|
+
for (const [table, idColumn] of Object.entries(config.tableIdColumns)) {
|
|
4524
|
+
this.tableIdColumns.set(table, idColumn);
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
async initialize() {
|
|
4529
|
+
await this.simulateLatency();
|
|
4530
|
+
if (this.shouldFail()) {
|
|
4531
|
+
return failure(
|
|
4532
|
+
new errors.DatabaseError(
|
|
4533
|
+
"Mock initialization failed",
|
|
4534
|
+
errors$1.DATABASE_ERROR_CODES.INIT_FAILED
|
|
4535
|
+
)
|
|
4536
|
+
);
|
|
4537
|
+
}
|
|
4538
|
+
this.isInitialized = true;
|
|
4539
|
+
return success();
|
|
4540
|
+
}
|
|
4541
|
+
async close() {
|
|
4542
|
+
await this.simulateLatency();
|
|
4543
|
+
this.data.clear();
|
|
4544
|
+
this.isInitialized = false;
|
|
4545
|
+
return success();
|
|
4546
|
+
}
|
|
4547
|
+
registerTable(name, table, idColumn) {
|
|
4548
|
+
if (idColumn && typeof idColumn === "string") {
|
|
4549
|
+
this.tableIdColumns.set(name, idColumn);
|
|
4550
|
+
}
|
|
4551
|
+
if (!this.data.has(name)) {
|
|
4552
|
+
this.data.set(name, /* @__PURE__ */ new Map());
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
async findById(table, id) {
|
|
4556
|
+
await this.simulateLatency();
|
|
4557
|
+
if (this.shouldFail()) {
|
|
4558
|
+
return failure(
|
|
4559
|
+
new errors.DatabaseError(
|
|
4560
|
+
"Mock findById failed",
|
|
4561
|
+
errors$1.DATABASE_ERROR_CODES.FIND_BY_ID_FAILED
|
|
4562
|
+
)
|
|
4563
|
+
);
|
|
4564
|
+
}
|
|
4565
|
+
const tableData = this.getTableData(table);
|
|
4566
|
+
const record = tableData.get(id);
|
|
4567
|
+
return success(record ? { ...record } : null);
|
|
3667
4568
|
}
|
|
3668
4569
|
/**
|
|
3669
|
-
*
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
4570
|
+
* Apply query options (filter, sort) to records
|
|
4571
|
+
*/
|
|
4572
|
+
applyQueryOptions(records, options) {
|
|
4573
|
+
let result = records;
|
|
4574
|
+
if (options?.filter) {
|
|
4575
|
+
result = this.applyFilter(result, options.filter);
|
|
4576
|
+
}
|
|
4577
|
+
if (options?.sort) {
|
|
4578
|
+
result = this.applySort(result, options.sort);
|
|
4579
|
+
}
|
|
4580
|
+
return result;
|
|
4581
|
+
}
|
|
4582
|
+
/**
|
|
4583
|
+
* Get pagination params with defaults
|
|
4584
|
+
*/
|
|
4585
|
+
getPaginationParams(options) {
|
|
4586
|
+
return {
|
|
4587
|
+
offset: options?.pagination?.offset ?? 0,
|
|
4588
|
+
limit: options?.pagination?.limit ?? DEFAULT_PAGINATION_LIMIT
|
|
4589
|
+
};
|
|
4590
|
+
}
|
|
4591
|
+
/**
|
|
4592
|
+
* Apply pagination to records
|
|
4593
|
+
*/
|
|
4594
|
+
applyPagination(records, offset, limit) {
|
|
4595
|
+
return records.slice(offset, offset + limit);
|
|
4596
|
+
}
|
|
4597
|
+
async findMany(table, options) {
|
|
4598
|
+
await this.simulateLatency();
|
|
4599
|
+
if (this.shouldFail()) {
|
|
4600
|
+
return failure(
|
|
4601
|
+
new errors.DatabaseError(
|
|
4602
|
+
"Mock findMany failed",
|
|
4603
|
+
errors$1.DATABASE_ERROR_CODES.FIND_MANY_FAILED
|
|
4604
|
+
)
|
|
4605
|
+
);
|
|
4606
|
+
}
|
|
4607
|
+
const tableData = this.getTableData(table);
|
|
4608
|
+
const allRecords = Array.from(tableData.values());
|
|
4609
|
+
const filteredRecords = this.applyQueryOptions(allRecords, options);
|
|
4610
|
+
const total = filteredRecords.length;
|
|
4611
|
+
const { offset, limit } = this.getPaginationParams(options);
|
|
4612
|
+
const paginatedRecords = this.applyPagination(
|
|
4613
|
+
filteredRecords,
|
|
4614
|
+
offset,
|
|
4615
|
+
limit
|
|
4616
|
+
);
|
|
4617
|
+
return success({
|
|
4618
|
+
data: paginatedRecords,
|
|
4619
|
+
total,
|
|
4620
|
+
pagination: calculatePagination(total, options?.pagination)
|
|
4621
|
+
});
|
|
4622
|
+
}
|
|
4623
|
+
async create(table, data) {
|
|
4624
|
+
await this.simulateLatency();
|
|
4625
|
+
if (this.shouldFail()) {
|
|
4626
|
+
return failure(
|
|
4627
|
+
new errors.DatabaseError(
|
|
4628
|
+
"Mock create failed",
|
|
4629
|
+
errors$1.DATABASE_ERROR_CODES.CREATE_FAILED
|
|
4630
|
+
)
|
|
4631
|
+
);
|
|
4632
|
+
}
|
|
4633
|
+
const tableData = this.getTableData(table);
|
|
4634
|
+
const idColumn = this.getIdColumn(table);
|
|
4635
|
+
const record = { ...data };
|
|
4636
|
+
if (!record[idColumn] && this.config.autoGenerateIds) {
|
|
4637
|
+
record[idColumn] = this.generateId();
|
|
4638
|
+
}
|
|
4639
|
+
const id = record[idColumn];
|
|
4640
|
+
if (!id) {
|
|
4641
|
+
return failure(
|
|
4642
|
+
new errors.DatabaseError(
|
|
4643
|
+
"Record must have an ID",
|
|
4644
|
+
errors$1.DATABASE_ERROR_CODES.CREATE_FAILED
|
|
4645
|
+
)
|
|
4646
|
+
);
|
|
4647
|
+
}
|
|
4648
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4649
|
+
record.created_at ??= now;
|
|
4650
|
+
record.updated_at ??= now;
|
|
4651
|
+
tableData.set(String(id), record);
|
|
4652
|
+
return success(record);
|
|
4653
|
+
}
|
|
4654
|
+
async update(table, id, data) {
|
|
4655
|
+
await this.simulateLatency();
|
|
4656
|
+
if (this.shouldFail()) {
|
|
4657
|
+
return failure(
|
|
4658
|
+
new errors.DatabaseError(
|
|
4659
|
+
"Mock update failed",
|
|
4660
|
+
errors$1.DATABASE_ERROR_CODES.UPDATE_FAILED
|
|
4661
|
+
)
|
|
4662
|
+
);
|
|
4663
|
+
}
|
|
4664
|
+
const tableData = this.getTableData(table);
|
|
4665
|
+
const existing = tableData.get(id);
|
|
4666
|
+
if (!existing) {
|
|
4667
|
+
return failure(
|
|
4668
|
+
new errors.DatabaseError(
|
|
4669
|
+
"Record not found",
|
|
4670
|
+
errors$1.DATABASE_ERROR_CODES.RECORD_NOT_FOUND
|
|
4671
|
+
)
|
|
4672
|
+
);
|
|
4673
|
+
}
|
|
4674
|
+
const updated = {
|
|
4675
|
+
...existing,
|
|
4676
|
+
...data,
|
|
4677
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4678
|
+
};
|
|
4679
|
+
tableData.set(id, updated);
|
|
4680
|
+
return success(updated);
|
|
4681
|
+
}
|
|
4682
|
+
async delete(table, id) {
|
|
4683
|
+
await this.simulateLatency();
|
|
4684
|
+
if (this.shouldFail()) {
|
|
4685
|
+
return failure(
|
|
4686
|
+
new errors.DatabaseError(
|
|
4687
|
+
"Mock delete failed",
|
|
4688
|
+
errors$1.DATABASE_ERROR_CODES.DELETE_FAILED
|
|
4689
|
+
)
|
|
4690
|
+
);
|
|
4691
|
+
}
|
|
4692
|
+
const tableData = this.getTableData(table);
|
|
4693
|
+
const existed = tableData.delete(id);
|
|
4694
|
+
if (!existed) {
|
|
4695
|
+
return failure(
|
|
4696
|
+
new errors.DatabaseError(
|
|
4697
|
+
"Record not found",
|
|
4698
|
+
errors$1.DATABASE_ERROR_CODES.RECORD_NOT_FOUND
|
|
4699
|
+
)
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
4702
|
+
return success();
|
|
4703
|
+
}
|
|
4704
|
+
async transaction(callback) {
|
|
4705
|
+
await this.simulateLatency();
|
|
4706
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
4707
|
+
for (const [table, tableData] of this.data.entries()) {
|
|
4708
|
+
snapshot.set(table, new Map(tableData));
|
|
4709
|
+
}
|
|
4710
|
+
this.transactionDepth++;
|
|
4711
|
+
this.transactionData = snapshot;
|
|
4712
|
+
try {
|
|
4713
|
+
const trx = {
|
|
4714
|
+
findById: /* @__PURE__ */ __name(async (table, id) => this.findById(table, id), "findById"),
|
|
4715
|
+
create: /* @__PURE__ */ __name(async (table, data) => this.create(table, data), "create"),
|
|
4716
|
+
update: /* @__PURE__ */ __name(async (table, id, data) => this.update(table, id, data), "update"),
|
|
4717
|
+
delete: /* @__PURE__ */ __name(async (table, id) => this.delete(table, id), "delete"),
|
|
4718
|
+
commit: /* @__PURE__ */ __name(async () => {
|
|
4719
|
+
}, "commit"),
|
|
4720
|
+
rollback: /* @__PURE__ */ __name(async () => {
|
|
4721
|
+
this.data = snapshot;
|
|
4722
|
+
}, "rollback")
|
|
4723
|
+
};
|
|
4724
|
+
const result = await callback(trx);
|
|
4725
|
+
this.transactionDepth--;
|
|
4726
|
+
this.transactionData = null;
|
|
4727
|
+
return success(result);
|
|
4728
|
+
} catch (error) {
|
|
4729
|
+
this.data = snapshot;
|
|
4730
|
+
this.transactionDepth--;
|
|
4731
|
+
this.transactionData = null;
|
|
4732
|
+
return failure(
|
|
4733
|
+
new errors.DatabaseError(
|
|
4734
|
+
`Transaction failed: ${error.message}`,
|
|
4735
|
+
errors$1.DATABASE_ERROR_CODES.TRANSACTION_FAILED,
|
|
4736
|
+
{ cause: error }
|
|
4737
|
+
)
|
|
4738
|
+
);
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
async exists(table, id) {
|
|
4742
|
+
await this.simulateLatency();
|
|
4743
|
+
const tableData = this.getTableData(table);
|
|
4744
|
+
return success(tableData.has(id));
|
|
4745
|
+
}
|
|
4746
|
+
async count(table, filter) {
|
|
4747
|
+
await this.simulateLatency();
|
|
4748
|
+
const tableData = this.getTableData(table);
|
|
4749
|
+
let records = Array.from(tableData.values());
|
|
4750
|
+
if (filter) {
|
|
4751
|
+
records = this.applyFilter(records, filter);
|
|
4752
|
+
}
|
|
4753
|
+
return success(records.length);
|
|
4754
|
+
}
|
|
4755
|
+
async healthCheck() {
|
|
4756
|
+
await this.simulateLatency();
|
|
4757
|
+
return success({
|
|
4758
|
+
isHealthy: this.isInitialized,
|
|
4759
|
+
responseTime: this.config.latency ?? 0,
|
|
4760
|
+
details: {
|
|
4761
|
+
adapter: "mock",
|
|
4762
|
+
tables: this.data.size,
|
|
4763
|
+
totalRecords: Array.from(this.data.values()).reduce(
|
|
4764
|
+
(sum, table) => sum + table.size,
|
|
4765
|
+
0
|
|
4766
|
+
)
|
|
4767
|
+
}
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
4770
|
+
// DatabaseAdapterType required methods
|
|
4771
|
+
async connect() {
|
|
4772
|
+
await this.simulateLatency();
|
|
4773
|
+
}
|
|
4774
|
+
async disconnect() {
|
|
4775
|
+
await this.close();
|
|
4776
|
+
}
|
|
4777
|
+
getClient() {
|
|
4778
|
+
return {
|
|
4779
|
+
type: "mock",
|
|
4780
|
+
data: this.data,
|
|
4781
|
+
config: this.config
|
|
4782
|
+
};
|
|
4783
|
+
}
|
|
4784
|
+
async query() {
|
|
4785
|
+
await this.simulateLatency();
|
|
4786
|
+
return [];
|
|
4787
|
+
}
|
|
4788
|
+
// Utility methods
|
|
4789
|
+
/**
|
|
4790
|
+
* Get fully-qualified table name with schema
|
|
4791
|
+
*/
|
|
4792
|
+
getQualifiedTableName(table, schema) {
|
|
4793
|
+
const targetSchema = schema ?? this.defaultSchema;
|
|
4794
|
+
if (table.includes(".")) {
|
|
4795
|
+
return table;
|
|
4796
|
+
}
|
|
4797
|
+
if (targetSchema === "public") {
|
|
4798
|
+
return table;
|
|
4799
|
+
}
|
|
4800
|
+
return `${targetSchema}.${table}`;
|
|
4801
|
+
}
|
|
4802
|
+
initializeTableData(initialData) {
|
|
4803
|
+
for (const [table, records] of Object.entries(initialData)) {
|
|
4804
|
+
const tableData = /* @__PURE__ */ new Map();
|
|
4805
|
+
const idColumn = this.getIdColumn(table);
|
|
4806
|
+
for (const record of records) {
|
|
4807
|
+
const id = record[idColumn];
|
|
4808
|
+
if (id) {
|
|
4809
|
+
tableData.set(String(id), { ...record });
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
this.data.set(table, tableData);
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
getTableData(table) {
|
|
4816
|
+
const qualifiedTable = this.getQualifiedTableName(table);
|
|
4817
|
+
if (!this.data.has(qualifiedTable)) {
|
|
4818
|
+
this.data.set(qualifiedTable, /* @__PURE__ */ new Map());
|
|
4819
|
+
}
|
|
4820
|
+
return this.data.get(qualifiedTable);
|
|
4821
|
+
}
|
|
4822
|
+
getIdColumn(table) {
|
|
4823
|
+
const baseTable = table.includes(".") ? table.split(".")[1] : table;
|
|
4824
|
+
return this.tableIdColumns.get(baseTable) ?? "id";
|
|
4825
|
+
}
|
|
4826
|
+
generateId() {
|
|
4827
|
+
return `mock-${Date.now()}-${Math.random().toString(RANDOM_STRING_BASE).substring(ID_SUBSTRING_START, ID_SUBSTRING_END)}`;
|
|
4828
|
+
}
|
|
4829
|
+
async simulateLatency() {
|
|
4830
|
+
if (this.config.latency && this.config.latency > 0) {
|
|
4831
|
+
await new Promise((resolve3) => setTimeout(resolve3, this.config.latency));
|
|
4832
|
+
}
|
|
4833
|
+
}
|
|
4834
|
+
shouldFail() {
|
|
4835
|
+
if (!this.config.failRate || this.config.failRate <= 0) return false;
|
|
4836
|
+
return Math.random() < this.config.failRate;
|
|
4837
|
+
}
|
|
4838
|
+
applyFilter(records, filter) {
|
|
4839
|
+
const { field, operator, value } = filter;
|
|
4840
|
+
const handler = FILTER_OPERATORS[operator];
|
|
4841
|
+
return records.filter((record) => {
|
|
4842
|
+
const fieldValue = record[field];
|
|
4843
|
+
return handler ? handler(fieldValue, value) : true;
|
|
4844
|
+
});
|
|
4845
|
+
}
|
|
4846
|
+
applySort(records, sort) {
|
|
4847
|
+
return records.sort((a, b) => {
|
|
4848
|
+
for (const { field, direction } of sort) {
|
|
4849
|
+
const aVal = a[field];
|
|
4850
|
+
const bVal = b[field];
|
|
4851
|
+
if (aVal === bVal) continue;
|
|
4852
|
+
const comparison = aVal < bVal ? -1 : 1;
|
|
4853
|
+
return direction === "asc" ? comparison : -comparison;
|
|
4854
|
+
}
|
|
4855
|
+
return 0;
|
|
4856
|
+
});
|
|
4857
|
+
}
|
|
4858
|
+
/**
|
|
4859
|
+
* Test utility: Clear all data
|
|
4860
|
+
*/
|
|
4861
|
+
clearAll() {
|
|
4862
|
+
this.data.clear();
|
|
4863
|
+
}
|
|
4864
|
+
/**
|
|
4865
|
+
* Test utility: Get current data for inspection
|
|
4866
|
+
*/
|
|
4867
|
+
getData(table) {
|
|
4868
|
+
if (table) {
|
|
4869
|
+
return Array.from(this.getTableData(table).values());
|
|
4870
|
+
}
|
|
4871
|
+
const result = {};
|
|
4872
|
+
for (const [tableName, tableData] of this.data.entries()) {
|
|
4873
|
+
result[tableName] = Array.from(tableData.values());
|
|
4874
|
+
}
|
|
4875
|
+
return result;
|
|
4876
|
+
}
|
|
4877
|
+
/**
|
|
4878
|
+
* Test utility: Set data directly
|
|
4879
|
+
*/
|
|
4880
|
+
setData(table, records) {
|
|
4881
|
+
const tableData = /* @__PURE__ */ new Map();
|
|
4882
|
+
const idColumn = this.getIdColumn(table);
|
|
4883
|
+
for (const record of records) {
|
|
4884
|
+
const id = record[idColumn];
|
|
4885
|
+
if (id) {
|
|
4886
|
+
tableData.set(String(id), { ...record });
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
this.data.set(table, tableData);
|
|
4890
|
+
}
|
|
4891
|
+
};
|
|
4892
|
+
var AdapterFactory = class {
|
|
4893
|
+
static {
|
|
4894
|
+
__name(this, "AdapterFactory");
|
|
4895
|
+
}
|
|
4896
|
+
/**
|
|
4897
|
+
* Creates a new database adapter instance based on the configuration
|
|
4898
|
+
*
|
|
4899
|
+
* This is the core factory method that instantiates the appropriate database adapter
|
|
4900
|
+
* based on the provided type and configuration. Uses TypeScript generics to ensure
|
|
4901
|
+
* type safety between adapter type and configuration.
|
|
4902
|
+
*
|
|
4903
|
+
* **Creation Process:**
|
|
4904
|
+
* 1. Validates input parameters (type and config)
|
|
4905
|
+
* 2. Uses switch statement to match adapter type
|
|
4906
|
+
* 3. Instantiates appropriate adapter class with type-safe config
|
|
4907
|
+
* 4. Returns DatabaseAdapterType interface implementation
|
|
3680
4908
|
*
|
|
3681
4909
|
* **Type Safety:**
|
|
3682
4910
|
* - Generic T extends DatabaseConfig ensures config matches adapter type
|
|
@@ -3754,6 +4982,9 @@ var AdapterFactory = class {
|
|
|
3754
4982
|
// SQL adapter - for raw SQL operations and custom database integrations
|
|
3755
4983
|
case db.ADAPTERS.SQL:
|
|
3756
4984
|
return new SQLAdapter(config);
|
|
4985
|
+
// Mock adapter - for in-memory testing without real database
|
|
4986
|
+
case db.ADAPTERS.MOCK:
|
|
4987
|
+
return new MockAdapter(config);
|
|
3757
4988
|
// Default case - handles unsupported or invalid adapter types
|
|
3758
4989
|
default:
|
|
3759
4990
|
throw new errors.DatabaseError(
|
|
@@ -3852,6 +5083,9 @@ var SoftDeleteAdapter = class {
|
|
|
3852
5083
|
async disconnect() {
|
|
3853
5084
|
return this.baseAdapter.disconnect();
|
|
3854
5085
|
}
|
|
5086
|
+
async close() {
|
|
5087
|
+
return this.baseAdapter.close();
|
|
5088
|
+
}
|
|
3855
5089
|
/**
|
|
3856
5090
|
* Gets the underlying database client.
|
|
3857
5091
|
*
|
|
@@ -4235,8 +5469,41 @@ var SoftDeleteAdapter = class {
|
|
|
4235
5469
|
return this.config.excludeTables?.includes(table) ?? false;
|
|
4236
5470
|
}
|
|
4237
5471
|
};
|
|
5472
|
+
var DATE_PART_MIN_WIDTH = 2;
|
|
5473
|
+
var CONTEXT_SOURCE_PATTERNS = [
|
|
5474
|
+
{ patterns: ["encrypt"], source: db.EXTENSION_SOURCE.Encryption },
|
|
5475
|
+
{
|
|
5476
|
+
patterns: ["softdelete", "soft_delete"],
|
|
5477
|
+
source: db.EXTENSION_SOURCE.SoftDelete
|
|
5478
|
+
},
|
|
5479
|
+
{ patterns: ["cach"], source: db.EXTENSION_SOURCE.Caching },
|
|
5480
|
+
{ patterns: ["audit"], source: db.EXTENSION_SOURCE.Audit },
|
|
5481
|
+
{
|
|
5482
|
+
patterns: ["replica", "read_replica"],
|
|
5483
|
+
source: db.EXTENSION_SOURCE.ReadReplica
|
|
5484
|
+
},
|
|
5485
|
+
{
|
|
5486
|
+
patterns: ["multi_write", "multiwrite"],
|
|
5487
|
+
source: db.EXTENSION_SOURCE.MultiWrite
|
|
5488
|
+
}
|
|
5489
|
+
];
|
|
5490
|
+
var MESSAGE_SOURCE_PATTERNS = [
|
|
5491
|
+
{ patterns: ["encrypt"], source: db.EXTENSION_SOURCE.Encryption },
|
|
5492
|
+
{
|
|
5493
|
+
patterns: ["soft delete", "softdelete"],
|
|
5494
|
+
source: db.EXTENSION_SOURCE.SoftDelete
|
|
5495
|
+
},
|
|
5496
|
+
{ patterns: ["cache", "caching"], source: db.EXTENSION_SOURCE.Caching },
|
|
5497
|
+
{
|
|
5498
|
+
patterns: ["replica", "read replica"],
|
|
5499
|
+
source: db.EXTENSION_SOURCE.ReadReplica
|
|
5500
|
+
},
|
|
5501
|
+
{
|
|
5502
|
+
patterns: ["multi-write", "multiwrite"],
|
|
5503
|
+
source: db.EXTENSION_SOURCE.MultiWrite
|
|
5504
|
+
}
|
|
5505
|
+
];
|
|
4238
5506
|
var AuditAdapter = class {
|
|
4239
|
-
// Using shared logger instance from @plyaz/logger
|
|
4240
5507
|
/**
|
|
4241
5508
|
* Creates a new AuditAdapter instance.
|
|
4242
5509
|
*
|
|
@@ -4250,9 +5517,11 @@ var AuditAdapter = class {
|
|
|
4250
5517
|
* ```typescript
|
|
4251
5518
|
* const auditAdapter = new AuditAdapter(baseAdapter, {
|
|
4252
5519
|
* enabled: true,
|
|
4253
|
-
* retentionDays:
|
|
5520
|
+
* retentionDays: 180,
|
|
4254
5521
|
* excludeFields: ['password', 'token'],
|
|
4255
5522
|
* excludeTables: ['temp_data'],
|
|
5523
|
+
* schema: 'audit',
|
|
5524
|
+
* usePartitionedTables: true,
|
|
4256
5525
|
* onAuditAfterWrite: async (event) => {
|
|
4257
5526
|
* await complianceService.recordAudit(event);
|
|
4258
5527
|
* }
|
|
@@ -4262,11 +5531,18 @@ var AuditAdapter = class {
|
|
|
4262
5531
|
constructor(baseAdapter, config) {
|
|
4263
5532
|
this.baseAdapter = baseAdapter;
|
|
4264
5533
|
this.config = config;
|
|
5534
|
+
this.auditSchema = config.schema ?? "audit";
|
|
5535
|
+
this.usePartitionedTables = config.usePartitionedTables ?? true;
|
|
4265
5536
|
}
|
|
4266
5537
|
static {
|
|
4267
5538
|
__name(this, "AuditAdapter");
|
|
4268
5539
|
}
|
|
4269
5540
|
auditContext = {};
|
|
5541
|
+
// Using shared logger instance from @plyaz/logger
|
|
5542
|
+
/** Cached schema-qualified table name */
|
|
5543
|
+
auditSchema;
|
|
5544
|
+
/** Whether to use daily partitioned tables */
|
|
5545
|
+
usePartitionedTables;
|
|
4270
5546
|
/**
|
|
4271
5547
|
* Initializes the audit adapter and underlying adapter.
|
|
4272
5548
|
*
|
|
@@ -4316,6 +5592,9 @@ var AuditAdapter = class {
|
|
|
4316
5592
|
async disconnect() {
|
|
4317
5593
|
return this.baseAdapter.disconnect();
|
|
4318
5594
|
}
|
|
5595
|
+
async close() {
|
|
5596
|
+
return this.baseAdapter.close();
|
|
5597
|
+
}
|
|
4319
5598
|
/**
|
|
4320
5599
|
* Gets the underlying database client.
|
|
4321
5600
|
*
|
|
@@ -4454,11 +5733,36 @@ var AuditAdapter = class {
|
|
|
4454
5733
|
}
|
|
4455
5734
|
async create(table, data) {
|
|
4456
5735
|
this.validateCreateParams(table, data);
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
5736
|
+
try {
|
|
5737
|
+
const result = await this.baseAdapter.create(table, data);
|
|
5738
|
+
if (result.success && this.shouldAudit(table)) {
|
|
5739
|
+
await this.logAudit({
|
|
5740
|
+
operation: db.AUDIT_OPERATION.Create,
|
|
5741
|
+
table,
|
|
5742
|
+
recordId: result.value?.id,
|
|
5743
|
+
changes: {
|
|
5744
|
+
after: result.value,
|
|
5745
|
+
encryptedFields: this.getEncryptedFields(table)
|
|
5746
|
+
},
|
|
5747
|
+
userId: this.auditContext.userId,
|
|
5748
|
+
requestId: this.auditContext.requestId,
|
|
5749
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
5750
|
+
ipAddress: this.auditContext.ipAddress,
|
|
5751
|
+
userAgent: this.auditContext.userAgent
|
|
5752
|
+
});
|
|
5753
|
+
}
|
|
5754
|
+
return result;
|
|
5755
|
+
} catch (error) {
|
|
5756
|
+
if (this.shouldAudit(table)) {
|
|
5757
|
+
await this.logOperationFailure({
|
|
5758
|
+
operation: db.AUDIT_OPERATION.Create,
|
|
5759
|
+
table,
|
|
5760
|
+
data,
|
|
5761
|
+
error
|
|
5762
|
+
});
|
|
5763
|
+
}
|
|
5764
|
+
throw error;
|
|
4460
5765
|
}
|
|
4461
|
-
return result;
|
|
4462
5766
|
}
|
|
4463
5767
|
validateCreateParams(table, data) {
|
|
4464
5768
|
if (!table || !data) {
|
|
@@ -4472,14 +5776,99 @@ var AuditAdapter = class {
|
|
|
4472
5776
|
);
|
|
4473
5777
|
}
|
|
4474
5778
|
}
|
|
4475
|
-
async
|
|
5779
|
+
async update(table, id, data) {
|
|
5780
|
+
const before = await this.baseAdapter.findById(table, id);
|
|
5781
|
+
try {
|
|
5782
|
+
const result = await this.baseAdapter.update(table, id, data);
|
|
5783
|
+
if (result.success && this.shouldAudit(table)) {
|
|
5784
|
+
await this.logAudit({
|
|
5785
|
+
operation: db.AUDIT_OPERATION.Update,
|
|
5786
|
+
table,
|
|
5787
|
+
recordId: id,
|
|
5788
|
+
changes: {
|
|
5789
|
+
before: before.success ? before.value : void 0,
|
|
5790
|
+
after: result.value,
|
|
5791
|
+
fields: Object.keys(data),
|
|
5792
|
+
encryptedFields: this.getEncryptedFields(table)
|
|
5793
|
+
},
|
|
5794
|
+
userId: this.auditContext.userId,
|
|
5795
|
+
requestId: this.auditContext.requestId,
|
|
5796
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
5797
|
+
ipAddress: this.auditContext.ipAddress,
|
|
5798
|
+
userAgent: this.auditContext.userAgent
|
|
5799
|
+
});
|
|
5800
|
+
}
|
|
5801
|
+
return result;
|
|
5802
|
+
} catch (error) {
|
|
5803
|
+
if (this.shouldAudit(table)) {
|
|
5804
|
+
await this.logOperationFailure({
|
|
5805
|
+
operation: db.AUDIT_OPERATION.Update,
|
|
5806
|
+
table,
|
|
5807
|
+
data,
|
|
5808
|
+
error,
|
|
5809
|
+
recordId: id,
|
|
5810
|
+
beforeState: before.value
|
|
5811
|
+
});
|
|
5812
|
+
}
|
|
5813
|
+
throw error;
|
|
5814
|
+
}
|
|
5815
|
+
}
|
|
5816
|
+
async delete(table, id) {
|
|
5817
|
+
const before = await this.baseAdapter.findById(table, id);
|
|
5818
|
+
try {
|
|
5819
|
+
const result = await this.baseAdapter.delete(table, id);
|
|
5820
|
+
if (result.success && this.shouldAudit(table)) {
|
|
5821
|
+
await this.logAudit({
|
|
5822
|
+
operation: db.AUDIT_OPERATION.Delete,
|
|
5823
|
+
table,
|
|
5824
|
+
recordId: id,
|
|
5825
|
+
changes: {
|
|
5826
|
+
before: before.success ? before.value : void 0
|
|
5827
|
+
},
|
|
5828
|
+
userId: this.auditContext.userId,
|
|
5829
|
+
requestId: this.auditContext.requestId,
|
|
5830
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
5831
|
+
ipAddress: this.auditContext.ipAddress,
|
|
5832
|
+
userAgent: this.auditContext.userAgent
|
|
5833
|
+
});
|
|
5834
|
+
}
|
|
5835
|
+
return result;
|
|
5836
|
+
} catch (error) {
|
|
5837
|
+
if (this.shouldAudit(table)) {
|
|
5838
|
+
await this.logOperationFailure({
|
|
5839
|
+
operation: db.AUDIT_OPERATION.Delete,
|
|
5840
|
+
table,
|
|
5841
|
+
data: null,
|
|
5842
|
+
error,
|
|
5843
|
+
recordId: id,
|
|
5844
|
+
beforeState: before.value
|
|
5845
|
+
});
|
|
5846
|
+
}
|
|
5847
|
+
throw error;
|
|
5848
|
+
}
|
|
5849
|
+
}
|
|
5850
|
+
/**
|
|
5851
|
+
* Logs operation failures to audit for compliance tracking.
|
|
5852
|
+
* Captures the before state, attempted changes, and error details.
|
|
5853
|
+
*/
|
|
5854
|
+
async logOperationFailure(options) {
|
|
5855
|
+
const { operation, table, data, error, recordId, beforeState } = options;
|
|
4476
5856
|
try {
|
|
5857
|
+
const errorSource = this.getErrorSource(error);
|
|
5858
|
+
const failedOperation = this.getFailedOperation(operation);
|
|
4477
5859
|
await this.logAudit({
|
|
4478
|
-
operation:
|
|
5860
|
+
operation: failedOperation,
|
|
4479
5861
|
table,
|
|
4480
|
-
recordId:
|
|
5862
|
+
recordId: recordId ?? data?.id,
|
|
4481
5863
|
changes: {
|
|
4482
|
-
|
|
5864
|
+
before: beforeState,
|
|
5865
|
+
attempted: data,
|
|
5866
|
+
failure: {
|
|
5867
|
+
source: errorSource,
|
|
5868
|
+
error_type: error.name,
|
|
5869
|
+
error_message: error.message,
|
|
5870
|
+
error_code: error.errorCode
|
|
5871
|
+
}
|
|
4483
5872
|
},
|
|
4484
5873
|
userId: this.auditContext.userId,
|
|
4485
5874
|
requestId: this.auditContext.requestId,
|
|
@@ -4489,51 +5878,63 @@ var AuditAdapter = class {
|
|
|
4489
5878
|
});
|
|
4490
5879
|
} catch (auditError) {
|
|
4491
5880
|
logger.logger.error(
|
|
4492
|
-
`
|
|
5881
|
+
`Failed to log operation failure to audit: ${auditError.message}`
|
|
4493
5882
|
);
|
|
4494
5883
|
}
|
|
4495
5884
|
}
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
userId: this.auditContext.userId,
|
|
4510
|
-
requestId: this.auditContext.requestId,
|
|
4511
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
4512
|
-
ipAddress: this.auditContext.ipAddress,
|
|
4513
|
-
userAgent: this.auditContext.userAgent
|
|
4514
|
-
});
|
|
5885
|
+
/**
|
|
5886
|
+
* Maps a successful operation to its failed counterpart.
|
|
5887
|
+
*/
|
|
5888
|
+
getFailedOperation(operation) {
|
|
5889
|
+
switch (operation) {
|
|
5890
|
+
case db.AUDIT_OPERATION.Create:
|
|
5891
|
+
return db.AUDIT_OPERATION.CreateFailed;
|
|
5892
|
+
case db.AUDIT_OPERATION.Update:
|
|
5893
|
+
return db.AUDIT_OPERATION.UpdateFailed;
|
|
5894
|
+
case db.AUDIT_OPERATION.Delete:
|
|
5895
|
+
return db.AUDIT_OPERATION.DeleteFailed;
|
|
5896
|
+
default:
|
|
5897
|
+
return operation;
|
|
4515
5898
|
}
|
|
4516
|
-
return result;
|
|
4517
5899
|
}
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
4532
|
-
ipAddress: this.auditContext.ipAddress,
|
|
4533
|
-
userAgent: this.auditContext.userAgent
|
|
4534
|
-
});
|
|
5900
|
+
/**
|
|
5901
|
+
* Determines which extension/layer caused the error based on error details.
|
|
5902
|
+
*/
|
|
5903
|
+
getErrorSource(error) {
|
|
5904
|
+
const errorMessage = error.message.toLowerCase();
|
|
5905
|
+
const dbError = error;
|
|
5906
|
+
const errorContext = dbError.context?.source?.toLowerCase();
|
|
5907
|
+
if (errorContext) {
|
|
5908
|
+
const contextSource = this.matchPatterns(
|
|
5909
|
+
errorContext,
|
|
5910
|
+
CONTEXT_SOURCE_PATTERNS
|
|
5911
|
+
);
|
|
5912
|
+
if (contextSource) return contextSource;
|
|
4535
5913
|
}
|
|
4536
|
-
|
|
5914
|
+
const messageSource = this.matchPatterns(
|
|
5915
|
+
errorMessage,
|
|
5916
|
+
MESSAGE_SOURCE_PATTERNS
|
|
5917
|
+
);
|
|
5918
|
+
if (messageSource) return messageSource;
|
|
5919
|
+
return db.EXTENSION_SOURCE.DatabaseAdapter;
|
|
5920
|
+
}
|
|
5921
|
+
/**
|
|
5922
|
+
* Matches a text against a list of pattern groups.
|
|
5923
|
+
*/
|
|
5924
|
+
matchPatterns(text, patternGroups) {
|
|
5925
|
+
for (const { patterns, source } of patternGroups) {
|
|
5926
|
+
if (patterns.some((pattern) => text.includes(pattern))) {
|
|
5927
|
+
return source;
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5930
|
+
return null;
|
|
5931
|
+
}
|
|
5932
|
+
/**
|
|
5933
|
+
* Gets the list of encrypted fields for a table.
|
|
5934
|
+
* Returns undefined if no fields are encrypted for this table.
|
|
5935
|
+
*/
|
|
5936
|
+
getEncryptedFields(table) {
|
|
5937
|
+
return this.config.encryptedFields?.[table];
|
|
4537
5938
|
}
|
|
4538
5939
|
/**
|
|
4539
5940
|
* Executes operations within a transaction with audit logging.
|
|
@@ -4710,19 +6111,46 @@ var AuditAdapter = class {
|
|
|
4710
6111
|
}
|
|
4711
6112
|
}
|
|
4712
6113
|
/**
|
|
4713
|
-
*
|
|
6114
|
+
* Generates the audit table name based on configuration.
|
|
4714
6115
|
*
|
|
4715
|
-
* **RESPONSIBILITY:**
|
|
4716
|
-
* **TABLE NAMING:**
|
|
4717
|
-
*
|
|
6116
|
+
* **RESPONSIBILITY:** Determines the correct table name for audit records
|
|
6117
|
+
* **TABLE NAMING:**
|
|
6118
|
+
* - Partitioned: `{schema}.audit_log_yyyy_mm_dd` (e.g., audit.audit_log_2024_12_01)
|
|
6119
|
+
* - Non-partitioned: `{schema}.audit_logs` (e.g., audit.audit_logs)
|
|
4718
6120
|
*
|
|
4719
6121
|
* @private
|
|
4720
|
-
* @param
|
|
4721
|
-
* @
|
|
4722
|
-
|
|
6122
|
+
* @param timestamp - Timestamp to use for partition date
|
|
6123
|
+
* @returns Schema-qualified table name
|
|
6124
|
+
*/
|
|
6125
|
+
getAuditTableName(timestamp) {
|
|
6126
|
+
if (this.usePartitionedTables) {
|
|
6127
|
+
const year = timestamp.getFullYear();
|
|
6128
|
+
const month = String(timestamp.getMonth() + 1).padStart(
|
|
6129
|
+
DATE_PART_MIN_WIDTH,
|
|
6130
|
+
"0"
|
|
6131
|
+
);
|
|
6132
|
+
const day = String(timestamp.getDate()).padStart(
|
|
6133
|
+
DATE_PART_MIN_WIDTH,
|
|
6134
|
+
"0"
|
|
6135
|
+
);
|
|
6136
|
+
return `${this.auditSchema}.audit_log_${year}_${month}_${day}`;
|
|
6137
|
+
}
|
|
6138
|
+
return `${this.auditSchema}.audit_logs`;
|
|
6139
|
+
}
|
|
6140
|
+
/**
|
|
6141
|
+
* Writes audit record to daily audit table.
|
|
6142
|
+
*
|
|
6143
|
+
* **RESPONSIBILITY:** Persists audit event to database
|
|
6144
|
+
* **TABLE NAMING:** Uses daily partitioned tables (audit.audit_log_yyyy_mm_dd) by default
|
|
6145
|
+
* **STRUCTURE:** Converts event to database record format
|
|
6146
|
+
*
|
|
6147
|
+
* @private
|
|
6148
|
+
* @param event - Audit event to write
|
|
6149
|
+
* @throws {DatabaseError} When write operation fails
|
|
6150
|
+
*
|
|
4723
6151
|
* @example
|
|
4724
6152
|
* ```typescript
|
|
4725
|
-
* // Internal usage - writes to
|
|
6153
|
+
* // Internal usage - writes to audit.audit_log_2024_12_01 table
|
|
4726
6154
|
* await this.writeAuditRecord({
|
|
4727
6155
|
* operation: 'UPDATE',
|
|
4728
6156
|
* table: 'users',
|
|
@@ -4733,7 +6161,7 @@ var AuditAdapter = class {
|
|
|
4733
6161
|
* ```
|
|
4734
6162
|
*/
|
|
4735
6163
|
async writeAuditRecord(event) {
|
|
4736
|
-
const tableName =
|
|
6164
|
+
const tableName = this.getAuditTableName(event.timestamp);
|
|
4737
6165
|
try {
|
|
4738
6166
|
const auditResult = await this.baseAdapter.create(tableName, {
|
|
4739
6167
|
operation: event.operation,
|
|
@@ -4821,6 +6249,9 @@ var EncryptionAdapter = class {
|
|
|
4821
6249
|
async disconnect() {
|
|
4822
6250
|
return this.baseAdapter.disconnect();
|
|
4823
6251
|
}
|
|
6252
|
+
async close() {
|
|
6253
|
+
return this.baseAdapter.close();
|
|
6254
|
+
}
|
|
4824
6255
|
getClient() {
|
|
4825
6256
|
return this.baseAdapter.getClient();
|
|
4826
6257
|
}
|
|
@@ -4890,14 +6321,10 @@ var EncryptionAdapter = class {
|
|
|
4890
6321
|
return this.config.enabled && isObject(data) && Boolean(this.config.fields[table]);
|
|
4891
6322
|
}
|
|
4892
6323
|
encryptSingleField(result, field) {
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
result[field]
|
|
4896
|
-
|
|
4897
|
-
);
|
|
4898
|
-
}
|
|
4899
|
-
} catch (error) {
|
|
4900
|
-
console.error(`Failed to encrypt field ${field}:`, error);
|
|
6324
|
+
if (result[field]) {
|
|
6325
|
+
result[field] = this.encrypt(
|
|
6326
|
+
String(result[field])
|
|
6327
|
+
);
|
|
4901
6328
|
}
|
|
4902
6329
|
}
|
|
4903
6330
|
decryptFields(table, data) {
|
|
@@ -4910,15 +6337,18 @@ var EncryptionAdapter = class {
|
|
|
4910
6337
|
return result;
|
|
4911
6338
|
}
|
|
4912
6339
|
decryptSingleField(result, field) {
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
6340
|
+
if (result[field]) {
|
|
6341
|
+
const fieldValue = String(result[field]);
|
|
6342
|
+
if (this.isEncryptedValue(fieldValue)) {
|
|
6343
|
+
try {
|
|
4917
6344
|
result[field] = this.decrypt(fieldValue);
|
|
6345
|
+
} catch (error) {
|
|
6346
|
+
console.warn(
|
|
6347
|
+
`Failed to decrypt field ${field}, returning as-is:`,
|
|
6348
|
+
error.message
|
|
6349
|
+
);
|
|
4918
6350
|
}
|
|
4919
6351
|
}
|
|
4920
|
-
} catch (error) {
|
|
4921
|
-
console.error(`Failed to decrypt field ${field}:`, error);
|
|
4922
6352
|
}
|
|
4923
6353
|
}
|
|
4924
6354
|
encrypt(text) {
|
|
@@ -5124,6 +6554,9 @@ var CachingAdapter = class {
|
|
|
5124
6554
|
async disconnect() {
|
|
5125
6555
|
return this.baseAdapter.disconnect();
|
|
5126
6556
|
}
|
|
6557
|
+
async close() {
|
|
6558
|
+
return this.baseAdapter.close();
|
|
6559
|
+
}
|
|
5127
6560
|
/**
|
|
5128
6561
|
* Gets the underlying database client.
|
|
5129
6562
|
*
|
|
@@ -5548,6 +6981,9 @@ var ReadReplicaAdapter = class {
|
|
|
5548
6981
|
async disconnect() {
|
|
5549
6982
|
return this.primaryAdapter.disconnect();
|
|
5550
6983
|
}
|
|
6984
|
+
async close() {
|
|
6985
|
+
return this.primaryAdapter.close();
|
|
6986
|
+
}
|
|
5551
6987
|
getClient() {
|
|
5552
6988
|
return this.primaryAdapter.getClient();
|
|
5553
6989
|
}
|
|
@@ -5625,7 +7061,11 @@ function buildAdapterChain(baseAdapter, config) {
|
|
|
5625
7061
|
adapter = new CachingAdapter(adapter, config.cache);
|
|
5626
7062
|
}
|
|
5627
7063
|
if (config.audit?.enabled) {
|
|
5628
|
-
adapter = new AuditAdapter(adapter,
|
|
7064
|
+
adapter = new AuditAdapter(adapter, {
|
|
7065
|
+
...config.audit,
|
|
7066
|
+
// Pass encrypted fields config so audit can log which fields are encrypted at rest
|
|
7067
|
+
encryptedFields: config.encryption?.enabled ? config.encryption.fields : void 0
|
|
7068
|
+
});
|
|
5629
7069
|
}
|
|
5630
7070
|
if (config.readReplica?.enabled) {
|
|
5631
7071
|
adapter = new ReadReplicaAdapter(adapter, {
|
|
@@ -5647,8 +7087,10 @@ function createAdapterConfig(config) {
|
|
|
5647
7087
|
// orm: ADAPTERS.SUPABASE,
|
|
5648
7088
|
connectionString: drizzleConfig.connectionString ?? drizzleConfig.url,
|
|
5649
7089
|
// Support both formats
|
|
5650
|
-
pool: drizzleConfig.poolSize ? { max: drizzleConfig.poolSize } : void 0
|
|
7090
|
+
pool: drizzleConfig.poolSize ? { max: drizzleConfig.poolSize } : void 0,
|
|
5651
7091
|
// Optional connection pooling
|
|
7092
|
+
tableIdColumns: drizzleConfig.tableIdColumns
|
|
7093
|
+
// Custom ID column mappings
|
|
5652
7094
|
};
|
|
5653
7095
|
}
|
|
5654
7096
|
if (config.adapter === db.ADAPTER_TYPES.SUPABASE) {
|
|
@@ -5662,15 +7104,28 @@ function createAdapterConfig(config) {
|
|
|
5662
7104
|
// Public API key
|
|
5663
7105
|
supabaseServiceKey: supabaseConfig.supabaseServiceKey,
|
|
5664
7106
|
// Service role key (admin)
|
|
5665
|
-
schema: supabaseConfig.schema
|
|
7107
|
+
schema: supabaseConfig.schema,
|
|
5666
7108
|
// Database schema
|
|
7109
|
+
tableIdColumns: supabaseConfig.tableIdColumns
|
|
7110
|
+
// Custom ID column mappings
|
|
7111
|
+
};
|
|
7112
|
+
}
|
|
7113
|
+
if (config.adapter === db.ADAPTER_TYPES.MOCK) {
|
|
7114
|
+
return {
|
|
7115
|
+
adapter: db.ADAPTERS.MOCK,
|
|
7116
|
+
...config.config
|
|
7117
|
+
// Pass through all mock config options
|
|
5667
7118
|
};
|
|
5668
7119
|
}
|
|
5669
7120
|
const sqlConfig = config.config;
|
|
5670
7121
|
return {
|
|
5671
7122
|
adapter: db.ADAPTERS.SQL,
|
|
5672
|
-
connectionString: sqlConfig.connectionString ?? sqlConfig.url
|
|
7123
|
+
connectionString: sqlConfig.connectionString ?? sqlConfig.url,
|
|
5673
7124
|
// Support both formats
|
|
7125
|
+
schema: sqlConfig.schema,
|
|
7126
|
+
// Default schema for all tables
|
|
7127
|
+
tableIdColumns: sqlConfig.tableIdColumns
|
|
7128
|
+
// Custom ID column mappings
|
|
5674
7129
|
};
|
|
5675
7130
|
}
|
|
5676
7131
|
__name(createAdapterConfig, "createAdapterConfig");
|
|
@@ -5751,18 +7206,33 @@ __name(createDatabaseService, "createDatabaseService");
|
|
|
5751
7206
|
|
|
5752
7207
|
// src/repository/BaseRepository.ts
|
|
5753
7208
|
var BaseRepository = class {
|
|
5754
|
-
|
|
5755
|
-
constructor(db, tableName) {
|
|
7209
|
+
constructor(db, tableName, defaultConfig) {
|
|
5756
7210
|
this.db = db;
|
|
5757
7211
|
this.tableName = tableName;
|
|
7212
|
+
this.defaultConfig = defaultConfig;
|
|
5758
7213
|
}
|
|
5759
7214
|
static {
|
|
5760
7215
|
__name(this, "BaseRepository");
|
|
5761
7216
|
}
|
|
7217
|
+
defaultConfig;
|
|
7218
|
+
/**
|
|
7219
|
+
* Merges default repository config with per-operation config
|
|
7220
|
+
* Per-operation config takes precedence over default config
|
|
7221
|
+
*/
|
|
7222
|
+
mergeConfig(operationConfig) {
|
|
7223
|
+
if (!this.defaultConfig && !operationConfig) {
|
|
7224
|
+
return void 0;
|
|
7225
|
+
}
|
|
7226
|
+
return {
|
|
7227
|
+
...this.defaultConfig,
|
|
7228
|
+
...operationConfig
|
|
7229
|
+
};
|
|
7230
|
+
}
|
|
5762
7231
|
/**
|
|
5763
7232
|
* Find a single entity by its primary key ID
|
|
5764
7233
|
*
|
|
5765
7234
|
* @param {string} id - The primary key ID of the entity to retrieve
|
|
7235
|
+
* @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
|
|
5766
7236
|
* @returns {Promise<DatabaseResult<T | null>>} Promise resolving to the entity or null if not found
|
|
5767
7237
|
*
|
|
5768
7238
|
* @example
|
|
@@ -5771,15 +7241,21 @@ var BaseRepository = class {
|
|
|
5771
7241
|
* if (result.success && result.value) {
|
|
5772
7242
|
* console.log('Found user:', result.value.name);
|
|
5773
7243
|
* }
|
|
7244
|
+
*
|
|
7245
|
+
* // Use specific adapter for this query
|
|
7246
|
+
* const analyticsResult = await userRepository.findById('user-123', {
|
|
7247
|
+
* adapter: 'analytics'
|
|
7248
|
+
* });
|
|
5774
7249
|
* ```
|
|
5775
7250
|
*/
|
|
5776
|
-
async findById(id) {
|
|
5777
|
-
return this.db.
|
|
7251
|
+
async findById(id, config) {
|
|
7252
|
+
return this.db.get(this.tableName, id, this.mergeConfig(config));
|
|
5778
7253
|
}
|
|
5779
7254
|
/**
|
|
5780
7255
|
* Find multiple entities with optional filtering, sorting, and pagination
|
|
5781
7256
|
*
|
|
5782
|
-
* @param {QueryOptions} [options] - Optional query configuration
|
|
7257
|
+
* @param {QueryOptions<T>} [options] - Optional query configuration with type-safe fields
|
|
7258
|
+
* @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
|
|
5783
7259
|
* @returns {Promise<DatabaseResult<PaginatedResult<T>>>} Promise resolving to paginated results
|
|
5784
7260
|
*
|
|
5785
7261
|
* @example
|
|
@@ -5789,15 +7265,21 @@ var BaseRepository = class {
|
|
|
5789
7265
|
* sort: [{ field: 'createdAt', direction: 'desc' }],
|
|
5790
7266
|
* pagination: { limit: 20, offset: 0 }
|
|
5791
7267
|
* });
|
|
7268
|
+
*
|
|
7269
|
+
* // Query from analytics database
|
|
7270
|
+
* const analyticsResult = await userRepository.findMany({}, {
|
|
7271
|
+
* adapter: 'analytics'
|
|
7272
|
+
* });
|
|
5792
7273
|
* ```
|
|
5793
7274
|
*/
|
|
5794
|
-
async findMany(options) {
|
|
5795
|
-
return this.db.
|
|
7275
|
+
async findMany(options, config) {
|
|
7276
|
+
return this.db.list(this.tableName, options, this.mergeConfig(config));
|
|
5796
7277
|
}
|
|
5797
7278
|
/**
|
|
5798
7279
|
* Create a new entity in the database
|
|
5799
7280
|
*
|
|
5800
|
-
* @param {T} data - The entity data to create
|
|
7281
|
+
* @param {CreateInput<T>} data - The entity data to create (id is auto-generated)
|
|
7282
|
+
* @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
|
|
5801
7283
|
* @returns {Promise<DatabaseResult<T>>} Promise resolving to the created entity
|
|
5802
7284
|
*
|
|
5803
7285
|
* @example
|
|
@@ -5806,16 +7288,25 @@ var BaseRepository = class {
|
|
|
5806
7288
|
* name: 'John Doe',
|
|
5807
7289
|
* email: 'john@example.com'
|
|
5808
7290
|
* });
|
|
7291
|
+
*
|
|
7292
|
+
* // Create in specific database/schema
|
|
7293
|
+
* const result = await userRepository.create({
|
|
7294
|
+
* name: 'Jane Doe'
|
|
7295
|
+
* }, {
|
|
7296
|
+
* adapter: 'secondary',
|
|
7297
|
+
* schema: 'backoffice'
|
|
7298
|
+
* });
|
|
5809
7299
|
* ```
|
|
5810
7300
|
*/
|
|
5811
|
-
async create(data) {
|
|
5812
|
-
return this.db.create(this.tableName, data);
|
|
7301
|
+
async create(data, config) {
|
|
7302
|
+
return this.db.create(this.tableName, data, this.mergeConfig(config));
|
|
5813
7303
|
}
|
|
5814
7304
|
/**
|
|
5815
7305
|
* Update an existing entity by ID
|
|
5816
7306
|
*
|
|
5817
7307
|
* @param {string} id - The primary key ID of the entity to update
|
|
5818
7308
|
* @param {Partial<T>} data - Partial entity data containing fields to update
|
|
7309
|
+
* @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
|
|
5819
7310
|
* @returns {Promise<DatabaseResult<T>>} Promise resolving to the updated entity
|
|
5820
7311
|
*
|
|
5821
7312
|
* @example
|
|
@@ -5824,15 +7315,29 @@ var BaseRepository = class {
|
|
|
5824
7315
|
* email: 'newemail@example.com',
|
|
5825
7316
|
* updatedAt: new Date()
|
|
5826
7317
|
* });
|
|
7318
|
+
*
|
|
7319
|
+
* // Update in specific adapter with custom ID column
|
|
7320
|
+
* const result = await userRepository.update('flag-key', {
|
|
7321
|
+
* value: true
|
|
7322
|
+
* }, {
|
|
7323
|
+
* adapter: 'config',
|
|
7324
|
+
* idColumn: 'key'
|
|
7325
|
+
* });
|
|
5827
7326
|
* ```
|
|
5828
7327
|
*/
|
|
5829
|
-
async update(id, data) {
|
|
5830
|
-
return this.db.update(
|
|
7328
|
+
async update(id, data, config) {
|
|
7329
|
+
return this.db.update(
|
|
7330
|
+
this.tableName,
|
|
7331
|
+
id,
|
|
7332
|
+
data,
|
|
7333
|
+
this.mergeConfig(config)
|
|
7334
|
+
);
|
|
5831
7335
|
}
|
|
5832
7336
|
/**
|
|
5833
7337
|
* Delete an entity by ID (hard delete)
|
|
5834
7338
|
*
|
|
5835
7339
|
* @param {string} id - The primary key ID of the entity to delete
|
|
7340
|
+
* @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
|
|
5836
7341
|
* @returns {Promise<DatabaseResult<void>>} Promise resolving when deletion is complete
|
|
5837
7342
|
*
|
|
5838
7343
|
* @warning This is a permanent operation that cannot be undone
|
|
@@ -5841,15 +7346,21 @@ var BaseRepository = class {
|
|
|
5841
7346
|
* @example
|
|
5842
7347
|
* ```typescript
|
|
5843
7348
|
* const result = await userRepository.delete('user-123');
|
|
7349
|
+
*
|
|
7350
|
+
* // Delete from specific adapter
|
|
7351
|
+
* const result = await userRepository.delete('user-123', {
|
|
7352
|
+
* adapter: 'archive'
|
|
7353
|
+
* });
|
|
5844
7354
|
* ```
|
|
5845
7355
|
*/
|
|
5846
|
-
async delete(id) {
|
|
5847
|
-
return this.db.delete(this.tableName, id);
|
|
7356
|
+
async delete(id, config) {
|
|
7357
|
+
return this.db.delete(this.tableName, id, this.mergeConfig(config));
|
|
5848
7358
|
}
|
|
5849
7359
|
/**
|
|
5850
7360
|
* Count entities matching optional filter criteria
|
|
5851
7361
|
*
|
|
5852
|
-
* @param {Filter} [filter] - Optional filter conditions
|
|
7362
|
+
* @param {Filter<T>} [filter] - Optional filter conditions with type-safe fields
|
|
7363
|
+
* @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
|
|
5853
7364
|
* @returns {Promise<DatabaseResult<number>>} Promise resolving to the count
|
|
5854
7365
|
*
|
|
5855
7366
|
* @example
|
|
@@ -5858,15 +7369,21 @@ var BaseRepository = class {
|
|
|
5858
7369
|
* const activeResult = await userRepository.count({
|
|
5859
7370
|
* field: 'status', operator: 'eq', value: 'active'
|
|
5860
7371
|
* });
|
|
7372
|
+
*
|
|
7373
|
+
* // Count in specific adapter
|
|
7374
|
+
* const archiveCount = await userRepository.count(undefined, {
|
|
7375
|
+
* adapter: 'archive'
|
|
7376
|
+
* });
|
|
5861
7377
|
* ```
|
|
5862
7378
|
*/
|
|
5863
|
-
async count(filter) {
|
|
5864
|
-
return this.db.count(this.tableName, filter);
|
|
7379
|
+
async count(filter, config) {
|
|
7380
|
+
return this.db.count(this.tableName, filter, this.mergeConfig(config));
|
|
5865
7381
|
}
|
|
5866
7382
|
/**
|
|
5867
7383
|
* Check if an entity exists by ID
|
|
5868
7384
|
*
|
|
5869
7385
|
* @param {string} id - The primary key ID to check
|
|
7386
|
+
* @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
|
|
5870
7387
|
* @returns {Promise<DatabaseResult<boolean>>} Promise resolving to existence status
|
|
5871
7388
|
*
|
|
5872
7389
|
* @example
|
|
@@ -5875,15 +7392,30 @@ var BaseRepository = class {
|
|
|
5875
7392
|
* if (existsResult.success && existsResult.value) {
|
|
5876
7393
|
* console.log('User exists');
|
|
5877
7394
|
* }
|
|
7395
|
+
*
|
|
7396
|
+
* // Check existence in specific adapter
|
|
7397
|
+
* const existsInArchive = await userRepository.exists('user-123', {
|
|
7398
|
+
* adapter: 'archive'
|
|
7399
|
+
* });
|
|
5878
7400
|
* ```
|
|
5879
7401
|
*/
|
|
5880
|
-
async exists(id) {
|
|
5881
|
-
|
|
7402
|
+
async exists(id, config) {
|
|
7403
|
+
const result = await this.db.get(
|
|
7404
|
+
this.tableName,
|
|
7405
|
+
id,
|
|
7406
|
+
this.mergeConfig(config)
|
|
7407
|
+
);
|
|
7408
|
+
return {
|
|
7409
|
+
success: result.success,
|
|
7410
|
+
value: result.success && result.value !== null,
|
|
7411
|
+
error: result.error
|
|
7412
|
+
};
|
|
5882
7413
|
}
|
|
5883
7414
|
/**
|
|
5884
7415
|
* Find the first entity matching filter criteria
|
|
5885
7416
|
*
|
|
5886
|
-
* @param {Filter} filter - Filter conditions
|
|
7417
|
+
* @param {Filter<T>} filter - Filter conditions with type-safe fields
|
|
7418
|
+
* @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
|
|
5887
7419
|
* @returns {Promise<DatabaseResult<T | null>>} Promise resolving to first match or null
|
|
5888
7420
|
*
|
|
5889
7421
|
* @example
|
|
@@ -5891,15 +7423,24 @@ var BaseRepository = class {
|
|
|
5891
7423
|
* const result = await userRepository.findOne({
|
|
5892
7424
|
* field: 'email', operator: 'eq', value: 'john@example.com'
|
|
5893
7425
|
* });
|
|
7426
|
+
*
|
|
7427
|
+
* // Find in specific adapter
|
|
7428
|
+
* const archivedUser = await userRepository.findOne({
|
|
7429
|
+
* field: 'email', operator: 'eq', value: 'john@example.com'
|
|
7430
|
+
* }, {
|
|
7431
|
+
* adapter: 'archive',
|
|
7432
|
+
* includeSoftDeleted: true
|
|
7433
|
+
* });
|
|
5894
7434
|
* ```
|
|
5895
7435
|
*/
|
|
5896
|
-
async findOne(filter) {
|
|
5897
|
-
return this.db.findOne(this.tableName, filter);
|
|
7436
|
+
async findOne(filter, config) {
|
|
7437
|
+
return this.db.findOne(this.tableName, filter, this.mergeConfig(config));
|
|
5898
7438
|
}
|
|
5899
7439
|
/**
|
|
5900
7440
|
* Soft delete an entity by ID (recoverable deletion)
|
|
5901
7441
|
*
|
|
5902
7442
|
* @param {string} id - The primary key ID of the entity to soft delete
|
|
7443
|
+
* @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
|
|
5903
7444
|
* @returns {Promise<DatabaseResult<void>>} Promise resolving when soft deletion is complete
|
|
5904
7445
|
*
|
|
5905
7446
|
* @see {@link delete} For permanent deletion
|
|
@@ -5908,245 +7449,673 @@ var BaseRepository = class {
|
|
|
5908
7449
|
* ```typescript
|
|
5909
7450
|
* const result = await userRepository.softDelete('user-123');
|
|
5910
7451
|
* // User is hidden but can be recovered
|
|
7452
|
+
*
|
|
7453
|
+
* // Soft delete in specific adapter
|
|
7454
|
+
* const result = await userRepository.softDelete('user-123', {
|
|
7455
|
+
* adapter: 'primary',
|
|
7456
|
+
* skipAudit: false
|
|
7457
|
+
* });
|
|
5911
7458
|
* ```
|
|
5912
7459
|
*/
|
|
5913
|
-
async softDelete(id) {
|
|
5914
|
-
return this.db.softDelete(this.tableName, id);
|
|
7460
|
+
async softDelete(id, config) {
|
|
7461
|
+
return this.db.softDelete(this.tableName, id, this.mergeConfig(config));
|
|
5915
7462
|
}
|
|
5916
7463
|
};
|
|
5917
|
-
var
|
|
5918
|
-
function UseReplica(options = {}) {
|
|
5919
|
-
return (target, propertyKey, descriptor) => {
|
|
5920
|
-
common.SetMetadata(USE_REPLICA_KEY, options)(target, propertyKey, descriptor);
|
|
5921
|
-
};
|
|
5922
|
-
}
|
|
5923
|
-
__name(UseReplica, "UseReplica");
|
|
5924
|
-
var RedisCache = class {
|
|
7464
|
+
var MultiWriteAdapter = class {
|
|
5925
7465
|
static {
|
|
5926
|
-
__name(this, "
|
|
7466
|
+
__name(this, "MultiWriteAdapter");
|
|
5927
7467
|
}
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
* const cache = new RedisCache({
|
|
5939
|
-
* url: 'redis://localhost:6379'
|
|
5940
|
-
* });
|
|
5941
|
-
*
|
|
5942
|
-
* // With custom default TTL
|
|
5943
|
-
* const cacheWithCustomTTL = new RedisCache({
|
|
5944
|
-
* url: 'redis://localhost:6379',
|
|
5945
|
-
* defaultTTL: 1800 // 30 minutes
|
|
5946
|
-
* });
|
|
5947
|
-
*
|
|
5948
|
-
* // Production configuration with options
|
|
5949
|
-
* const productionCache = new RedisCache({
|
|
5950
|
-
* url: 'redis://redis-cluster.example.com:6379',
|
|
5951
|
-
* defaultTTL: 3600,
|
|
5952
|
-
* // Additional Redis options can be passed here
|
|
5953
|
-
* });
|
|
5954
|
-
* ```
|
|
5955
|
-
*/
|
|
5956
|
-
constructor(config$1) {
|
|
5957
|
-
this.redis = new ioredis.Redis(config$1.url);
|
|
5958
|
-
this.defaultTTL = config$1.defaultTTL ?? config.NUMERIX.THIRTY_SIX_HUNDERD;
|
|
7468
|
+
baseAdapter;
|
|
7469
|
+
config;
|
|
7470
|
+
constructor(baseAdapter, config) {
|
|
7471
|
+
this.baseAdapter = baseAdapter;
|
|
7472
|
+
this.config = {
|
|
7473
|
+
mode: "best-effort",
|
|
7474
|
+
onSecondaryFailure: "log",
|
|
7475
|
+
timeout: 5e3,
|
|
7476
|
+
...config
|
|
7477
|
+
};
|
|
5959
7478
|
}
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
* if (result.success && result.value) {
|
|
5972
|
-
* console.log('User:', result.value.name);
|
|
5973
|
-
* } else {
|
|
5974
|
-
* console.log('Profile not found or cache error');
|
|
5975
|
-
* }
|
|
5976
|
-
*
|
|
5977
|
-
* // Get product list
|
|
5978
|
-
* const products = await cache.get<Product[]>('products:featured');
|
|
5979
|
-
* if (products.success && products.value) {
|
|
5980
|
-
* products.value.forEach(product => {
|
|
5981
|
-
* console.log(product.name, product.price);
|
|
5982
|
-
* });
|
|
5983
|
-
* }
|
|
5984
|
-
* ```
|
|
5985
|
-
*/
|
|
5986
|
-
async get(key) {
|
|
5987
|
-
try {
|
|
5988
|
-
const value = await this.redis.get(key);
|
|
5989
|
-
if (!value) {
|
|
5990
|
-
return success();
|
|
7479
|
+
async initialize() {
|
|
7480
|
+
return this.baseAdapter.initialize();
|
|
7481
|
+
}
|
|
7482
|
+
async close() {
|
|
7483
|
+
return this.baseAdapter.close();
|
|
7484
|
+
}
|
|
7485
|
+
async connect() {
|
|
7486
|
+
await this.baseAdapter.connect();
|
|
7487
|
+
for (const secondary of this.config.adapters) {
|
|
7488
|
+
if (typeof secondary.connect === "function") {
|
|
7489
|
+
await secondary.connect();
|
|
5991
7490
|
}
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
async disconnect() {
|
|
7494
|
+
await this.baseAdapter.disconnect();
|
|
7495
|
+
for (const secondary of this.config.adapters) {
|
|
7496
|
+
if (typeof secondary.disconnect === "function") {
|
|
7497
|
+
await secondary.disconnect();
|
|
5995
7498
|
}
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
7499
|
+
}
|
|
7500
|
+
}
|
|
7501
|
+
getClient() {
|
|
7502
|
+
return this.baseAdapter.getClient();
|
|
7503
|
+
}
|
|
7504
|
+
async query(sql2, params) {
|
|
7505
|
+
return this.baseAdapter.query(sql2, params);
|
|
7506
|
+
}
|
|
7507
|
+
registerTable(name, table, idColumn) {
|
|
7508
|
+
this.baseAdapter.registerTable(name, table, idColumn);
|
|
7509
|
+
for (const secondary of this.config.adapters) {
|
|
7510
|
+
if (typeof secondary.registerTable === "function") {
|
|
7511
|
+
secondary.registerTable(name, table, idColumn);
|
|
7512
|
+
}
|
|
7513
|
+
}
|
|
7514
|
+
}
|
|
7515
|
+
// Read operations - always use primary
|
|
7516
|
+
async findById(table, id) {
|
|
7517
|
+
return this.baseAdapter.findById(table, id);
|
|
7518
|
+
}
|
|
7519
|
+
async findMany(table, options) {
|
|
7520
|
+
return this.baseAdapter.findMany(table, options);
|
|
7521
|
+
}
|
|
7522
|
+
async exists(table, id) {
|
|
7523
|
+
return this.baseAdapter.exists(table, id);
|
|
7524
|
+
}
|
|
7525
|
+
async count(table, filter) {
|
|
7526
|
+
return this.baseAdapter.count(table, filter);
|
|
7527
|
+
}
|
|
7528
|
+
// Write operations - replicate to secondaries
|
|
7529
|
+
async create(table, data) {
|
|
7530
|
+
const primaryResult = await this.baseAdapter.create(table, data);
|
|
7531
|
+
if (!primaryResult.success) {
|
|
7532
|
+
return primaryResult;
|
|
7533
|
+
}
|
|
7534
|
+
await this.replicateWrite(
|
|
7535
|
+
() => Promise.all(
|
|
7536
|
+
this.config.adapters.map((adapter) => adapter.create(table, data))
|
|
7537
|
+
)
|
|
7538
|
+
);
|
|
7539
|
+
return primaryResult;
|
|
7540
|
+
}
|
|
7541
|
+
async update(table, id, data) {
|
|
7542
|
+
const primaryResult = await this.baseAdapter.update(table, id, data);
|
|
7543
|
+
if (!primaryResult.success) {
|
|
7544
|
+
return primaryResult;
|
|
7545
|
+
}
|
|
7546
|
+
await this.replicateWrite(
|
|
7547
|
+
() => Promise.all(
|
|
7548
|
+
this.config.adapters.map((adapter) => adapter.update(table, id, data))
|
|
7549
|
+
)
|
|
7550
|
+
);
|
|
7551
|
+
return primaryResult;
|
|
7552
|
+
}
|
|
7553
|
+
async delete(table, id) {
|
|
7554
|
+
const primaryResult = await this.baseAdapter.delete(table, id);
|
|
7555
|
+
if (!primaryResult.success) {
|
|
7556
|
+
return primaryResult;
|
|
7557
|
+
}
|
|
7558
|
+
await this.replicateWrite(
|
|
7559
|
+
() => Promise.all(
|
|
7560
|
+
this.config.adapters.map((adapter) => adapter.delete(table, id))
|
|
7561
|
+
)
|
|
7562
|
+
);
|
|
7563
|
+
return primaryResult;
|
|
7564
|
+
}
|
|
7565
|
+
async transaction(callback) {
|
|
7566
|
+
return this.baseAdapter.transaction(callback);
|
|
7567
|
+
}
|
|
7568
|
+
async healthCheck() {
|
|
7569
|
+
return this.baseAdapter.healthCheck();
|
|
7570
|
+
}
|
|
7571
|
+
/**
|
|
7572
|
+
* Replicate write operation to secondary adapters
|
|
7573
|
+
*/
|
|
7574
|
+
async replicateWrite(fn) {
|
|
7575
|
+
if (this.config.mode === "strict") {
|
|
7576
|
+
const results = await Promise.allSettled([
|
|
7577
|
+
this.executeWithTimeout(fn(), this.config.timeout)
|
|
7578
|
+
]);
|
|
7579
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
7580
|
+
if (failures.length > 0) {
|
|
7581
|
+
this.handleSecondaryFailure(failures);
|
|
7582
|
+
}
|
|
7583
|
+
} else {
|
|
7584
|
+
this.executeWithTimeout(fn(), this.config.timeout).then((results) => {
|
|
7585
|
+
const failures = results.filter((r) => !r.success);
|
|
7586
|
+
if (failures.length > 0) {
|
|
7587
|
+
this.handleSecondaryFailure(
|
|
7588
|
+
failures.map((f) => ({
|
|
7589
|
+
status: "rejected",
|
|
7590
|
+
reason: f.error
|
|
7591
|
+
}))
|
|
7592
|
+
);
|
|
7593
|
+
}
|
|
7594
|
+
}).catch((error) => {
|
|
7595
|
+
this.handleSecondaryFailure([
|
|
7596
|
+
{ status: "rejected", reason: error }
|
|
7597
|
+
]);
|
|
7598
|
+
});
|
|
6005
7599
|
}
|
|
6006
7600
|
}
|
|
6007
7601
|
/**
|
|
6008
|
-
*
|
|
6009
|
-
* Automatically handles JSON serialization.
|
|
6010
|
-
*
|
|
6011
|
-
* @param key Cache key
|
|
6012
|
-
* @param value Value to cache
|
|
6013
|
-
* @param ttl Time to live in seconds (uses default if not specified)
|
|
6014
|
-
* @returns DatabaseResult indicating operation success
|
|
6015
|
-
*
|
|
6016
|
-
* @example
|
|
6017
|
-
* ```typescript
|
|
6018
|
-
* // Cache with default TTL
|
|
6019
|
-
* await cache.set('user:123', { id: 123, name: 'John' });
|
|
6020
|
-
*
|
|
6021
|
-
* // Cache with custom TTL (5 minutes)
|
|
6022
|
-
* await cache.set('session:abc123', sessionData, 300);
|
|
6023
|
-
*
|
|
6024
|
-
* // Cache with long TTL for static data
|
|
6025
|
-
* await cache.set('config:app-settings', appSettings, 86400); // 24 hours
|
|
6026
|
-
*
|
|
6027
|
-
* // Cache with short TTL for frequently changing data
|
|
6028
|
-
* await cache.set('metrics:realtime', realtimeData, 60); // 1 minute
|
|
6029
|
-
* ```
|
|
7602
|
+
* Execute operation with timeout
|
|
6030
7603
|
*/
|
|
6031
|
-
async
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
new errors.DatabaseError(
|
|
6039
|
-
"Cache set failed",
|
|
6040
|
-
errors$1.DATABASE_ERROR_CODES.CACHE_SET_FAILED,
|
|
6041
|
-
{ context: { source: "RedisCache.set", key, cause: error } }
|
|
7604
|
+
async executeWithTimeout(promise, timeoutMs) {
|
|
7605
|
+
return Promise.race([
|
|
7606
|
+
promise,
|
|
7607
|
+
new Promise(
|
|
7608
|
+
(_, reject) => setTimeout(
|
|
7609
|
+
() => reject(new Error("Secondary write timeout")),
|
|
7610
|
+
timeoutMs
|
|
6042
7611
|
)
|
|
6043
|
-
)
|
|
6044
|
-
|
|
7612
|
+
)
|
|
7613
|
+
]);
|
|
6045
7614
|
}
|
|
6046
7615
|
/**
|
|
6047
|
-
*
|
|
6048
|
-
*
|
|
6049
|
-
* @param key Cache key
|
|
6050
|
-
* @returns DatabaseResult indicating operation success
|
|
6051
|
-
*
|
|
6052
|
-
* @example
|
|
6053
|
-
* ```typescript
|
|
6054
|
-
* // Delete specific cache entry
|
|
6055
|
-
* await cache.del('user:123');
|
|
6056
|
-
*
|
|
6057
|
-
* // Delete session on logout
|
|
6058
|
-
* await cache.del(`session:${sessionId}`);
|
|
6059
|
-
*
|
|
6060
|
-
* // Delete cached configuration
|
|
6061
|
-
* await cache.del('config:feature-flags');
|
|
6062
|
-
* ```
|
|
7616
|
+
* Handle secondary adapter failures
|
|
6063
7617
|
*/
|
|
6064
|
-
|
|
7618
|
+
handleSecondaryFailure(failures) {
|
|
7619
|
+
const errorMessages = failures.map(
|
|
7620
|
+
(f) => f.status === "rejected" ? f.reason?.message : "Unknown error"
|
|
7621
|
+
).join(", ");
|
|
7622
|
+
const message = `Multi-write secondary failure: ${errorMessages}`;
|
|
7623
|
+
switch (this.config.onSecondaryFailure) {
|
|
7624
|
+
case "log":
|
|
7625
|
+
console.log(`[MultiWrite] ${message}`);
|
|
7626
|
+
break;
|
|
7627
|
+
case "warn":
|
|
7628
|
+
console.warn(`[MultiWrite] ${message}`);
|
|
7629
|
+
break;
|
|
7630
|
+
case "throw":
|
|
7631
|
+
throw new errors.DatabasePackageError(message, errors$1.ERROR_CODES.DB_UPDATE_FAILED);
|
|
7632
|
+
}
|
|
7633
|
+
}
|
|
7634
|
+
};
|
|
7635
|
+
var EMA_EXISTING_WEIGHT = 0.8;
|
|
7636
|
+
var EMA_NEW_WEIGHT = 0.2;
|
|
7637
|
+
var MultiReadAdapter = class {
|
|
7638
|
+
static {
|
|
7639
|
+
__name(this, "MultiReadAdapter");
|
|
7640
|
+
}
|
|
7641
|
+
baseAdapter;
|
|
7642
|
+
config;
|
|
7643
|
+
currentReadIndex = 0;
|
|
7644
|
+
replicaHealth = /* @__PURE__ */ new Map();
|
|
7645
|
+
healthCheckTimer;
|
|
7646
|
+
constructor(baseAdapter, config) {
|
|
7647
|
+
this.baseAdapter = baseAdapter;
|
|
7648
|
+
this.config = {
|
|
7649
|
+
strategy: "round-robin",
|
|
7650
|
+
fallbackToPrimary: true,
|
|
7651
|
+
healthCheckInterval: 3e4,
|
|
7652
|
+
maxFailures: 3,
|
|
7653
|
+
...config
|
|
7654
|
+
};
|
|
7655
|
+
this.initializeHealthTracking();
|
|
7656
|
+
if (this.config.adapters.length > 0) {
|
|
7657
|
+
this.startHealthChecks();
|
|
7658
|
+
}
|
|
7659
|
+
}
|
|
7660
|
+
async initialize() {
|
|
7661
|
+
return this.baseAdapter.initialize();
|
|
7662
|
+
}
|
|
7663
|
+
async close() {
|
|
7664
|
+
this.stopHealthChecks();
|
|
7665
|
+
return this.baseAdapter.close();
|
|
7666
|
+
}
|
|
7667
|
+
async connect() {
|
|
7668
|
+
await this.baseAdapter.connect();
|
|
7669
|
+
for (const replica of this.config.adapters) {
|
|
7670
|
+
if (typeof replica.connect === "function") {
|
|
7671
|
+
await replica.connect();
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7674
|
+
}
|
|
7675
|
+
async disconnect() {
|
|
7676
|
+
await this.baseAdapter.disconnect();
|
|
7677
|
+
for (const replica of this.config.adapters) {
|
|
7678
|
+
if (typeof replica.disconnect === "function") {
|
|
7679
|
+
await replica.disconnect();
|
|
7680
|
+
}
|
|
7681
|
+
}
|
|
7682
|
+
}
|
|
7683
|
+
getClient() {
|
|
7684
|
+
return this.baseAdapter.getClient();
|
|
7685
|
+
}
|
|
7686
|
+
async query(sql2, params) {
|
|
7687
|
+
return this.baseAdapter.query(sql2, params);
|
|
7688
|
+
}
|
|
7689
|
+
registerTable(name, table, idColumn) {
|
|
7690
|
+
this.baseAdapter.registerTable(name, table, idColumn);
|
|
7691
|
+
for (const replica of this.config.adapters) {
|
|
7692
|
+
if (typeof replica.registerTable === "function") {
|
|
7693
|
+
replica.registerTable(name, table, idColumn);
|
|
7694
|
+
}
|
|
7695
|
+
}
|
|
7696
|
+
}
|
|
7697
|
+
// Read operations - use replicas with load balancing
|
|
7698
|
+
async findById(table, id) {
|
|
7699
|
+
return this.readFromReplicas((adapter) => adapter.findById(table, id));
|
|
7700
|
+
}
|
|
7701
|
+
async findMany(table, options) {
|
|
7702
|
+
return this.readFromReplicas(
|
|
7703
|
+
(adapter) => adapter.findMany(table, options)
|
|
7704
|
+
);
|
|
7705
|
+
}
|
|
7706
|
+
async exists(table, id) {
|
|
7707
|
+
return this.readFromReplicas((adapter) => adapter.exists(table, id));
|
|
7708
|
+
}
|
|
7709
|
+
async count(table, filter) {
|
|
7710
|
+
return this.readFromReplicas((adapter) => adapter.count(table, filter));
|
|
7711
|
+
}
|
|
7712
|
+
// Write operations - always use primary
|
|
7713
|
+
async create(table, data) {
|
|
7714
|
+
return this.baseAdapter.create(table, data);
|
|
7715
|
+
}
|
|
7716
|
+
async update(table, id, data) {
|
|
7717
|
+
return this.baseAdapter.update(table, id, data);
|
|
7718
|
+
}
|
|
7719
|
+
async delete(table, id) {
|
|
7720
|
+
return this.baseAdapter.delete(table, id);
|
|
7721
|
+
}
|
|
7722
|
+
async transaction(callback) {
|
|
7723
|
+
return this.baseAdapter.transaction(callback);
|
|
7724
|
+
}
|
|
7725
|
+
async healthCheck() {
|
|
7726
|
+
return this.baseAdapter.healthCheck();
|
|
7727
|
+
}
|
|
7728
|
+
/**
|
|
7729
|
+
* Read from replica adapters using configured strategy
|
|
7730
|
+
*/
|
|
7731
|
+
async readFromReplicas(fn) {
|
|
7732
|
+
const healthyReplicas = this.config.adapters.filter(
|
|
7733
|
+
(adapter) => this.isReplicaHealthy(adapter)
|
|
7734
|
+
);
|
|
7735
|
+
if (healthyReplicas.length === 0) {
|
|
7736
|
+
if (this.config.fallbackToPrimary) {
|
|
7737
|
+
return fn(this.baseAdapter);
|
|
7738
|
+
}
|
|
7739
|
+
return failure(
|
|
7740
|
+
new errors.DatabasePackageError(
|
|
7741
|
+
"No healthy read replicas available",
|
|
7742
|
+
errors$1.ERROR_CODES.DB_CONNECTION_FAILED
|
|
7743
|
+
)
|
|
7744
|
+
);
|
|
7745
|
+
}
|
|
7746
|
+
const selectedReplica = this.selectReplica(healthyReplicas);
|
|
7747
|
+
const startTime = Date.now();
|
|
6065
7748
|
try {
|
|
6066
|
-
await
|
|
6067
|
-
|
|
7749
|
+
const result = await fn(selectedReplica);
|
|
7750
|
+
this.updateHealthMetrics(selectedReplica, true, Date.now() - startTime);
|
|
7751
|
+
if (result.success) {
|
|
7752
|
+
return result;
|
|
7753
|
+
}
|
|
7754
|
+
if (this.config.fallbackToPrimary) {
|
|
7755
|
+
this.updateHealthMetrics(
|
|
7756
|
+
selectedReplica,
|
|
7757
|
+
false,
|
|
7758
|
+
Date.now() - startTime
|
|
7759
|
+
);
|
|
7760
|
+
return fn(this.baseAdapter);
|
|
7761
|
+
}
|
|
7762
|
+
return result;
|
|
6068
7763
|
} catch (error) {
|
|
7764
|
+
this.updateHealthMetrics(selectedReplica, false, Date.now() - startTime);
|
|
7765
|
+
if (this.config.fallbackToPrimary) {
|
|
7766
|
+
return fn(this.baseAdapter);
|
|
7767
|
+
}
|
|
6069
7768
|
return failure(
|
|
6070
|
-
new errors.
|
|
6071
|
-
|
|
6072
|
-
errors$1.
|
|
6073
|
-
{
|
|
6074
|
-
context: {
|
|
6075
|
-
source: "RedisCache.del",
|
|
6076
|
-
key,
|
|
6077
|
-
cause: error
|
|
6078
|
-
}
|
|
6079
|
-
}
|
|
7769
|
+
new errors.DatabasePackageError(
|
|
7770
|
+
`Read from replica failed: ${error.message}`,
|
|
7771
|
+
errors$1.ERROR_CODES.DB_QUERY_FAILED,
|
|
7772
|
+
{ cause: error }
|
|
6080
7773
|
)
|
|
6081
7774
|
);
|
|
6082
7775
|
}
|
|
6083
7776
|
}
|
|
6084
7777
|
/**
|
|
6085
|
-
*
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
7778
|
+
* Select a replica based on load balancing strategy
|
|
7779
|
+
*/
|
|
7780
|
+
selectReplica(replicas) {
|
|
7781
|
+
switch (this.config.strategy) {
|
|
7782
|
+
case "round-robin":
|
|
7783
|
+
this.currentReadIndex = (this.currentReadIndex + 1) % replicas.length;
|
|
7784
|
+
return replicas[this.currentReadIndex];
|
|
7785
|
+
case "random":
|
|
7786
|
+
return replicas[Math.floor(Math.random() * replicas.length)];
|
|
7787
|
+
case "fastest":
|
|
7788
|
+
return replicas.reduce((fastest, current) => {
|
|
7789
|
+
const fastestHealth = this.replicaHealth.get(fastest);
|
|
7790
|
+
const currentHealth = this.replicaHealth.get(current);
|
|
7791
|
+
if (!fastestHealth || !currentHealth) return fastest;
|
|
7792
|
+
return currentHealth.avgResponseTime < fastestHealth.avgResponseTime ? current : fastest;
|
|
7793
|
+
});
|
|
7794
|
+
case "least-conn":
|
|
7795
|
+
return this.selectReplica(
|
|
7796
|
+
replicas.filter((r) => this.isReplicaHealthy(r))
|
|
7797
|
+
);
|
|
7798
|
+
default:
|
|
7799
|
+
return replicas[0];
|
|
7800
|
+
}
|
|
7801
|
+
}
|
|
7802
|
+
/**
|
|
7803
|
+
* Initialize health tracking for all replicas
|
|
7804
|
+
*/
|
|
7805
|
+
initializeHealthTracking() {
|
|
7806
|
+
for (const adapter of this.config.adapters) {
|
|
7807
|
+
this.replicaHealth.set(adapter, {
|
|
7808
|
+
adapter,
|
|
7809
|
+
isHealthy: true,
|
|
7810
|
+
failureCount: 0,
|
|
7811
|
+
lastChecked: Date.now(),
|
|
7812
|
+
avgResponseTime: 0
|
|
7813
|
+
});
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7816
|
+
/**
|
|
7817
|
+
* Update health metrics for a replica
|
|
7818
|
+
*/
|
|
7819
|
+
updateHealthMetrics(adapter, success2, responseTime) {
|
|
7820
|
+
const health = this.replicaHealth.get(adapter);
|
|
7821
|
+
if (!health) return;
|
|
7822
|
+
if (success2) {
|
|
7823
|
+
health.failureCount = 0;
|
|
7824
|
+
health.isHealthy = true;
|
|
7825
|
+
health.avgResponseTime = health.avgResponseTime * EMA_EXISTING_WEIGHT + responseTime * EMA_NEW_WEIGHT;
|
|
7826
|
+
} else {
|
|
7827
|
+
health.failureCount++;
|
|
7828
|
+
if (health.failureCount >= this.config.maxFailures) {
|
|
7829
|
+
health.isHealthy = false;
|
|
7830
|
+
}
|
|
7831
|
+
}
|
|
7832
|
+
health.lastChecked = Date.now();
|
|
7833
|
+
}
|
|
7834
|
+
/**
|
|
7835
|
+
* Check if replica is healthy
|
|
7836
|
+
*/
|
|
7837
|
+
isReplicaHealthy(adapter) {
|
|
7838
|
+
const health = this.replicaHealth.get(adapter);
|
|
7839
|
+
return health?.isHealthy ?? true;
|
|
7840
|
+
}
|
|
7841
|
+
/**
|
|
7842
|
+
* Start health check interval
|
|
7843
|
+
*/
|
|
7844
|
+
startHealthChecks() {
|
|
7845
|
+
this.healthCheckTimer = setInterval(async () => {
|
|
7846
|
+
for (const adapter of this.config.adapters) {
|
|
7847
|
+
const startTime = Date.now();
|
|
7848
|
+
try {
|
|
7849
|
+
const result = await adapter.healthCheck();
|
|
7850
|
+
const responseTime = Date.now() - startTime;
|
|
7851
|
+
this.updateHealthMetrics(adapter, result.success, responseTime);
|
|
7852
|
+
} catch {
|
|
7853
|
+
this.updateHealthMetrics(adapter, false, Date.now() - startTime);
|
|
7854
|
+
}
|
|
7855
|
+
}
|
|
7856
|
+
}, this.config.healthCheckInterval);
|
|
7857
|
+
}
|
|
7858
|
+
/**
|
|
7859
|
+
* Stop health checks
|
|
7860
|
+
*/
|
|
7861
|
+
stopHealthChecks() {
|
|
7862
|
+
if (this.healthCheckTimer) {
|
|
7863
|
+
clearInterval(this.healthCheckTimer);
|
|
7864
|
+
this.healthCheckTimer = void 0;
|
|
7865
|
+
}
|
|
7866
|
+
}
|
|
7867
|
+
/**
|
|
7868
|
+
* Get health status of all replicas
|
|
7869
|
+
*/
|
|
7870
|
+
getHealthStatus() {
|
|
7871
|
+
const status = {};
|
|
7872
|
+
let index = 0;
|
|
7873
|
+
for (const [, health] of this.replicaHealth.entries()) {
|
|
7874
|
+
status[`replica-${index++}`] = health;
|
|
7875
|
+
}
|
|
7876
|
+
return status;
|
|
7877
|
+
}
|
|
7878
|
+
/**
|
|
7879
|
+
* Cleanup resources
|
|
7880
|
+
*/
|
|
7881
|
+
dispose() {
|
|
7882
|
+
this.stopHealthChecks();
|
|
7883
|
+
this.replicaHealth.clear();
|
|
7884
|
+
}
|
|
7885
|
+
};
|
|
7886
|
+
var USE_REPLICA_KEY = "use_replica";
|
|
7887
|
+
function UseReplica(options = {}) {
|
|
7888
|
+
return (target, propertyKey, descriptor) => {
|
|
7889
|
+
common.SetMetadata(USE_REPLICA_KEY, options)(target, propertyKey, descriptor);
|
|
7890
|
+
};
|
|
7891
|
+
}
|
|
7892
|
+
__name(UseReplica, "UseReplica");
|
|
7893
|
+
var RedisCache = class {
|
|
7894
|
+
static {
|
|
7895
|
+
__name(this, "RedisCache");
|
|
7896
|
+
}
|
|
7897
|
+
redis;
|
|
7898
|
+
defaultTTL;
|
|
7899
|
+
// Using shared logger instance from @plyaz/logger
|
|
7900
|
+
/**
|
|
7901
|
+
* Creates a new RedisCache instance.
|
|
7902
|
+
* @param config Redis configuration
|
|
6090
7903
|
*
|
|
6091
7904
|
* @example
|
|
6092
7905
|
* ```typescript
|
|
6093
|
-
* //
|
|
6094
|
-
*
|
|
7906
|
+
* // Basic configuration
|
|
7907
|
+
* const cache = new RedisCache({
|
|
7908
|
+
* url: 'redis://localhost:6379'
|
|
7909
|
+
* });
|
|
6095
7910
|
*
|
|
6096
|
-
* //
|
|
6097
|
-
*
|
|
7911
|
+
* // With custom default TTL
|
|
7912
|
+
* const cacheWithCustomTTL = new RedisCache({
|
|
7913
|
+
* url: 'redis://localhost:6379',
|
|
7914
|
+
* defaultTTL: 1800 // 30 minutes
|
|
7915
|
+
* });
|
|
6098
7916
|
*
|
|
6099
|
-
* //
|
|
6100
|
-
*
|
|
7917
|
+
* // Production configuration with options
|
|
7918
|
+
* const productionCache = new RedisCache({
|
|
7919
|
+
* url: 'redis://redis-cluster.example.com:6379',
|
|
7920
|
+
* defaultTTL: 3600,
|
|
7921
|
+
* // Additional Redis options can be passed here
|
|
7922
|
+
* });
|
|
7923
|
+
* ```
|
|
7924
|
+
*/
|
|
7925
|
+
constructor(config$1) {
|
|
7926
|
+
this.redis = new ioredis.Redis(config$1.url);
|
|
7927
|
+
this.defaultTTL = config$1.defaultTTL ?? config.NUMERIX.THIRTY_SIX_HUNDERD;
|
|
7928
|
+
}
|
|
7929
|
+
/**
|
|
7930
|
+
* Retrieves a value from cache by key.
|
|
7931
|
+
* Automatically handles JSON deserialization.
|
|
6101
7932
|
*
|
|
6102
|
-
*
|
|
6103
|
-
*
|
|
7933
|
+
* @param key Cache key
|
|
7934
|
+
* @returns DatabaseResult containing cached value or null if not found
|
|
6104
7935
|
*
|
|
6105
|
-
*
|
|
6106
|
-
*
|
|
7936
|
+
* @example
|
|
7937
|
+
* ```typescript
|
|
7938
|
+
* // Get user profile
|
|
7939
|
+
* const result = await cache.get<UserProfile>('profile:123');
|
|
7940
|
+
* if (result.success && result.value) {
|
|
7941
|
+
* console.log('User:', result.value.name);
|
|
7942
|
+
* } else {
|
|
7943
|
+
* console.log('Profile not found or cache error');
|
|
7944
|
+
* }
|
|
7945
|
+
*
|
|
7946
|
+
* // Get product list
|
|
7947
|
+
* const products = await cache.get<Product[]>('products:featured');
|
|
7948
|
+
* if (products.success && products.value) {
|
|
7949
|
+
* products.value.forEach(product => {
|
|
7950
|
+
* console.log(product.name, product.price);
|
|
7951
|
+
* });
|
|
7952
|
+
* }
|
|
6107
7953
|
* ```
|
|
6108
7954
|
*/
|
|
6109
|
-
async
|
|
7955
|
+
async get(key) {
|
|
6110
7956
|
try {
|
|
6111
|
-
const
|
|
6112
|
-
if (
|
|
6113
|
-
|
|
7957
|
+
const value = await this.redis.get(key);
|
|
7958
|
+
if (!value) {
|
|
7959
|
+
return success();
|
|
7960
|
+
}
|
|
7961
|
+
const parsed = JSON.parse(value);
|
|
7962
|
+
if (parsed && typeof parsed === "object") {
|
|
7963
|
+
return success(parsed);
|
|
6114
7964
|
}
|
|
6115
7965
|
return success();
|
|
6116
7966
|
} catch (error) {
|
|
6117
7967
|
return failure(
|
|
6118
7968
|
new errors.DatabaseError(
|
|
6119
|
-
"Cache
|
|
6120
|
-
errors$1.DATABASE_ERROR_CODES.
|
|
6121
|
-
{
|
|
6122
|
-
context: {
|
|
6123
|
-
source: "RedisCache.invalidatePattern",
|
|
6124
|
-
pattern,
|
|
6125
|
-
cause: error
|
|
6126
|
-
}
|
|
6127
|
-
}
|
|
7969
|
+
"Cache get failed",
|
|
7970
|
+
errors$1.DATABASE_ERROR_CODES.CACHE_GET_FAILED,
|
|
7971
|
+
{ context: { source: "RedisCache.get", key, cause: error } }
|
|
6128
7972
|
)
|
|
6129
7973
|
);
|
|
6130
7974
|
}
|
|
6131
7975
|
}
|
|
6132
7976
|
/**
|
|
6133
|
-
*
|
|
6134
|
-
*
|
|
7977
|
+
* Sets a value in cache with optional TTL.
|
|
7978
|
+
* Automatically handles JSON serialization.
|
|
6135
7979
|
*
|
|
6136
|
-
* @param
|
|
6137
|
-
* @param
|
|
6138
|
-
* @param
|
|
6139
|
-
* @returns
|
|
7980
|
+
* @param key Cache key
|
|
7981
|
+
* @param value Value to cache
|
|
7982
|
+
* @param ttl Time to live in seconds (uses default if not specified)
|
|
7983
|
+
* @returns DatabaseResult indicating operation success
|
|
6140
7984
|
*
|
|
6141
7985
|
* @example
|
|
6142
7986
|
* ```typescript
|
|
6143
|
-
* //
|
|
6144
|
-
*
|
|
6145
|
-
* // Returns: 'db:users:findById:eyJpYXJhbTAiOiIxMjMifQ=='
|
|
7987
|
+
* // Cache with default TTL
|
|
7988
|
+
* await cache.set('user:123', { id: 123, name: 'John' });
|
|
6146
7989
|
*
|
|
6147
|
-
* //
|
|
6148
|
-
*
|
|
6149
|
-
*
|
|
7990
|
+
* // Cache with custom TTL (5 minutes)
|
|
7991
|
+
* await cache.set('session:abc123', sessionData, 300);
|
|
7992
|
+
*
|
|
7993
|
+
* // Cache with long TTL for static data
|
|
7994
|
+
* await cache.set('config:app-settings', appSettings, 86400); // 24 hours
|
|
7995
|
+
*
|
|
7996
|
+
* // Cache with short TTL for frequently changing data
|
|
7997
|
+
* await cache.set('metrics:realtime', realtimeData, 60); // 1 minute
|
|
7998
|
+
* ```
|
|
7999
|
+
*/
|
|
8000
|
+
async set(key, value, ttl) {
|
|
8001
|
+
try {
|
|
8002
|
+
const ttlValue = ttl ?? this.defaultTTL;
|
|
8003
|
+
await this.redis.set(key, JSON.stringify(value), "EX", ttlValue);
|
|
8004
|
+
return success();
|
|
8005
|
+
} catch (error) {
|
|
8006
|
+
return failure(
|
|
8007
|
+
new errors.DatabaseError(
|
|
8008
|
+
"Cache set failed",
|
|
8009
|
+
errors$1.DATABASE_ERROR_CODES.CACHE_SET_FAILED,
|
|
8010
|
+
{ context: { source: "RedisCache.set", key, cause: error } }
|
|
8011
|
+
)
|
|
8012
|
+
);
|
|
8013
|
+
}
|
|
8014
|
+
}
|
|
8015
|
+
/**
|
|
8016
|
+
* Deletes a value from cache.
|
|
8017
|
+
*
|
|
8018
|
+
* @param key Cache key
|
|
8019
|
+
* @returns DatabaseResult indicating operation success
|
|
8020
|
+
*
|
|
8021
|
+
* @example
|
|
8022
|
+
* ```typescript
|
|
8023
|
+
* // Delete specific cache entry
|
|
8024
|
+
* await cache.del('user:123');
|
|
8025
|
+
*
|
|
8026
|
+
* // Delete session on logout
|
|
8027
|
+
* await cache.del(`session:${sessionId}`);
|
|
8028
|
+
*
|
|
8029
|
+
* // Delete cached configuration
|
|
8030
|
+
* await cache.del('config:feature-flags');
|
|
8031
|
+
* ```
|
|
8032
|
+
*/
|
|
8033
|
+
async del(key) {
|
|
8034
|
+
try {
|
|
8035
|
+
await this.redis.del(key);
|
|
8036
|
+
return success();
|
|
8037
|
+
} catch (error) {
|
|
8038
|
+
return failure(
|
|
8039
|
+
new errors.DatabaseError(
|
|
8040
|
+
"Cache delete failed",
|
|
8041
|
+
errors$1.DATABASE_ERROR_CODES.CACHE_DELETE_FAILED,
|
|
8042
|
+
{
|
|
8043
|
+
context: {
|
|
8044
|
+
source: "RedisCache.del",
|
|
8045
|
+
key,
|
|
8046
|
+
cause: error
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
)
|
|
8050
|
+
);
|
|
8051
|
+
}
|
|
8052
|
+
}
|
|
8053
|
+
/**
|
|
8054
|
+
* Invalidates all cache entries matching a pattern.
|
|
8055
|
+
* Useful for clearing related cache entries when data changes.
|
|
8056
|
+
*
|
|
8057
|
+
* @param pattern Redis key pattern (e.g., 'users:*')
|
|
8058
|
+
* @returns DatabaseResult indicating operation success
|
|
8059
|
+
*
|
|
8060
|
+
* @example
|
|
8061
|
+
* ```typescript
|
|
8062
|
+
* // Invalidate all user-related caches
|
|
8063
|
+
* await cache.invalidatePattern('users:*');
|
|
8064
|
+
*
|
|
8065
|
+
* // Invalidate all caches for a specific category
|
|
8066
|
+
* await cache.invalidatePattern('products:category:electronics:*');
|
|
8067
|
+
*
|
|
8068
|
+
* // Invalidate all session caches
|
|
8069
|
+
* await cache.invalidatePattern('session:*');
|
|
8070
|
+
*
|
|
8071
|
+
* // Invalidate all caches for a tenant
|
|
8072
|
+
* await cache.invalidatePattern(`tenant:${tenantId}:*`);
|
|
8073
|
+
*
|
|
8074
|
+
* // Invalidate all analytics caches
|
|
8075
|
+
* await cache.invalidatePattern('analytics:*');
|
|
8076
|
+
* ```
|
|
8077
|
+
*/
|
|
8078
|
+
async invalidatePattern(pattern) {
|
|
8079
|
+
try {
|
|
8080
|
+
const keys = await this.redis.keys(pattern);
|
|
8081
|
+
if (keys.length > 0) {
|
|
8082
|
+
await this.redis.del(...keys);
|
|
8083
|
+
}
|
|
8084
|
+
return success();
|
|
8085
|
+
} catch (error) {
|
|
8086
|
+
return failure(
|
|
8087
|
+
new errors.DatabaseError(
|
|
8088
|
+
"Cache invalidate failed",
|
|
8089
|
+
errors$1.DATABASE_ERROR_CODES.CACHE_INVALIDATE_FAILED,
|
|
8090
|
+
{
|
|
8091
|
+
context: {
|
|
8092
|
+
source: "RedisCache.invalidatePattern",
|
|
8093
|
+
pattern,
|
|
8094
|
+
cause: error
|
|
8095
|
+
}
|
|
8096
|
+
}
|
|
8097
|
+
)
|
|
8098
|
+
);
|
|
8099
|
+
}
|
|
8100
|
+
}
|
|
8101
|
+
/**
|
|
8102
|
+
* Generates a cache key for database queries.
|
|
8103
|
+
* Creates consistent, URL-safe keys that include table, operation, and parameters.
|
|
8104
|
+
*
|
|
8105
|
+
* @param table Database table name
|
|
8106
|
+
* @param operation Database operation type
|
|
8107
|
+
* @param params Query parameters object
|
|
8108
|
+
* @returns Generated cache key
|
|
8109
|
+
*
|
|
8110
|
+
* @example
|
|
8111
|
+
* ```typescript
|
|
8112
|
+
* // Simple key generation
|
|
8113
|
+
* const key = cache.generateKey('users', 'findById', { id: '123' });
|
|
8114
|
+
* // Returns: 'db:users:findById:eyJpYXJhbTAiOiIxMjMifQ=='
|
|
8115
|
+
*
|
|
8116
|
+
* // Complex query key
|
|
8117
|
+
* const complexKey = cache.generateKey('products', 'findMany', {
|
|
8118
|
+
* filter: { field: 'category', operator: 'eq', value: 'electronics' },
|
|
6150
8119
|
* sort: [{ field: 'price', direction: 'asc' }],
|
|
6151
8120
|
* pagination: { limit: 20, offset: 0 }
|
|
6152
8121
|
* });
|
|
@@ -8476,6 +10445,934 @@ __name(exports.DataValidationPipe, "DataValidationPipe");
|
|
|
8476
10445
|
exports.DataValidationPipe = __decorateClass([
|
|
8477
10446
|
common.Injectable()
|
|
8478
10447
|
], exports.DataValidationPipe);
|
|
10448
|
+
var DESCRIPTION_MAX_LENGTH = 60;
|
|
10449
|
+
var FALLBACK_DESCRIPTION_LENGTH = 50;
|
|
10450
|
+
var PROGRESS_LOG_INTERVAL = 10;
|
|
10451
|
+
var ERROR_MESSAGE_MAX_LENGTH = 300;
|
|
10452
|
+
var MigrationManager = class {
|
|
10453
|
+
static {
|
|
10454
|
+
__name(this, "MigrationManager");
|
|
10455
|
+
}
|
|
10456
|
+
adapter;
|
|
10457
|
+
migrationsPath;
|
|
10458
|
+
tableName;
|
|
10459
|
+
schema;
|
|
10460
|
+
constructor(config) {
|
|
10461
|
+
this.adapter = config.adapter;
|
|
10462
|
+
this.migrationsPath = path__namespace.resolve(config.migrationsPath ?? "./migrations");
|
|
10463
|
+
this.schema = config.schema ?? "public";
|
|
10464
|
+
this.tableName = this.schema !== "public" ? `${this.schema}.${config.tableName ?? "schema_migrations"}` : config.tableName ?? "schema_migrations";
|
|
10465
|
+
}
|
|
10466
|
+
/**
|
|
10467
|
+
* Initialize migrations table if it doesn't exist
|
|
10468
|
+
*/
|
|
10469
|
+
async initialize() {
|
|
10470
|
+
try {
|
|
10471
|
+
const createTableSQL = `
|
|
10472
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
10473
|
+
version VARCHAR(255) PRIMARY KEY,
|
|
10474
|
+
name VARCHAR(255) NOT NULL,
|
|
10475
|
+
file_path VARCHAR(500),
|
|
10476
|
+
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
10477
|
+
execution_time INTEGER NOT NULL
|
|
10478
|
+
)
|
|
10479
|
+
`;
|
|
10480
|
+
if (typeof this.adapter.query === "function") {
|
|
10481
|
+
await this.adapter.query(createTableSQL);
|
|
10482
|
+
await this.adapter.query(
|
|
10483
|
+
`
|
|
10484
|
+
DO $$
|
|
10485
|
+
BEGIN
|
|
10486
|
+
IF NOT EXISTS (
|
|
10487
|
+
SELECT 1 FROM information_schema.columns
|
|
10488
|
+
WHERE table_name = '${this.tableName.split(".").pop()}'
|
|
10489
|
+
AND column_name = 'file_path'
|
|
10490
|
+
) THEN
|
|
10491
|
+
ALTER TABLE ${this.tableName} ADD COLUMN file_path VARCHAR(500);
|
|
10492
|
+
END IF;
|
|
10493
|
+
END $$;
|
|
10494
|
+
`
|
|
10495
|
+
).catch(() => {
|
|
10496
|
+
});
|
|
10497
|
+
}
|
|
10498
|
+
return success();
|
|
10499
|
+
} catch (error) {
|
|
10500
|
+
return failure(
|
|
10501
|
+
new errors.DatabaseError(
|
|
10502
|
+
`Failed to initialize migrations table: ${error.message}`,
|
|
10503
|
+
errors$1.DATABASE_ERROR_CODES.INIT_FAILED,
|
|
10504
|
+
{ cause: error }
|
|
10505
|
+
)
|
|
10506
|
+
);
|
|
10507
|
+
}
|
|
10508
|
+
}
|
|
10509
|
+
/**
|
|
10510
|
+
* Discover migration files from migrations directory (including subdirectories)
|
|
10511
|
+
*/
|
|
10512
|
+
async discoverMigrations() {
|
|
10513
|
+
if (!fs__namespace.existsSync(this.migrationsPath)) {
|
|
10514
|
+
return [];
|
|
10515
|
+
}
|
|
10516
|
+
const migrations = [];
|
|
10517
|
+
const scanDirectory = /* @__PURE__ */ __name((dir) => {
|
|
10518
|
+
const entries = fs__namespace.readdirSync(dir, { withFileTypes: true });
|
|
10519
|
+
for (const entry of entries) {
|
|
10520
|
+
const fullPath = path__namespace.join(dir, entry.name);
|
|
10521
|
+
if (entry.isDirectory()) {
|
|
10522
|
+
scanDirectory(fullPath);
|
|
10523
|
+
} else if (entry.isFile()) {
|
|
10524
|
+
const match = entry.name.match(/^(\d+)_(.+)\.(ts|js|sql)$/);
|
|
10525
|
+
if (match) {
|
|
10526
|
+
const [, version, name] = match;
|
|
10527
|
+
migrations.push({
|
|
10528
|
+
filePath: fullPath,
|
|
10529
|
+
version,
|
|
10530
|
+
name: name.replace(/_/g, " ")
|
|
10531
|
+
});
|
|
10532
|
+
}
|
|
10533
|
+
}
|
|
10534
|
+
}
|
|
10535
|
+
}, "scanDirectory");
|
|
10536
|
+
scanDirectory(this.migrationsPath);
|
|
10537
|
+
return migrations.sort((a, b) => a.version.localeCompare(b.version));
|
|
10538
|
+
}
|
|
10539
|
+
/**
|
|
10540
|
+
* Parse SQL content to extract UP and DOWN sections
|
|
10541
|
+
*/
|
|
10542
|
+
parseSqlSections(sql2) {
|
|
10543
|
+
const hasUpMarker = sql2.includes("-- UP");
|
|
10544
|
+
const hasDownMarker = sql2.includes("-- DOWN");
|
|
10545
|
+
if (hasUpMarker && hasDownMarker) {
|
|
10546
|
+
const parts = sql2.split("-- DOWN");
|
|
10547
|
+
return {
|
|
10548
|
+
upSQL: parts[0].replace("-- UP", "").trim(),
|
|
10549
|
+
downSQL: parts[1].trim()
|
|
10550
|
+
};
|
|
10551
|
+
}
|
|
10552
|
+
if (hasDownMarker) {
|
|
10553
|
+
const parts = sql2.split("-- DOWN");
|
|
10554
|
+
return {
|
|
10555
|
+
upSQL: parts[0].trim(),
|
|
10556
|
+
downSQL: parts[1].trim()
|
|
10557
|
+
};
|
|
10558
|
+
}
|
|
10559
|
+
return { upSQL: sql2.trim(), downSQL: null };
|
|
10560
|
+
}
|
|
10561
|
+
/**
|
|
10562
|
+
* Process dollar-quoted string delimiters ($$ or $tag$)
|
|
10563
|
+
* Returns updated state for tracking if we're inside a dollar block
|
|
10564
|
+
*/
|
|
10565
|
+
processDollarDelimiters(line, inDollarBlock, dollarTag) {
|
|
10566
|
+
const dollarMatch = line.match(/\$([a-zA-Z_]*)\$/g);
|
|
10567
|
+
if (!dollarMatch) return { inDollarBlock, dollarTag };
|
|
10568
|
+
let currentInBlock = inDollarBlock;
|
|
10569
|
+
let currentTag = dollarTag;
|
|
10570
|
+
for (const match of dollarMatch) {
|
|
10571
|
+
if (!currentInBlock) {
|
|
10572
|
+
currentInBlock = true;
|
|
10573
|
+
currentTag = match;
|
|
10574
|
+
} else if (match === currentTag) {
|
|
10575
|
+
currentInBlock = false;
|
|
10576
|
+
currentTag = "";
|
|
10577
|
+
}
|
|
10578
|
+
}
|
|
10579
|
+
return { inDollarBlock: currentInBlock, dollarTag: currentTag };
|
|
10580
|
+
}
|
|
10581
|
+
/**
|
|
10582
|
+
* Filter out comment-only statements
|
|
10583
|
+
*/
|
|
10584
|
+
isNonCommentStatement(statement) {
|
|
10585
|
+
const withoutComments = statement.replace(/--.*$/gm, "").trim();
|
|
10586
|
+
return withoutComments.length > 0;
|
|
10587
|
+
}
|
|
10588
|
+
/**
|
|
10589
|
+
* Split SQL into individual statements for better error reporting
|
|
10590
|
+
* Handles $$ delimited blocks (functions, triggers) correctly
|
|
10591
|
+
*/
|
|
10592
|
+
splitSqlStatements(sql2) {
|
|
10593
|
+
const statements = [];
|
|
10594
|
+
let current = "";
|
|
10595
|
+
let inDollarBlock = false;
|
|
10596
|
+
let dollarTag = "";
|
|
10597
|
+
for (const line of sql2.split("\n")) {
|
|
10598
|
+
const trimmedLine = line.trim();
|
|
10599
|
+
const isEmptyOrComment = trimmedLine === "" || trimmedLine.startsWith("--");
|
|
10600
|
+
current += line + "\n";
|
|
10601
|
+
if (isEmptyOrComment) continue;
|
|
10602
|
+
const dollarState = this.processDollarDelimiters(line, inDollarBlock, dollarTag);
|
|
10603
|
+
inDollarBlock = dollarState.inDollarBlock;
|
|
10604
|
+
dollarTag = dollarState.dollarTag;
|
|
10605
|
+
const isEndOfStatement = !inDollarBlock && trimmedLine.endsWith(";");
|
|
10606
|
+
if (isEndOfStatement && current.trim()) {
|
|
10607
|
+
statements.push(current.trim());
|
|
10608
|
+
current = "";
|
|
10609
|
+
}
|
|
10610
|
+
}
|
|
10611
|
+
if (current.trim()) {
|
|
10612
|
+
statements.push(current.trim());
|
|
10613
|
+
}
|
|
10614
|
+
return statements.filter((s) => this.isNonCommentStatement(s));
|
|
10615
|
+
}
|
|
10616
|
+
/**
|
|
10617
|
+
* Extract a short description from a SQL statement for logging
|
|
10618
|
+
*/
|
|
10619
|
+
getStatementDescription(statement) {
|
|
10620
|
+
const firstLine = statement.split("\n").find((l) => l.trim() && !l.trim().startsWith("--"))?.trim() ?? "";
|
|
10621
|
+
const patterns = [
|
|
10622
|
+
/^(CREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|INDEX|UNIQUE\s+INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+NOT\s+EXISTS\s+)?([^\s(]+)/i,
|
|
10623
|
+
/^(ALTER\s+TABLE)\s+([^\s]+)/i,
|
|
10624
|
+
/^(DROP\s+(?:TABLE|INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+EXISTS\s+)?([^\s(;]+)/i,
|
|
10625
|
+
/^(INSERT\s+INTO)\s+([^\s(]+)/i,
|
|
10626
|
+
/^(COMMENT\s+ON\s+(?:TABLE|COLUMN|INDEX|FUNCTION|TYPE))\s+([^\s]+)/i,
|
|
10627
|
+
/^(GRANT|REVOKE)\s+.+\s+ON\s+([^\s]+)/i
|
|
10628
|
+
];
|
|
10629
|
+
for (const pattern of patterns) {
|
|
10630
|
+
const match = firstLine.match(pattern);
|
|
10631
|
+
if (match) {
|
|
10632
|
+
return `${match[1]} ${match[2]}`.slice(0, DESCRIPTION_MAX_LENGTH);
|
|
10633
|
+
}
|
|
10634
|
+
}
|
|
10635
|
+
const truncated = firstLine.slice(0, FALLBACK_DESCRIPTION_LENGTH);
|
|
10636
|
+
const suffix = firstLine.length > FALLBACK_DESCRIPTION_LENGTH ? "..." : "";
|
|
10637
|
+
return truncated + suffix;
|
|
10638
|
+
}
|
|
10639
|
+
/**
|
|
10640
|
+
* Execute SQL statements individually with better error reporting
|
|
10641
|
+
*/
|
|
10642
|
+
async executeSqlStatements(adapter, sql2, migrationVersion) {
|
|
10643
|
+
const statements = this.splitSqlStatements(sql2);
|
|
10644
|
+
const total = statements.length;
|
|
10645
|
+
console.log(` → ${total} statements to execute`);
|
|
10646
|
+
for (let i = 0; i < statements.length; i++) {
|
|
10647
|
+
const statement = statements[i];
|
|
10648
|
+
const description = this.getStatementDescription(statement);
|
|
10649
|
+
try {
|
|
10650
|
+
await adapter.query(statement);
|
|
10651
|
+
const isInterval = (i + 1) % PROGRESS_LOG_INTERVAL === 0;
|
|
10652
|
+
const isLast = i === total - 1;
|
|
10653
|
+
const isSignificant = Boolean(description.match(/^(CREATE TABLE|CREATE FUNCTION|CREATE TRIGGER)/i));
|
|
10654
|
+
if (isInterval || isLast || isSignificant) {
|
|
10655
|
+
console.log(` ✓ [${i + 1}/${total}] ${description}`);
|
|
10656
|
+
}
|
|
10657
|
+
} catch (error) {
|
|
10658
|
+
console.log(` ✗ [${i + 1}/${total}] ${description}`);
|
|
10659
|
+
const rawMessage = error.message;
|
|
10660
|
+
const errorMessage = rawMessage.replace(/^SQL Error:\s*/i, "").replace(/^Failed to execute query:.*?-\s*/i, "").slice(0, ERROR_MESSAGE_MAX_LENGTH);
|
|
10661
|
+
throw new errors.DatabaseError(
|
|
10662
|
+
`Migration ${migrationVersion} failed at statement ${i + 1}/${total}:
|
|
10663
|
+
Statement: ${description}
|
|
10664
|
+
Error: ${errorMessage}`,
|
|
10665
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
|
|
10666
|
+
{ cause: error }
|
|
10667
|
+
);
|
|
10668
|
+
}
|
|
10669
|
+
}
|
|
10670
|
+
}
|
|
10671
|
+
/**
|
|
10672
|
+
* Load SQL migration from file
|
|
10673
|
+
*/
|
|
10674
|
+
loadSqlMigration(migrationFile) {
|
|
10675
|
+
const sql2 = fs__namespace.readFileSync(migrationFile.filePath, "utf-8");
|
|
10676
|
+
const { upSQL, downSQL } = this.parseSqlSections(sql2);
|
|
10677
|
+
return {
|
|
10678
|
+
version: migrationFile.version,
|
|
10679
|
+
name: migrationFile.name,
|
|
10680
|
+
up: /* @__PURE__ */ __name(async (adapter) => {
|
|
10681
|
+
if (typeof adapter.query === "function") {
|
|
10682
|
+
await this.executeSqlStatements(
|
|
10683
|
+
adapter,
|
|
10684
|
+
upSQL,
|
|
10685
|
+
migrationFile.version
|
|
10686
|
+
);
|
|
10687
|
+
}
|
|
10688
|
+
}, "up"),
|
|
10689
|
+
down: /* @__PURE__ */ __name(async (adapter) => {
|
|
10690
|
+
if (downSQL && typeof adapter.query === "function") {
|
|
10691
|
+
await this.executeSqlStatements(
|
|
10692
|
+
adapter,
|
|
10693
|
+
downSQL,
|
|
10694
|
+
migrationFile.version
|
|
10695
|
+
);
|
|
10696
|
+
} else {
|
|
10697
|
+
console.warn(
|
|
10698
|
+
`[Migrations] No DOWN migration for ${migrationFile.version}`
|
|
10699
|
+
);
|
|
10700
|
+
}
|
|
10701
|
+
}, "down")
|
|
10702
|
+
};
|
|
10703
|
+
}
|
|
10704
|
+
/**
|
|
10705
|
+
* Load TypeScript/JavaScript migration from file
|
|
10706
|
+
*/
|
|
10707
|
+
async loadJsMigration(migrationFile) {
|
|
10708
|
+
const importPath = migrationFile.filePath.startsWith("/") ? migrationFile.filePath : new URL(`file:///${migrationFile.filePath.replace(/\\/g, "/")}`).href;
|
|
10709
|
+
const migrationModule = await import(importPath);
|
|
10710
|
+
return {
|
|
10711
|
+
version: migrationFile.version,
|
|
10712
|
+
name: migrationFile.name,
|
|
10713
|
+
up: migrationModule.up ?? migrationModule.default?.up,
|
|
10714
|
+
down: migrationModule.down ?? migrationModule.default?.down
|
|
10715
|
+
};
|
|
10716
|
+
}
|
|
10717
|
+
/**
|
|
10718
|
+
* Load migration from file
|
|
10719
|
+
*/
|
|
10720
|
+
async loadMigration(migrationFile) {
|
|
10721
|
+
const ext = path__namespace.extname(migrationFile.filePath);
|
|
10722
|
+
switch (ext) {
|
|
10723
|
+
case ".sql":
|
|
10724
|
+
return this.loadSqlMigration(migrationFile);
|
|
10725
|
+
case ".ts":
|
|
10726
|
+
case ".js":
|
|
10727
|
+
return this.loadJsMigration(migrationFile);
|
|
10728
|
+
default:
|
|
10729
|
+
throw new errors.DatabaseError(
|
|
10730
|
+
`Unsupported migration file extension: ${ext}`,
|
|
10731
|
+
errors$1.DATABASE_ERROR_CODES.INVALID_PARAMETERS,
|
|
10732
|
+
{ cause: new Error(`Unsupported extension: ${ext}`) }
|
|
10733
|
+
);
|
|
10734
|
+
}
|
|
10735
|
+
}
|
|
10736
|
+
/**
|
|
10737
|
+
* Get applied migrations from database
|
|
10738
|
+
*/
|
|
10739
|
+
async getAppliedMigrations() {
|
|
10740
|
+
try {
|
|
10741
|
+
if (typeof this.adapter.query === "function") {
|
|
10742
|
+
const result = await this.adapter.query(
|
|
10743
|
+
`SELECT * FROM ${this.tableName} ORDER BY version ASC`
|
|
10744
|
+
);
|
|
10745
|
+
return Array.isArray(result) ? result : result.rows || [];
|
|
10746
|
+
}
|
|
10747
|
+
return [];
|
|
10748
|
+
} catch {
|
|
10749
|
+
return [];
|
|
10750
|
+
}
|
|
10751
|
+
}
|
|
10752
|
+
/**
|
|
10753
|
+
* Record migration as applied
|
|
10754
|
+
*/
|
|
10755
|
+
async recordMigration(version, name, executionTime, filePath) {
|
|
10756
|
+
if (typeof this.adapter.query === "function") {
|
|
10757
|
+
const relativePath = filePath ? path__namespace.relative(this.migrationsPath, filePath) : null;
|
|
10758
|
+
await this.adapter.query(
|
|
10759
|
+
`INSERT INTO ${this.tableName} (version, name, file_path, execution_time) VALUES ($1, $2, $3, $4)`,
|
|
10760
|
+
[version, name, relativePath, executionTime]
|
|
10761
|
+
);
|
|
10762
|
+
}
|
|
10763
|
+
}
|
|
10764
|
+
/**
|
|
10765
|
+
* Remove migration record (for rollback)
|
|
10766
|
+
*/
|
|
10767
|
+
async unrecordMigration(version) {
|
|
10768
|
+
if (typeof this.adapter.query === "function") {
|
|
10769
|
+
await this.adapter.query(
|
|
10770
|
+
`DELETE FROM ${this.tableName} WHERE version = $1`,
|
|
10771
|
+
[version]
|
|
10772
|
+
);
|
|
10773
|
+
}
|
|
10774
|
+
}
|
|
10775
|
+
/**
|
|
10776
|
+
* Get migration status (applied and pending)
|
|
10777
|
+
*/
|
|
10778
|
+
async status() {
|
|
10779
|
+
try {
|
|
10780
|
+
await this.initialize();
|
|
10781
|
+
const allMigrations = await this.discoverMigrations();
|
|
10782
|
+
const appliedMigrations = await this.getAppliedMigrations();
|
|
10783
|
+
const appliedVersions = new Set(appliedMigrations.map((m) => m.version));
|
|
10784
|
+
const pending = allMigrations.filter((m) => !appliedVersions.has(m.version)).map((m) => `${m.version}_${m.name}`);
|
|
10785
|
+
return success({
|
|
10786
|
+
applied: appliedMigrations,
|
|
10787
|
+
pending
|
|
10788
|
+
});
|
|
10789
|
+
} catch (error) {
|
|
10790
|
+
return failure(
|
|
10791
|
+
new errors.DatabaseError(
|
|
10792
|
+
`Failed to get migration status: ${error.message}`,
|
|
10793
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
|
|
10794
|
+
{ cause: error }
|
|
10795
|
+
)
|
|
10796
|
+
);
|
|
10797
|
+
}
|
|
10798
|
+
}
|
|
10799
|
+
/**
|
|
10800
|
+
* Run all pending migrations
|
|
10801
|
+
*/
|
|
10802
|
+
/* eslint-disable max-depth, complexity */
|
|
10803
|
+
async up(targetVersion) {
|
|
10804
|
+
try {
|
|
10805
|
+
await this.initialize();
|
|
10806
|
+
const allMigrations = await this.discoverMigrations();
|
|
10807
|
+
const appliedMigrations = await this.getAppliedMigrations();
|
|
10808
|
+
const appliedVersions = new Set(appliedMigrations.map((m) => m.version));
|
|
10809
|
+
let applied = 0;
|
|
10810
|
+
for (const migrationFile of allMigrations) {
|
|
10811
|
+
if (appliedVersions.has(migrationFile.version)) {
|
|
10812
|
+
continue;
|
|
10813
|
+
}
|
|
10814
|
+
if (targetVersion && migrationFile.version > targetVersion) {
|
|
10815
|
+
break;
|
|
10816
|
+
}
|
|
10817
|
+
console.log(
|
|
10818
|
+
`[Migrations] Applying ${migrationFile.version}_${migrationFile.name}...`
|
|
10819
|
+
);
|
|
10820
|
+
const migration = await this.loadMigration(migrationFile);
|
|
10821
|
+
const startTime = Date.now();
|
|
10822
|
+
if (typeof this.adapter.transaction === "function") {
|
|
10823
|
+
const txResult = await this.adapter.transaction(async () => {
|
|
10824
|
+
await migration.up(this.adapter);
|
|
10825
|
+
});
|
|
10826
|
+
if (!txResult.success) {
|
|
10827
|
+
throw txResult.error ?? new errors.DatabaseError(
|
|
10828
|
+
`Migration ${migration.version} failed`,
|
|
10829
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED
|
|
10830
|
+
);
|
|
10831
|
+
}
|
|
10832
|
+
} else {
|
|
10833
|
+
await migration.up(this.adapter);
|
|
10834
|
+
}
|
|
10835
|
+
const executionTime = Date.now() - startTime;
|
|
10836
|
+
await this.recordMigration(
|
|
10837
|
+
migration.version,
|
|
10838
|
+
migration.name,
|
|
10839
|
+
executionTime,
|
|
10840
|
+
migrationFile.filePath
|
|
10841
|
+
);
|
|
10842
|
+
console.log(
|
|
10843
|
+
`[Migrations] Applied ${migration.version} in ${executionTime}ms`
|
|
10844
|
+
);
|
|
10845
|
+
applied++;
|
|
10846
|
+
}
|
|
10847
|
+
return success(applied);
|
|
10848
|
+
} catch (error) {
|
|
10849
|
+
return failure(
|
|
10850
|
+
new errors.DatabaseError(
|
|
10851
|
+
`Migration failed: ${error.message}`,
|
|
10852
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
|
|
10853
|
+
{ cause: error }
|
|
10854
|
+
)
|
|
10855
|
+
);
|
|
10856
|
+
}
|
|
10857
|
+
}
|
|
10858
|
+
/**
|
|
10859
|
+
* Rollback last migration or rollback to specific version
|
|
10860
|
+
*/
|
|
10861
|
+
async down(steps = 1) {
|
|
10862
|
+
try {
|
|
10863
|
+
const appliedMigrations = await this.getAppliedMigrations();
|
|
10864
|
+
if (appliedMigrations.length === 0) {
|
|
10865
|
+
return success(0);
|
|
10866
|
+
}
|
|
10867
|
+
const toRollback = appliedMigrations.slice(-steps).reverse();
|
|
10868
|
+
let rolledBack = 0;
|
|
10869
|
+
for (const appliedMigration of toRollback) {
|
|
10870
|
+
console.log(
|
|
10871
|
+
`[Migrations] Rolling back ${appliedMigration.version}_${appliedMigration.name}...`
|
|
10872
|
+
);
|
|
10873
|
+
const allMigrations = await this.discoverMigrations();
|
|
10874
|
+
const migrationFile = allMigrations.find(
|
|
10875
|
+
(m) => m.version === appliedMigration.version
|
|
10876
|
+
);
|
|
10877
|
+
if (!migrationFile) {
|
|
10878
|
+
console.warn(
|
|
10879
|
+
`[Migrations] Migration file not found for version ${appliedMigration.version}`
|
|
10880
|
+
);
|
|
10881
|
+
continue;
|
|
10882
|
+
}
|
|
10883
|
+
const migration = await this.loadMigration(migrationFile);
|
|
10884
|
+
const startTime = Date.now();
|
|
10885
|
+
if (typeof this.adapter.transaction === "function") {
|
|
10886
|
+
const txResult = await this.adapter.transaction(async () => {
|
|
10887
|
+
await migration.down(this.adapter);
|
|
10888
|
+
});
|
|
10889
|
+
if (!txResult.success) {
|
|
10890
|
+
throw txResult.error ?? new errors.DatabaseError(
|
|
10891
|
+
`Rollback ${appliedMigration.version} failed`,
|
|
10892
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED
|
|
10893
|
+
);
|
|
10894
|
+
}
|
|
10895
|
+
} else {
|
|
10896
|
+
await migration.down(this.adapter);
|
|
10897
|
+
}
|
|
10898
|
+
const executionTime = Date.now() - startTime;
|
|
10899
|
+
await this.unrecordMigration(appliedMigration.version);
|
|
10900
|
+
console.log(
|
|
10901
|
+
`[Migrations] Rolled back ${appliedMigration.version} in ${executionTime}ms`
|
|
10902
|
+
);
|
|
10903
|
+
rolledBack++;
|
|
10904
|
+
}
|
|
10905
|
+
return success(rolledBack);
|
|
10906
|
+
} catch (error) {
|
|
10907
|
+
return failure(
|
|
10908
|
+
new errors.DatabaseError(
|
|
10909
|
+
`Rollback failed: ${error.message}`,
|
|
10910
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
|
|
10911
|
+
{ cause: error }
|
|
10912
|
+
)
|
|
10913
|
+
);
|
|
10914
|
+
}
|
|
10915
|
+
}
|
|
10916
|
+
/**
|
|
10917
|
+
* Reset database (rollback all migrations)
|
|
10918
|
+
*/
|
|
10919
|
+
async reset() {
|
|
10920
|
+
const appliedMigrations = await this.getAppliedMigrations();
|
|
10921
|
+
return this.down(appliedMigrations.length);
|
|
10922
|
+
}
|
|
10923
|
+
/**
|
|
10924
|
+
* Clear migration history (delete all records from tracking table)
|
|
10925
|
+
*
|
|
10926
|
+
* Use this to force fresh migrations in test/development environments
|
|
10927
|
+
* without rolling back actual database changes.
|
|
10928
|
+
*
|
|
10929
|
+
* @example
|
|
10930
|
+
* ```typescript
|
|
10931
|
+
* // Clear history and re-run all migrations
|
|
10932
|
+
* await migrationManager.clearHistory();
|
|
10933
|
+
* await migrationManager.up();
|
|
10934
|
+
* ```
|
|
10935
|
+
*/
|
|
10936
|
+
async clearHistory() {
|
|
10937
|
+
try {
|
|
10938
|
+
if (typeof this.adapter.query === "function") {
|
|
10939
|
+
await this.adapter.query(`DELETE FROM ${this.tableName}`);
|
|
10940
|
+
}
|
|
10941
|
+
return success();
|
|
10942
|
+
} catch (error) {
|
|
10943
|
+
return failure(
|
|
10944
|
+
new errors.DatabaseError(
|
|
10945
|
+
`Failed to clear migration history: ${error.message}`,
|
|
10946
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
|
|
10947
|
+
{ cause: error }
|
|
10948
|
+
)
|
|
10949
|
+
);
|
|
10950
|
+
}
|
|
10951
|
+
}
|
|
10952
|
+
};
|
|
10953
|
+
var DESCRIPTION_MAX_LENGTH2 = 60;
|
|
10954
|
+
var FALLBACK_DESCRIPTION_LENGTH2 = 50;
|
|
10955
|
+
var PROGRESS_LOG_INTERVAL2 = 10;
|
|
10956
|
+
var ERROR_MESSAGE_MAX_LENGTH2 = 300;
|
|
10957
|
+
var SeedManager = class {
|
|
10958
|
+
static {
|
|
10959
|
+
__name(this, "SeedManager");
|
|
10960
|
+
}
|
|
10961
|
+
adapter;
|
|
10962
|
+
seedsPath;
|
|
10963
|
+
tableName;
|
|
10964
|
+
schema;
|
|
10965
|
+
skipExisting;
|
|
10966
|
+
constructor(config) {
|
|
10967
|
+
this.adapter = config.adapter;
|
|
10968
|
+
this.seedsPath = path__namespace.resolve(config.seedsPath ?? "./seeds");
|
|
10969
|
+
this.schema = config.schema ?? "public";
|
|
10970
|
+
this.tableName = this.schema !== "public" ? `${this.schema}.${config.tableName ?? "seed_history"}` : config.tableName ?? "seed_history";
|
|
10971
|
+
this.skipExisting = config.skipExisting ?? false;
|
|
10972
|
+
}
|
|
10973
|
+
/**
|
|
10974
|
+
* Initialize seeds tracking table if it doesn't exist
|
|
10975
|
+
*/
|
|
10976
|
+
async initialize() {
|
|
10977
|
+
try {
|
|
10978
|
+
const createTableSQL = `
|
|
10979
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
10980
|
+
name VARCHAR(255) PRIMARY KEY,
|
|
10981
|
+
file_path VARCHAR(500),
|
|
10982
|
+
run_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
10983
|
+
execution_time INTEGER NOT NULL
|
|
10984
|
+
)
|
|
10985
|
+
`;
|
|
10986
|
+
if (typeof this.adapter.query === "function") {
|
|
10987
|
+
await this.adapter.query(createTableSQL);
|
|
10988
|
+
await this.adapter.query(
|
|
10989
|
+
`
|
|
10990
|
+
DO $$
|
|
10991
|
+
BEGIN
|
|
10992
|
+
IF NOT EXISTS (
|
|
10993
|
+
SELECT 1 FROM information_schema.columns
|
|
10994
|
+
WHERE table_name = '${this.tableName.split(".").pop()}'
|
|
10995
|
+
AND column_name = 'file_path'
|
|
10996
|
+
) THEN
|
|
10997
|
+
ALTER TABLE ${this.tableName} ADD COLUMN file_path VARCHAR(500);
|
|
10998
|
+
END IF;
|
|
10999
|
+
END $$;
|
|
11000
|
+
`
|
|
11001
|
+
).catch(() => {
|
|
11002
|
+
});
|
|
11003
|
+
}
|
|
11004
|
+
return success();
|
|
11005
|
+
} catch (error) {
|
|
11006
|
+
return failure(
|
|
11007
|
+
new errors.DatabaseError(
|
|
11008
|
+
`Failed to initialize seeds table: ${error.message}`,
|
|
11009
|
+
errors$1.DATABASE_ERROR_CODES.INIT_FAILED,
|
|
11010
|
+
{ cause: error }
|
|
11011
|
+
)
|
|
11012
|
+
);
|
|
11013
|
+
}
|
|
11014
|
+
}
|
|
11015
|
+
/**
|
|
11016
|
+
* Discover seed files from seeds directory
|
|
11017
|
+
*/
|
|
11018
|
+
async discoverSeeds() {
|
|
11019
|
+
if (!fs__namespace.existsSync(this.seedsPath)) {
|
|
11020
|
+
return [];
|
|
11021
|
+
}
|
|
11022
|
+
const files = fs__namespace.readdirSync(this.seedsPath);
|
|
11023
|
+
const seeds = [];
|
|
11024
|
+
for (const file of files) {
|
|
11025
|
+
const match = file.match(/^(\d+)_(.+)\.(ts|js|sql)$/);
|
|
11026
|
+
if (match) {
|
|
11027
|
+
const [, order, name] = match;
|
|
11028
|
+
seeds.push({
|
|
11029
|
+
filePath: path__namespace.join(this.seedsPath, file),
|
|
11030
|
+
order: Number.parseInt(order, 10),
|
|
11031
|
+
name
|
|
11032
|
+
});
|
|
11033
|
+
}
|
|
11034
|
+
}
|
|
11035
|
+
return seeds.sort((a, b) => a.order - b.order);
|
|
11036
|
+
}
|
|
11037
|
+
/**
|
|
11038
|
+
* Process dollar-quoted string delimiters ($$ or $tag$)
|
|
11039
|
+
*/
|
|
11040
|
+
processDollarDelimiters(line, inDollarBlock, dollarTag) {
|
|
11041
|
+
const dollarMatch = line.match(/\$([a-zA-Z_]*)\$/g);
|
|
11042
|
+
if (!dollarMatch) return { inDollarBlock, dollarTag };
|
|
11043
|
+
let currentInBlock = inDollarBlock;
|
|
11044
|
+
let currentTag = dollarTag;
|
|
11045
|
+
for (const match of dollarMatch) {
|
|
11046
|
+
if (!currentInBlock) {
|
|
11047
|
+
currentInBlock = true;
|
|
11048
|
+
currentTag = match;
|
|
11049
|
+
} else if (match === currentTag) {
|
|
11050
|
+
currentInBlock = false;
|
|
11051
|
+
currentTag = "";
|
|
11052
|
+
}
|
|
11053
|
+
}
|
|
11054
|
+
return { inDollarBlock: currentInBlock, dollarTag: currentTag };
|
|
11055
|
+
}
|
|
11056
|
+
/**
|
|
11057
|
+
* Filter out comment-only statements
|
|
11058
|
+
*/
|
|
11059
|
+
isNonCommentStatement(statement) {
|
|
11060
|
+
const withoutComments = statement.replace(/--.*$/gm, "").trim();
|
|
11061
|
+
return withoutComments.length > 0;
|
|
11062
|
+
}
|
|
11063
|
+
/**
|
|
11064
|
+
* Split SQL into individual statements for better error reporting
|
|
11065
|
+
*/
|
|
11066
|
+
splitSqlStatements(sql2) {
|
|
11067
|
+
const statements = [];
|
|
11068
|
+
let current = "";
|
|
11069
|
+
let inDollarBlock = false;
|
|
11070
|
+
let dollarTag = "";
|
|
11071
|
+
for (const line of sql2.split("\n")) {
|
|
11072
|
+
const trimmedLine = line.trim();
|
|
11073
|
+
const isEmptyOrComment = trimmedLine === "" || trimmedLine.startsWith("--");
|
|
11074
|
+
current += line + "\n";
|
|
11075
|
+
if (isEmptyOrComment) continue;
|
|
11076
|
+
const dollarState = this.processDollarDelimiters(line, inDollarBlock, dollarTag);
|
|
11077
|
+
inDollarBlock = dollarState.inDollarBlock;
|
|
11078
|
+
dollarTag = dollarState.dollarTag;
|
|
11079
|
+
if (!inDollarBlock && trimmedLine.endsWith(";") && current.trim()) {
|
|
11080
|
+
statements.push(current.trim());
|
|
11081
|
+
current = "";
|
|
11082
|
+
}
|
|
11083
|
+
}
|
|
11084
|
+
if (current.trim()) {
|
|
11085
|
+
statements.push(current.trim());
|
|
11086
|
+
}
|
|
11087
|
+
return statements.filter((s) => this.isNonCommentStatement(s));
|
|
11088
|
+
}
|
|
11089
|
+
/**
|
|
11090
|
+
* Extract a short description from a SQL statement for logging
|
|
11091
|
+
*/
|
|
11092
|
+
getStatementDescription(statement) {
|
|
11093
|
+
const firstLine = statement.split("\n").find((l) => l.trim() && !l.trim().startsWith("--"))?.trim() ?? "";
|
|
11094
|
+
const patterns = [
|
|
11095
|
+
/^(CREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|INDEX|UNIQUE\s+INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+NOT\s+EXISTS\s+)?([^\s(]+)/i,
|
|
11096
|
+
/^(ALTER\s+TABLE)\s+([^\s]+)/i,
|
|
11097
|
+
/^(DROP\s+(?:TABLE|INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+EXISTS\s+)?([^\s(;]+)/i,
|
|
11098
|
+
/^(INSERT\s+INTO)\s+([^\s(]+)/i,
|
|
11099
|
+
/^(COMMENT\s+ON\s+(?:TABLE|COLUMN|INDEX|FUNCTION|TYPE))\s+([^\s]+)/i,
|
|
11100
|
+
/^(GRANT|REVOKE)\s+.+\s+ON\s+([^\s]+)/i
|
|
11101
|
+
];
|
|
11102
|
+
for (const pattern of patterns) {
|
|
11103
|
+
const match = firstLine.match(pattern);
|
|
11104
|
+
if (match) {
|
|
11105
|
+
return `${match[1]} ${match[2]}`.slice(0, DESCRIPTION_MAX_LENGTH2);
|
|
11106
|
+
}
|
|
11107
|
+
}
|
|
11108
|
+
const truncated = firstLine.slice(0, FALLBACK_DESCRIPTION_LENGTH2);
|
|
11109
|
+
const suffix = firstLine.length > FALLBACK_DESCRIPTION_LENGTH2 ? "..." : "";
|
|
11110
|
+
return truncated + suffix;
|
|
11111
|
+
}
|
|
11112
|
+
/**
|
|
11113
|
+
* Execute SQL statements individually with better error reporting
|
|
11114
|
+
*/
|
|
11115
|
+
async executeSqlStatements(sql2, seedName) {
|
|
11116
|
+
const statements = this.splitSqlStatements(sql2);
|
|
11117
|
+
const total = statements.length;
|
|
11118
|
+
console.log(` → ${total} statements to execute`);
|
|
11119
|
+
for (let i = 0; i < statements.length; i++) {
|
|
11120
|
+
const statement = statements[i];
|
|
11121
|
+
const description = this.getStatementDescription(statement);
|
|
11122
|
+
try {
|
|
11123
|
+
await this.adapter.query(statement);
|
|
11124
|
+
const isInterval = (i + 1) % PROGRESS_LOG_INTERVAL2 === 0;
|
|
11125
|
+
const isLast = i === total - 1;
|
|
11126
|
+
const isSignificant = Boolean(description.match(/^(INSERT INTO)/i));
|
|
11127
|
+
if (isInterval || isLast || isSignificant) {
|
|
11128
|
+
console.log(` ✓ [${i + 1}/${total}] ${description}`);
|
|
11129
|
+
}
|
|
11130
|
+
} catch (error) {
|
|
11131
|
+
console.log(` ✗ [${i + 1}/${total}] ${description}`);
|
|
11132
|
+
const rawMessage = error.message;
|
|
11133
|
+
const errorMessage = rawMessage.replace(/^SQL Error:\s*/i, "").replace(/^Failed to execute query:.*?-\s*/i, "").slice(0, ERROR_MESSAGE_MAX_LENGTH2);
|
|
11134
|
+
throw new errors.DatabaseError(
|
|
11135
|
+
`Seed "${seedName}" failed at statement ${i + 1}/${total}:
|
|
11136
|
+
Statement: ${description}
|
|
11137
|
+
Error: ${errorMessage}`,
|
|
11138
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
|
|
11139
|
+
{ cause: error }
|
|
11140
|
+
);
|
|
11141
|
+
}
|
|
11142
|
+
}
|
|
11143
|
+
}
|
|
11144
|
+
/**
|
|
11145
|
+
* Load SQL seed from file
|
|
11146
|
+
*/
|
|
11147
|
+
loadSqlSeed(seedFile) {
|
|
11148
|
+
const sql2 = fs__namespace.readFileSync(seedFile.filePath, "utf-8");
|
|
11149
|
+
return {
|
|
11150
|
+
name: seedFile.name,
|
|
11151
|
+
run: /* @__PURE__ */ __name(async () => {
|
|
11152
|
+
if (typeof this.adapter.query === "function") {
|
|
11153
|
+
await this.executeSqlStatements(sql2, seedFile.name);
|
|
11154
|
+
}
|
|
11155
|
+
}, "run"),
|
|
11156
|
+
// SQL seeds don't have cleanup by default
|
|
11157
|
+
cleanup: void 0
|
|
11158
|
+
};
|
|
11159
|
+
}
|
|
11160
|
+
/**
|
|
11161
|
+
* Load seed from file (supports .ts, .js, and .sql)
|
|
11162
|
+
*/
|
|
11163
|
+
// eslint-disable-next-line complexity
|
|
11164
|
+
async loadSeed(seedFile) {
|
|
11165
|
+
const ext = path__namespace.extname(seedFile.filePath);
|
|
11166
|
+
if (ext === ".sql") {
|
|
11167
|
+
return this.loadSqlSeed(seedFile);
|
|
11168
|
+
}
|
|
11169
|
+
const importPath = seedFile.filePath.startsWith("/") ? seedFile.filePath : new URL(`file:///${seedFile.filePath.replace(/\\/g, "/")}`).href;
|
|
11170
|
+
const seedModule = await import(importPath);
|
|
11171
|
+
return {
|
|
11172
|
+
name: seedFile.name,
|
|
11173
|
+
run: seedModule.run ?? seedModule.default?.run ?? seedModule.seed ?? seedModule.default,
|
|
11174
|
+
cleanup: seedModule.cleanup ?? seedModule.default?.cleanup
|
|
11175
|
+
};
|
|
11176
|
+
}
|
|
11177
|
+
/**
|
|
11178
|
+
* Get executed seeds from database
|
|
11179
|
+
*/
|
|
11180
|
+
async getExecutedSeeds() {
|
|
11181
|
+
try {
|
|
11182
|
+
if (typeof this.adapter.query === "function") {
|
|
11183
|
+
const result = await this.adapter.query(
|
|
11184
|
+
`SELECT * FROM ${this.tableName} ORDER BY run_at ASC`
|
|
11185
|
+
);
|
|
11186
|
+
return Array.isArray(result) ? result : result.rows || [];
|
|
11187
|
+
}
|
|
11188
|
+
return [];
|
|
11189
|
+
} catch {
|
|
11190
|
+
return [];
|
|
11191
|
+
}
|
|
11192
|
+
}
|
|
11193
|
+
/**
|
|
11194
|
+
* Record seed as executed
|
|
11195
|
+
*/
|
|
11196
|
+
async recordSeed(name, executionTime, filePath) {
|
|
11197
|
+
if (typeof this.adapter.query === "function") {
|
|
11198
|
+
const relativePath = filePath ? path__namespace.relative(this.seedsPath, filePath) : null;
|
|
11199
|
+
await this.adapter.query(
|
|
11200
|
+
`INSERT INTO ${this.tableName} (name, file_path, execution_time) VALUES ($1, $2, $3)
|
|
11201
|
+
ON CONFLICT (name) DO UPDATE SET run_at = CURRENT_TIMESTAMP, file_path = $2, execution_time = $3`,
|
|
11202
|
+
[name, relativePath, executionTime]
|
|
11203
|
+
);
|
|
11204
|
+
}
|
|
11205
|
+
}
|
|
11206
|
+
/**
|
|
11207
|
+
* Remove seed record (for reset)
|
|
11208
|
+
*/
|
|
11209
|
+
async unrecordSeed(name) {
|
|
11210
|
+
if (typeof this.adapter.query === "function") {
|
|
11211
|
+
await this.adapter.query(
|
|
11212
|
+
`DELETE FROM ${this.tableName} WHERE name = $1`,
|
|
11213
|
+
[name]
|
|
11214
|
+
);
|
|
11215
|
+
}
|
|
11216
|
+
}
|
|
11217
|
+
/**
|
|
11218
|
+
* Execute a seed function with optional transaction support
|
|
11219
|
+
*/
|
|
11220
|
+
async executeSeed(seed) {
|
|
11221
|
+
if (typeof this.adapter.transaction === "function") {
|
|
11222
|
+
const txResult = await this.adapter.transaction(async () => {
|
|
11223
|
+
await seed.run(this.adapter);
|
|
11224
|
+
});
|
|
11225
|
+
if (!txResult.success) {
|
|
11226
|
+
throw txResult.error ?? new errors.DatabaseError(
|
|
11227
|
+
`Seed ${seed.name} failed`,
|
|
11228
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED
|
|
11229
|
+
);
|
|
11230
|
+
}
|
|
11231
|
+
} else {
|
|
11232
|
+
await seed.run(this.adapter);
|
|
11233
|
+
}
|
|
11234
|
+
}
|
|
11235
|
+
/**
|
|
11236
|
+
* Check if a seed should be skipped and log if applicable
|
|
11237
|
+
*/
|
|
11238
|
+
shouldSkipSeed(seedFile, seedName, executedNames) {
|
|
11239
|
+
if (seedName && seedFile.name !== seedName) return true;
|
|
11240
|
+
if (this.skipExisting && executedNames.has(seedFile.name)) {
|
|
11241
|
+
console.log(`[Seeds] Skipping ${seedFile.name} (already executed)`);
|
|
11242
|
+
return true;
|
|
11243
|
+
}
|
|
11244
|
+
return false;
|
|
11245
|
+
}
|
|
11246
|
+
/**
|
|
11247
|
+
* Run all seeds or a specific seed
|
|
11248
|
+
*/
|
|
11249
|
+
async run(seedName) {
|
|
11250
|
+
try {
|
|
11251
|
+
await this.initialize();
|
|
11252
|
+
const allSeeds = await this.discoverSeeds();
|
|
11253
|
+
const executedSeeds = await this.getExecutedSeeds();
|
|
11254
|
+
const executedNames = new Set(executedSeeds.map((s) => s.name));
|
|
11255
|
+
let executed = 0;
|
|
11256
|
+
for (const seedFile of allSeeds) {
|
|
11257
|
+
if (this.shouldSkipSeed(seedFile, seedName, executedNames)) {
|
|
11258
|
+
continue;
|
|
11259
|
+
}
|
|
11260
|
+
console.log(`[Seeds] Running ${seedFile.name}...`);
|
|
11261
|
+
const seed = await this.loadSeed(seedFile);
|
|
11262
|
+
const startTime = Date.now();
|
|
11263
|
+
await this.executeSeed(seed);
|
|
11264
|
+
const executionTime = Date.now() - startTime;
|
|
11265
|
+
await this.recordSeed(seed.name, executionTime, seedFile.filePath);
|
|
11266
|
+
console.log(`[Seeds] Executed ${seed.name} in ${executionTime}ms`);
|
|
11267
|
+
executed++;
|
|
11268
|
+
if (seedName) break;
|
|
11269
|
+
}
|
|
11270
|
+
return success(executed);
|
|
11271
|
+
} catch (error) {
|
|
11272
|
+
return failure(
|
|
11273
|
+
new errors.DatabaseError(
|
|
11274
|
+
`Seed execution failed: ${error.message}`,
|
|
11275
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
|
|
11276
|
+
{ cause: error }
|
|
11277
|
+
)
|
|
11278
|
+
);
|
|
11279
|
+
}
|
|
11280
|
+
}
|
|
11281
|
+
/**
|
|
11282
|
+
* Execute cleanup function with optional transaction support
|
|
11283
|
+
*/
|
|
11284
|
+
async executeCleanup(seed) {
|
|
11285
|
+
if (!seed.cleanup) return;
|
|
11286
|
+
if (typeof this.adapter.transaction === "function") {
|
|
11287
|
+
const txResult = await this.adapter.transaction(async () => {
|
|
11288
|
+
await seed.cleanup(this.adapter);
|
|
11289
|
+
});
|
|
11290
|
+
if (!txResult.success) {
|
|
11291
|
+
throw txResult.error ?? new errors.DatabaseError(
|
|
11292
|
+
`Seed cleanup for ${seed.name} failed`,
|
|
11293
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED
|
|
11294
|
+
);
|
|
11295
|
+
}
|
|
11296
|
+
} else {
|
|
11297
|
+
await seed.cleanup(this.adapter);
|
|
11298
|
+
}
|
|
11299
|
+
}
|
|
11300
|
+
/**
|
|
11301
|
+
* Reset all seeds (run cleanup functions)
|
|
11302
|
+
*/
|
|
11303
|
+
async reset() {
|
|
11304
|
+
try {
|
|
11305
|
+
const allSeeds = await this.discoverSeeds();
|
|
11306
|
+
let cleaned = 0;
|
|
11307
|
+
for (const seedFile of allSeeds.reverse()) {
|
|
11308
|
+
console.log(`[Seeds] Cleaning up ${seedFile.name}...`);
|
|
11309
|
+
const seed = await this.loadSeed(seedFile);
|
|
11310
|
+
if (!seed.cleanup) {
|
|
11311
|
+
console.warn(`[Seeds] No cleanup function for ${seed.name}`);
|
|
11312
|
+
continue;
|
|
11313
|
+
}
|
|
11314
|
+
const startTime = Date.now();
|
|
11315
|
+
await this.executeCleanup(seed);
|
|
11316
|
+
const executionTime = Date.now() - startTime;
|
|
11317
|
+
await this.unrecordSeed(seed.name);
|
|
11318
|
+
console.log(`[Seeds] Cleaned ${seed.name} in ${executionTime}ms`);
|
|
11319
|
+
cleaned++;
|
|
11320
|
+
}
|
|
11321
|
+
return success(cleaned);
|
|
11322
|
+
} catch (error) {
|
|
11323
|
+
return failure(
|
|
11324
|
+
new errors.DatabaseError(
|
|
11325
|
+
`Seed cleanup failed: ${error.message}`,
|
|
11326
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
|
|
11327
|
+
{ cause: error }
|
|
11328
|
+
)
|
|
11329
|
+
);
|
|
11330
|
+
}
|
|
11331
|
+
}
|
|
11332
|
+
/**
|
|
11333
|
+
* Get seed execution status
|
|
11334
|
+
*/
|
|
11335
|
+
async status() {
|
|
11336
|
+
try {
|
|
11337
|
+
await this.initialize();
|
|
11338
|
+
const allSeeds = await this.discoverSeeds();
|
|
11339
|
+
const executedSeeds = await this.getExecutedSeeds();
|
|
11340
|
+
const executedNames = new Set(executedSeeds.map((s) => s.name));
|
|
11341
|
+
const pending = allSeeds.filter((s) => !executedNames.has(s.name)).map((s) => s.name);
|
|
11342
|
+
return success({
|
|
11343
|
+
executed: executedSeeds,
|
|
11344
|
+
pending
|
|
11345
|
+
});
|
|
11346
|
+
} catch (error) {
|
|
11347
|
+
return failure(
|
|
11348
|
+
new errors.DatabaseError(
|
|
11349
|
+
`Failed to get seed status: ${error.message}`,
|
|
11350
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
|
|
11351
|
+
{ cause: error }
|
|
11352
|
+
)
|
|
11353
|
+
);
|
|
11354
|
+
}
|
|
11355
|
+
}
|
|
11356
|
+
/**
|
|
11357
|
+
* Clear seed history (doesn't clean data, just removes tracking records)
|
|
11358
|
+
*/
|
|
11359
|
+
async clearHistory() {
|
|
11360
|
+
try {
|
|
11361
|
+
if (typeof this.adapter.query === "function") {
|
|
11362
|
+
await this.adapter.query(`DELETE FROM ${this.tableName}`);
|
|
11363
|
+
}
|
|
11364
|
+
return success();
|
|
11365
|
+
} catch (error) {
|
|
11366
|
+
return failure(
|
|
11367
|
+
new errors.DatabaseError(
|
|
11368
|
+
`Failed to clear seed history: ${error.message}`,
|
|
11369
|
+
errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
|
|
11370
|
+
{ cause: error }
|
|
11371
|
+
)
|
|
11372
|
+
);
|
|
11373
|
+
}
|
|
11374
|
+
}
|
|
11375
|
+
};
|
|
8479
11376
|
|
|
8480
11377
|
exports.AdapterFactory = AdapterFactory;
|
|
8481
11378
|
exports.AlertManager = AlertManager;
|
|
@@ -8493,9 +11390,14 @@ exports.DynamicPool = DynamicPool;
|
|
|
8493
11390
|
exports.EncryptionAdapter = EncryptionAdapter;
|
|
8494
11391
|
exports.HealthManager = HealthManager;
|
|
8495
11392
|
exports.MetricsCollector = MetricsCollector;
|
|
11393
|
+
exports.MigrationManager = MigrationManager;
|
|
11394
|
+
exports.MockAdapter = MockAdapter;
|
|
11395
|
+
exports.MultiReadAdapter = MultiReadAdapter;
|
|
11396
|
+
exports.MultiWriteAdapter = MultiWriteAdapter;
|
|
8496
11397
|
exports.ReadReplicaAdapter = ReadReplicaAdapter;
|
|
8497
11398
|
exports.RedisCache = RedisCache;
|
|
8498
11399
|
exports.SQLAdapter = SQLAdapter;
|
|
11400
|
+
exports.SeedManager = SeedManager;
|
|
8499
11401
|
exports.ShardKeyManager = ShardKeyManager;
|
|
8500
11402
|
exports.ShardRouter = ShardRouter;
|
|
8501
11403
|
exports.SoftDeleteAdapter = SoftDeleteAdapter;
|