@plyaz/db 0.1.0 → 0.2.0

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