@plyaz/db 0.1.0 → 0.1.1

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.
Files changed (62) hide show
  1. package/README.md +98 -134
  2. package/dist/adapters/drizzle/DrizzleAdapter.d.ts +109 -9
  3. package/dist/adapters/drizzle/DrizzleAdapter.d.ts.map +1 -1
  4. package/dist/adapters/index.d.ts +5 -0
  5. package/dist/adapters/index.d.ts.map +1 -1
  6. package/dist/adapters/mock/MockAdapter.d.ts +88 -0
  7. package/dist/adapters/mock/MockAdapter.d.ts.map +1 -0
  8. package/dist/adapters/sql/SQLAdapter.d.ts +28 -6
  9. package/dist/adapters/sql/SQLAdapter.d.ts.map +1 -1
  10. package/dist/adapters/supabase/SupabaseAdapter.d.ts +9 -2
  11. package/dist/adapters/supabase/SupabaseAdapter.d.ts.map +1 -1
  12. package/dist/advanced/multi-tenancy/TenantRepository.d.ts +1 -7
  13. package/dist/advanced/multi-tenancy/TenantRepository.d.ts.map +1 -1
  14. package/dist/advanced/read-replica/ReadReplicaAdapter.d.ts +3 -2
  15. package/dist/advanced/read-replica/ReadReplicaAdapter.d.ts.map +1 -1
  16. package/dist/cli/index.d.ts +27 -0
  17. package/dist/cli/index.d.ts.map +1 -0
  18. package/dist/cli/index.js +8420 -0
  19. package/dist/cli/index.js.map +1 -0
  20. package/dist/extensions/AuditExtension.d.ts +56 -9
  21. package/dist/extensions/AuditExtension.d.ts.map +1 -1
  22. package/dist/extensions/CachingAdapter.d.ts +5 -4
  23. package/dist/extensions/CachingAdapter.d.ts.map +1 -1
  24. package/dist/extensions/EncryptionExtension.d.ts +5 -4
  25. package/dist/extensions/EncryptionExtension.d.ts.map +1 -1
  26. package/dist/extensions/MultiReadExtension.d.ts +95 -0
  27. package/dist/extensions/MultiReadExtension.d.ts.map +1 -0
  28. package/dist/extensions/MultiWriteExtension.d.ts +67 -0
  29. package/dist/extensions/MultiWriteExtension.d.ts.map +1 -0
  30. package/dist/extensions/ReadReplicaAdapter.d.ts +4 -3
  31. package/dist/extensions/ReadReplicaAdapter.d.ts.map +1 -1
  32. package/dist/extensions/SoftDeleteExtension.d.ts +5 -4
  33. package/dist/extensions/SoftDeleteExtension.d.ts.map +1 -1
  34. package/dist/extensions/index.d.ts +4 -0
  35. package/dist/extensions/index.d.ts.map +1 -1
  36. package/dist/factory/AdapterFactory.d.ts.map +1 -1
  37. package/dist/factory/createDatabaseService.d.ts.map +1 -1
  38. package/dist/index.cjs +3012 -392
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.ts +6 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.mjs +3439 -842
  43. package/dist/index.mjs.map +1 -1
  44. package/dist/migrations/MigrationManager.d.ts +106 -0
  45. package/dist/migrations/MigrationManager.d.ts.map +1 -0
  46. package/dist/migrations/generateDownMigration.d.ts +25 -0
  47. package/dist/migrations/generateDownMigration.d.ts.map +1 -0
  48. package/dist/migrations/index.d.ts +10 -0
  49. package/dist/migrations/index.d.ts.map +1 -0
  50. package/dist/repository/BaseRepository.d.ts +109 -23
  51. package/dist/repository/BaseRepository.d.ts.map +1 -1
  52. package/dist/seeds/SeedManager.d.ts +96 -0
  53. package/dist/seeds/SeedManager.d.ts.map +1 -0
  54. package/dist/seeds/index.d.ts +10 -0
  55. package/dist/seeds/index.d.ts.map +1 -0
  56. package/dist/service/DatabaseService.d.ts +89 -13
  57. package/dist/service/DatabaseService.d.ts.map +1 -1
  58. package/dist/service/EventEmitter.d.ts +3 -14
  59. package/dist/service/EventEmitter.d.ts.map +1 -1
  60. package/dist/service/HealthManager.d.ts +42 -3
  61. package/dist/service/HealthManager.d.ts.map +1 -1
  62. package/package.json +6 -2
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.adapter.connect === "function") {
337
- await this.adapter.connect();
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.adapter.healthCheck();
403
- const status = {
404
- isHealthy: result.success,
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
- if (typeof this.adapter.disconnect === "function") {
446
- await this.adapter.disconnect();
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(table, id);
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(table, input);
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(table, id, input);
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(table, id);
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
- registerTable() {
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
- this.tableMap.set(name, table);
1529
- if (idColumn) {
1530
- this.idColumnMap.set(name, idColumn);
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
- * Updates an existing record by ID.
1679
- * @template T - The expected type of the record.
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 update(table, id, data) {
2151
+ async createRawSql(table, data) {
1692
2152
  try {
1693
- const tableObj = this.getTable(table);
1694
- const idColumn = this.getIdColumn(table);
1695
- const result = await this.db.update(tableObj).set(data).where(drizzleOrm.eq(idColumn, id)).returning();
1696
- return success(result[0]);
1697
- } catch (error) {
1698
- return failure(
1699
- new errors.DatabaseError(
1700
- `Failed to update in table ${table}: ${error.message}`,
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
- * This method is used internally by other methods to ensure that operations are performed
1923
- * on valid, registered tables. If the table is not found, it throws a DatabaseError.
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
- "Table is not registered with the adapter",
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(column, value[0], value[1]);
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.registerTable(name);
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;
@@ -2885,6 +3629,8 @@ var SQLAdapter = class {
2885
3629
  config;
2886
3630
  tableMap = /* @__PURE__ */ new Map();
2887
3631
  idColumnMap = /* @__PURE__ */ new Map();
3632
+ configIdColumns;
3633
+ defaultSchema;
2888
3634
  /**
2889
3635
  * Creates a new SQLAdapter instance.
2890
3636
  * @param {SQLAdapterConfig} config - Configuration for the SQL adapter.
@@ -2897,10 +3643,22 @@ var SQLAdapter = class {
2897
3643
  */
2898
3644
  constructor(config) {
2899
3645
  this.config = config;
3646
+ this.defaultSchema = config.schema ?? "public";
2900
3647
  this.pool = new pg.Pool({
2901
3648
  connectionString: config.connectionString,
2902
3649
  ...config.pool
2903
3650
  });
3651
+ this.configIdColumns = config.tableIdColumns ?? {};
3652
+ }
3653
+ /**
3654
+ * Get fully-qualified table name with schema
3655
+ */
3656
+ getQualifiedTableName(table, schema) {
3657
+ const targetSchema = schema ?? this.defaultSchema;
3658
+ if (table.includes(".")) {
3659
+ return table;
3660
+ }
3661
+ return `${targetSchema}.${table}`;
2904
3662
  }
2905
3663
  /**
2906
3664
  * Initialize the adapter.
@@ -2914,7 +3672,11 @@ var SQLAdapter = class {
2914
3672
  */
2915
3673
  async initialize() {
2916
3674
  try {
2917
- await this.pool.connect();
3675
+ const client = await this.pool.connect();
3676
+ if (this.defaultSchema && this.defaultSchema !== "public") {
3677
+ await client.query(`SET search_path TO ${this.defaultSchema}, public`);
3678
+ }
3679
+ client.release();
2918
3680
  return success();
2919
3681
  } catch (error) {
2920
3682
  return failure(
@@ -2953,6 +3715,23 @@ var SQLAdapter = class {
2953
3715
  async disconnect() {
2954
3716
  await this.pool.end();
2955
3717
  }
3718
+ /**
3719
+ * Closes the database connection and cleanup resources.
3720
+ * @returns Promise resolving to DatabaseResult indicating success or failure.
3721
+ */
3722
+ async close() {
3723
+ try {
3724
+ await this.disconnect();
3725
+ return success();
3726
+ } catch (error) {
3727
+ return failure(
3728
+ new errors.DatabaseError(
3729
+ `Failed to close connection: ${error.message}`,
3730
+ errors$1.DATABASE_ERROR_CODES.DISCONNECT_FAILED
3731
+ )
3732
+ );
3733
+ }
3734
+ }
2956
3735
  /**
2957
3736
  * Gets the underlying PostgreSQL client instance.
2958
3737
  * @template TClient - The type of the database client to return.
@@ -3033,8 +3812,9 @@ var SQLAdapter = class {
3033
3812
  const validationError = this.validateBasicParams(table, id);
3034
3813
  if (validationError) return failure(validationError);
3035
3814
  const tableName = this.getTableName(table);
3036
- const idColumn = this.idColumnMap.get(table) ?? "id";
3037
- const sql2 = `SELECT * FROM "${tableName}" WHERE "${idColumn}" = $1`;
3815
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3816
+ const idColumn = this.getIdColumn(table);
3817
+ const sql2 = `SELECT * FROM ${qualifiedTable} WHERE "${idColumn}" = $1`;
3038
3818
  const result = await this.pool.query(sql2, [id]);
3039
3819
  if (!result?.rows) {
3040
3820
  return failure(
@@ -3097,6 +3877,7 @@ var SQLAdapter = class {
3097
3877
  async findMany(table, options) {
3098
3878
  try {
3099
3879
  const tableName = this.getTableName(table);
3880
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3100
3881
  const params = [];
3101
3882
  let whereClause = "";
3102
3883
  let paramIndex = 1;
@@ -3104,7 +3885,7 @@ var SQLAdapter = class {
3104
3885
  whereClause = this.buildWhereClause(options.filter, params, paramIndex);
3105
3886
  paramIndex += params.length;
3106
3887
  }
3107
- const countSql = `SELECT COUNT(*) as total FROM ${tableName}${whereClause}`;
3888
+ const countSql = `SELECT COUNT(*) as total FROM ${qualifiedTable}${whereClause}`;
3108
3889
  const countResult = await this.pool.query(countSql, params);
3109
3890
  if (!countResult.rows || countResult.rows.length === 0) {
3110
3891
  throw new errors.DatabaseError(
@@ -3132,7 +3913,7 @@ var SQLAdapter = class {
3132
3913
  limitClause += ` OFFSET $${paramIndex++}`;
3133
3914
  params.push(options.pagination.offset);
3134
3915
  }
3135
- const sql2 = `SELECT * FROM ${tableName}${whereClause}${orderClause}${limitClause}`;
3916
+ const sql2 = `SELECT * FROM ${qualifiedTable}${whereClause}${orderClause}${limitClause}`;
3136
3917
  const result = await this.pool.query(sql2, params);
3137
3918
  return success({
3138
3919
  data: result.rows,
@@ -3172,11 +3953,12 @@ var SQLAdapter = class {
3172
3953
  const validationError = this.validateCreateParams(table, data);
3173
3954
  if (validationError) return failure(validationError);
3174
3955
  const tableName = this.getTableName(table);
3956
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3175
3957
  const keys = Object.keys(data);
3176
3958
  const values = Object.values(data);
3177
3959
  const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
3178
3960
  const escapedKeys = keys.map((k) => `"${k}"`).join(", ");
3179
- const sql2 = `INSERT INTO "${tableName}" (${escapedKeys}) VALUES (${placeholders}) RETURNING *`;
3961
+ const sql2 = `INSERT INTO ${qualifiedTable} (${escapedKeys}) VALUES (${placeholders}) RETURNING *`;
3180
3962
  const result = await this.pool.query(sql2, values);
3181
3963
  if (!result?.rows?.length) {
3182
3964
  return failure(
@@ -3222,11 +4004,12 @@ var SQLAdapter = class {
3222
4004
  const validationError = this.validateUpdateParams(table, id, data);
3223
4005
  if (validationError) return failure(validationError);
3224
4006
  const tableName = this.getTableName(table);
4007
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3225
4008
  const keys = Object.keys(data);
3226
4009
  const values = Object.values(data);
3227
4010
  const setClause = keys.map((key, i) => `"${key}" = $${i + 1}`).join(", ");
3228
- const idColumn = this.idColumnMap.get(table) ?? "id";
3229
- const sql2 = `UPDATE "${tableName}" SET ${setClause} WHERE "${idColumn}" = $${keys.length + 1} RETURNING *`;
4011
+ const idColumn = this.getIdColumn(table);
4012
+ const sql2 = `UPDATE ${qualifiedTable} SET ${setClause} WHERE "${idColumn}" = $${keys.length + 1} RETURNING *`;
3230
4013
  const result = await this.pool.query(sql2, [...values, id]);
3231
4014
  if (!result.rows?.length) {
3232
4015
  return failure(
@@ -3274,8 +4057,9 @@ var SQLAdapter = class {
3274
4057
  );
3275
4058
  }
3276
4059
  const tableName = this.getTableName(table);
3277
- const idColumn = this.idColumnMap.get(table) ?? "id";
3278
- const sql2 = `DELETE FROM "${tableName}" WHERE "${idColumn}" = $1`;
4060
+ const qualifiedTable = this.getQualifiedTableName(tableName);
4061
+ const idColumn = this.getIdColumn(table);
4062
+ const sql2 = `DELETE FROM ${qualifiedTable} WHERE "${idColumn}" = $1`;
3279
4063
  const result = await this.pool.query(sql2, [id]);
3280
4064
  if (!result) {
3281
4065
  return failure(
@@ -3323,34 +4107,39 @@ var SQLAdapter = class {
3323
4107
  const trx = {
3324
4108
  findById: /* @__PURE__ */ __name(async (table, id) => {
3325
4109
  const tableName = this.getTableName(table);
3326
- const idColumn = this.idColumnMap.get(table) ?? "id";
3327
- const sql2 = `SELECT * FROM ${tableName} WHERE ${idColumn} = $1`;
4110
+ const qualifiedTable = this.getQualifiedTableName(tableName);
4111
+ const idColumn = this.getIdColumn(table);
4112
+ const sql2 = `SELECT * FROM ${qualifiedTable} WHERE "${idColumn}" = $1`;
3328
4113
  const result2 = await client.query(sql2, [id]);
3329
4114
  return success(result2.rows[0] ?? null);
3330
4115
  }, "findById"),
3331
4116
  create: /* @__PURE__ */ __name(async (table, data) => {
3332
4117
  const tableName = this.getTableName(table);
4118
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3333
4119
  const keys = Object.keys(data);
3334
4120
  const values = Object.values(data);
3335
4121
  const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
3336
- const sql2 = `INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders}) RETURNING *`;
4122
+ const escapedKeys = keys.map((k) => `"${k}"`).join(", ");
4123
+ const sql2 = `INSERT INTO ${qualifiedTable} (${escapedKeys}) VALUES (${placeholders}) RETURNING *`;
3337
4124
  const result2 = await client.query(sql2, values);
3338
4125
  return success(result2.rows[0]);
3339
4126
  }, "create"),
3340
4127
  update: /* @__PURE__ */ __name(async (table, id, data) => {
3341
4128
  const tableName = this.getTableName(table);
4129
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3342
4130
  const keys = Object.keys(data);
3343
4131
  const values = Object.values(data);
3344
- const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
3345
- const idColumn = this.idColumnMap.get(table) ?? "id";
3346
- const sql2 = `UPDATE ${tableName} SET ${setClause} WHERE ${idColumn} = $${keys.length + 1} RETURNING *`;
4132
+ const setClause = keys.map((key, i) => `"${key}" = $${i + 1}`).join(", ");
4133
+ const idColumn = this.getIdColumn(table);
4134
+ const sql2 = `UPDATE ${qualifiedTable} SET ${setClause} WHERE "${idColumn}" = $${keys.length + 1} RETURNING *`;
3347
4135
  const result2 = await client.query(sql2, [...values, id]);
3348
4136
  return success(result2.rows[0]);
3349
4137
  }, "update"),
3350
4138
  delete: /* @__PURE__ */ __name(async (table, id) => {
3351
4139
  const tableName = this.getTableName(table);
3352
- const idColumn = this.idColumnMap.get(table) ?? "id";
3353
- const sql2 = `DELETE FROM ${tableName} WHERE ${idColumn} = $1`;
4140
+ const qualifiedTable = this.getQualifiedTableName(tableName);
4141
+ const idColumn = this.getIdColumn(table);
4142
+ const sql2 = `DELETE FROM ${qualifiedTable} WHERE "${idColumn}" = $1`;
3354
4143
  await client.query(sql2, [id]);
3355
4144
  return success();
3356
4145
  }, "delete"),
@@ -3397,8 +4186,9 @@ var SQLAdapter = class {
3397
4186
  async exists(table, id) {
3398
4187
  try {
3399
4188
  const tableName = this.getTableName(table);
3400
- const idColumn = this.idColumnMap.get(table) ?? "id";
3401
- const sql2 = `SELECT 1 FROM ${tableName} WHERE ${idColumn} = $1 LIMIT 1`;
4189
+ const qualifiedTable = this.getQualifiedTableName(tableName);
4190
+ const idColumn = this.getIdColumn(table);
4191
+ const sql2 = `SELECT 1 FROM ${qualifiedTable} WHERE "${idColumn}" = $1 LIMIT 1`;
3402
4192
  const result = await this.pool.query(sql2, [id]);
3403
4193
  return success(result.rows.length > 0);
3404
4194
  } catch (error) {
@@ -3430,12 +4220,13 @@ var SQLAdapter = class {
3430
4220
  async count(table, filter) {
3431
4221
  try {
3432
4222
  const tableName = this.getTableName(table);
4223
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3433
4224
  let whereClause = "";
3434
4225
  let params = [];
3435
4226
  if (filter) {
3436
4227
  whereClause = this.buildWhereClause(filter, params, 1);
3437
4228
  }
3438
- const sql2 = `SELECT COUNT(*) as count FROM ${tableName}${whereClause}`;
4229
+ const sql2 = `SELECT COUNT(*) as count FROM ${qualifiedTable}${whereClause}`;
3439
4230
  const result = await this.pool.query(sql2, params);
3440
4231
  const rowCount = Number.parseInt(result.rows[0].count);
3441
4232
  return success(Number(rowCount));
@@ -3488,22 +4279,43 @@ var SQLAdapter = class {
3488
4279
  * @private
3489
4280
  * @param {string} name - Logical table name.
3490
4281
  * @returns {string} Actual table name.
3491
- * @throws {DatabaseError} If table is not registered.
3492
4282
  * @description
3493
- * Retrieves the actual table name that has been previously registered with the adapter.
3494
- * This method is used internally by other methods to ensure that operations are performed
3495
- * on valid, registered tables. If the table is not found, it throws a DatabaseError.
4283
+ * Retrieves the actual table name. If not registered, auto-registers it with
4284
+ * the same logical name as the physical table name. This enables seamless
4285
+ * table operations without manual registration (matching Supabase adapter behavior).
3496
4286
  */
3497
4287
  getTableName(name) {
3498
- const tableName = this.tableMap.get(name);
4288
+ let tableName = this.tableMap.get(name);
3499
4289
  if (!tableName) {
3500
- throw new errors.DatabaseError(
3501
- `Table ${name} is not registered with the adapter`,
3502
- errors$1.DATABASE_ERROR_CODES.TABLE_NOT_REGISTERED
3503
- );
4290
+ const hasRuntimeIdColumn = this.idColumnMap.has(name);
4291
+ const customIdColumn = hasRuntimeIdColumn ? void 0 : this.configIdColumns[name];
4292
+ this.registerTable(name, name, customIdColumn);
4293
+ tableName = name;
3504
4294
  }
3505
4295
  return tableName;
3506
4296
  }
4297
+ /**
4298
+ * Get the ID column for a table.
4299
+ * @private
4300
+ * @param {string} table - Logical table name.
4301
+ * @returns {string} ID column name (defaults to 'id').
4302
+ * @description
4303
+ * Retrieves the ID column for a table. Checks in this order:
4304
+ * 1. Runtime registered ID column (from registerTable calls)
4305
+ * 2. Config-provided ID column (from tableIdColumns in config)
4306
+ * 3. Default 'id' column
4307
+ */
4308
+ getIdColumn(table) {
4309
+ const runtimeIdColumn = this.idColumnMap.get(table);
4310
+ if (runtimeIdColumn) {
4311
+ return runtimeIdColumn;
4312
+ }
4313
+ const configIdColumn = this.configIdColumns[table];
4314
+ if (configIdColumn) {
4315
+ return configIdColumn;
4316
+ }
4317
+ return "id";
4318
+ }
3507
4319
  validateBasicParams(table, id) {
3508
4320
  if (!table || !id) {
3509
4321
  return new errors.DatabaseError(
@@ -3661,35 +4473,448 @@ var SQLAdapter = class {
3661
4473
  return clause;
3662
4474
  }
3663
4475
  };
3664
- var AdapterFactory = class {
4476
+ var DEFAULT_PAGINATION_LIMIT = 50;
4477
+ var RANDOM_STRING_BASE = 36;
4478
+ var ID_SUBSTRING_START = 2;
4479
+ var ID_SUBSTRING_END = 9;
4480
+ var BETWEEN_VALUES_LENGTH = 2;
4481
+ var FILTER_OPERATORS = {
4482
+ eq: /* @__PURE__ */ __name((fieldValue, value) => fieldValue === value, "eq"),
4483
+ ne: /* @__PURE__ */ __name((fieldValue, value) => fieldValue !== value, "ne"),
4484
+ gt: /* @__PURE__ */ __name((fieldValue, value) => fieldValue > value, "gt"),
4485
+ gte: /* @__PURE__ */ __name((fieldValue, value) => fieldValue >= value, "gte"),
4486
+ lt: /* @__PURE__ */ __name((fieldValue, value) => fieldValue < value, "lt"),
4487
+ lte: /* @__PURE__ */ __name((fieldValue, value) => fieldValue <= value, "lte"),
4488
+ in: /* @__PURE__ */ __name((fieldValue, value) => Array.isArray(value) && value.includes(fieldValue), "in"),
4489
+ like: /* @__PURE__ */ __name((fieldValue, value) => String(fieldValue).toLowerCase().includes(String(value).toLowerCase()), "like"),
4490
+ between: /* @__PURE__ */ __name((fieldValue, value) => {
4491
+ const betweenValues = value;
4492
+ return Array.isArray(betweenValues) && betweenValues.length === BETWEEN_VALUES_LENGTH && fieldValue >= betweenValues[0] && fieldValue <= betweenValues[1];
4493
+ }, "between"),
4494
+ isNull: /* @__PURE__ */ __name((fieldValue) => fieldValue === null || fieldValue === void 0, "isNull"),
4495
+ isNotNull: /* @__PURE__ */ __name((fieldValue) => fieldValue !== null && fieldValue !== void 0, "isNotNull")
4496
+ };
4497
+ var MockAdapter = class {
3665
4498
  static {
3666
- __name(this, "AdapterFactory");
4499
+ __name(this, "MockAdapter");
4500
+ }
4501
+ data = /* @__PURE__ */ new Map();
4502
+ config;
4503
+ tableIdColumns = /* @__PURE__ */ new Map();
4504
+ defaultSchema;
4505
+ isInitialized = false;
4506
+ transactionDepth = 0;
4507
+ transactionData = null;
4508
+ constructor(config = {}) {
4509
+ this.config = {
4510
+ autoGenerateIds: true,
4511
+ latency: 0,
4512
+ failRate: 0,
4513
+ ...config
4514
+ };
4515
+ this.defaultSchema = config.schema ?? "public";
4516
+ if (config.initialData) {
4517
+ this.initializeTableData(config.initialData);
4518
+ }
4519
+ if (config.tableIdColumns) {
4520
+ for (const [table, idColumn] of Object.entries(config.tableIdColumns)) {
4521
+ this.tableIdColumns.set(table, idColumn);
4522
+ }
4523
+ }
4524
+ }
4525
+ async initialize() {
4526
+ await this.simulateLatency();
4527
+ if (this.shouldFail()) {
4528
+ return failure(
4529
+ new errors.DatabaseError(
4530
+ "Mock initialization failed",
4531
+ errors$1.DATABASE_ERROR_CODES.INIT_FAILED
4532
+ )
4533
+ );
4534
+ }
4535
+ this.isInitialized = true;
4536
+ return success();
4537
+ }
4538
+ async close() {
4539
+ await this.simulateLatency();
4540
+ this.data.clear();
4541
+ this.isInitialized = false;
4542
+ return success();
4543
+ }
4544
+ registerTable(name, table, idColumn) {
4545
+ if (idColumn && typeof idColumn === "string") {
4546
+ this.tableIdColumns.set(name, idColumn);
4547
+ }
4548
+ if (!this.data.has(name)) {
4549
+ this.data.set(name, /* @__PURE__ */ new Map());
4550
+ }
4551
+ }
4552
+ async findById(table, id) {
4553
+ await this.simulateLatency();
4554
+ if (this.shouldFail()) {
4555
+ return failure(
4556
+ new errors.DatabaseError(
4557
+ "Mock findById failed",
4558
+ errors$1.DATABASE_ERROR_CODES.FIND_BY_ID_FAILED
4559
+ )
4560
+ );
4561
+ }
4562
+ const tableData = this.getTableData(table);
4563
+ const record = tableData.get(id);
4564
+ return success(record ? { ...record } : null);
3667
4565
  }
3668
4566
  /**
3669
- * Creates a new database adapter instance based on the configuration
3670
- *
3671
- * This is the core factory method that instantiates the appropriate database adapter
3672
- * based on the provided type and configuration. Uses TypeScript generics to ensure
3673
- * type safety between adapter type and configuration.
3674
- *
3675
- * **Creation Process:**
3676
- * 1. Validates input parameters (type and config)
3677
- * 2. Uses switch statement to match adapter type
3678
- * 3. Instantiates appropriate adapter class with type-safe config
3679
- * 4. Returns DatabaseAdapterType interface implementation
3680
- *
3681
- * **Type Safety:**
3682
- * - Generic T extends DatabaseConfig ensures config matches adapter type
3683
- * - Type assertions (as DrizzleAdapterConfig) provide compile-time safety
3684
- * - Runtime validation prevents invalid configurations
3685
- *
3686
- * @template T - Database configuration type that extends DatabaseConfig
3687
- * @param {T["adapter"]} type - Adapter type from ADAPTERS enum (drizzle, supabase, sql)
3688
- * @param {T} config - Database configuration object matching the adapter type
3689
- * @returns {DatabaseAdapterType} The appropriate adapter implementation
3690
- *
3691
- * @throws {Error} If adapter type is not provided
3692
- * @throws {Error} If adapter configuration is not provided
4567
+ * Apply query options (filter, sort) to records
4568
+ */
4569
+ applyQueryOptions(records, options) {
4570
+ let result = records;
4571
+ if (options?.filter) {
4572
+ result = this.applyFilter(result, options.filter);
4573
+ }
4574
+ if (options?.sort) {
4575
+ result = this.applySort(result, options.sort);
4576
+ }
4577
+ return result;
4578
+ }
4579
+ /**
4580
+ * Get pagination params with defaults
4581
+ */
4582
+ getPaginationParams(options) {
4583
+ return {
4584
+ offset: options?.pagination?.offset ?? 0,
4585
+ limit: options?.pagination?.limit ?? DEFAULT_PAGINATION_LIMIT
4586
+ };
4587
+ }
4588
+ /**
4589
+ * Apply pagination to records
4590
+ */
4591
+ applyPagination(records, offset, limit) {
4592
+ return records.slice(offset, offset + limit);
4593
+ }
4594
+ async findMany(table, options) {
4595
+ await this.simulateLatency();
4596
+ if (this.shouldFail()) {
4597
+ return failure(
4598
+ new errors.DatabaseError(
4599
+ "Mock findMany failed",
4600
+ errors$1.DATABASE_ERROR_CODES.FIND_MANY_FAILED
4601
+ )
4602
+ );
4603
+ }
4604
+ const tableData = this.getTableData(table);
4605
+ const allRecords = Array.from(tableData.values());
4606
+ const filteredRecords = this.applyQueryOptions(allRecords, options);
4607
+ const total = filteredRecords.length;
4608
+ const { offset, limit } = this.getPaginationParams(options);
4609
+ const paginatedRecords = this.applyPagination(
4610
+ filteredRecords,
4611
+ offset,
4612
+ limit
4613
+ );
4614
+ return success({
4615
+ data: paginatedRecords,
4616
+ total,
4617
+ pagination: calculatePagination(total, options?.pagination)
4618
+ });
4619
+ }
4620
+ async create(table, data) {
4621
+ await this.simulateLatency();
4622
+ if (this.shouldFail()) {
4623
+ return failure(
4624
+ new errors.DatabaseError(
4625
+ "Mock create failed",
4626
+ errors$1.DATABASE_ERROR_CODES.CREATE_FAILED
4627
+ )
4628
+ );
4629
+ }
4630
+ const tableData = this.getTableData(table);
4631
+ const idColumn = this.getIdColumn(table);
4632
+ const record = { ...data };
4633
+ if (!record[idColumn] && this.config.autoGenerateIds) {
4634
+ record[idColumn] = this.generateId();
4635
+ }
4636
+ const id = record[idColumn];
4637
+ if (!id) {
4638
+ return failure(
4639
+ new errors.DatabaseError(
4640
+ "Record must have an ID",
4641
+ errors$1.DATABASE_ERROR_CODES.CREATE_FAILED
4642
+ )
4643
+ );
4644
+ }
4645
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4646
+ record.created_at ??= now;
4647
+ record.updated_at ??= now;
4648
+ tableData.set(String(id), record);
4649
+ return success(record);
4650
+ }
4651
+ async update(table, id, data) {
4652
+ await this.simulateLatency();
4653
+ if (this.shouldFail()) {
4654
+ return failure(
4655
+ new errors.DatabaseError(
4656
+ "Mock update failed",
4657
+ errors$1.DATABASE_ERROR_CODES.UPDATE_FAILED
4658
+ )
4659
+ );
4660
+ }
4661
+ const tableData = this.getTableData(table);
4662
+ const existing = tableData.get(id);
4663
+ if (!existing) {
4664
+ return failure(
4665
+ new errors.DatabaseError(
4666
+ "Record not found",
4667
+ errors$1.DATABASE_ERROR_CODES.RECORD_NOT_FOUND
4668
+ )
4669
+ );
4670
+ }
4671
+ const updated = {
4672
+ ...existing,
4673
+ ...data,
4674
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
4675
+ };
4676
+ tableData.set(id, updated);
4677
+ return success(updated);
4678
+ }
4679
+ async delete(table, id) {
4680
+ await this.simulateLatency();
4681
+ if (this.shouldFail()) {
4682
+ return failure(
4683
+ new errors.DatabaseError(
4684
+ "Mock delete failed",
4685
+ errors$1.DATABASE_ERROR_CODES.DELETE_FAILED
4686
+ )
4687
+ );
4688
+ }
4689
+ const tableData = this.getTableData(table);
4690
+ const existed = tableData.delete(id);
4691
+ if (!existed) {
4692
+ return failure(
4693
+ new errors.DatabaseError(
4694
+ "Record not found",
4695
+ errors$1.DATABASE_ERROR_CODES.RECORD_NOT_FOUND
4696
+ )
4697
+ );
4698
+ }
4699
+ return success();
4700
+ }
4701
+ async transaction(callback) {
4702
+ await this.simulateLatency();
4703
+ const snapshot = /* @__PURE__ */ new Map();
4704
+ for (const [table, tableData] of this.data.entries()) {
4705
+ snapshot.set(table, new Map(tableData));
4706
+ }
4707
+ this.transactionDepth++;
4708
+ this.transactionData = snapshot;
4709
+ try {
4710
+ const trx = {
4711
+ findById: /* @__PURE__ */ __name(async (table, id) => this.findById(table, id), "findById"),
4712
+ create: /* @__PURE__ */ __name(async (table, data) => this.create(table, data), "create"),
4713
+ update: /* @__PURE__ */ __name(async (table, id, data) => this.update(table, id, data), "update"),
4714
+ delete: /* @__PURE__ */ __name(async (table, id) => this.delete(table, id), "delete"),
4715
+ commit: /* @__PURE__ */ __name(async () => {
4716
+ }, "commit"),
4717
+ rollback: /* @__PURE__ */ __name(async () => {
4718
+ this.data = snapshot;
4719
+ }, "rollback")
4720
+ };
4721
+ const result = await callback(trx);
4722
+ this.transactionDepth--;
4723
+ this.transactionData = null;
4724
+ return success(result);
4725
+ } catch (error) {
4726
+ this.data = snapshot;
4727
+ this.transactionDepth--;
4728
+ this.transactionData = null;
4729
+ return failure(
4730
+ new errors.DatabaseError(
4731
+ `Transaction failed: ${error.message}`,
4732
+ errors$1.DATABASE_ERROR_CODES.TRANSACTION_FAILED,
4733
+ { cause: error }
4734
+ )
4735
+ );
4736
+ }
4737
+ }
4738
+ async exists(table, id) {
4739
+ await this.simulateLatency();
4740
+ const tableData = this.getTableData(table);
4741
+ return success(tableData.has(id));
4742
+ }
4743
+ async count(table, filter) {
4744
+ await this.simulateLatency();
4745
+ const tableData = this.getTableData(table);
4746
+ let records = Array.from(tableData.values());
4747
+ if (filter) {
4748
+ records = this.applyFilter(records, filter);
4749
+ }
4750
+ return success(records.length);
4751
+ }
4752
+ async healthCheck() {
4753
+ await this.simulateLatency();
4754
+ return success({
4755
+ isHealthy: this.isInitialized,
4756
+ responseTime: this.config.latency ?? 0,
4757
+ details: {
4758
+ adapter: "mock",
4759
+ tables: this.data.size,
4760
+ totalRecords: Array.from(this.data.values()).reduce(
4761
+ (sum, table) => sum + table.size,
4762
+ 0
4763
+ )
4764
+ }
4765
+ });
4766
+ }
4767
+ // DatabaseAdapterType required methods
4768
+ async connect() {
4769
+ await this.simulateLatency();
4770
+ }
4771
+ async disconnect() {
4772
+ await this.close();
4773
+ }
4774
+ getClient() {
4775
+ return {
4776
+ type: "mock",
4777
+ data: this.data,
4778
+ config: this.config
4779
+ };
4780
+ }
4781
+ async query() {
4782
+ await this.simulateLatency();
4783
+ return [];
4784
+ }
4785
+ // Utility methods
4786
+ /**
4787
+ * Get fully-qualified table name with schema
4788
+ */
4789
+ getQualifiedTableName(table, schema) {
4790
+ const targetSchema = schema ?? this.defaultSchema;
4791
+ if (table.includes(".")) {
4792
+ return table;
4793
+ }
4794
+ if (targetSchema === "public") {
4795
+ return table;
4796
+ }
4797
+ return `${targetSchema}.${table}`;
4798
+ }
4799
+ initializeTableData(initialData) {
4800
+ for (const [table, records] of Object.entries(initialData)) {
4801
+ const tableData = /* @__PURE__ */ new Map();
4802
+ const idColumn = this.getIdColumn(table);
4803
+ for (const record of records) {
4804
+ const id = record[idColumn];
4805
+ if (id) {
4806
+ tableData.set(String(id), { ...record });
4807
+ }
4808
+ }
4809
+ this.data.set(table, tableData);
4810
+ }
4811
+ }
4812
+ getTableData(table) {
4813
+ const qualifiedTable = this.getQualifiedTableName(table);
4814
+ if (!this.data.has(qualifiedTable)) {
4815
+ this.data.set(qualifiedTable, /* @__PURE__ */ new Map());
4816
+ }
4817
+ return this.data.get(qualifiedTable);
4818
+ }
4819
+ getIdColumn(table) {
4820
+ const baseTable = table.includes(".") ? table.split(".")[1] : table;
4821
+ return this.tableIdColumns.get(baseTable) ?? "id";
4822
+ }
4823
+ generateId() {
4824
+ return `mock-${Date.now()}-${Math.random().toString(RANDOM_STRING_BASE).substring(ID_SUBSTRING_START, ID_SUBSTRING_END)}`;
4825
+ }
4826
+ async simulateLatency() {
4827
+ if (this.config.latency && this.config.latency > 0) {
4828
+ await new Promise((resolve3) => setTimeout(resolve3, this.config.latency));
4829
+ }
4830
+ }
4831
+ shouldFail() {
4832
+ if (!this.config.failRate || this.config.failRate <= 0) return false;
4833
+ return Math.random() < this.config.failRate;
4834
+ }
4835
+ applyFilter(records, filter) {
4836
+ const { field, operator, value } = filter;
4837
+ const handler = FILTER_OPERATORS[operator];
4838
+ return records.filter((record) => {
4839
+ const fieldValue = record[field];
4840
+ return handler ? handler(fieldValue, value) : true;
4841
+ });
4842
+ }
4843
+ applySort(records, sort) {
4844
+ return records.sort((a, b) => {
4845
+ for (const { field, direction } of sort) {
4846
+ const aVal = a[field];
4847
+ const bVal = b[field];
4848
+ if (aVal === bVal) continue;
4849
+ const comparison = aVal < bVal ? -1 : 1;
4850
+ return direction === "asc" ? comparison : -comparison;
4851
+ }
4852
+ return 0;
4853
+ });
4854
+ }
4855
+ /**
4856
+ * Test utility: Clear all data
4857
+ */
4858
+ clearAll() {
4859
+ this.data.clear();
4860
+ }
4861
+ /**
4862
+ * Test utility: Get current data for inspection
4863
+ */
4864
+ getData(table) {
4865
+ if (table) {
4866
+ return Array.from(this.getTableData(table).values());
4867
+ }
4868
+ const result = {};
4869
+ for (const [tableName, tableData] of this.data.entries()) {
4870
+ result[tableName] = Array.from(tableData.values());
4871
+ }
4872
+ return result;
4873
+ }
4874
+ /**
4875
+ * Test utility: Set data directly
4876
+ */
4877
+ setData(table, records) {
4878
+ const tableData = /* @__PURE__ */ new Map();
4879
+ const idColumn = this.getIdColumn(table);
4880
+ for (const record of records) {
4881
+ const id = record[idColumn];
4882
+ if (id) {
4883
+ tableData.set(String(id), { ...record });
4884
+ }
4885
+ }
4886
+ this.data.set(table, tableData);
4887
+ }
4888
+ };
4889
+ var AdapterFactory = class {
4890
+ static {
4891
+ __name(this, "AdapterFactory");
4892
+ }
4893
+ /**
4894
+ * Creates a new database adapter instance based on the configuration
4895
+ *
4896
+ * This is the core factory method that instantiates the appropriate database adapter
4897
+ * based on the provided type and configuration. Uses TypeScript generics to ensure
4898
+ * type safety between adapter type and configuration.
4899
+ *
4900
+ * **Creation Process:**
4901
+ * 1. Validates input parameters (type and config)
4902
+ * 2. Uses switch statement to match adapter type
4903
+ * 3. Instantiates appropriate adapter class with type-safe config
4904
+ * 4. Returns DatabaseAdapterType interface implementation
4905
+ *
4906
+ * **Type Safety:**
4907
+ * - Generic T extends DatabaseConfig ensures config matches adapter type
4908
+ * - Type assertions (as DrizzleAdapterConfig) provide compile-time safety
4909
+ * - Runtime validation prevents invalid configurations
4910
+ *
4911
+ * @template T - Database configuration type that extends DatabaseConfig
4912
+ * @param {T["adapter"]} type - Adapter type from ADAPTERS enum (drizzle, supabase, sql)
4913
+ * @param {T} config - Database configuration object matching the adapter type
4914
+ * @returns {DatabaseAdapterType} The appropriate adapter implementation
4915
+ *
4916
+ * @throws {Error} If adapter type is not provided
4917
+ * @throws {Error} If adapter configuration is not provided
3693
4918
  * @throws {Error} If unsupported adapter type is specified
3694
4919
  * @throws {Error} If adapter instantiation fails
3695
4920
  *
@@ -3754,6 +4979,9 @@ var AdapterFactory = class {
3754
4979
  // SQL adapter - for raw SQL operations and custom database integrations
3755
4980
  case db.ADAPTERS.SQL:
3756
4981
  return new SQLAdapter(config);
4982
+ // Mock adapter - for in-memory testing without real database
4983
+ case db.ADAPTERS.MOCK:
4984
+ return new MockAdapter(config);
3757
4985
  // Default case - handles unsupported or invalid adapter types
3758
4986
  default:
3759
4987
  throw new errors.DatabaseError(
@@ -3852,6 +5080,9 @@ var SoftDeleteAdapter = class {
3852
5080
  async disconnect() {
3853
5081
  return this.baseAdapter.disconnect();
3854
5082
  }
5083
+ async close() {
5084
+ return this.baseAdapter.close();
5085
+ }
3855
5086
  /**
3856
5087
  * Gets the underlying database client.
3857
5088
  *
@@ -4235,8 +5466,41 @@ var SoftDeleteAdapter = class {
4235
5466
  return this.config.excludeTables?.includes(table) ?? false;
4236
5467
  }
4237
5468
  };
5469
+ var DATE_PART_MIN_WIDTH = 2;
5470
+ var CONTEXT_SOURCE_PATTERNS = [
5471
+ { patterns: ["encrypt"], source: db.EXTENSION_SOURCE.Encryption },
5472
+ {
5473
+ patterns: ["softdelete", "soft_delete"],
5474
+ source: db.EXTENSION_SOURCE.SoftDelete
5475
+ },
5476
+ { patterns: ["cach"], source: db.EXTENSION_SOURCE.Caching },
5477
+ { patterns: ["audit"], source: db.EXTENSION_SOURCE.Audit },
5478
+ {
5479
+ patterns: ["replica", "read_replica"],
5480
+ source: db.EXTENSION_SOURCE.ReadReplica
5481
+ },
5482
+ {
5483
+ patterns: ["multi_write", "multiwrite"],
5484
+ source: db.EXTENSION_SOURCE.MultiWrite
5485
+ }
5486
+ ];
5487
+ var MESSAGE_SOURCE_PATTERNS = [
5488
+ { patterns: ["encrypt"], source: db.EXTENSION_SOURCE.Encryption },
5489
+ {
5490
+ patterns: ["soft delete", "softdelete"],
5491
+ source: db.EXTENSION_SOURCE.SoftDelete
5492
+ },
5493
+ { patterns: ["cache", "caching"], source: db.EXTENSION_SOURCE.Caching },
5494
+ {
5495
+ patterns: ["replica", "read replica"],
5496
+ source: db.EXTENSION_SOURCE.ReadReplica
5497
+ },
5498
+ {
5499
+ patterns: ["multi-write", "multiwrite"],
5500
+ source: db.EXTENSION_SOURCE.MultiWrite
5501
+ }
5502
+ ];
4238
5503
  var AuditAdapter = class {
4239
- // Using shared logger instance from @plyaz/logger
4240
5504
  /**
4241
5505
  * Creates a new AuditAdapter instance.
4242
5506
  *
@@ -4250,9 +5514,11 @@ var AuditAdapter = class {
4250
5514
  * ```typescript
4251
5515
  * const auditAdapter = new AuditAdapter(baseAdapter, {
4252
5516
  * enabled: true,
4253
- * retentionDays: 90,
5517
+ * retentionDays: 180,
4254
5518
  * excludeFields: ['password', 'token'],
4255
5519
  * excludeTables: ['temp_data'],
5520
+ * schema: 'audit',
5521
+ * usePartitionedTables: true,
4256
5522
  * onAuditAfterWrite: async (event) => {
4257
5523
  * await complianceService.recordAudit(event);
4258
5524
  * }
@@ -4262,11 +5528,18 @@ var AuditAdapter = class {
4262
5528
  constructor(baseAdapter, config) {
4263
5529
  this.baseAdapter = baseAdapter;
4264
5530
  this.config = config;
5531
+ this.auditSchema = config.schema ?? "audit";
5532
+ this.usePartitionedTables = config.usePartitionedTables ?? true;
4265
5533
  }
4266
5534
  static {
4267
5535
  __name(this, "AuditAdapter");
4268
5536
  }
4269
5537
  auditContext = {};
5538
+ // Using shared logger instance from @plyaz/logger
5539
+ /** Cached schema-qualified table name */
5540
+ auditSchema;
5541
+ /** Whether to use daily partitioned tables */
5542
+ usePartitionedTables;
4270
5543
  /**
4271
5544
  * Initializes the audit adapter and underlying adapter.
4272
5545
  *
@@ -4316,6 +5589,9 @@ var AuditAdapter = class {
4316
5589
  async disconnect() {
4317
5590
  return this.baseAdapter.disconnect();
4318
5591
  }
5592
+ async close() {
5593
+ return this.baseAdapter.close();
5594
+ }
4319
5595
  /**
4320
5596
  * Gets the underlying database client.
4321
5597
  *
@@ -4454,11 +5730,36 @@ var AuditAdapter = class {
4454
5730
  }
4455
5731
  async create(table, data) {
4456
5732
  this.validateCreateParams(table, data);
4457
- const result = await this.baseAdapter.create(table, data);
4458
- if (result.success && this.shouldAudit(table)) {
4459
- await this.handleCreateAudit(table, result.value);
5733
+ try {
5734
+ const result = await this.baseAdapter.create(table, data);
5735
+ if (result.success && this.shouldAudit(table)) {
5736
+ await this.logAudit({
5737
+ operation: db.AUDIT_OPERATION.Create,
5738
+ table,
5739
+ recordId: result.value?.id,
5740
+ changes: {
5741
+ after: result.value,
5742
+ encryptedFields: this.getEncryptedFields(table)
5743
+ },
5744
+ userId: this.auditContext.userId,
5745
+ requestId: this.auditContext.requestId,
5746
+ timestamp: /* @__PURE__ */ new Date(),
5747
+ ipAddress: this.auditContext.ipAddress,
5748
+ userAgent: this.auditContext.userAgent
5749
+ });
5750
+ }
5751
+ return result;
5752
+ } catch (error) {
5753
+ if (this.shouldAudit(table)) {
5754
+ await this.logOperationFailure({
5755
+ operation: db.AUDIT_OPERATION.Create,
5756
+ table,
5757
+ data,
5758
+ error
5759
+ });
5760
+ }
5761
+ throw error;
4460
5762
  }
4461
- return result;
4462
5763
  }
4463
5764
  validateCreateParams(table, data) {
4464
5765
  if (!table || !data) {
@@ -4472,14 +5773,99 @@ var AuditAdapter = class {
4472
5773
  );
4473
5774
  }
4474
5775
  }
4475
- async handleCreateAudit(table, value) {
5776
+ async update(table, id, data) {
5777
+ const before = await this.baseAdapter.findById(table, id);
5778
+ try {
5779
+ const result = await this.baseAdapter.update(table, id, data);
5780
+ if (result.success && this.shouldAudit(table)) {
5781
+ await this.logAudit({
5782
+ operation: db.AUDIT_OPERATION.Update,
5783
+ table,
5784
+ recordId: id,
5785
+ changes: {
5786
+ before: before.success ? before.value : void 0,
5787
+ after: result.value,
5788
+ fields: Object.keys(data),
5789
+ encryptedFields: this.getEncryptedFields(table)
5790
+ },
5791
+ userId: this.auditContext.userId,
5792
+ requestId: this.auditContext.requestId,
5793
+ timestamp: /* @__PURE__ */ new Date(),
5794
+ ipAddress: this.auditContext.ipAddress,
5795
+ userAgent: this.auditContext.userAgent
5796
+ });
5797
+ }
5798
+ return result;
5799
+ } catch (error) {
5800
+ if (this.shouldAudit(table)) {
5801
+ await this.logOperationFailure({
5802
+ operation: db.AUDIT_OPERATION.Update,
5803
+ table,
5804
+ data,
5805
+ error,
5806
+ recordId: id,
5807
+ beforeState: before.value
5808
+ });
5809
+ }
5810
+ throw error;
5811
+ }
5812
+ }
5813
+ async delete(table, id) {
5814
+ const before = await this.baseAdapter.findById(table, id);
5815
+ try {
5816
+ const result = await this.baseAdapter.delete(table, id);
5817
+ if (result.success && this.shouldAudit(table)) {
5818
+ await this.logAudit({
5819
+ operation: db.AUDIT_OPERATION.Delete,
5820
+ table,
5821
+ recordId: id,
5822
+ changes: {
5823
+ before: before.success ? before.value : void 0
5824
+ },
5825
+ userId: this.auditContext.userId,
5826
+ requestId: this.auditContext.requestId,
5827
+ timestamp: /* @__PURE__ */ new Date(),
5828
+ ipAddress: this.auditContext.ipAddress,
5829
+ userAgent: this.auditContext.userAgent
5830
+ });
5831
+ }
5832
+ return result;
5833
+ } catch (error) {
5834
+ if (this.shouldAudit(table)) {
5835
+ await this.logOperationFailure({
5836
+ operation: db.AUDIT_OPERATION.Delete,
5837
+ table,
5838
+ data: null,
5839
+ error,
5840
+ recordId: id,
5841
+ beforeState: before.value
5842
+ });
5843
+ }
5844
+ throw error;
5845
+ }
5846
+ }
5847
+ /**
5848
+ * Logs operation failures to audit for compliance tracking.
5849
+ * Captures the before state, attempted changes, and error details.
5850
+ */
5851
+ async logOperationFailure(options) {
5852
+ const { operation, table, data, error, recordId, beforeState } = options;
4476
5853
  try {
5854
+ const errorSource = this.getErrorSource(error);
5855
+ const failedOperation = this.getFailedOperation(operation);
4477
5856
  await this.logAudit({
4478
- operation: db.AUDIT_OPERATION.CREATE,
5857
+ operation: failedOperation,
4479
5858
  table,
4480
- recordId: value?.id,
5859
+ recordId: recordId ?? data?.id,
4481
5860
  changes: {
4482
- after: value
5861
+ before: beforeState,
5862
+ attempted: data,
5863
+ failure: {
5864
+ source: errorSource,
5865
+ error_type: error.name,
5866
+ error_message: error.message,
5867
+ error_code: error.errorCode
5868
+ }
4483
5869
  },
4484
5870
  userId: this.auditContext.userId,
4485
5871
  requestId: this.auditContext.requestId,
@@ -4489,55 +5875,67 @@ var AuditAdapter = class {
4489
5875
  });
4490
5876
  } catch (auditError) {
4491
5877
  logger.logger.error(
4492
- `Audit logging failed for CREATE operation: ${auditError.message}`
5878
+ `Failed to log operation failure to audit: ${auditError.message}`
4493
5879
  );
4494
5880
  }
4495
5881
  }
4496
- async update(table, id, data) {
4497
- const before = await this.baseAdapter.findById(table, id);
4498
- const result = await this.baseAdapter.update(table, id, data);
4499
- if (result.success && this.shouldAudit(table)) {
4500
- await this.logAudit({
4501
- operation: db.AUDIT_OPERATION.UPDATE,
4502
- table,
4503
- recordId: id,
4504
- changes: {
4505
- before: before.success ? before.value : void 0,
4506
- after: result.value,
4507
- fields: Object.keys(data)
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
- });
5882
+ /**
5883
+ * Maps a successful operation to its failed counterpart.
5884
+ */
5885
+ getFailedOperation(operation) {
5886
+ switch (operation) {
5887
+ case db.AUDIT_OPERATION.Create:
5888
+ return db.AUDIT_OPERATION.CreateFailed;
5889
+ case db.AUDIT_OPERATION.Update:
5890
+ return db.AUDIT_OPERATION.UpdateFailed;
5891
+ case db.AUDIT_OPERATION.Delete:
5892
+ return db.AUDIT_OPERATION.DeleteFailed;
5893
+ default:
5894
+ return operation;
4515
5895
  }
4516
- return result;
4517
5896
  }
4518
- async delete(table, id) {
4519
- const before = await this.baseAdapter.findById(table, id);
4520
- const result = await this.baseAdapter.delete(table, id);
4521
- if (result.success && this.shouldAudit(table)) {
4522
- await this.logAudit({
4523
- operation: db.AUDIT_OPERATION.DELETE,
4524
- table,
4525
- recordId: id,
4526
- changes: {
4527
- before: before.success ? before.value : void 0
4528
- },
4529
- userId: this.auditContext.userId,
4530
- requestId: this.auditContext.requestId,
4531
- timestamp: /* @__PURE__ */ new Date(),
4532
- ipAddress: this.auditContext.ipAddress,
4533
- userAgent: this.auditContext.userAgent
4534
- });
5897
+ /**
5898
+ * Determines which extension/layer caused the error based on error details.
5899
+ */
5900
+ getErrorSource(error) {
5901
+ const errorMessage = error.message.toLowerCase();
5902
+ const dbError = error;
5903
+ const errorContext = dbError.context?.source?.toLowerCase();
5904
+ if (errorContext) {
5905
+ const contextSource = this.matchPatterns(
5906
+ errorContext,
5907
+ CONTEXT_SOURCE_PATTERNS
5908
+ );
5909
+ if (contextSource) return contextSource;
4535
5910
  }
4536
- return result;
5911
+ const messageSource = this.matchPatterns(
5912
+ errorMessage,
5913
+ MESSAGE_SOURCE_PATTERNS
5914
+ );
5915
+ if (messageSource) return messageSource;
5916
+ return db.EXTENSION_SOURCE.DatabaseAdapter;
4537
5917
  }
4538
5918
  /**
4539
- * Executes operations within a transaction with audit logging.
4540
- *
5919
+ * Matches a text against a list of pattern groups.
5920
+ */
5921
+ matchPatterns(text, patternGroups) {
5922
+ for (const { patterns, source } of patternGroups) {
5923
+ if (patterns.some((pattern) => text.includes(pattern))) {
5924
+ return source;
5925
+ }
5926
+ }
5927
+ return null;
5928
+ }
5929
+ /**
5930
+ * Gets the list of encrypted fields for a table.
5931
+ * Returns undefined if no fields are encrypted for this table.
5932
+ */
5933
+ getEncryptedFields(table) {
5934
+ return this.config.encryptedFields?.[table];
5935
+ }
5936
+ /**
5937
+ * Executes operations within a transaction with audit logging.
5938
+ *
4541
5939
  * **RESPONSIBILITY:** Passes transaction to base adapter
4542
5940
  * **BEHAVIOR:** Individual operations within transaction are audited
4543
5941
  * **ATOMICITY:** Audit records are part of the transaction
@@ -4709,11 +6107,38 @@ var AuditAdapter = class {
4709
6107
  );
4710
6108
  }
4711
6109
  }
6110
+ /**
6111
+ * Generates the audit table name based on configuration.
6112
+ *
6113
+ * **RESPONSIBILITY:** Determines the correct table name for audit records
6114
+ * **TABLE NAMING:**
6115
+ * - Partitioned: `{schema}.audit_log_yyyy_mm_dd` (e.g., audit.audit_log_2024_12_01)
6116
+ * - Non-partitioned: `{schema}.audit_logs` (e.g., audit.audit_logs)
6117
+ *
6118
+ * @private
6119
+ * @param timestamp - Timestamp to use for partition date
6120
+ * @returns Schema-qualified table name
6121
+ */
6122
+ getAuditTableName(timestamp) {
6123
+ if (this.usePartitionedTables) {
6124
+ const year = timestamp.getFullYear();
6125
+ const month = String(timestamp.getMonth() + 1).padStart(
6126
+ DATE_PART_MIN_WIDTH,
6127
+ "0"
6128
+ );
6129
+ const day = String(timestamp.getDate()).padStart(
6130
+ DATE_PART_MIN_WIDTH,
6131
+ "0"
6132
+ );
6133
+ return `${this.auditSchema}.audit_log_${year}_${month}_${day}`;
6134
+ }
6135
+ return `${this.auditSchema}.audit_logs`;
6136
+ }
4712
6137
  /**
4713
6138
  * Writes audit record to daily audit table.
4714
6139
  *
4715
6140
  * **RESPONSIBILITY:** Persists audit event to database
4716
- * **TABLE NAMING:** Uses daily tables (audit_YYYYMMDD)
6141
+ * **TABLE NAMING:** Uses daily partitioned tables (audit.audit_log_yyyy_mm_dd) by default
4717
6142
  * **STRUCTURE:** Converts event to database record format
4718
6143
  *
4719
6144
  * @private
@@ -4722,7 +6147,7 @@ var AuditAdapter = class {
4722
6147
  *
4723
6148
  * @example
4724
6149
  * ```typescript
4725
- * // Internal usage - writes to audit_20241201 table
6150
+ * // Internal usage - writes to audit.audit_log_2024_12_01 table
4726
6151
  * await this.writeAuditRecord({
4727
6152
  * operation: 'UPDATE',
4728
6153
  * table: 'users',
@@ -4733,7 +6158,7 @@ var AuditAdapter = class {
4733
6158
  * ```
4734
6159
  */
4735
6160
  async writeAuditRecord(event) {
4736
- const tableName = "audit_logs";
6161
+ const tableName = this.getAuditTableName(event.timestamp);
4737
6162
  try {
4738
6163
  const auditResult = await this.baseAdapter.create(tableName, {
4739
6164
  operation: event.operation,
@@ -4821,6 +6246,9 @@ var EncryptionAdapter = class {
4821
6246
  async disconnect() {
4822
6247
  return this.baseAdapter.disconnect();
4823
6248
  }
6249
+ async close() {
6250
+ return this.baseAdapter.close();
6251
+ }
4824
6252
  getClient() {
4825
6253
  return this.baseAdapter.getClient();
4826
6254
  }
@@ -4890,14 +6318,10 @@ var EncryptionAdapter = class {
4890
6318
  return this.config.enabled && isObject(data) && Boolean(this.config.fields[table]);
4891
6319
  }
4892
6320
  encryptSingleField(result, field) {
4893
- try {
4894
- if (result[field]) {
4895
- result[field] = this.encrypt(
4896
- String(result[field])
4897
- );
4898
- }
4899
- } catch (error) {
4900
- console.error(`Failed to encrypt field ${field}:`, error);
6321
+ if (result[field]) {
6322
+ result[field] = this.encrypt(
6323
+ String(result[field])
6324
+ );
4901
6325
  }
4902
6326
  }
4903
6327
  decryptFields(table, data) {
@@ -4910,15 +6334,18 @@ var EncryptionAdapter = class {
4910
6334
  return result;
4911
6335
  }
4912
6336
  decryptSingleField(result, field) {
4913
- try {
4914
- if (result[field]) {
4915
- const fieldValue = String(result[field]);
4916
- if (this.isEncryptedValue(fieldValue)) {
6337
+ if (result[field]) {
6338
+ const fieldValue = String(result[field]);
6339
+ if (this.isEncryptedValue(fieldValue)) {
6340
+ try {
4917
6341
  result[field] = this.decrypt(fieldValue);
6342
+ } catch (error) {
6343
+ console.warn(
6344
+ `Failed to decrypt field ${field}, returning as-is:`,
6345
+ error.message
6346
+ );
4918
6347
  }
4919
6348
  }
4920
- } catch (error) {
4921
- console.error(`Failed to decrypt field ${field}:`, error);
4922
6349
  }
4923
6350
  }
4924
6351
  encrypt(text) {
@@ -5124,6 +6551,9 @@ var CachingAdapter = class {
5124
6551
  async disconnect() {
5125
6552
  return this.baseAdapter.disconnect();
5126
6553
  }
6554
+ async close() {
6555
+ return this.baseAdapter.close();
6556
+ }
5127
6557
  /**
5128
6558
  * Gets the underlying database client.
5129
6559
  *
@@ -5548,6 +6978,9 @@ var ReadReplicaAdapter = class {
5548
6978
  async disconnect() {
5549
6979
  return this.primaryAdapter.disconnect();
5550
6980
  }
6981
+ async close() {
6982
+ return this.primaryAdapter.close();
6983
+ }
5551
6984
  getClient() {
5552
6985
  return this.primaryAdapter.getClient();
5553
6986
  }
@@ -5625,7 +7058,11 @@ function buildAdapterChain(baseAdapter, config) {
5625
7058
  adapter = new CachingAdapter(adapter, config.cache);
5626
7059
  }
5627
7060
  if (config.audit?.enabled) {
5628
- adapter = new AuditAdapter(adapter, config.audit);
7061
+ adapter = new AuditAdapter(adapter, {
7062
+ ...config.audit,
7063
+ // Pass encrypted fields config so audit can log which fields are encrypted at rest
7064
+ encryptedFields: config.encryption?.enabled ? config.encryption.fields : void 0
7065
+ });
5629
7066
  }
5630
7067
  if (config.readReplica?.enabled) {
5631
7068
  adapter = new ReadReplicaAdapter(adapter, {
@@ -5647,8 +7084,10 @@ function createAdapterConfig(config) {
5647
7084
  // orm: ADAPTERS.SUPABASE,
5648
7085
  connectionString: drizzleConfig.connectionString ?? drizzleConfig.url,
5649
7086
  // Support both formats
5650
- pool: drizzleConfig.poolSize ? { max: drizzleConfig.poolSize } : void 0
7087
+ pool: drizzleConfig.poolSize ? { max: drizzleConfig.poolSize } : void 0,
5651
7088
  // Optional connection pooling
7089
+ tableIdColumns: drizzleConfig.tableIdColumns
7090
+ // Custom ID column mappings
5652
7091
  };
5653
7092
  }
5654
7093
  if (config.adapter === db.ADAPTER_TYPES.SUPABASE) {
@@ -5662,15 +7101,28 @@ function createAdapterConfig(config) {
5662
7101
  // Public API key
5663
7102
  supabaseServiceKey: supabaseConfig.supabaseServiceKey,
5664
7103
  // Service role key (admin)
5665
- schema: supabaseConfig.schema
7104
+ schema: supabaseConfig.schema,
5666
7105
  // Database schema
7106
+ tableIdColumns: supabaseConfig.tableIdColumns
7107
+ // Custom ID column mappings
7108
+ };
7109
+ }
7110
+ if (config.adapter === db.ADAPTER_TYPES.MOCK) {
7111
+ return {
7112
+ adapter: db.ADAPTERS.MOCK,
7113
+ ...config.config
7114
+ // Pass through all mock config options
5667
7115
  };
5668
7116
  }
5669
7117
  const sqlConfig = config.config;
5670
7118
  return {
5671
7119
  adapter: db.ADAPTERS.SQL,
5672
- connectionString: sqlConfig.connectionString ?? sqlConfig.url
7120
+ connectionString: sqlConfig.connectionString ?? sqlConfig.url,
5673
7121
  // Support both formats
7122
+ schema: sqlConfig.schema,
7123
+ // Default schema for all tables
7124
+ tableIdColumns: sqlConfig.tableIdColumns
7125
+ // Custom ID column mappings
5674
7126
  };
5675
7127
  }
5676
7128
  __name(createAdapterConfig, "createAdapterConfig");
@@ -5751,18 +7203,33 @@ __name(createDatabaseService, "createDatabaseService");
5751
7203
 
5752
7204
  // src/repository/BaseRepository.ts
5753
7205
  var BaseRepository = class {
5754
- // Using shared logger instance from @plyaz/logger
5755
- constructor(db, tableName) {
7206
+ constructor(db, tableName, defaultConfig) {
5756
7207
  this.db = db;
5757
7208
  this.tableName = tableName;
7209
+ this.defaultConfig = defaultConfig;
5758
7210
  }
5759
7211
  static {
5760
7212
  __name(this, "BaseRepository");
5761
7213
  }
7214
+ defaultConfig;
7215
+ /**
7216
+ * Merges default repository config with per-operation config
7217
+ * Per-operation config takes precedence over default config
7218
+ */
7219
+ mergeConfig(operationConfig) {
7220
+ if (!this.defaultConfig && !operationConfig) {
7221
+ return void 0;
7222
+ }
7223
+ return {
7224
+ ...this.defaultConfig,
7225
+ ...operationConfig
7226
+ };
7227
+ }
5762
7228
  /**
5763
7229
  * Find a single entity by its primary key ID
5764
7230
  *
5765
7231
  * @param {string} id - The primary key ID of the entity to retrieve
7232
+ * @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
5766
7233
  * @returns {Promise<DatabaseResult<T | null>>} Promise resolving to the entity or null if not found
5767
7234
  *
5768
7235
  * @example
@@ -5771,15 +7238,21 @@ var BaseRepository = class {
5771
7238
  * if (result.success && result.value) {
5772
7239
  * console.log('Found user:', result.value.name);
5773
7240
  * }
7241
+ *
7242
+ * // Use specific adapter for this query
7243
+ * const analyticsResult = await userRepository.findById('user-123', {
7244
+ * adapter: 'analytics'
7245
+ * });
5774
7246
  * ```
5775
7247
  */
5776
- async findById(id) {
5777
- return this.db.findById(this.tableName, id);
7248
+ async findById(id, config) {
7249
+ return this.db.get(this.tableName, id, this.mergeConfig(config));
5778
7250
  }
5779
7251
  /**
5780
7252
  * Find multiple entities with optional filtering, sorting, and pagination
5781
7253
  *
5782
- * @param {QueryOptions} [options] - Optional query configuration
7254
+ * @param {QueryOptions<T>} [options] - Optional query configuration with type-safe fields
7255
+ * @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
5783
7256
  * @returns {Promise<DatabaseResult<PaginatedResult<T>>>} Promise resolving to paginated results
5784
7257
  *
5785
7258
  * @example
@@ -5789,15 +7262,21 @@ var BaseRepository = class {
5789
7262
  * sort: [{ field: 'createdAt', direction: 'desc' }],
5790
7263
  * pagination: { limit: 20, offset: 0 }
5791
7264
  * });
7265
+ *
7266
+ * // Query from analytics database
7267
+ * const analyticsResult = await userRepository.findMany({}, {
7268
+ * adapter: 'analytics'
7269
+ * });
5792
7270
  * ```
5793
7271
  */
5794
- async findMany(options) {
5795
- return this.db.findMany(this.tableName, options);
7272
+ async findMany(options, config) {
7273
+ return this.db.list(this.tableName, options, this.mergeConfig(config));
5796
7274
  }
5797
7275
  /**
5798
7276
  * Create a new entity in the database
5799
7277
  *
5800
- * @param {T} data - The entity data to create
7278
+ * @param {CreateInput<T>} data - The entity data to create (id is auto-generated)
7279
+ * @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
5801
7280
  * @returns {Promise<DatabaseResult<T>>} Promise resolving to the created entity
5802
7281
  *
5803
7282
  * @example
@@ -5806,16 +7285,25 @@ var BaseRepository = class {
5806
7285
  * name: 'John Doe',
5807
7286
  * email: 'john@example.com'
5808
7287
  * });
7288
+ *
7289
+ * // Create in specific database/schema
7290
+ * const result = await userRepository.create({
7291
+ * name: 'Jane Doe'
7292
+ * }, {
7293
+ * adapter: 'secondary',
7294
+ * schema: 'backoffice'
7295
+ * });
5809
7296
  * ```
5810
7297
  */
5811
- async create(data) {
5812
- return this.db.create(this.tableName, data);
7298
+ async create(data, config) {
7299
+ return this.db.create(this.tableName, data, this.mergeConfig(config));
5813
7300
  }
5814
7301
  /**
5815
7302
  * Update an existing entity by ID
5816
7303
  *
5817
7304
  * @param {string} id - The primary key ID of the entity to update
5818
7305
  * @param {Partial<T>} data - Partial entity data containing fields to update
7306
+ * @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
5819
7307
  * @returns {Promise<DatabaseResult<T>>} Promise resolving to the updated entity
5820
7308
  *
5821
7309
  * @example
@@ -5824,15 +7312,29 @@ var BaseRepository = class {
5824
7312
  * email: 'newemail@example.com',
5825
7313
  * updatedAt: new Date()
5826
7314
  * });
7315
+ *
7316
+ * // Update in specific adapter with custom ID column
7317
+ * const result = await userRepository.update('flag-key', {
7318
+ * value: true
7319
+ * }, {
7320
+ * adapter: 'config',
7321
+ * idColumn: 'key'
7322
+ * });
5827
7323
  * ```
5828
7324
  */
5829
- async update(id, data) {
5830
- return this.db.update(this.tableName, id, data);
7325
+ async update(id, data, config) {
7326
+ return this.db.update(
7327
+ this.tableName,
7328
+ id,
7329
+ data,
7330
+ this.mergeConfig(config)
7331
+ );
5831
7332
  }
5832
7333
  /**
5833
7334
  * Delete an entity by ID (hard delete)
5834
7335
  *
5835
7336
  * @param {string} id - The primary key ID of the entity to delete
7337
+ * @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
5836
7338
  * @returns {Promise<DatabaseResult<void>>} Promise resolving when deletion is complete
5837
7339
  *
5838
7340
  * @warning This is a permanent operation that cannot be undone
@@ -5841,15 +7343,21 @@ var BaseRepository = class {
5841
7343
  * @example
5842
7344
  * ```typescript
5843
7345
  * const result = await userRepository.delete('user-123');
7346
+ *
7347
+ * // Delete from specific adapter
7348
+ * const result = await userRepository.delete('user-123', {
7349
+ * adapter: 'archive'
7350
+ * });
5844
7351
  * ```
5845
7352
  */
5846
- async delete(id) {
5847
- return this.db.delete(this.tableName, id);
7353
+ async delete(id, config) {
7354
+ return this.db.delete(this.tableName, id, this.mergeConfig(config));
5848
7355
  }
5849
7356
  /**
5850
7357
  * Count entities matching optional filter criteria
5851
7358
  *
5852
- * @param {Filter} [filter] - Optional filter conditions
7359
+ * @param {Filter<T>} [filter] - Optional filter conditions with type-safe fields
7360
+ * @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
5853
7361
  * @returns {Promise<DatabaseResult<number>>} Promise resolving to the count
5854
7362
  *
5855
7363
  * @example
@@ -5858,15 +7366,21 @@ var BaseRepository = class {
5858
7366
  * const activeResult = await userRepository.count({
5859
7367
  * field: 'status', operator: 'eq', value: 'active'
5860
7368
  * });
7369
+ *
7370
+ * // Count in specific adapter
7371
+ * const archiveCount = await userRepository.count(undefined, {
7372
+ * adapter: 'archive'
7373
+ * });
5861
7374
  * ```
5862
7375
  */
5863
- async count(filter) {
5864
- return this.db.count(this.tableName, filter);
7376
+ async count(filter, config) {
7377
+ return this.db.count(this.tableName, filter, this.mergeConfig(config));
5865
7378
  }
5866
7379
  /**
5867
7380
  * Check if an entity exists by ID
5868
7381
  *
5869
7382
  * @param {string} id - The primary key ID to check
7383
+ * @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
5870
7384
  * @returns {Promise<DatabaseResult<boolean>>} Promise resolving to existence status
5871
7385
  *
5872
7386
  * @example
@@ -5875,15 +7389,30 @@ var BaseRepository = class {
5875
7389
  * if (existsResult.success && existsResult.value) {
5876
7390
  * console.log('User exists');
5877
7391
  * }
7392
+ *
7393
+ * // Check existence in specific adapter
7394
+ * const existsInArchive = await userRepository.exists('user-123', {
7395
+ * adapter: 'archive'
7396
+ * });
5878
7397
  * ```
5879
7398
  */
5880
- async exists(id) {
5881
- return this.db.exists(this.tableName, id);
7399
+ async exists(id, config) {
7400
+ const result = await this.db.get(
7401
+ this.tableName,
7402
+ id,
7403
+ this.mergeConfig(config)
7404
+ );
7405
+ return {
7406
+ success: result.success,
7407
+ value: result.success && result.value !== null,
7408
+ error: result.error
7409
+ };
5882
7410
  }
5883
7411
  /**
5884
7412
  * Find the first entity matching filter criteria
5885
7413
  *
5886
- * @param {Filter} filter - Filter conditions to match
7414
+ * @param {Filter<T>} filter - Filter conditions with type-safe fields
7415
+ * @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
5887
7416
  * @returns {Promise<DatabaseResult<T | null>>} Promise resolving to first match or null
5888
7417
  *
5889
7418
  * @example
@@ -5891,15 +7420,24 @@ var BaseRepository = class {
5891
7420
  * const result = await userRepository.findOne({
5892
7421
  * field: 'email', operator: 'eq', value: 'john@example.com'
5893
7422
  * });
7423
+ *
7424
+ * // Find in specific adapter
7425
+ * const archivedUser = await userRepository.findOne({
7426
+ * field: 'email', operator: 'eq', value: 'john@example.com'
7427
+ * }, {
7428
+ * adapter: 'archive',
7429
+ * includeSoftDeleted: true
7430
+ * });
5894
7431
  * ```
5895
7432
  */
5896
- async findOne(filter) {
5897
- return this.db.findOne(this.tableName, filter);
7433
+ async findOne(filter, config) {
7434
+ return this.db.findOne(this.tableName, filter, this.mergeConfig(config));
5898
7435
  }
5899
7436
  /**
5900
7437
  * Soft delete an entity by ID (recoverable deletion)
5901
7438
  *
5902
7439
  * @param {string} id - The primary key ID of the entity to soft delete
7440
+ * @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
5903
7441
  * @returns {Promise<DatabaseResult<void>>} Promise resolving when soft deletion is complete
5904
7442
  *
5905
7443
  * @see {@link delete} For permanent deletion
@@ -5908,241 +7446,669 @@ var BaseRepository = class {
5908
7446
  * ```typescript
5909
7447
  * const result = await userRepository.softDelete('user-123');
5910
7448
  * // User is hidden but can be recovered
7449
+ *
7450
+ * // Soft delete in specific adapter
7451
+ * const result = await userRepository.softDelete('user-123', {
7452
+ * adapter: 'primary',
7453
+ * skipAudit: false
7454
+ * });
5911
7455
  * ```
5912
7456
  */
5913
- async softDelete(id) {
5914
- return this.db.softDelete(this.tableName, id);
7457
+ async softDelete(id, config) {
7458
+ return this.db.softDelete(this.tableName, id, this.mergeConfig(config));
5915
7459
  }
5916
7460
  };
5917
- var USE_REPLICA_KEY = "use_replica";
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 {
7461
+ var MultiWriteAdapter = class {
5925
7462
  static {
5926
- __name(this, "RedisCache");
7463
+ __name(this, "MultiWriteAdapter");
5927
7464
  }
5928
- redis;
5929
- defaultTTL;
5930
- // Using shared logger instance from @plyaz/logger
5931
- /**
5932
- * Creates a new RedisCache instance.
5933
- * @param config Redis configuration
5934
- *
5935
- * @example
5936
- * ```typescript
5937
- * // Basic configuration
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;
7465
+ baseAdapter;
7466
+ config;
7467
+ constructor(baseAdapter, config) {
7468
+ this.baseAdapter = baseAdapter;
7469
+ this.config = {
7470
+ mode: "best-effort",
7471
+ onSecondaryFailure: "log",
7472
+ timeout: 5e3,
7473
+ ...config
7474
+ };
5959
7475
  }
5960
- /**
5961
- * Retrieves a value from cache by key.
5962
- * Automatically handles JSON deserialization.
5963
- *
5964
- * @param key Cache key
5965
- * @returns DatabaseResult containing cached value or null if not found
5966
- *
5967
- * @example
5968
- * ```typescript
5969
- * // Get user profile
5970
- * const result = await cache.get<UserProfile>('profile:123');
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();
7476
+ async initialize() {
7477
+ return this.baseAdapter.initialize();
7478
+ }
7479
+ async close() {
7480
+ return this.baseAdapter.close();
7481
+ }
7482
+ async connect() {
7483
+ await this.baseAdapter.connect();
7484
+ for (const secondary of this.config.adapters) {
7485
+ if (typeof secondary.connect === "function") {
7486
+ await secondary.connect();
5991
7487
  }
5992
- const parsed = JSON.parse(value);
5993
- if (parsed && typeof parsed === "object") {
5994
- return success(parsed);
7488
+ }
7489
+ }
7490
+ async disconnect() {
7491
+ await this.baseAdapter.disconnect();
7492
+ for (const secondary of this.config.adapters) {
7493
+ if (typeof secondary.disconnect === "function") {
7494
+ await secondary.disconnect();
5995
7495
  }
5996
- return success();
5997
- } catch (error) {
5998
- return failure(
5999
- new errors.DatabaseError(
6000
- "Cache get failed",
6001
- errors$1.DATABASE_ERROR_CODES.CACHE_GET_FAILED,
6002
- { context: { source: "RedisCache.get", key, cause: error } }
6003
- )
6004
- );
7496
+ }
7497
+ }
7498
+ getClient() {
7499
+ return this.baseAdapter.getClient();
7500
+ }
7501
+ async query(sql2, params) {
7502
+ return this.baseAdapter.query(sql2, params);
7503
+ }
7504
+ registerTable(name, table, idColumn) {
7505
+ this.baseAdapter.registerTable(name, table, idColumn);
7506
+ for (const secondary of this.config.adapters) {
7507
+ if (typeof secondary.registerTable === "function") {
7508
+ secondary.registerTable(name, table, idColumn);
7509
+ }
7510
+ }
7511
+ }
7512
+ // Read operations - always use primary
7513
+ async findById(table, id) {
7514
+ return this.baseAdapter.findById(table, id);
7515
+ }
7516
+ async findMany(table, options) {
7517
+ return this.baseAdapter.findMany(table, options);
7518
+ }
7519
+ async exists(table, id) {
7520
+ return this.baseAdapter.exists(table, id);
7521
+ }
7522
+ async count(table, filter) {
7523
+ return this.baseAdapter.count(table, filter);
7524
+ }
7525
+ // Write operations - replicate to secondaries
7526
+ async create(table, data) {
7527
+ const primaryResult = await this.baseAdapter.create(table, data);
7528
+ if (!primaryResult.success) {
7529
+ return primaryResult;
7530
+ }
7531
+ await this.replicateWrite(
7532
+ () => Promise.all(
7533
+ this.config.adapters.map((adapter) => adapter.create(table, data))
7534
+ )
7535
+ );
7536
+ return primaryResult;
7537
+ }
7538
+ async update(table, id, data) {
7539
+ const primaryResult = await this.baseAdapter.update(table, id, data);
7540
+ if (!primaryResult.success) {
7541
+ return primaryResult;
7542
+ }
7543
+ await this.replicateWrite(
7544
+ () => Promise.all(
7545
+ this.config.adapters.map((adapter) => adapter.update(table, id, data))
7546
+ )
7547
+ );
7548
+ return primaryResult;
7549
+ }
7550
+ async delete(table, id) {
7551
+ const primaryResult = await this.baseAdapter.delete(table, id);
7552
+ if (!primaryResult.success) {
7553
+ return primaryResult;
7554
+ }
7555
+ await this.replicateWrite(
7556
+ () => Promise.all(
7557
+ this.config.adapters.map((adapter) => adapter.delete(table, id))
7558
+ )
7559
+ );
7560
+ return primaryResult;
7561
+ }
7562
+ async transaction(callback) {
7563
+ return this.baseAdapter.transaction(callback);
7564
+ }
7565
+ async healthCheck() {
7566
+ return this.baseAdapter.healthCheck();
7567
+ }
7568
+ /**
7569
+ * Replicate write operation to secondary adapters
7570
+ */
7571
+ async replicateWrite(fn) {
7572
+ if (this.config.mode === "strict") {
7573
+ const results = await Promise.allSettled([
7574
+ this.executeWithTimeout(fn(), this.config.timeout)
7575
+ ]);
7576
+ const failures = results.filter((r) => r.status === "rejected");
7577
+ if (failures.length > 0) {
7578
+ this.handleSecondaryFailure(failures);
7579
+ }
7580
+ } else {
7581
+ this.executeWithTimeout(fn(), this.config.timeout).then((results) => {
7582
+ const failures = results.filter((r) => !r.success);
7583
+ if (failures.length > 0) {
7584
+ this.handleSecondaryFailure(
7585
+ failures.map((f) => ({
7586
+ status: "rejected",
7587
+ reason: f.error
7588
+ }))
7589
+ );
7590
+ }
7591
+ }).catch((error) => {
7592
+ this.handleSecondaryFailure([
7593
+ { status: "rejected", reason: error }
7594
+ ]);
7595
+ });
6005
7596
  }
6006
7597
  }
6007
7598
  /**
6008
- * Sets a value in cache with optional TTL.
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
- * ```
7599
+ * Execute operation with timeout
6030
7600
  */
6031
- async set(key, value, ttl) {
6032
- try {
6033
- const ttlValue = ttl ?? this.defaultTTL;
6034
- await this.redis.set(key, JSON.stringify(value), "EX", ttlValue);
6035
- return success();
6036
- } catch (error) {
6037
- return failure(
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 } }
7601
+ async executeWithTimeout(promise, timeoutMs) {
7602
+ return Promise.race([
7603
+ promise,
7604
+ new Promise(
7605
+ (_, reject) => setTimeout(
7606
+ () => reject(new Error("Secondary write timeout")),
7607
+ timeoutMs
6042
7608
  )
6043
- );
6044
- }
7609
+ )
7610
+ ]);
6045
7611
  }
6046
7612
  /**
6047
- * Deletes a value from cache.
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
- * ```
7613
+ * Handle secondary adapter failures
6063
7614
  */
6064
- async del(key) {
7615
+ handleSecondaryFailure(failures) {
7616
+ const errorMessages = failures.map(
7617
+ (f) => f.status === "rejected" ? f.reason?.message : "Unknown error"
7618
+ ).join(", ");
7619
+ const message = `Multi-write secondary failure: ${errorMessages}`;
7620
+ switch (this.config.onSecondaryFailure) {
7621
+ case "log":
7622
+ console.log(`[MultiWrite] ${message}`);
7623
+ break;
7624
+ case "warn":
7625
+ console.warn(`[MultiWrite] ${message}`);
7626
+ break;
7627
+ case "throw":
7628
+ throw new errors.DatabasePackageError(message, errors$1.ERROR_CODES.DB_UPDATE_FAILED);
7629
+ }
7630
+ }
7631
+ };
7632
+ var EMA_EXISTING_WEIGHT = 0.8;
7633
+ var EMA_NEW_WEIGHT = 0.2;
7634
+ var MultiReadAdapter = class {
7635
+ static {
7636
+ __name(this, "MultiReadAdapter");
7637
+ }
7638
+ baseAdapter;
7639
+ config;
7640
+ currentReadIndex = 0;
7641
+ replicaHealth = /* @__PURE__ */ new Map();
7642
+ healthCheckTimer;
7643
+ constructor(baseAdapter, config) {
7644
+ this.baseAdapter = baseAdapter;
7645
+ this.config = {
7646
+ strategy: "round-robin",
7647
+ fallbackToPrimary: true,
7648
+ healthCheckInterval: 3e4,
7649
+ maxFailures: 3,
7650
+ ...config
7651
+ };
7652
+ this.initializeHealthTracking();
7653
+ if (this.config.adapters.length > 0) {
7654
+ this.startHealthChecks();
7655
+ }
7656
+ }
7657
+ async initialize() {
7658
+ return this.baseAdapter.initialize();
7659
+ }
7660
+ async close() {
7661
+ this.stopHealthChecks();
7662
+ return this.baseAdapter.close();
7663
+ }
7664
+ async connect() {
7665
+ await this.baseAdapter.connect();
7666
+ for (const replica of this.config.adapters) {
7667
+ if (typeof replica.connect === "function") {
7668
+ await replica.connect();
7669
+ }
7670
+ }
7671
+ }
7672
+ async disconnect() {
7673
+ await this.baseAdapter.disconnect();
7674
+ for (const replica of this.config.adapters) {
7675
+ if (typeof replica.disconnect === "function") {
7676
+ await replica.disconnect();
7677
+ }
7678
+ }
7679
+ }
7680
+ getClient() {
7681
+ return this.baseAdapter.getClient();
7682
+ }
7683
+ async query(sql2, params) {
7684
+ return this.baseAdapter.query(sql2, params);
7685
+ }
7686
+ registerTable(name, table, idColumn) {
7687
+ this.baseAdapter.registerTable(name, table, idColumn);
7688
+ for (const replica of this.config.adapters) {
7689
+ if (typeof replica.registerTable === "function") {
7690
+ replica.registerTable(name, table, idColumn);
7691
+ }
7692
+ }
7693
+ }
7694
+ // Read operations - use replicas with load balancing
7695
+ async findById(table, id) {
7696
+ return this.readFromReplicas((adapter) => adapter.findById(table, id));
7697
+ }
7698
+ async findMany(table, options) {
7699
+ return this.readFromReplicas(
7700
+ (adapter) => adapter.findMany(table, options)
7701
+ );
7702
+ }
7703
+ async exists(table, id) {
7704
+ return this.readFromReplicas((adapter) => adapter.exists(table, id));
7705
+ }
7706
+ async count(table, filter) {
7707
+ return this.readFromReplicas((adapter) => adapter.count(table, filter));
7708
+ }
7709
+ // Write operations - always use primary
7710
+ async create(table, data) {
7711
+ return this.baseAdapter.create(table, data);
7712
+ }
7713
+ async update(table, id, data) {
7714
+ return this.baseAdapter.update(table, id, data);
7715
+ }
7716
+ async delete(table, id) {
7717
+ return this.baseAdapter.delete(table, id);
7718
+ }
7719
+ async transaction(callback) {
7720
+ return this.baseAdapter.transaction(callback);
7721
+ }
7722
+ async healthCheck() {
7723
+ return this.baseAdapter.healthCheck();
7724
+ }
7725
+ /**
7726
+ * Read from replica adapters using configured strategy
7727
+ */
7728
+ async readFromReplicas(fn) {
7729
+ const healthyReplicas = this.config.adapters.filter(
7730
+ (adapter) => this.isReplicaHealthy(adapter)
7731
+ );
7732
+ if (healthyReplicas.length === 0) {
7733
+ if (this.config.fallbackToPrimary) {
7734
+ return fn(this.baseAdapter);
7735
+ }
7736
+ return failure(
7737
+ new errors.DatabasePackageError(
7738
+ "No healthy read replicas available",
7739
+ errors$1.ERROR_CODES.DB_CONNECTION_FAILED
7740
+ )
7741
+ );
7742
+ }
7743
+ const selectedReplica = this.selectReplica(healthyReplicas);
7744
+ const startTime = Date.now();
6065
7745
  try {
6066
- await this.redis.del(key);
6067
- return success();
7746
+ const result = await fn(selectedReplica);
7747
+ this.updateHealthMetrics(selectedReplica, true, Date.now() - startTime);
7748
+ if (result.success) {
7749
+ return result;
7750
+ }
7751
+ if (this.config.fallbackToPrimary) {
7752
+ this.updateHealthMetrics(
7753
+ selectedReplica,
7754
+ false,
7755
+ Date.now() - startTime
7756
+ );
7757
+ return fn(this.baseAdapter);
7758
+ }
7759
+ return result;
6068
7760
  } catch (error) {
7761
+ this.updateHealthMetrics(selectedReplica, false, Date.now() - startTime);
7762
+ if (this.config.fallbackToPrimary) {
7763
+ return fn(this.baseAdapter);
7764
+ }
6069
7765
  return failure(
6070
- new errors.DatabaseError(
6071
- "Cache delete failed",
6072
- errors$1.DATABASE_ERROR_CODES.CACHE_DELETE_FAILED,
6073
- {
6074
- context: {
6075
- source: "RedisCache.del",
6076
- key,
6077
- cause: error
6078
- }
6079
- }
7766
+ new errors.DatabasePackageError(
7767
+ `Read from replica failed: ${error.message}`,
7768
+ errors$1.ERROR_CODES.DB_QUERY_FAILED,
7769
+ { cause: error }
6080
7770
  )
6081
7771
  );
6082
7772
  }
6083
7773
  }
6084
7774
  /**
6085
- * Invalidates all cache entries matching a pattern.
6086
- * Useful for clearing related cache entries when data changes.
6087
- *
6088
- * @param pattern Redis key pattern (e.g., 'users:*')
6089
- * @returns DatabaseResult indicating operation success
7775
+ * Select a replica based on load balancing strategy
7776
+ */
7777
+ selectReplica(replicas) {
7778
+ switch (this.config.strategy) {
7779
+ case "round-robin":
7780
+ this.currentReadIndex = (this.currentReadIndex + 1) % replicas.length;
7781
+ return replicas[this.currentReadIndex];
7782
+ case "random":
7783
+ return replicas[Math.floor(Math.random() * replicas.length)];
7784
+ case "fastest":
7785
+ return replicas.reduce((fastest, current) => {
7786
+ const fastestHealth = this.replicaHealth.get(fastest);
7787
+ const currentHealth = this.replicaHealth.get(current);
7788
+ if (!fastestHealth || !currentHealth) return fastest;
7789
+ return currentHealth.avgResponseTime < fastestHealth.avgResponseTime ? current : fastest;
7790
+ });
7791
+ case "least-conn":
7792
+ return this.selectReplica(
7793
+ replicas.filter((r) => this.isReplicaHealthy(r))
7794
+ );
7795
+ default:
7796
+ return replicas[0];
7797
+ }
7798
+ }
7799
+ /**
7800
+ * Initialize health tracking for all replicas
7801
+ */
7802
+ initializeHealthTracking() {
7803
+ for (const adapter of this.config.adapters) {
7804
+ this.replicaHealth.set(adapter, {
7805
+ adapter,
7806
+ isHealthy: true,
7807
+ failureCount: 0,
7808
+ lastChecked: Date.now(),
7809
+ avgResponseTime: 0
7810
+ });
7811
+ }
7812
+ }
7813
+ /**
7814
+ * Update health metrics for a replica
7815
+ */
7816
+ updateHealthMetrics(adapter, success2, responseTime) {
7817
+ const health = this.replicaHealth.get(adapter);
7818
+ if (!health) return;
7819
+ if (success2) {
7820
+ health.failureCount = 0;
7821
+ health.isHealthy = true;
7822
+ health.avgResponseTime = health.avgResponseTime * EMA_EXISTING_WEIGHT + responseTime * EMA_NEW_WEIGHT;
7823
+ } else {
7824
+ health.failureCount++;
7825
+ if (health.failureCount >= this.config.maxFailures) {
7826
+ health.isHealthy = false;
7827
+ }
7828
+ }
7829
+ health.lastChecked = Date.now();
7830
+ }
7831
+ /**
7832
+ * Check if replica is healthy
7833
+ */
7834
+ isReplicaHealthy(adapter) {
7835
+ const health = this.replicaHealth.get(adapter);
7836
+ return health?.isHealthy ?? true;
7837
+ }
7838
+ /**
7839
+ * Start health check interval
7840
+ */
7841
+ startHealthChecks() {
7842
+ this.healthCheckTimer = setInterval(async () => {
7843
+ for (const adapter of this.config.adapters) {
7844
+ const startTime = Date.now();
7845
+ try {
7846
+ const result = await adapter.healthCheck();
7847
+ const responseTime = Date.now() - startTime;
7848
+ this.updateHealthMetrics(adapter, result.success, responseTime);
7849
+ } catch {
7850
+ this.updateHealthMetrics(adapter, false, Date.now() - startTime);
7851
+ }
7852
+ }
7853
+ }, this.config.healthCheckInterval);
7854
+ }
7855
+ /**
7856
+ * Stop health checks
7857
+ */
7858
+ stopHealthChecks() {
7859
+ if (this.healthCheckTimer) {
7860
+ clearInterval(this.healthCheckTimer);
7861
+ this.healthCheckTimer = void 0;
7862
+ }
7863
+ }
7864
+ /**
7865
+ * Get health status of all replicas
7866
+ */
7867
+ getHealthStatus() {
7868
+ const status = {};
7869
+ let index = 0;
7870
+ for (const [, health] of this.replicaHealth.entries()) {
7871
+ status[`replica-${index++}`] = health;
7872
+ }
7873
+ return status;
7874
+ }
7875
+ /**
7876
+ * Cleanup resources
7877
+ */
7878
+ dispose() {
7879
+ this.stopHealthChecks();
7880
+ this.replicaHealth.clear();
7881
+ }
7882
+ };
7883
+ var USE_REPLICA_KEY = "use_replica";
7884
+ function UseReplica(options = {}) {
7885
+ return (target, propertyKey, descriptor) => {
7886
+ common.SetMetadata(USE_REPLICA_KEY, options)(target, propertyKey, descriptor);
7887
+ };
7888
+ }
7889
+ __name(UseReplica, "UseReplica");
7890
+ var RedisCache = class {
7891
+ static {
7892
+ __name(this, "RedisCache");
7893
+ }
7894
+ redis;
7895
+ defaultTTL;
7896
+ // Using shared logger instance from @plyaz/logger
7897
+ /**
7898
+ * Creates a new RedisCache instance.
7899
+ * @param config Redis configuration
6090
7900
  *
6091
7901
  * @example
6092
7902
  * ```typescript
6093
- * // Invalidate all user-related caches
6094
- * await cache.invalidatePattern('users:*');
7903
+ * // Basic configuration
7904
+ * const cache = new RedisCache({
7905
+ * url: 'redis://localhost:6379'
7906
+ * });
6095
7907
  *
6096
- * // Invalidate all caches for a specific category
6097
- * await cache.invalidatePattern('products:category:electronics:*');
7908
+ * // With custom default TTL
7909
+ * const cacheWithCustomTTL = new RedisCache({
7910
+ * url: 'redis://localhost:6379',
7911
+ * defaultTTL: 1800 // 30 minutes
7912
+ * });
6098
7913
  *
6099
- * // Invalidate all session caches
6100
- * await cache.invalidatePattern('session:*');
7914
+ * // Production configuration with options
7915
+ * const productionCache = new RedisCache({
7916
+ * url: 'redis://redis-cluster.example.com:6379',
7917
+ * defaultTTL: 3600,
7918
+ * // Additional Redis options can be passed here
7919
+ * });
7920
+ * ```
7921
+ */
7922
+ constructor(config$1) {
7923
+ this.redis = new ioredis.Redis(config$1.url);
7924
+ this.defaultTTL = config$1.defaultTTL ?? config.NUMERIX.THIRTY_SIX_HUNDERD;
7925
+ }
7926
+ /**
7927
+ * Retrieves a value from cache by key.
7928
+ * Automatically handles JSON deserialization.
6101
7929
  *
6102
- * // Invalidate all caches for a tenant
6103
- * await cache.invalidatePattern(`tenant:${tenantId}:*`);
7930
+ * @param key Cache key
7931
+ * @returns DatabaseResult containing cached value or null if not found
6104
7932
  *
6105
- * // Invalidate all analytics caches
6106
- * await cache.invalidatePattern('analytics:*');
7933
+ * @example
7934
+ * ```typescript
7935
+ * // Get user profile
7936
+ * const result = await cache.get<UserProfile>('profile:123');
7937
+ * if (result.success && result.value) {
7938
+ * console.log('User:', result.value.name);
7939
+ * } else {
7940
+ * console.log('Profile not found or cache error');
7941
+ * }
7942
+ *
7943
+ * // Get product list
7944
+ * const products = await cache.get<Product[]>('products:featured');
7945
+ * if (products.success && products.value) {
7946
+ * products.value.forEach(product => {
7947
+ * console.log(product.name, product.price);
7948
+ * });
7949
+ * }
6107
7950
  * ```
6108
7951
  */
6109
- async invalidatePattern(pattern) {
7952
+ async get(key) {
6110
7953
  try {
6111
- const keys = await this.redis.keys(pattern);
6112
- if (keys.length > 0) {
6113
- await this.redis.del(...keys);
7954
+ const value = await this.redis.get(key);
7955
+ if (!value) {
7956
+ return success();
7957
+ }
7958
+ const parsed = JSON.parse(value);
7959
+ if (parsed && typeof parsed === "object") {
7960
+ return success(parsed);
6114
7961
  }
6115
7962
  return success();
6116
7963
  } catch (error) {
6117
7964
  return failure(
6118
7965
  new errors.DatabaseError(
6119
- "Cache invalidate failed",
6120
- errors$1.DATABASE_ERROR_CODES.CACHE_INVALIDATE_FAILED,
6121
- {
6122
- context: {
6123
- source: "RedisCache.invalidatePattern",
6124
- pattern,
6125
- cause: error
6126
- }
6127
- }
7966
+ "Cache get failed",
7967
+ errors$1.DATABASE_ERROR_CODES.CACHE_GET_FAILED,
7968
+ { context: { source: "RedisCache.get", key, cause: error } }
6128
7969
  )
6129
7970
  );
6130
7971
  }
6131
7972
  }
6132
7973
  /**
6133
- * Generates a cache key for database queries.
6134
- * Creates consistent, URL-safe keys that include table, operation, and parameters.
7974
+ * Sets a value in cache with optional TTL.
7975
+ * Automatically handles JSON serialization.
6135
7976
  *
6136
- * @param table Database table name
6137
- * @param operation Database operation type
6138
- * @param params Query parameters object
6139
- * @returns Generated cache key
7977
+ * @param key Cache key
7978
+ * @param value Value to cache
7979
+ * @param ttl Time to live in seconds (uses default if not specified)
7980
+ * @returns DatabaseResult indicating operation success
6140
7981
  *
6141
7982
  * @example
6142
7983
  * ```typescript
6143
- * // Simple key generation
6144
- * const key = cache.generateKey('users', 'findById', { id: '123' });
6145
- * // Returns: 'db:users:findById:eyJpYXJhbTAiOiIxMjMifQ=='
7984
+ * // Cache with default TTL
7985
+ * await cache.set('user:123', { id: 123, name: 'John' });
7986
+ *
7987
+ * // Cache with custom TTL (5 minutes)
7988
+ * await cache.set('session:abc123', sessionData, 300);
7989
+ *
7990
+ * // Cache with long TTL for static data
7991
+ * await cache.set('config:app-settings', appSettings, 86400); // 24 hours
7992
+ *
7993
+ * // Cache with short TTL for frequently changing data
7994
+ * await cache.set('metrics:realtime', realtimeData, 60); // 1 minute
7995
+ * ```
7996
+ */
7997
+ async set(key, value, ttl) {
7998
+ try {
7999
+ const ttlValue = ttl ?? this.defaultTTL;
8000
+ await this.redis.set(key, JSON.stringify(value), "EX", ttlValue);
8001
+ return success();
8002
+ } catch (error) {
8003
+ return failure(
8004
+ new errors.DatabaseError(
8005
+ "Cache set failed",
8006
+ errors$1.DATABASE_ERROR_CODES.CACHE_SET_FAILED,
8007
+ { context: { source: "RedisCache.set", key, cause: error } }
8008
+ )
8009
+ );
8010
+ }
8011
+ }
8012
+ /**
8013
+ * Deletes a value from cache.
8014
+ *
8015
+ * @param key Cache key
8016
+ * @returns DatabaseResult indicating operation success
8017
+ *
8018
+ * @example
8019
+ * ```typescript
8020
+ * // Delete specific cache entry
8021
+ * await cache.del('user:123');
8022
+ *
8023
+ * // Delete session on logout
8024
+ * await cache.del(`session:${sessionId}`);
8025
+ *
8026
+ * // Delete cached configuration
8027
+ * await cache.del('config:feature-flags');
8028
+ * ```
8029
+ */
8030
+ async del(key) {
8031
+ try {
8032
+ await this.redis.del(key);
8033
+ return success();
8034
+ } catch (error) {
8035
+ return failure(
8036
+ new errors.DatabaseError(
8037
+ "Cache delete failed",
8038
+ errors$1.DATABASE_ERROR_CODES.CACHE_DELETE_FAILED,
8039
+ {
8040
+ context: {
8041
+ source: "RedisCache.del",
8042
+ key,
8043
+ cause: error
8044
+ }
8045
+ }
8046
+ )
8047
+ );
8048
+ }
8049
+ }
8050
+ /**
8051
+ * Invalidates all cache entries matching a pattern.
8052
+ * Useful for clearing related cache entries when data changes.
8053
+ *
8054
+ * @param pattern Redis key pattern (e.g., 'users:*')
8055
+ * @returns DatabaseResult indicating operation success
8056
+ *
8057
+ * @example
8058
+ * ```typescript
8059
+ * // Invalidate all user-related caches
8060
+ * await cache.invalidatePattern('users:*');
8061
+ *
8062
+ * // Invalidate all caches for a specific category
8063
+ * await cache.invalidatePattern('products:category:electronics:*');
8064
+ *
8065
+ * // Invalidate all session caches
8066
+ * await cache.invalidatePattern('session:*');
8067
+ *
8068
+ * // Invalidate all caches for a tenant
8069
+ * await cache.invalidatePattern(`tenant:${tenantId}:*`);
8070
+ *
8071
+ * // Invalidate all analytics caches
8072
+ * await cache.invalidatePattern('analytics:*');
8073
+ * ```
8074
+ */
8075
+ async invalidatePattern(pattern) {
8076
+ try {
8077
+ const keys = await this.redis.keys(pattern);
8078
+ if (keys.length > 0) {
8079
+ await this.redis.del(...keys);
8080
+ }
8081
+ return success();
8082
+ } catch (error) {
8083
+ return failure(
8084
+ new errors.DatabaseError(
8085
+ "Cache invalidate failed",
8086
+ errors$1.DATABASE_ERROR_CODES.CACHE_INVALIDATE_FAILED,
8087
+ {
8088
+ context: {
8089
+ source: "RedisCache.invalidatePattern",
8090
+ pattern,
8091
+ cause: error
8092
+ }
8093
+ }
8094
+ )
8095
+ );
8096
+ }
8097
+ }
8098
+ /**
8099
+ * Generates a cache key for database queries.
8100
+ * Creates consistent, URL-safe keys that include table, operation, and parameters.
8101
+ *
8102
+ * @param table Database table name
8103
+ * @param operation Database operation type
8104
+ * @param params Query parameters object
8105
+ * @returns Generated cache key
8106
+ *
8107
+ * @example
8108
+ * ```typescript
8109
+ * // Simple key generation
8110
+ * const key = cache.generateKey('users', 'findById', { id: '123' });
8111
+ * // Returns: 'db:users:findById:eyJpYXJhbTAiOiIxMjMifQ=='
6146
8112
  *
6147
8113
  * // Complex query key
6148
8114
  * const complexKey = cache.generateKey('products', 'findMany', {
@@ -8476,6 +10442,655 @@ __name(exports.DataValidationPipe, "DataValidationPipe");
8476
10442
  exports.DataValidationPipe = __decorateClass([
8477
10443
  common.Injectable()
8478
10444
  ], exports.DataValidationPipe);
10445
+ var MigrationManager = class {
10446
+ static {
10447
+ __name(this, "MigrationManager");
10448
+ }
10449
+ adapter;
10450
+ migrationsPath;
10451
+ tableName;
10452
+ schema;
10453
+ constructor(config) {
10454
+ this.adapter = config.adapter;
10455
+ this.migrationsPath = path__namespace.resolve(config.migrationsPath ?? "./migrations");
10456
+ this.schema = config.schema ?? "public";
10457
+ this.tableName = this.schema !== "public" ? `${this.schema}.${config.tableName ?? "schema_migrations"}` : config.tableName ?? "schema_migrations";
10458
+ }
10459
+ /**
10460
+ * Initialize migrations table if it doesn't exist
10461
+ */
10462
+ async initialize() {
10463
+ try {
10464
+ const createTableSQL = `
10465
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
10466
+ version VARCHAR(255) PRIMARY KEY,
10467
+ name VARCHAR(255) NOT NULL,
10468
+ file_path VARCHAR(500),
10469
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
10470
+ execution_time INTEGER NOT NULL
10471
+ )
10472
+ `;
10473
+ if (typeof this.adapter.query === "function") {
10474
+ await this.adapter.query(createTableSQL);
10475
+ await this.adapter.query(
10476
+ `
10477
+ DO $$
10478
+ BEGIN
10479
+ IF NOT EXISTS (
10480
+ SELECT 1 FROM information_schema.columns
10481
+ WHERE table_name = '${this.tableName.split(".").pop()}'
10482
+ AND column_name = 'file_path'
10483
+ ) THEN
10484
+ ALTER TABLE ${this.tableName} ADD COLUMN file_path VARCHAR(500);
10485
+ END IF;
10486
+ END $$;
10487
+ `
10488
+ ).catch(() => {
10489
+ });
10490
+ }
10491
+ return success();
10492
+ } catch (error) {
10493
+ return failure(
10494
+ new errors.DatabaseError(
10495
+ `Failed to initialize migrations table: ${error.message}`,
10496
+ errors$1.DATABASE_ERROR_CODES.INIT_FAILED,
10497
+ { cause: error }
10498
+ )
10499
+ );
10500
+ }
10501
+ }
10502
+ /**
10503
+ * Discover migration files from migrations directory (including subdirectories)
10504
+ */
10505
+ async discoverMigrations() {
10506
+ if (!fs__namespace.existsSync(this.migrationsPath)) {
10507
+ return [];
10508
+ }
10509
+ const migrations = [];
10510
+ const scanDirectory = /* @__PURE__ */ __name((dir) => {
10511
+ const entries = fs__namespace.readdirSync(dir, { withFileTypes: true });
10512
+ for (const entry of entries) {
10513
+ const fullPath = path__namespace.join(dir, entry.name);
10514
+ if (entry.isDirectory()) {
10515
+ scanDirectory(fullPath);
10516
+ } else if (entry.isFile()) {
10517
+ const match = entry.name.match(/^(\d+)_(.+)\.(ts|js|sql)$/);
10518
+ if (match) {
10519
+ const [, version, name] = match;
10520
+ migrations.push({
10521
+ filePath: fullPath,
10522
+ version,
10523
+ name: name.replace(/_/g, " ")
10524
+ });
10525
+ }
10526
+ }
10527
+ }
10528
+ }, "scanDirectory");
10529
+ scanDirectory(this.migrationsPath);
10530
+ return migrations.sort((a, b) => a.version.localeCompare(b.version));
10531
+ }
10532
+ /**
10533
+ * Parse SQL content to extract UP and DOWN sections
10534
+ */
10535
+ parseSqlSections(sql2) {
10536
+ const hasUpMarker = sql2.includes("-- UP");
10537
+ const hasDownMarker = sql2.includes("-- DOWN");
10538
+ if (hasUpMarker && hasDownMarker) {
10539
+ const parts = sql2.split("-- DOWN");
10540
+ return {
10541
+ upSQL: parts[0].replace("-- UP", "").trim(),
10542
+ downSQL: parts[1].trim()
10543
+ };
10544
+ }
10545
+ if (hasDownMarker) {
10546
+ const parts = sql2.split("-- DOWN");
10547
+ return {
10548
+ upSQL: parts[0].trim(),
10549
+ downSQL: parts[1].trim()
10550
+ };
10551
+ }
10552
+ return { upSQL: sql2.trim(), downSQL: null };
10553
+ }
10554
+ /**
10555
+ * Load SQL migration from file
10556
+ */
10557
+ loadSqlMigration(migrationFile) {
10558
+ const sql2 = fs__namespace.readFileSync(migrationFile.filePath, "utf-8");
10559
+ const { upSQL, downSQL } = this.parseSqlSections(sql2);
10560
+ return {
10561
+ version: migrationFile.version,
10562
+ name: migrationFile.name,
10563
+ up: /* @__PURE__ */ __name(async (adapter) => {
10564
+ if (typeof adapter.query === "function") {
10565
+ await adapter.query(upSQL);
10566
+ }
10567
+ }, "up"),
10568
+ down: /* @__PURE__ */ __name(async (adapter) => {
10569
+ if (downSQL && typeof adapter.query === "function") {
10570
+ await adapter.query(downSQL);
10571
+ } else {
10572
+ console.warn(
10573
+ `[Migrations] No DOWN migration for ${migrationFile.version}`
10574
+ );
10575
+ }
10576
+ }, "down")
10577
+ };
10578
+ }
10579
+ /**
10580
+ * Load TypeScript/JavaScript migration from file
10581
+ */
10582
+ async loadJsMigration(migrationFile) {
10583
+ const importPath = migrationFile.filePath.startsWith("/") ? migrationFile.filePath : new URL(`file:///${migrationFile.filePath.replace(/\\/g, "/")}`).href;
10584
+ const migrationModule = await import(importPath);
10585
+ return {
10586
+ version: migrationFile.version,
10587
+ name: migrationFile.name,
10588
+ up: migrationModule.up ?? migrationModule.default?.up,
10589
+ down: migrationModule.down ?? migrationModule.default?.down
10590
+ };
10591
+ }
10592
+ /**
10593
+ * Load migration from file
10594
+ */
10595
+ async loadMigration(migrationFile) {
10596
+ const ext = path__namespace.extname(migrationFile.filePath);
10597
+ switch (ext) {
10598
+ case ".sql":
10599
+ return this.loadSqlMigration(migrationFile);
10600
+ case ".ts":
10601
+ case ".js":
10602
+ return this.loadJsMigration(migrationFile);
10603
+ default:
10604
+ throw new errors.DatabaseError(
10605
+ `Unsupported migration file extension: ${ext}`,
10606
+ errors$1.DATABASE_ERROR_CODES.INVALID_PARAMETERS,
10607
+ { cause: new Error(`Unsupported extension: ${ext}`) }
10608
+ );
10609
+ }
10610
+ }
10611
+ /**
10612
+ * Get applied migrations from database
10613
+ */
10614
+ async getAppliedMigrations() {
10615
+ try {
10616
+ if (typeof this.adapter.query === "function") {
10617
+ const result = await this.adapter.query(
10618
+ `SELECT * FROM ${this.tableName} ORDER BY version ASC`
10619
+ );
10620
+ return Array.isArray(result) ? result : result.rows || [];
10621
+ }
10622
+ return [];
10623
+ } catch {
10624
+ return [];
10625
+ }
10626
+ }
10627
+ /**
10628
+ * Record migration as applied
10629
+ */
10630
+ async recordMigration(version, name, executionTime, filePath) {
10631
+ if (typeof this.adapter.query === "function") {
10632
+ const relativePath = filePath ? path__namespace.relative(this.migrationsPath, filePath) : null;
10633
+ await this.adapter.query(
10634
+ `INSERT INTO ${this.tableName} (version, name, file_path, execution_time) VALUES ($1, $2, $3, $4)`,
10635
+ [version, name, relativePath, executionTime]
10636
+ );
10637
+ }
10638
+ }
10639
+ /**
10640
+ * Remove migration record (for rollback)
10641
+ */
10642
+ async unrecordMigration(version) {
10643
+ if (typeof this.adapter.query === "function") {
10644
+ await this.adapter.query(
10645
+ `DELETE FROM ${this.tableName} WHERE version = $1`,
10646
+ [version]
10647
+ );
10648
+ }
10649
+ }
10650
+ /**
10651
+ * Get migration status (applied and pending)
10652
+ */
10653
+ async status() {
10654
+ try {
10655
+ await this.initialize();
10656
+ const allMigrations = await this.discoverMigrations();
10657
+ const appliedMigrations = await this.getAppliedMigrations();
10658
+ const appliedVersions = new Set(appliedMigrations.map((m) => m.version));
10659
+ const pending = allMigrations.filter((m) => !appliedVersions.has(m.version)).map((m) => `${m.version}_${m.name}`);
10660
+ return success({
10661
+ applied: appliedMigrations,
10662
+ pending
10663
+ });
10664
+ } catch (error) {
10665
+ return failure(
10666
+ new errors.DatabaseError(
10667
+ `Failed to get migration status: ${error.message}`,
10668
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
10669
+ { cause: error }
10670
+ )
10671
+ );
10672
+ }
10673
+ }
10674
+ /**
10675
+ * Run all pending migrations
10676
+ */
10677
+ async up(targetVersion) {
10678
+ try {
10679
+ await this.initialize();
10680
+ const allMigrations = await this.discoverMigrations();
10681
+ const appliedMigrations = await this.getAppliedMigrations();
10682
+ const appliedVersions = new Set(appliedMigrations.map((m) => m.version));
10683
+ let applied = 0;
10684
+ for (const migrationFile of allMigrations) {
10685
+ if (appliedVersions.has(migrationFile.version)) {
10686
+ continue;
10687
+ }
10688
+ if (targetVersion && migrationFile.version > targetVersion) {
10689
+ break;
10690
+ }
10691
+ console.log(
10692
+ `[Migrations] Applying ${migrationFile.version}_${migrationFile.name}...`
10693
+ );
10694
+ const migration = await this.loadMigration(migrationFile);
10695
+ const startTime = Date.now();
10696
+ if (typeof this.adapter.transaction === "function") {
10697
+ await this.adapter.transaction(async () => {
10698
+ await migration.up(this.adapter);
10699
+ });
10700
+ } else {
10701
+ await migration.up(this.adapter);
10702
+ }
10703
+ const executionTime = Date.now() - startTime;
10704
+ await this.recordMigration(
10705
+ migration.version,
10706
+ migration.name,
10707
+ executionTime,
10708
+ migrationFile.filePath
10709
+ );
10710
+ console.log(
10711
+ `[Migrations] Applied ${migration.version} in ${executionTime}ms`
10712
+ );
10713
+ applied++;
10714
+ }
10715
+ return success(applied);
10716
+ } catch (error) {
10717
+ return failure(
10718
+ new errors.DatabaseError(
10719
+ `Migration failed: ${error.message}`,
10720
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
10721
+ { cause: error }
10722
+ )
10723
+ );
10724
+ }
10725
+ }
10726
+ /**
10727
+ * Rollback last migration or rollback to specific version
10728
+ */
10729
+ async down(steps = 1) {
10730
+ try {
10731
+ const appliedMigrations = await this.getAppliedMigrations();
10732
+ if (appliedMigrations.length === 0) {
10733
+ return success(0);
10734
+ }
10735
+ const toRollback = appliedMigrations.slice(-steps).reverse();
10736
+ let rolledBack = 0;
10737
+ for (const appliedMigration of toRollback) {
10738
+ console.log(
10739
+ `[Migrations] Rolling back ${appliedMigration.version}_${appliedMigration.name}...`
10740
+ );
10741
+ const allMigrations = await this.discoverMigrations();
10742
+ const migrationFile = allMigrations.find(
10743
+ (m) => m.version === appliedMigration.version
10744
+ );
10745
+ if (!migrationFile) {
10746
+ console.warn(
10747
+ `[Migrations] Migration file not found for version ${appliedMigration.version}`
10748
+ );
10749
+ continue;
10750
+ }
10751
+ const migration = await this.loadMigration(migrationFile);
10752
+ const startTime = Date.now();
10753
+ if (typeof this.adapter.transaction === "function") {
10754
+ await this.adapter.transaction(async () => {
10755
+ await migration.down(this.adapter);
10756
+ });
10757
+ } else {
10758
+ await migration.down(this.adapter);
10759
+ }
10760
+ const executionTime = Date.now() - startTime;
10761
+ await this.unrecordMigration(appliedMigration.version);
10762
+ console.log(
10763
+ `[Migrations] Rolled back ${appliedMigration.version} in ${executionTime}ms`
10764
+ );
10765
+ rolledBack++;
10766
+ }
10767
+ return success(rolledBack);
10768
+ } catch (error) {
10769
+ return failure(
10770
+ new errors.DatabaseError(
10771
+ `Rollback failed: ${error.message}`,
10772
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
10773
+ { cause: error }
10774
+ )
10775
+ );
10776
+ }
10777
+ }
10778
+ /**
10779
+ * Reset database (rollback all migrations)
10780
+ */
10781
+ async reset() {
10782
+ const appliedMigrations = await this.getAppliedMigrations();
10783
+ return this.down(appliedMigrations.length);
10784
+ }
10785
+ /**
10786
+ * Clear migration history (delete all records from tracking table)
10787
+ *
10788
+ * Use this to force fresh migrations in test/development environments
10789
+ * without rolling back actual database changes.
10790
+ *
10791
+ * @example
10792
+ * ```typescript
10793
+ * // Clear history and re-run all migrations
10794
+ * await migrationManager.clearHistory();
10795
+ * await migrationManager.up();
10796
+ * ```
10797
+ */
10798
+ async clearHistory() {
10799
+ try {
10800
+ if (typeof this.adapter.query === "function") {
10801
+ await this.adapter.query(`DELETE FROM ${this.tableName}`);
10802
+ }
10803
+ return success();
10804
+ } catch (error) {
10805
+ return failure(
10806
+ new errors.DatabaseError(
10807
+ `Failed to clear migration history: ${error.message}`,
10808
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
10809
+ { cause: error }
10810
+ )
10811
+ );
10812
+ }
10813
+ }
10814
+ };
10815
+ var SeedManager = class {
10816
+ static {
10817
+ __name(this, "SeedManager");
10818
+ }
10819
+ adapter;
10820
+ seedsPath;
10821
+ tableName;
10822
+ schema;
10823
+ skipExisting;
10824
+ constructor(config) {
10825
+ this.adapter = config.adapter;
10826
+ this.seedsPath = path__namespace.resolve(config.seedsPath ?? "./seeds");
10827
+ this.schema = config.schema ?? "public";
10828
+ this.tableName = this.schema !== "public" ? `${this.schema}.${config.tableName ?? "seed_history"}` : config.tableName ?? "seed_history";
10829
+ this.skipExisting = config.skipExisting ?? false;
10830
+ }
10831
+ /**
10832
+ * Initialize seeds tracking table if it doesn't exist
10833
+ */
10834
+ async initialize() {
10835
+ try {
10836
+ const createTableSQL = `
10837
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
10838
+ name VARCHAR(255) PRIMARY KEY,
10839
+ file_path VARCHAR(500),
10840
+ run_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
10841
+ execution_time INTEGER NOT NULL
10842
+ )
10843
+ `;
10844
+ if (typeof this.adapter.query === "function") {
10845
+ await this.adapter.query(createTableSQL);
10846
+ await this.adapter.query(
10847
+ `
10848
+ DO $$
10849
+ BEGIN
10850
+ IF NOT EXISTS (
10851
+ SELECT 1 FROM information_schema.columns
10852
+ WHERE table_name = '${this.tableName.split(".").pop()}'
10853
+ AND column_name = 'file_path'
10854
+ ) THEN
10855
+ ALTER TABLE ${this.tableName} ADD COLUMN file_path VARCHAR(500);
10856
+ END IF;
10857
+ END $$;
10858
+ `
10859
+ ).catch(() => {
10860
+ });
10861
+ }
10862
+ return success();
10863
+ } catch (error) {
10864
+ return failure(
10865
+ new errors.DatabaseError(
10866
+ `Failed to initialize seeds table: ${error.message}`,
10867
+ errors$1.DATABASE_ERROR_CODES.INIT_FAILED,
10868
+ { cause: error }
10869
+ )
10870
+ );
10871
+ }
10872
+ }
10873
+ /**
10874
+ * Discover seed files from seeds directory
10875
+ */
10876
+ async discoverSeeds() {
10877
+ if (!fs__namespace.existsSync(this.seedsPath)) {
10878
+ return [];
10879
+ }
10880
+ const files = fs__namespace.readdirSync(this.seedsPath);
10881
+ const seeds = [];
10882
+ for (const file of files) {
10883
+ const match = file.match(/^(\d+)_(.+)\.(ts|js)$/);
10884
+ if (match) {
10885
+ const [, order, name] = match;
10886
+ seeds.push({
10887
+ filePath: path__namespace.join(this.seedsPath, file),
10888
+ order: Number.parseInt(order, 10),
10889
+ name
10890
+ });
10891
+ }
10892
+ }
10893
+ return seeds.sort((a, b) => a.order - b.order);
10894
+ }
10895
+ /**
10896
+ * Load seed from file
10897
+ */
10898
+ async loadSeed(seedFile) {
10899
+ const importPath = seedFile.filePath.startsWith("/") ? seedFile.filePath : new URL(`file:///${seedFile.filePath.replace(/\\/g, "/")}`).href;
10900
+ const seedModule = await import(importPath);
10901
+ return {
10902
+ name: seedFile.name,
10903
+ run: seedModule.run ?? seedModule.default?.run ?? seedModule.seed ?? seedModule.default,
10904
+ cleanup: seedModule.cleanup ?? seedModule.default?.cleanup
10905
+ };
10906
+ }
10907
+ /**
10908
+ * Get executed seeds from database
10909
+ */
10910
+ async getExecutedSeeds() {
10911
+ try {
10912
+ if (typeof this.adapter.query === "function") {
10913
+ const result = await this.adapter.query(
10914
+ `SELECT * FROM ${this.tableName} ORDER BY run_at ASC`
10915
+ );
10916
+ return Array.isArray(result) ? result : result.rows || [];
10917
+ }
10918
+ return [];
10919
+ } catch {
10920
+ return [];
10921
+ }
10922
+ }
10923
+ /**
10924
+ * Record seed as executed
10925
+ */
10926
+ async recordSeed(name, executionTime, filePath) {
10927
+ if (typeof this.adapter.query === "function") {
10928
+ const relativePath = filePath ? path__namespace.relative(this.seedsPath, filePath) : null;
10929
+ await this.adapter.query(
10930
+ `INSERT INTO ${this.tableName} (name, file_path, execution_time) VALUES ($1, $2, $3)
10931
+ ON CONFLICT (name) DO UPDATE SET run_at = CURRENT_TIMESTAMP, file_path = $2, execution_time = $3`,
10932
+ [name, relativePath, executionTime]
10933
+ );
10934
+ }
10935
+ }
10936
+ /**
10937
+ * Remove seed record (for reset)
10938
+ */
10939
+ async unrecordSeed(name) {
10940
+ if (typeof this.adapter.query === "function") {
10941
+ await this.adapter.query(
10942
+ `DELETE FROM ${this.tableName} WHERE name = $1`,
10943
+ [name]
10944
+ );
10945
+ }
10946
+ }
10947
+ /**
10948
+ * Execute a seed function with optional transaction support
10949
+ */
10950
+ async executeSeed(seed) {
10951
+ if (typeof this.adapter.transaction === "function") {
10952
+ await this.adapter.transaction(async () => {
10953
+ await seed.run(this.adapter);
10954
+ });
10955
+ } else {
10956
+ await seed.run(this.adapter);
10957
+ }
10958
+ }
10959
+ /**
10960
+ * Check if a seed should be skipped and log if applicable
10961
+ */
10962
+ shouldSkipSeed(seedFile, seedName, executedNames) {
10963
+ if (seedName && seedFile.name !== seedName) return true;
10964
+ if (this.skipExisting && executedNames.has(seedFile.name)) {
10965
+ console.log(`[Seeds] Skipping ${seedFile.name} (already executed)`);
10966
+ return true;
10967
+ }
10968
+ return false;
10969
+ }
10970
+ /**
10971
+ * Run all seeds or a specific seed
10972
+ */
10973
+ async run(seedName) {
10974
+ try {
10975
+ await this.initialize();
10976
+ const allSeeds = await this.discoverSeeds();
10977
+ const executedSeeds = await this.getExecutedSeeds();
10978
+ const executedNames = new Set(executedSeeds.map((s) => s.name));
10979
+ let executed = 0;
10980
+ for (const seedFile of allSeeds) {
10981
+ if (this.shouldSkipSeed(seedFile, seedName, executedNames)) {
10982
+ continue;
10983
+ }
10984
+ console.log(`[Seeds] Running ${seedFile.name}...`);
10985
+ const seed = await this.loadSeed(seedFile);
10986
+ const startTime = Date.now();
10987
+ await this.executeSeed(seed);
10988
+ const executionTime = Date.now() - startTime;
10989
+ await this.recordSeed(seed.name, executionTime, seedFile.filePath);
10990
+ console.log(`[Seeds] Executed ${seed.name} in ${executionTime}ms`);
10991
+ executed++;
10992
+ if (seedName) break;
10993
+ }
10994
+ return success(executed);
10995
+ } catch (error) {
10996
+ return failure(
10997
+ new errors.DatabaseError(
10998
+ `Seed execution failed: ${error.message}`,
10999
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
11000
+ { cause: error }
11001
+ )
11002
+ );
11003
+ }
11004
+ }
11005
+ /**
11006
+ * Execute cleanup function with optional transaction support
11007
+ */
11008
+ async executeCleanup(seed) {
11009
+ if (!seed.cleanup) return;
11010
+ if (typeof this.adapter.transaction === "function") {
11011
+ await this.adapter.transaction(async () => {
11012
+ await seed.cleanup(this.adapter);
11013
+ });
11014
+ } else {
11015
+ await seed.cleanup(this.adapter);
11016
+ }
11017
+ }
11018
+ /**
11019
+ * Reset all seeds (run cleanup functions)
11020
+ */
11021
+ async reset() {
11022
+ try {
11023
+ const allSeeds = await this.discoverSeeds();
11024
+ let cleaned = 0;
11025
+ for (const seedFile of allSeeds.reverse()) {
11026
+ console.log(`[Seeds] Cleaning up ${seedFile.name}...`);
11027
+ const seed = await this.loadSeed(seedFile);
11028
+ if (!seed.cleanup) {
11029
+ console.warn(`[Seeds] No cleanup function for ${seed.name}`);
11030
+ continue;
11031
+ }
11032
+ const startTime = Date.now();
11033
+ await this.executeCleanup(seed);
11034
+ const executionTime = Date.now() - startTime;
11035
+ await this.unrecordSeed(seed.name);
11036
+ console.log(`[Seeds] Cleaned ${seed.name} in ${executionTime}ms`);
11037
+ cleaned++;
11038
+ }
11039
+ return success(cleaned);
11040
+ } catch (error) {
11041
+ return failure(
11042
+ new errors.DatabaseError(
11043
+ `Seed cleanup failed: ${error.message}`,
11044
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
11045
+ { cause: error }
11046
+ )
11047
+ );
11048
+ }
11049
+ }
11050
+ /**
11051
+ * Get seed execution status
11052
+ */
11053
+ async status() {
11054
+ try {
11055
+ await this.initialize();
11056
+ const allSeeds = await this.discoverSeeds();
11057
+ const executedSeeds = await this.getExecutedSeeds();
11058
+ const executedNames = new Set(executedSeeds.map((s) => s.name));
11059
+ const pending = allSeeds.filter((s) => !executedNames.has(s.name)).map((s) => s.name);
11060
+ return success({
11061
+ executed: executedSeeds,
11062
+ pending
11063
+ });
11064
+ } catch (error) {
11065
+ return failure(
11066
+ new errors.DatabaseError(
11067
+ `Failed to get seed status: ${error.message}`,
11068
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
11069
+ { cause: error }
11070
+ )
11071
+ );
11072
+ }
11073
+ }
11074
+ /**
11075
+ * Clear seed history (doesn't clean data, just removes tracking records)
11076
+ */
11077
+ async clearHistory() {
11078
+ try {
11079
+ if (typeof this.adapter.query === "function") {
11080
+ await this.adapter.query(`DELETE FROM ${this.tableName}`);
11081
+ }
11082
+ return success();
11083
+ } catch (error) {
11084
+ return failure(
11085
+ new errors.DatabaseError(
11086
+ `Failed to clear seed history: ${error.message}`,
11087
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
11088
+ { cause: error }
11089
+ )
11090
+ );
11091
+ }
11092
+ }
11093
+ };
8479
11094
 
8480
11095
  exports.AdapterFactory = AdapterFactory;
8481
11096
  exports.AlertManager = AlertManager;
@@ -8493,9 +11108,14 @@ exports.DynamicPool = DynamicPool;
8493
11108
  exports.EncryptionAdapter = EncryptionAdapter;
8494
11109
  exports.HealthManager = HealthManager;
8495
11110
  exports.MetricsCollector = MetricsCollector;
11111
+ exports.MigrationManager = MigrationManager;
11112
+ exports.MockAdapter = MockAdapter;
11113
+ exports.MultiReadAdapter = MultiReadAdapter;
11114
+ exports.MultiWriteAdapter = MultiWriteAdapter;
8496
11115
  exports.ReadReplicaAdapter = ReadReplicaAdapter;
8497
11116
  exports.RedisCache = RedisCache;
8498
11117
  exports.SQLAdapter = SQLAdapter;
11118
+ exports.SeedManager = SeedManager;
8499
11119
  exports.ShardKeyManager = ShardKeyManager;
8500
11120
  exports.ShardRouter = ShardRouter;
8501
11121
  exports.SoftDeleteAdapter = SoftDeleteAdapter;