@plyaz/db 0.0.1 → 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 +11 -6
  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 +3017 -401
  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 +3443 -850
  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.
@@ -2762,19 +3504,17 @@ var SupabaseAdapter = class {
2762
3504
  * @private
2763
3505
  * @param {string} name - Logical table name.
2764
3506
  * @returns {string} Actual table name.
2765
- * @throws {DatabaseError} If table is not registered.
2766
3507
  * @description
2767
- * Retrieves the actual table name that has been previously registered with the adapter.
2768
- * This method is used internally by other methods to ensure that operations are performed
2769
- * on valid, registered tables. If the table is not found, it throws a DatabaseError.
3508
+ * Retrieves the actual table name. If not registered, auto-registers it.
3509
+ * This enables seamless table operations without manual registration.
2770
3510
  */
2771
3511
  getTableName(name) {
2772
- const tableName = this.tableMap.get(name);
3512
+ let tableName = this.tableMap.get(name);
2773
3513
  if (!tableName) {
2774
- throw new errors.DatabaseError(
2775
- `Table ${name} is not registered with the adapter`,
2776
- errors$1.DATABASE_ERROR_CODES.TABLE_NOT_REGISTERED
2777
- );
3514
+ const hasRuntimeIdColumn = this.idColumnMap.has(name);
3515
+ const customIdColumn = hasRuntimeIdColumn ? void 0 : this.configIdColumns[name];
3516
+ this.registerTable(name, name, customIdColumn);
3517
+ tableName = name;
2778
3518
  }
2779
3519
  return tableName;
2780
3520
  }
@@ -2889,6 +3629,8 @@ var SQLAdapter = class {
2889
3629
  config;
2890
3630
  tableMap = /* @__PURE__ */ new Map();
2891
3631
  idColumnMap = /* @__PURE__ */ new Map();
3632
+ configIdColumns;
3633
+ defaultSchema;
2892
3634
  /**
2893
3635
  * Creates a new SQLAdapter instance.
2894
3636
  * @param {SQLAdapterConfig} config - Configuration for the SQL adapter.
@@ -2901,10 +3643,22 @@ var SQLAdapter = class {
2901
3643
  */
2902
3644
  constructor(config) {
2903
3645
  this.config = config;
3646
+ this.defaultSchema = config.schema ?? "public";
2904
3647
  this.pool = new pg.Pool({
2905
3648
  connectionString: config.connectionString,
2906
3649
  ...config.pool
2907
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}`;
2908
3662
  }
2909
3663
  /**
2910
3664
  * Initialize the adapter.
@@ -2918,7 +3672,11 @@ var SQLAdapter = class {
2918
3672
  */
2919
3673
  async initialize() {
2920
3674
  try {
2921
- 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();
2922
3680
  return success();
2923
3681
  } catch (error) {
2924
3682
  return failure(
@@ -2957,6 +3715,23 @@ var SQLAdapter = class {
2957
3715
  async disconnect() {
2958
3716
  await this.pool.end();
2959
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
+ }
2960
3735
  /**
2961
3736
  * Gets the underlying PostgreSQL client instance.
2962
3737
  * @template TClient - The type of the database client to return.
@@ -3037,8 +3812,9 @@ var SQLAdapter = class {
3037
3812
  const validationError = this.validateBasicParams(table, id);
3038
3813
  if (validationError) return failure(validationError);
3039
3814
  const tableName = this.getTableName(table);
3040
- const idColumn = this.idColumnMap.get(table) ?? "id";
3041
- 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`;
3042
3818
  const result = await this.pool.query(sql2, [id]);
3043
3819
  if (!result?.rows) {
3044
3820
  return failure(
@@ -3101,6 +3877,7 @@ var SQLAdapter = class {
3101
3877
  async findMany(table, options) {
3102
3878
  try {
3103
3879
  const tableName = this.getTableName(table);
3880
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3104
3881
  const params = [];
3105
3882
  let whereClause = "";
3106
3883
  let paramIndex = 1;
@@ -3108,7 +3885,7 @@ var SQLAdapter = class {
3108
3885
  whereClause = this.buildWhereClause(options.filter, params, paramIndex);
3109
3886
  paramIndex += params.length;
3110
3887
  }
3111
- const countSql = `SELECT COUNT(*) as total FROM ${tableName}${whereClause}`;
3888
+ const countSql = `SELECT COUNT(*) as total FROM ${qualifiedTable}${whereClause}`;
3112
3889
  const countResult = await this.pool.query(countSql, params);
3113
3890
  if (!countResult.rows || countResult.rows.length === 0) {
3114
3891
  throw new errors.DatabaseError(
@@ -3136,7 +3913,7 @@ var SQLAdapter = class {
3136
3913
  limitClause += ` OFFSET $${paramIndex++}`;
3137
3914
  params.push(options.pagination.offset);
3138
3915
  }
3139
- const sql2 = `SELECT * FROM ${tableName}${whereClause}${orderClause}${limitClause}`;
3916
+ const sql2 = `SELECT * FROM ${qualifiedTable}${whereClause}${orderClause}${limitClause}`;
3140
3917
  const result = await this.pool.query(sql2, params);
3141
3918
  return success({
3142
3919
  data: result.rows,
@@ -3176,11 +3953,12 @@ var SQLAdapter = class {
3176
3953
  const validationError = this.validateCreateParams(table, data);
3177
3954
  if (validationError) return failure(validationError);
3178
3955
  const tableName = this.getTableName(table);
3956
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3179
3957
  const keys = Object.keys(data);
3180
3958
  const values = Object.values(data);
3181
3959
  const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
3182
3960
  const escapedKeys = keys.map((k) => `"${k}"`).join(", ");
3183
- const sql2 = `INSERT INTO "${tableName}" (${escapedKeys}) VALUES (${placeholders}) RETURNING *`;
3961
+ const sql2 = `INSERT INTO ${qualifiedTable} (${escapedKeys}) VALUES (${placeholders}) RETURNING *`;
3184
3962
  const result = await this.pool.query(sql2, values);
3185
3963
  if (!result?.rows?.length) {
3186
3964
  return failure(
@@ -3226,11 +4004,12 @@ var SQLAdapter = class {
3226
4004
  const validationError = this.validateUpdateParams(table, id, data);
3227
4005
  if (validationError) return failure(validationError);
3228
4006
  const tableName = this.getTableName(table);
4007
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3229
4008
  const keys = Object.keys(data);
3230
4009
  const values = Object.values(data);
3231
4010
  const setClause = keys.map((key, i) => `"${key}" = $${i + 1}`).join(", ");
3232
- const idColumn = this.idColumnMap.get(table) ?? "id";
3233
- 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 *`;
3234
4013
  const result = await this.pool.query(sql2, [...values, id]);
3235
4014
  if (!result.rows?.length) {
3236
4015
  return failure(
@@ -3278,8 +4057,9 @@ var SQLAdapter = class {
3278
4057
  );
3279
4058
  }
3280
4059
  const tableName = this.getTableName(table);
3281
- const idColumn = this.idColumnMap.get(table) ?? "id";
3282
- 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`;
3283
4063
  const result = await this.pool.query(sql2, [id]);
3284
4064
  if (!result) {
3285
4065
  return failure(
@@ -3327,34 +4107,39 @@ var SQLAdapter = class {
3327
4107
  const trx = {
3328
4108
  findById: /* @__PURE__ */ __name(async (table, id) => {
3329
4109
  const tableName = this.getTableName(table);
3330
- const idColumn = this.idColumnMap.get(table) ?? "id";
3331
- 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`;
3332
4113
  const result2 = await client.query(sql2, [id]);
3333
4114
  return success(result2.rows[0] ?? null);
3334
4115
  }, "findById"),
3335
4116
  create: /* @__PURE__ */ __name(async (table, data) => {
3336
4117
  const tableName = this.getTableName(table);
4118
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3337
4119
  const keys = Object.keys(data);
3338
4120
  const values = Object.values(data);
3339
4121
  const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
3340
- 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 *`;
3341
4124
  const result2 = await client.query(sql2, values);
3342
4125
  return success(result2.rows[0]);
3343
4126
  }, "create"),
3344
4127
  update: /* @__PURE__ */ __name(async (table, id, data) => {
3345
4128
  const tableName = this.getTableName(table);
4129
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3346
4130
  const keys = Object.keys(data);
3347
4131
  const values = Object.values(data);
3348
- const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
3349
- const idColumn = this.idColumnMap.get(table) ?? "id";
3350
- 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 *`;
3351
4135
  const result2 = await client.query(sql2, [...values, id]);
3352
4136
  return success(result2.rows[0]);
3353
4137
  }, "update"),
3354
4138
  delete: /* @__PURE__ */ __name(async (table, id) => {
3355
4139
  const tableName = this.getTableName(table);
3356
- const idColumn = this.idColumnMap.get(table) ?? "id";
3357
- 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`;
3358
4143
  await client.query(sql2, [id]);
3359
4144
  return success();
3360
4145
  }, "delete"),
@@ -3401,8 +4186,9 @@ var SQLAdapter = class {
3401
4186
  async exists(table, id) {
3402
4187
  try {
3403
4188
  const tableName = this.getTableName(table);
3404
- const idColumn = this.idColumnMap.get(table) ?? "id";
3405
- 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`;
3406
4192
  const result = await this.pool.query(sql2, [id]);
3407
4193
  return success(result.rows.length > 0);
3408
4194
  } catch (error) {
@@ -3434,12 +4220,13 @@ var SQLAdapter = class {
3434
4220
  async count(table, filter) {
3435
4221
  try {
3436
4222
  const tableName = this.getTableName(table);
4223
+ const qualifiedTable = this.getQualifiedTableName(tableName);
3437
4224
  let whereClause = "";
3438
4225
  let params = [];
3439
4226
  if (filter) {
3440
4227
  whereClause = this.buildWhereClause(filter, params, 1);
3441
4228
  }
3442
- const sql2 = `SELECT COUNT(*) as count FROM ${tableName}${whereClause}`;
4229
+ const sql2 = `SELECT COUNT(*) as count FROM ${qualifiedTable}${whereClause}`;
3443
4230
  const result = await this.pool.query(sql2, params);
3444
4231
  const rowCount = Number.parseInt(result.rows[0].count);
3445
4232
  return success(Number(rowCount));
@@ -3492,22 +4279,43 @@ var SQLAdapter = class {
3492
4279
  * @private
3493
4280
  * @param {string} name - Logical table name.
3494
4281
  * @returns {string} Actual table name.
3495
- * @throws {DatabaseError} If table is not registered.
3496
4282
  * @description
3497
- * Retrieves the actual table name that has been previously registered with the adapter.
3498
- * This method is used internally by other methods to ensure that operations are performed
3499
- * 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).
3500
4286
  */
3501
4287
  getTableName(name) {
3502
- const tableName = this.tableMap.get(name);
4288
+ let tableName = this.tableMap.get(name);
3503
4289
  if (!tableName) {
3504
- throw new errors.DatabaseError(
3505
- `Table ${name} is not registered with the adapter`,
3506
- errors$1.DATABASE_ERROR_CODES.TABLE_NOT_REGISTERED
3507
- );
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;
3508
4294
  }
3509
4295
  return tableName;
3510
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
+ }
3511
4319
  validateBasicParams(table, id) {
3512
4320
  if (!table || !id) {
3513
4321
  return new errors.DatabaseError(
@@ -3665,36 +4473,449 @@ var SQLAdapter = class {
3665
4473
  return clause;
3666
4474
  }
3667
4475
  };
3668
- 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 {
3669
4498
  static {
3670
- __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);
3671
4565
  }
3672
4566
  /**
3673
- * Creates a new database adapter instance based on the configuration
3674
- *
3675
- * This is the core factory method that instantiates the appropriate database adapter
3676
- * based on the provided type and configuration. Uses TypeScript generics to ensure
3677
- * type safety between adapter type and configuration.
3678
- *
3679
- * **Creation Process:**
3680
- * 1. Validates input parameters (type and config)
3681
- * 2. Uses switch statement to match adapter type
3682
- * 3. Instantiates appropriate adapter class with type-safe config
3683
- * 4. Returns DatabaseAdapterType interface implementation
3684
- *
3685
- * **Type Safety:**
3686
- * - Generic T extends DatabaseConfig ensures config matches adapter type
3687
- * - Type assertions (as DrizzleAdapterConfig) provide compile-time safety
3688
- * - Runtime validation prevents invalid configurations
3689
- *
3690
- * @template T - Database configuration type that extends DatabaseConfig
3691
- * @param {T["adapter"]} type - Adapter type from ADAPTERS enum (drizzle, supabase, sql)
3692
- * @param {T} config - Database configuration object matching the adapter type
3693
- * @returns {DatabaseAdapterType} The appropriate adapter implementation
3694
- *
3695
- * @throws {Error} If adapter type is not provided
3696
- * @throws {Error} If adapter configuration is not provided
3697
- * @throws {Error} If unsupported adapter type is specified
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
4918
+ * @throws {Error} If unsupported adapter type is specified
3698
4919
  * @throws {Error} If adapter instantiation fails
3699
4920
  *
3700
4921
  * @example
@@ -3758,6 +4979,9 @@ var AdapterFactory = class {
3758
4979
  // SQL adapter - for raw SQL operations and custom database integrations
3759
4980
  case db.ADAPTERS.SQL:
3760
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);
3761
4985
  // Default case - handles unsupported or invalid adapter types
3762
4986
  default:
3763
4987
  throw new errors.DatabaseError(
@@ -3856,6 +5080,9 @@ var SoftDeleteAdapter = class {
3856
5080
  async disconnect() {
3857
5081
  return this.baseAdapter.disconnect();
3858
5082
  }
5083
+ async close() {
5084
+ return this.baseAdapter.close();
5085
+ }
3859
5086
  /**
3860
5087
  * Gets the underlying database client.
3861
5088
  *
@@ -4239,8 +5466,41 @@ var SoftDeleteAdapter = class {
4239
5466
  return this.config.excludeTables?.includes(table) ?? false;
4240
5467
  }
4241
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
+ ];
4242
5503
  var AuditAdapter = class {
4243
- // Using shared logger instance from @plyaz/logger
4244
5504
  /**
4245
5505
  * Creates a new AuditAdapter instance.
4246
5506
  *
@@ -4254,9 +5514,11 @@ var AuditAdapter = class {
4254
5514
  * ```typescript
4255
5515
  * const auditAdapter = new AuditAdapter(baseAdapter, {
4256
5516
  * enabled: true,
4257
- * retentionDays: 90,
5517
+ * retentionDays: 180,
4258
5518
  * excludeFields: ['password', 'token'],
4259
5519
  * excludeTables: ['temp_data'],
5520
+ * schema: 'audit',
5521
+ * usePartitionedTables: true,
4260
5522
  * onAuditAfterWrite: async (event) => {
4261
5523
  * await complianceService.recordAudit(event);
4262
5524
  * }
@@ -4266,11 +5528,18 @@ var AuditAdapter = class {
4266
5528
  constructor(baseAdapter, config) {
4267
5529
  this.baseAdapter = baseAdapter;
4268
5530
  this.config = config;
5531
+ this.auditSchema = config.schema ?? "audit";
5532
+ this.usePartitionedTables = config.usePartitionedTables ?? true;
4269
5533
  }
4270
5534
  static {
4271
5535
  __name(this, "AuditAdapter");
4272
5536
  }
4273
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;
4274
5543
  /**
4275
5544
  * Initializes the audit adapter and underlying adapter.
4276
5545
  *
@@ -4320,6 +5589,9 @@ var AuditAdapter = class {
4320
5589
  async disconnect() {
4321
5590
  return this.baseAdapter.disconnect();
4322
5591
  }
5592
+ async close() {
5593
+ return this.baseAdapter.close();
5594
+ }
4323
5595
  /**
4324
5596
  * Gets the underlying database client.
4325
5597
  *
@@ -4458,11 +5730,36 @@ var AuditAdapter = class {
4458
5730
  }
4459
5731
  async create(table, data) {
4460
5732
  this.validateCreateParams(table, data);
4461
- const result = await this.baseAdapter.create(table, data);
4462
- if (result.success && this.shouldAudit(table)) {
4463
- 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;
4464
5762
  }
4465
- return result;
4466
5763
  }
4467
5764
  validateCreateParams(table, data) {
4468
5765
  if (!table || !data) {
@@ -4476,14 +5773,99 @@ var AuditAdapter = class {
4476
5773
  );
4477
5774
  }
4478
5775
  }
4479
- 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;
4480
5853
  try {
5854
+ const errorSource = this.getErrorSource(error);
5855
+ const failedOperation = this.getFailedOperation(operation);
4481
5856
  await this.logAudit({
4482
- operation: db.AUDIT_OPERATION.CREATE,
5857
+ operation: failedOperation,
4483
5858
  table,
4484
- recordId: value?.id,
5859
+ recordId: recordId ?? data?.id,
4485
5860
  changes: {
4486
- 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
+ }
4487
5869
  },
4488
5870
  userId: this.auditContext.userId,
4489
5871
  requestId: this.auditContext.requestId,
@@ -4493,54 +5875,66 @@ var AuditAdapter = class {
4493
5875
  });
4494
5876
  } catch (auditError) {
4495
5877
  logger.logger.error(
4496
- `Audit logging failed for CREATE operation: ${auditError.message}`
5878
+ `Failed to log operation failure to audit: ${auditError.message}`
4497
5879
  );
4498
5880
  }
4499
5881
  }
4500
- async update(table, id, data) {
4501
- const before = await this.baseAdapter.findById(table, id);
4502
- const result = await this.baseAdapter.update(table, id, data);
4503
- if (result.success && this.shouldAudit(table)) {
4504
- await this.logAudit({
4505
- operation: db.AUDIT_OPERATION.UPDATE,
4506
- table,
4507
- recordId: id,
4508
- changes: {
4509
- before: before.success ? before.value : void 0,
4510
- after: result.value,
4511
- fields: Object.keys(data)
4512
- },
4513
- userId: this.auditContext.userId,
4514
- requestId: this.auditContext.requestId,
4515
- timestamp: /* @__PURE__ */ new Date(),
4516
- ipAddress: this.auditContext.ipAddress,
4517
- userAgent: this.auditContext.userAgent
4518
- });
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;
4519
5895
  }
4520
- return result;
4521
5896
  }
4522
- async delete(table, id) {
4523
- const before = await this.baseAdapter.findById(table, id);
4524
- const result = await this.baseAdapter.delete(table, id);
4525
- if (result.success && this.shouldAudit(table)) {
4526
- await this.logAudit({
4527
- operation: db.AUDIT_OPERATION.DELETE,
4528
- table,
4529
- recordId: id,
4530
- changes: {
4531
- before: before.success ? before.value : void 0
4532
- },
4533
- userId: this.auditContext.userId,
4534
- requestId: this.auditContext.requestId,
4535
- timestamp: /* @__PURE__ */ new Date(),
4536
- ipAddress: this.auditContext.ipAddress,
4537
- userAgent: this.auditContext.userAgent
4538
- });
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;
4539
5910
  }
4540
- 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;
4541
5917
  }
4542
5918
  /**
4543
- * Executes operations within a transaction with audit logging.
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.
4544
5938
  *
4545
5939
  * **RESPONSIBILITY:** Passes transaction to base adapter
4546
5940
  * **BEHAVIOR:** Individual operations within transaction are audited
@@ -4713,11 +6107,38 @@ var AuditAdapter = class {
4713
6107
  );
4714
6108
  }
4715
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
+ }
4716
6137
  /**
4717
6138
  * Writes audit record to daily audit table.
4718
6139
  *
4719
6140
  * **RESPONSIBILITY:** Persists audit event to database
4720
- * **TABLE NAMING:** Uses daily tables (audit_YYYYMMDD)
6141
+ * **TABLE NAMING:** Uses daily partitioned tables (audit.audit_log_yyyy_mm_dd) by default
4721
6142
  * **STRUCTURE:** Converts event to database record format
4722
6143
  *
4723
6144
  * @private
@@ -4726,7 +6147,7 @@ var AuditAdapter = class {
4726
6147
  *
4727
6148
  * @example
4728
6149
  * ```typescript
4729
- * // Internal usage - writes to audit_20241201 table
6150
+ * // Internal usage - writes to audit.audit_log_2024_12_01 table
4730
6151
  * await this.writeAuditRecord({
4731
6152
  * operation: 'UPDATE',
4732
6153
  * table: 'users',
@@ -4737,7 +6158,7 @@ var AuditAdapter = class {
4737
6158
  * ```
4738
6159
  */
4739
6160
  async writeAuditRecord(event) {
4740
- const tableName = "audit_logs";
6161
+ const tableName = this.getAuditTableName(event.timestamp);
4741
6162
  try {
4742
6163
  const auditResult = await this.baseAdapter.create(tableName, {
4743
6164
  operation: event.operation,
@@ -4825,6 +6246,9 @@ var EncryptionAdapter = class {
4825
6246
  async disconnect() {
4826
6247
  return this.baseAdapter.disconnect();
4827
6248
  }
6249
+ async close() {
6250
+ return this.baseAdapter.close();
6251
+ }
4828
6252
  getClient() {
4829
6253
  return this.baseAdapter.getClient();
4830
6254
  }
@@ -4894,14 +6318,10 @@ var EncryptionAdapter = class {
4894
6318
  return this.config.enabled && isObject(data) && Boolean(this.config.fields[table]);
4895
6319
  }
4896
6320
  encryptSingleField(result, field) {
4897
- try {
4898
- if (result[field]) {
4899
- result[field] = this.encrypt(
4900
- String(result[field])
4901
- );
4902
- }
4903
- } catch (error) {
4904
- console.error(`Failed to encrypt field ${field}:`, error);
6321
+ if (result[field]) {
6322
+ result[field] = this.encrypt(
6323
+ String(result[field])
6324
+ );
4905
6325
  }
4906
6326
  }
4907
6327
  decryptFields(table, data) {
@@ -4914,15 +6334,18 @@ var EncryptionAdapter = class {
4914
6334
  return result;
4915
6335
  }
4916
6336
  decryptSingleField(result, field) {
4917
- try {
4918
- if (result[field]) {
4919
- const fieldValue = String(result[field]);
4920
- if (this.isEncryptedValue(fieldValue)) {
6337
+ if (result[field]) {
6338
+ const fieldValue = String(result[field]);
6339
+ if (this.isEncryptedValue(fieldValue)) {
6340
+ try {
4921
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
+ );
4922
6347
  }
4923
6348
  }
4924
- } catch (error) {
4925
- console.error(`Failed to decrypt field ${field}:`, error);
4926
6349
  }
4927
6350
  }
4928
6351
  encrypt(text) {
@@ -5128,6 +6551,9 @@ var CachingAdapter = class {
5128
6551
  async disconnect() {
5129
6552
  return this.baseAdapter.disconnect();
5130
6553
  }
6554
+ async close() {
6555
+ return this.baseAdapter.close();
6556
+ }
5131
6557
  /**
5132
6558
  * Gets the underlying database client.
5133
6559
  *
@@ -5552,6 +6978,9 @@ var ReadReplicaAdapter = class {
5552
6978
  async disconnect() {
5553
6979
  return this.primaryAdapter.disconnect();
5554
6980
  }
6981
+ async close() {
6982
+ return this.primaryAdapter.close();
6983
+ }
5555
6984
  getClient() {
5556
6985
  return this.primaryAdapter.getClient();
5557
6986
  }
@@ -5629,7 +7058,11 @@ function buildAdapterChain(baseAdapter, config) {
5629
7058
  adapter = new CachingAdapter(adapter, config.cache);
5630
7059
  }
5631
7060
  if (config.audit?.enabled) {
5632
- 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
+ });
5633
7066
  }
5634
7067
  if (config.readReplica?.enabled) {
5635
7068
  adapter = new ReadReplicaAdapter(adapter, {
@@ -5651,8 +7084,10 @@ function createAdapterConfig(config) {
5651
7084
  // orm: ADAPTERS.SUPABASE,
5652
7085
  connectionString: drizzleConfig.connectionString ?? drizzleConfig.url,
5653
7086
  // Support both formats
5654
- pool: drizzleConfig.poolSize ? { max: drizzleConfig.poolSize } : void 0
7087
+ pool: drizzleConfig.poolSize ? { max: drizzleConfig.poolSize } : void 0,
5655
7088
  // Optional connection pooling
7089
+ tableIdColumns: drizzleConfig.tableIdColumns
7090
+ // Custom ID column mappings
5656
7091
  };
5657
7092
  }
5658
7093
  if (config.adapter === db.ADAPTER_TYPES.SUPABASE) {
@@ -5666,15 +7101,28 @@ function createAdapterConfig(config) {
5666
7101
  // Public API key
5667
7102
  supabaseServiceKey: supabaseConfig.supabaseServiceKey,
5668
7103
  // Service role key (admin)
5669
- schema: supabaseConfig.schema
7104
+ schema: supabaseConfig.schema,
5670
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
5671
7115
  };
5672
7116
  }
5673
7117
  const sqlConfig = config.config;
5674
7118
  return {
5675
7119
  adapter: db.ADAPTERS.SQL,
5676
- connectionString: sqlConfig.connectionString ?? sqlConfig.url
7120
+ connectionString: sqlConfig.connectionString ?? sqlConfig.url,
5677
7121
  // Support both formats
7122
+ schema: sqlConfig.schema,
7123
+ // Default schema for all tables
7124
+ tableIdColumns: sqlConfig.tableIdColumns
7125
+ // Custom ID column mappings
5678
7126
  };
5679
7127
  }
5680
7128
  __name(createAdapterConfig, "createAdapterConfig");
@@ -5755,18 +7203,33 @@ __name(createDatabaseService, "createDatabaseService");
5755
7203
 
5756
7204
  // src/repository/BaseRepository.ts
5757
7205
  var BaseRepository = class {
5758
- // Using shared logger instance from @plyaz/logger
5759
- constructor(db, tableName) {
7206
+ constructor(db, tableName, defaultConfig) {
5760
7207
  this.db = db;
5761
7208
  this.tableName = tableName;
7209
+ this.defaultConfig = defaultConfig;
5762
7210
  }
5763
7211
  static {
5764
7212
  __name(this, "BaseRepository");
5765
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
+ }
5766
7228
  /**
5767
7229
  * Find a single entity by its primary key ID
5768
7230
  *
5769
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.)
5770
7233
  * @returns {Promise<DatabaseResult<T | null>>} Promise resolving to the entity or null if not found
5771
7234
  *
5772
7235
  * @example
@@ -5775,15 +7238,21 @@ var BaseRepository = class {
5775
7238
  * if (result.success && result.value) {
5776
7239
  * console.log('Found user:', result.value.name);
5777
7240
  * }
7241
+ *
7242
+ * // Use specific adapter for this query
7243
+ * const analyticsResult = await userRepository.findById('user-123', {
7244
+ * adapter: 'analytics'
7245
+ * });
5778
7246
  * ```
5779
7247
  */
5780
- async findById(id) {
5781
- return this.db.findById(this.tableName, id);
7248
+ async findById(id, config) {
7249
+ return this.db.get(this.tableName, id, this.mergeConfig(config));
5782
7250
  }
5783
7251
  /**
5784
7252
  * Find multiple entities with optional filtering, sorting, and pagination
5785
7253
  *
5786
- * @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.)
5787
7256
  * @returns {Promise<DatabaseResult<PaginatedResult<T>>>} Promise resolving to paginated results
5788
7257
  *
5789
7258
  * @example
@@ -5793,15 +7262,21 @@ var BaseRepository = class {
5793
7262
  * sort: [{ field: 'createdAt', direction: 'desc' }],
5794
7263
  * pagination: { limit: 20, offset: 0 }
5795
7264
  * });
7265
+ *
7266
+ * // Query from analytics database
7267
+ * const analyticsResult = await userRepository.findMany({}, {
7268
+ * adapter: 'analytics'
7269
+ * });
5796
7270
  * ```
5797
7271
  */
5798
- async findMany(options) {
5799
- return this.db.findMany(this.tableName, options);
7272
+ async findMany(options, config) {
7273
+ return this.db.list(this.tableName, options, this.mergeConfig(config));
5800
7274
  }
5801
7275
  /**
5802
7276
  * Create a new entity in the database
5803
7277
  *
5804
- * @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.)
5805
7280
  * @returns {Promise<DatabaseResult<T>>} Promise resolving to the created entity
5806
7281
  *
5807
7282
  * @example
@@ -5810,16 +7285,25 @@ var BaseRepository = class {
5810
7285
  * name: 'John Doe',
5811
7286
  * email: 'john@example.com'
5812
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
+ * });
5813
7296
  * ```
5814
7297
  */
5815
- async create(data) {
5816
- return this.db.create(this.tableName, data);
7298
+ async create(data, config) {
7299
+ return this.db.create(this.tableName, data, this.mergeConfig(config));
5817
7300
  }
5818
7301
  /**
5819
7302
  * Update an existing entity by ID
5820
7303
  *
5821
7304
  * @param {string} id - The primary key ID of the entity to update
5822
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.)
5823
7307
  * @returns {Promise<DatabaseResult<T>>} Promise resolving to the updated entity
5824
7308
  *
5825
7309
  * @example
@@ -5828,15 +7312,29 @@ var BaseRepository = class {
5828
7312
  * email: 'newemail@example.com',
5829
7313
  * updatedAt: new Date()
5830
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
+ * });
5831
7323
  * ```
5832
7324
  */
5833
- async update(id, data) {
5834
- 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
+ );
5835
7332
  }
5836
7333
  /**
5837
7334
  * Delete an entity by ID (hard delete)
5838
7335
  *
5839
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.)
5840
7338
  * @returns {Promise<DatabaseResult<void>>} Promise resolving when deletion is complete
5841
7339
  *
5842
7340
  * @warning This is a permanent operation that cannot be undone
@@ -5845,15 +7343,21 @@ var BaseRepository = class {
5845
7343
  * @example
5846
7344
  * ```typescript
5847
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
+ * });
5848
7351
  * ```
5849
7352
  */
5850
- async delete(id) {
5851
- return this.db.delete(this.tableName, id);
7353
+ async delete(id, config) {
7354
+ return this.db.delete(this.tableName, id, this.mergeConfig(config));
5852
7355
  }
5853
7356
  /**
5854
7357
  * Count entities matching optional filter criteria
5855
7358
  *
5856
- * @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.)
5857
7361
  * @returns {Promise<DatabaseResult<number>>} Promise resolving to the count
5858
7362
  *
5859
7363
  * @example
@@ -5862,15 +7366,21 @@ var BaseRepository = class {
5862
7366
  * const activeResult = await userRepository.count({
5863
7367
  * field: 'status', operator: 'eq', value: 'active'
5864
7368
  * });
7369
+ *
7370
+ * // Count in specific adapter
7371
+ * const archiveCount = await userRepository.count(undefined, {
7372
+ * adapter: 'archive'
7373
+ * });
5865
7374
  * ```
5866
7375
  */
5867
- async count(filter) {
5868
- return this.db.count(this.tableName, filter);
7376
+ async count(filter, config) {
7377
+ return this.db.count(this.tableName, filter, this.mergeConfig(config));
5869
7378
  }
5870
7379
  /**
5871
7380
  * Check if an entity exists by ID
5872
7381
  *
5873
7382
  * @param {string} id - The primary key ID to check
7383
+ * @param {OperationConfig} [config] - Optional per-operation configuration (adapter selection, schema override, etc.)
5874
7384
  * @returns {Promise<DatabaseResult<boolean>>} Promise resolving to existence status
5875
7385
  *
5876
7386
  * @example
@@ -5879,15 +7389,30 @@ var BaseRepository = class {
5879
7389
  * if (existsResult.success && existsResult.value) {
5880
7390
  * console.log('User exists');
5881
7391
  * }
7392
+ *
7393
+ * // Check existence in specific adapter
7394
+ * const existsInArchive = await userRepository.exists('user-123', {
7395
+ * adapter: 'archive'
7396
+ * });
5882
7397
  * ```
5883
7398
  */
5884
- async exists(id) {
5885
- 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
+ };
5886
7410
  }
5887
7411
  /**
5888
7412
  * Find the first entity matching filter criteria
5889
7413
  *
5890
- * @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.)
5891
7416
  * @returns {Promise<DatabaseResult<T | null>>} Promise resolving to first match or null
5892
7417
  *
5893
7418
  * @example
@@ -5895,15 +7420,24 @@ var BaseRepository = class {
5895
7420
  * const result = await userRepository.findOne({
5896
7421
  * field: 'email', operator: 'eq', value: 'john@example.com'
5897
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
+ * });
5898
7431
  * ```
5899
7432
  */
5900
- async findOne(filter) {
5901
- return this.db.findOne(this.tableName, filter);
7433
+ async findOne(filter, config) {
7434
+ return this.db.findOne(this.tableName, filter, this.mergeConfig(config));
5902
7435
  }
5903
7436
  /**
5904
7437
  * Soft delete an entity by ID (recoverable deletion)
5905
7438
  *
5906
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.)
5907
7441
  * @returns {Promise<DatabaseResult<void>>} Promise resolving when soft deletion is complete
5908
7442
  *
5909
7443
  * @see {@link delete} For permanent deletion
@@ -5912,242 +7446,670 @@ var BaseRepository = class {
5912
7446
  * ```typescript
5913
7447
  * const result = await userRepository.softDelete('user-123');
5914
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
+ * });
5915
7455
  * ```
5916
7456
  */
5917
- async softDelete(id) {
5918
- return this.db.softDelete(this.tableName, id);
7457
+ async softDelete(id, config) {
7458
+ return this.db.softDelete(this.tableName, id, this.mergeConfig(config));
5919
7459
  }
5920
7460
  };
5921
- var USE_REPLICA_KEY = "use_replica";
5922
- function UseReplica(options = {}) {
5923
- return (target, propertyKey, descriptor) => {
5924
- common.SetMetadata(USE_REPLICA_KEY, options)(target, propertyKey, descriptor);
5925
- };
5926
- }
5927
- __name(UseReplica, "UseReplica");
5928
- var RedisCache = class {
7461
+ var MultiWriteAdapter = class {
5929
7462
  static {
5930
- __name(this, "RedisCache");
7463
+ __name(this, "MultiWriteAdapter");
5931
7464
  }
5932
- redis;
5933
- defaultTTL;
5934
- // Using shared logger instance from @plyaz/logger
5935
- /**
5936
- * Creates a new RedisCache instance.
5937
- * @param config Redis configuration
5938
- *
5939
- * @example
5940
- * ```typescript
5941
- * // Basic configuration
5942
- * const cache = new RedisCache({
5943
- * url: 'redis://localhost:6379'
5944
- * });
5945
- *
5946
- * // With custom default TTL
5947
- * const cacheWithCustomTTL = new RedisCache({
5948
- * url: 'redis://localhost:6379',
5949
- * defaultTTL: 1800 // 30 minutes
5950
- * });
5951
- *
5952
- * // Production configuration with options
5953
- * const productionCache = new RedisCache({
5954
- * url: 'redis://redis-cluster.example.com:6379',
5955
- * defaultTTL: 3600,
5956
- * // Additional Redis options can be passed here
5957
- * });
5958
- * ```
5959
- */
5960
- constructor(config$1) {
5961
- this.redis = new ioredis.Redis(config$1.url);
5962
- 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
+ };
5963
7475
  }
5964
- /**
5965
- * Retrieves a value from cache by key.
5966
- * Automatically handles JSON deserialization.
5967
- *
5968
- * @param key Cache key
5969
- * @returns DatabaseResult containing cached value or null if not found
5970
- *
5971
- * @example
5972
- * ```typescript
5973
- * // Get user profile
5974
- * const result = await cache.get<UserProfile>('profile:123');
5975
- * if (result.success && result.value) {
5976
- * console.log('User:', result.value.name);
5977
- * } else {
5978
- * console.log('Profile not found or cache error');
5979
- * }
5980
- *
5981
- * // Get product list
5982
- * const products = await cache.get<Product[]>('products:featured');
5983
- * if (products.success && products.value) {
5984
- * products.value.forEach(product => {
5985
- * console.log(product.name, product.price);
5986
- * });
5987
- * }
5988
- * ```
5989
- */
5990
- async get(key) {
5991
- try {
5992
- const value = await this.redis.get(key);
5993
- if (!value) {
5994
- 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();
5995
7487
  }
5996
- const parsed = JSON.parse(value);
5997
- if (parsed && typeof parsed === "object") {
5998
- 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();
5999
7495
  }
6000
- return success();
6001
- } catch (error) {
6002
- return failure(
6003
- new errors.DatabaseError(
6004
- "Cache get failed",
6005
- errors$1.DATABASE_ERROR_CODES.CACHE_GET_FAILED,
6006
- { context: { source: "RedisCache.get", key, cause: error } }
6007
- )
6008
- );
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
+ });
6009
7596
  }
6010
7597
  }
6011
7598
  /**
6012
- * Sets a value in cache with optional TTL.
6013
- * Automatically handles JSON serialization.
6014
- *
6015
- * @param key Cache key
6016
- * @param value Value to cache
6017
- * @param ttl Time to live in seconds (uses default if not specified)
6018
- * @returns DatabaseResult indicating operation success
6019
- *
6020
- * @example
6021
- * ```typescript
6022
- * // Cache with default TTL
6023
- * await cache.set('user:123', { id: 123, name: 'John' });
6024
- *
6025
- * // Cache with custom TTL (5 minutes)
6026
- * await cache.set('session:abc123', sessionData, 300);
6027
- *
6028
- * // Cache with long TTL for static data
6029
- * await cache.set('config:app-settings', appSettings, 86400); // 24 hours
6030
- *
6031
- * // Cache with short TTL for frequently changing data
6032
- * await cache.set('metrics:realtime', realtimeData, 60); // 1 minute
6033
- * ```
7599
+ * Execute operation with timeout
6034
7600
  */
6035
- async set(key, value, ttl) {
6036
- try {
6037
- const ttlValue = ttl ?? this.defaultTTL;
6038
- await this.redis.set(key, JSON.stringify(value), "EX", ttlValue);
6039
- return success();
6040
- } catch (error) {
6041
- return failure(
6042
- new errors.DatabaseError(
6043
- "Cache set failed",
6044
- errors$1.DATABASE_ERROR_CODES.CACHE_SET_FAILED,
6045
- { 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
6046
7608
  )
6047
- );
6048
- }
7609
+ )
7610
+ ]);
6049
7611
  }
6050
7612
  /**
6051
- * Deletes a value from cache.
6052
- *
6053
- * @param key Cache key
6054
- * @returns DatabaseResult indicating operation success
6055
- *
6056
- * @example
6057
- * ```typescript
6058
- * // Delete specific cache entry
6059
- * await cache.del('user:123');
6060
- *
6061
- * // Delete session on logout
6062
- * await cache.del(`session:${sessionId}`);
6063
- *
6064
- * // Delete cached configuration
6065
- * await cache.del('config:feature-flags');
6066
- * ```
7613
+ * Handle secondary adapter failures
6067
7614
  */
6068
- 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();
6069
7745
  try {
6070
- await this.redis.del(key);
6071
- 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;
6072
7760
  } catch (error) {
7761
+ this.updateHealthMetrics(selectedReplica, false, Date.now() - startTime);
7762
+ if (this.config.fallbackToPrimary) {
7763
+ return fn(this.baseAdapter);
7764
+ }
6073
7765
  return failure(
6074
- new errors.DatabaseError(
6075
- "Cache delete failed",
6076
- errors$1.DATABASE_ERROR_CODES.CACHE_DELETE_FAILED,
6077
- {
6078
- context: {
6079
- source: "RedisCache.del",
6080
- key,
6081
- cause: error
6082
- }
6083
- }
7766
+ new errors.DatabasePackageError(
7767
+ `Read from replica failed: ${error.message}`,
7768
+ errors$1.ERROR_CODES.DB_QUERY_FAILED,
7769
+ { cause: error }
6084
7770
  )
6085
7771
  );
6086
7772
  }
6087
7773
  }
6088
7774
  /**
6089
- * Invalidates all cache entries matching a pattern.
6090
- * Useful for clearing related cache entries when data changes.
6091
- *
6092
- * @param pattern Redis key pattern (e.g., 'users:*')
6093
- * @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
6094
7900
  *
6095
7901
  * @example
6096
7902
  * ```typescript
6097
- * // Invalidate all user-related caches
6098
- * await cache.invalidatePattern('users:*');
7903
+ * // Basic configuration
7904
+ * const cache = new RedisCache({
7905
+ * url: 'redis://localhost:6379'
7906
+ * });
6099
7907
  *
6100
- * // Invalidate all caches for a specific category
6101
- * 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
+ * });
6102
7913
  *
6103
- * // Invalidate all session caches
6104
- * 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.
6105
7929
  *
6106
- * // Invalidate all caches for a tenant
6107
- * await cache.invalidatePattern(`tenant:${tenantId}:*`);
7930
+ * @param key Cache key
7931
+ * @returns DatabaseResult containing cached value or null if not found
6108
7932
  *
6109
- * // Invalidate all analytics caches
6110
- * 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
+ * }
6111
7950
  * ```
6112
7951
  */
6113
- async invalidatePattern(pattern) {
7952
+ async get(key) {
6114
7953
  try {
6115
- const keys = await this.redis.keys(pattern);
6116
- if (keys.length > 0) {
6117
- 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);
6118
7961
  }
6119
7962
  return success();
6120
7963
  } catch (error) {
6121
7964
  return failure(
6122
7965
  new errors.DatabaseError(
6123
- "Cache invalidate failed",
6124
- errors$1.DATABASE_ERROR_CODES.CACHE_INVALIDATE_FAILED,
6125
- {
6126
- context: {
6127
- source: "RedisCache.invalidatePattern",
6128
- pattern,
6129
- cause: error
6130
- }
6131
- }
7966
+ "Cache get failed",
7967
+ errors$1.DATABASE_ERROR_CODES.CACHE_GET_FAILED,
7968
+ { context: { source: "RedisCache.get", key, cause: error } }
6132
7969
  )
6133
7970
  );
6134
7971
  }
6135
7972
  }
6136
7973
  /**
6137
- * Generates a cache key for database queries.
6138
- * 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.
6139
7976
  *
6140
- * @param table Database table name
6141
- * @param operation Database operation type
6142
- * @param params Query parameters object
6143
- * @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
6144
7981
  *
6145
7982
  * @example
6146
7983
  * ```typescript
6147
- * // Simple key generation
6148
- * const key = cache.generateKey('users', 'findById', { id: '123' });
6149
- * // Returns: 'db:users:findById:eyJpYXJhbTAiOiIxMjMifQ=='
6150
- *
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=='
8112
+ *
6151
8113
  * // Complex query key
6152
8114
  * const complexKey = cache.generateKey('products', 'findMany', {
6153
8115
  * filter: { field: 'category', operator: 'eq', value: 'electronics' },
@@ -8480,6 +10442,655 @@ __name(exports.DataValidationPipe, "DataValidationPipe");
8480
10442
  exports.DataValidationPipe = __decorateClass([
8481
10443
  common.Injectable()
8482
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
+ };
8483
11094
 
8484
11095
  exports.AdapterFactory = AdapterFactory;
8485
11096
  exports.AlertManager = AlertManager;
@@ -8497,9 +11108,14 @@ exports.DynamicPool = DynamicPool;
8497
11108
  exports.EncryptionAdapter = EncryptionAdapter;
8498
11109
  exports.HealthManager = HealthManager;
8499
11110
  exports.MetricsCollector = MetricsCollector;
11111
+ exports.MigrationManager = MigrationManager;
11112
+ exports.MockAdapter = MockAdapter;
11113
+ exports.MultiReadAdapter = MultiReadAdapter;
11114
+ exports.MultiWriteAdapter = MultiWriteAdapter;
8500
11115
  exports.ReadReplicaAdapter = ReadReplicaAdapter;
8501
11116
  exports.RedisCache = RedisCache;
8502
11117
  exports.SQLAdapter = SQLAdapter;
11118
+ exports.SeedManager = SeedManager;
8503
11119
  exports.ShardKeyManager = ShardKeyManager;
8504
11120
  exports.ShardRouter = ShardRouter;
8505
11121
  exports.SoftDeleteAdapter = SoftDeleteAdapter;