@ciscode/database-kit 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +50 -4
  2. package/README.md +487 -148
  3. package/dist/adapters/mongo.adapter.d.ts +53 -3
  4. package/dist/adapters/mongo.adapter.d.ts.map +1 -1
  5. package/dist/adapters/mongo.adapter.js +410 -27
  6. package/dist/adapters/mongo.adapter.js.map +1 -1
  7. package/dist/adapters/postgres.adapter.d.ts +50 -3
  8. package/dist/adapters/postgres.adapter.d.ts.map +1 -1
  9. package/dist/adapters/postgres.adapter.js +439 -45
  10. package/dist/adapters/postgres.adapter.js.map +1 -1
  11. package/dist/config/database.config.d.ts +1 -1
  12. package/dist/config/database.config.d.ts.map +1 -1
  13. package/dist/config/database.config.js +13 -13
  14. package/dist/config/database.config.js.map +1 -1
  15. package/dist/config/database.constants.js +7 -7
  16. package/dist/contracts/database.contracts.d.ts +283 -6
  17. package/dist/contracts/database.contracts.d.ts.map +1 -1
  18. package/dist/contracts/database.contracts.js +6 -1
  19. package/dist/contracts/database.contracts.js.map +1 -1
  20. package/dist/database-kit.module.d.ts +2 -2
  21. package/dist/database-kit.module.d.ts.map +1 -1
  22. package/dist/database-kit.module.js +1 -2
  23. package/dist/database-kit.module.js.map +1 -1
  24. package/dist/filters/database-exception.filter.d.ts +1 -1
  25. package/dist/filters/database-exception.filter.d.ts.map +1 -1
  26. package/dist/filters/database-exception.filter.js +43 -43
  27. package/dist/filters/database-exception.filter.js.map +1 -1
  28. package/dist/index.d.ts +10 -10
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/middleware/database.decorators.d.ts.map +1 -1
  32. package/dist/middleware/database.decorators.js.map +1 -1
  33. package/dist/services/database.service.d.ts +83 -5
  34. package/dist/services/database.service.d.ts.map +1 -1
  35. package/dist/services/database.service.js +136 -16
  36. package/dist/services/database.service.js.map +1 -1
  37. package/dist/services/logger.service.d.ts +1 -1
  38. package/dist/services/logger.service.d.ts.map +1 -1
  39. package/dist/services/logger.service.js +1 -1
  40. package/dist/services/logger.service.js.map +1 -1
  41. package/dist/utils/pagination.utils.d.ts +2 -2
  42. package/dist/utils/pagination.utils.d.ts.map +1 -1
  43. package/dist/utils/pagination.utils.js +9 -6
  44. package/dist/utils/pagination.utils.js.map +1 -1
  45. package/dist/utils/validation.utils.d.ts.map +1 -1
  46. package/dist/utils/validation.utils.js +5 -5
  47. package/dist/utils/validation.utils.js.map +1 -1
  48. package/package.json +28 -8
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- // src/adapters/postgres.adapter.ts
3
2
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
4
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -15,8 +14,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
15
14
  var PostgresAdapter_1;
16
15
  Object.defineProperty(exports, "__esModule", { value: true });
17
16
  exports.PostgresAdapter = void 0;
18
- const knex_1 = __importDefault(require("knex"));
19
17
  const common_1 = require("@nestjs/common");
18
+ const knex_1 = __importDefault(require("knex"));
19
+ const database_contracts_1 = require("../contracts/database.contracts");
20
20
  /**
21
21
  * PostgreSQL adapter for DatabaseKit.
22
22
  * Handles PostgreSQL connection and repository creation via Knex.
@@ -41,15 +41,25 @@ let PostgresAdapter = PostgresAdapter_1 = class PostgresAdapter {
41
41
  * @returns Knex instance
42
42
  */
43
43
  connect(overrides = {}) {
44
+ var _a, _b, _c, _d, _e;
44
45
  if (!this.knexInstance) {
45
- this.logger.log('Creating PostgreSQL connection pool...');
46
+ this.logger.log("Creating PostgreSQL connection pool...");
47
+ // Apply pool configuration from config
48
+ const poolConfig = this.config.pool || {};
49
+ const pool = {
50
+ min: (_a = poolConfig.min) !== null && _a !== void 0 ? _a : 0,
51
+ max: (_b = poolConfig.max) !== null && _b !== void 0 ? _b : 10,
52
+ idleTimeoutMillis: (_c = poolConfig.idleTimeoutMs) !== null && _c !== void 0 ? _c : 30000,
53
+ acquireTimeoutMillis: (_d = poolConfig.acquireTimeoutMs) !== null && _d !== void 0 ? _d : 60000,
54
+ };
46
55
  this.knexInstance = (0, knex_1.default)({
47
- client: 'pg',
56
+ client: "pg",
48
57
  connection: this.config.connectionString,
49
- pool: { min: 0, max: 10 },
58
+ pool,
59
+ acquireConnectionTimeout: (_e = poolConfig.acquireTimeoutMs) !== null && _e !== void 0 ? _e : 60000,
50
60
  ...overrides,
51
61
  });
52
- this.logger.log('PostgreSQL connection pool created');
62
+ this.logger.log("PostgreSQL connection pool created");
53
63
  }
54
64
  return this.knexInstance;
55
65
  }
@@ -60,7 +70,7 @@ let PostgresAdapter = PostgresAdapter_1 = class PostgresAdapter {
60
70
  if (this.knexInstance) {
61
71
  await this.knexInstance.destroy();
62
72
  this.knexInstance = undefined;
63
- this.logger.log('PostgreSQL connection pool destroyed');
73
+ this.logger.log("PostgreSQL connection pool destroyed");
64
74
  }
65
75
  }
66
76
  /**
@@ -69,7 +79,7 @@ let PostgresAdapter = PostgresAdapter_1 = class PostgresAdapter {
69
79
  */
70
80
  getKnex() {
71
81
  if (!this.knexInstance) {
72
- throw new Error('PostgreSQL not connected. Call connect() first.');
82
+ throw new Error("PostgreSQL not connected. Call connect() first.");
73
83
  }
74
84
  return this.knexInstance;
75
85
  }
@@ -79,19 +89,142 @@ let PostgresAdapter = PostgresAdapter_1 = class PostgresAdapter {
79
89
  isConnected() {
80
90
  return !!this.knexInstance;
81
91
  }
92
+ /**
93
+ * Performs a health check on the PostgreSQL connection.
94
+ * Executes a simple query to verify the database is responsive.
95
+ *
96
+ * @returns Health check result with status and response time
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const health = await adapter.healthCheck();
101
+ * if (!health.healthy) {
102
+ * console.error('Database unhealthy:', health.error);
103
+ * }
104
+ * ```
105
+ */
106
+ async healthCheck() {
107
+ var _a, _b, _c, _d, _e, _f, _g, _h;
108
+ const startTime = Date.now();
109
+ try {
110
+ if (!this.knexInstance) {
111
+ return {
112
+ healthy: false,
113
+ responseTimeMs: Date.now() - startTime,
114
+ type: "postgres",
115
+ error: "Not connected to PostgreSQL",
116
+ };
117
+ }
118
+ // Execute simple query to verify connection
119
+ const result = await this.knexInstance.raw("SELECT version(), current_database()");
120
+ const row = (_a = result.rows) === null || _a === void 0 ? void 0 : _a[0];
121
+ // Get pool info if available
122
+ const pool = this.knexInstance.client.pool;
123
+ return {
124
+ healthy: true,
125
+ responseTimeMs: Date.now() - startTime,
126
+ type: "postgres",
127
+ details: {
128
+ version: (_b = row === null || row === void 0 ? void 0 : row.version) === null || _b === void 0 ? void 0 : _b.split(" ").slice(0, 2).join(" "),
129
+ activeConnections: (_d = (_c = pool === null || pool === void 0 ? void 0 : pool.numUsed) === null || _c === void 0 ? void 0 : _c.call(pool)) !== null && _d !== void 0 ? _d : 0,
130
+ poolSize: ((_f = (_e = pool === null || pool === void 0 ? void 0 : pool.numUsed) === null || _e === void 0 ? void 0 : _e.call(pool)) !== null && _f !== void 0 ? _f : 0) + ((_h = (_g = pool === null || pool === void 0 ? void 0 : pool.numFree) === null || _g === void 0 ? void 0 : _g.call(pool)) !== null && _h !== void 0 ? _h : 0),
131
+ },
132
+ };
133
+ }
134
+ catch (error) {
135
+ return {
136
+ healthy: false,
137
+ responseTimeMs: Date.now() - startTime,
138
+ type: "postgres",
139
+ error: error instanceof Error ? error.message : "Unknown error",
140
+ };
141
+ }
142
+ }
82
143
  /**
83
144
  * Creates a repository for a PostgreSQL table.
84
145
  * The repository provides a standardized CRUD interface.
85
146
  *
86
147
  * @param cfg - Configuration for the entity/table
148
+ * @param trx - Optional Knex transaction for transaction support
87
149
  * @returns Repository instance with CRUD methods
88
150
  */
89
- createRepository(cfg) {
90
- const kx = this.getKnex();
151
+ createRepository(cfg, trx) {
152
+ var _a, _b, _c, _d, _e;
153
+ const kx = trx || this.getKnex();
91
154
  const table = cfg.table;
92
- const pk = cfg.primaryKey || 'id';
155
+ const pk = cfg.primaryKey || "id";
93
156
  const allowed = cfg.columns || [];
94
157
  const baseFilter = cfg.defaultFilter || {};
158
+ // Soft delete configuration
159
+ const softDeleteEnabled = (_a = cfg.softDelete) !== null && _a !== void 0 ? _a : false;
160
+ const softDeleteField = (_b = cfg.softDeleteField) !== null && _b !== void 0 ? _b : "deleted_at";
161
+ // Timestamp configuration
162
+ const timestampsEnabled = (_c = cfg.timestamps) !== null && _c !== void 0 ? _c : false;
163
+ const createdAtField = (_d = cfg.createdAtField) !== null && _d !== void 0 ? _d : "created_at";
164
+ const updatedAtField = (_e = cfg.updatedAtField) !== null && _e !== void 0 ? _e : "updated_at";
165
+ // Hooks configuration
166
+ const hooks = cfg.hooks;
167
+ // Create not-deleted filter for soft delete
168
+ const notDeletedFilter = softDeleteEnabled
169
+ ? { [softDeleteField]: { isNull: true } }
170
+ : {};
171
+ // Helper to add createdAt timestamp
172
+ const addCreatedAt = (data) => {
173
+ if (timestampsEnabled) {
174
+ return { ...data, [createdAtField]: new Date() };
175
+ }
176
+ return data;
177
+ };
178
+ // Helper to add updatedAt timestamp
179
+ const addUpdatedAt = (data) => {
180
+ if (timestampsEnabled) {
181
+ return { ...data, [updatedAtField]: new Date() };
182
+ }
183
+ return data;
184
+ };
185
+ // Hook helper functions
186
+ const runBeforeCreate = async (data) => {
187
+ if (hooks === null || hooks === void 0 ? void 0 : hooks.beforeCreate) {
188
+ const result = await hooks.beforeCreate({
189
+ data,
190
+ operation: "create",
191
+ isBulk: false,
192
+ });
193
+ return result !== null && result !== void 0 ? result : data;
194
+ }
195
+ return data;
196
+ };
197
+ const runAfterCreate = async (entity) => {
198
+ if (hooks === null || hooks === void 0 ? void 0 : hooks.afterCreate) {
199
+ await hooks.afterCreate(entity);
200
+ }
201
+ };
202
+ const runBeforeUpdate = async (data) => {
203
+ if (hooks === null || hooks === void 0 ? void 0 : hooks.beforeUpdate) {
204
+ const result = await hooks.beforeUpdate({
205
+ data,
206
+ operation: "update",
207
+ isBulk: false,
208
+ });
209
+ return result !== null && result !== void 0 ? result : data;
210
+ }
211
+ return data;
212
+ };
213
+ const runAfterUpdate = async (entity) => {
214
+ if (hooks === null || hooks === void 0 ? void 0 : hooks.afterUpdate) {
215
+ await hooks.afterUpdate(entity);
216
+ }
217
+ };
218
+ const runBeforeDelete = async (id) => {
219
+ if (hooks === null || hooks === void 0 ? void 0 : hooks.beforeDelete) {
220
+ await hooks.beforeDelete(id);
221
+ }
222
+ };
223
+ const runAfterDelete = async (success) => {
224
+ if (hooks === null || hooks === void 0 ? void 0 : hooks.afterDelete) {
225
+ await hooks.afterDelete(success);
226
+ }
227
+ };
95
228
  const assertFieldAllowed = (field) => {
96
229
  if (allowed.length && !allowed.includes(field)) {
97
230
  throw new Error(`Field "${field}" is not allowed for table "${table}". Add it to columns[] in config.`);
@@ -100,20 +233,20 @@ let PostgresAdapter = PostgresAdapter_1 = class PostgresAdapter {
100
233
  const applyFilter = (qb, filter) => {
101
234
  Object.entries(filter).forEach(([key, value]) => {
102
235
  assertFieldAllowed(key);
103
- if (value && typeof value === 'object' && !Array.isArray(value)) {
236
+ if (value && typeof value === "object" && !Array.isArray(value)) {
104
237
  const ops = value;
105
238
  if (ops.eq !== undefined)
106
239
  qb.where(key, ops.eq);
107
240
  if (ops.ne !== undefined)
108
241
  qb.whereNot(key, ops.ne);
109
242
  if (ops.gt !== undefined)
110
- qb.where(key, '>', ops.gt);
243
+ qb.where(key, ">", ops.gt);
111
244
  if (ops.gte !== undefined)
112
- qb.where(key, '>=', ops.gte);
245
+ qb.where(key, ">=", ops.gte);
113
246
  if (ops.lt !== undefined)
114
- qb.where(key, '<', ops.lt);
247
+ qb.where(key, "<", ops.lt);
115
248
  if (ops.lte !== undefined)
116
- qb.where(key, '<=', ops.lte);
249
+ qb.where(key, "<=", ops.lte);
117
250
  if (ops.in)
118
251
  qb.whereIn(key, ops.in);
119
252
  if (ops.nin)
@@ -133,11 +266,11 @@ let PostgresAdapter = PostgresAdapter_1 = class PostgresAdapter {
133
266
  const applySort = (qb, sort) => {
134
267
  if (!sort)
135
268
  return;
136
- if (typeof sort === 'string') {
137
- const parts = sort.split(',');
269
+ if (typeof sort === "string") {
270
+ const parts = sort.split(",");
138
271
  for (const p of parts) {
139
- const dir = p.startsWith('-') ? 'desc' : 'asc';
140
- const col = p.replace(/^[-+]/, '');
272
+ const dir = p.startsWith("-") ? "desc" : "asc";
273
+ const col = p.replace(/^[-+]/, "");
141
274
  assertFieldAllowed(col);
142
275
  qb.orderBy(col, dir);
143
276
  }
@@ -145,7 +278,7 @@ let PostgresAdapter = PostgresAdapter_1 = class PostgresAdapter {
145
278
  else {
146
279
  Object.entries(sort).forEach(([col, dir]) => {
147
280
  assertFieldAllowed(col);
148
- const direction = dir === -1 || String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
281
+ const direction = dir === -1 || String(dir).toLowerCase() === "desc" ? "desc" : "asc";
149
282
  qb.orderBy(col, direction);
150
283
  });
151
284
  }
@@ -156,70 +289,331 @@ let PostgresAdapter = PostgresAdapter_1 = class PostgresAdapter {
156
289
  };
157
290
  const repo = {
158
291
  async create(data) {
159
- const [row] = await kx(table).insert(data).returning('*');
160
- return row;
292
+ // Run beforeCreate hook
293
+ let processedData = await runBeforeCreate(data);
294
+ processedData = addCreatedAt(processedData);
295
+ const [row] = await kx(table).insert(processedData).returning("*");
296
+ const entity = row;
297
+ // Run afterCreate hook
298
+ await runAfterCreate(entity);
299
+ return entity;
161
300
  },
162
301
  async findById(id) {
163
- const row = await kx(table)
164
- .select('*')
165
- .where({ [pk]: id, ...baseFilter })
166
- .first();
302
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter };
303
+ const qb = kx(table)
304
+ .select("*")
305
+ .where({ [pk]: id });
306
+ applyFilter(qb, mergedFilter);
307
+ const row = await qb.first();
167
308
  return row || null;
168
309
  },
169
310
  async findAll(filter = {}) {
170
- const mergedFilter = { ...baseFilter, ...filter };
171
- const qb = kx(table).select('*');
311
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter, ...filter };
312
+ const qb = kx(table).select("*");
172
313
  applyFilter(qb, mergedFilter);
173
314
  const rows = await qb;
174
315
  return rows;
175
316
  },
317
+ async findOne(filter) {
318
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter, ...filter };
319
+ const qb = kx(table).select("*");
320
+ applyFilter(qb, mergedFilter);
321
+ const row = await qb.first();
322
+ return row || null;
323
+ },
176
324
  async findPage(options = {}) {
177
325
  var _a;
178
326
  const { filter = {}, page = 1, limit = 10, sort } = options;
179
- const mergedFilter = { ...baseFilter, ...filter };
327
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter, ...filter };
180
328
  const offset = Math.max(0, (page - 1) * limit);
181
- const qb = kx(table).select('*');
329
+ const qb = kx(table).select("*");
182
330
  applyFilter(qb, mergedFilter);
183
331
  applySort(qb, sort);
184
332
  const data = (await qb.clone().limit(limit).offset(offset));
185
333
  const countRow = await kx(table)
186
- .count({ count: '*' })
334
+ .count({ count: "*" })
187
335
  .modify((q) => applyFilter(q, mergedFilter));
188
336
  const total = Number(((_a = countRow[0]) === null || _a === void 0 ? void 0 : _a.count) || 0);
189
337
  return shapePage(data, page, limit, total);
190
338
  },
191
339
  async updateById(id, update) {
192
- const [row] = await kx(table)
193
- .where({ [pk]: id })
194
- .update(update)
195
- .returning('*');
196
- return row || null;
340
+ // Run beforeUpdate hook
341
+ let processedUpdate = await runBeforeUpdate(update);
342
+ processedUpdate = addUpdatedAt(processedUpdate);
343
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter };
344
+ const qb = kx(table).where({ [pk]: id });
345
+ applyFilter(qb, mergedFilter);
346
+ const [row] = await qb.update(processedUpdate).returning("*");
347
+ const entity = row || null;
348
+ // Run afterUpdate hook
349
+ await runAfterUpdate(entity);
350
+ return entity;
197
351
  },
198
352
  async deleteById(id) {
199
- const [row] = await kx(table)
200
- .where({ [pk]: id })
201
- .delete()
202
- .returning('*');
203
- return !!row;
353
+ // Run beforeDelete hook
354
+ await runBeforeDelete(id);
355
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter };
356
+ let success;
357
+ // If soft delete is enabled, update instead of delete
358
+ if (softDeleteEnabled) {
359
+ const qb = kx(table).where({ [pk]: id });
360
+ applyFilter(qb, mergedFilter);
361
+ const affectedRows = await qb.update({
362
+ [softDeleteField]: new Date(),
363
+ });
364
+ success = affectedRows > 0;
365
+ }
366
+ else {
367
+ const qb = kx(table).where({ [pk]: id });
368
+ applyFilter(qb, mergedFilter);
369
+ const affectedRows = await qb.delete();
370
+ success = affectedRows > 0;
371
+ }
372
+ // Run afterDelete hook
373
+ await runAfterDelete(success);
374
+ return success;
204
375
  },
205
376
  async count(filter = {}) {
206
- const mergedFilter = { ...baseFilter, ...filter };
377
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter, ...filter };
207
378
  const [{ count }] = await kx(table)
208
- .count({ count: '*' })
379
+ .count({ count: "*" })
209
380
  .modify((q) => applyFilter(q, mergedFilter));
210
381
  return Number(count || 0);
211
382
  },
212
383
  async exists(filter = {}) {
213
- const mergedFilter = { ...baseFilter, ...filter };
384
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter, ...filter };
214
385
  const row = await kx(table)
215
386
  .select([pk])
216
387
  .modify((q) => applyFilter(q, mergedFilter))
217
388
  .first();
218
389
  return !!row;
219
390
  },
391
+ // -----------------------------
392
+ // Bulk Operations
393
+ // -----------------------------
394
+ async insertMany(data) {
395
+ if (data.length === 0)
396
+ return [];
397
+ // Add createdAt timestamp to each record
398
+ const timestampedData = data.map((item) => addCreatedAt(item));
399
+ const rows = await kx(table).insert(timestampedData).returning("*");
400
+ return rows;
401
+ },
402
+ async updateMany(filter, update) {
403
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter, ...filter };
404
+ const timestampedUpdate = addUpdatedAt(update);
405
+ const affectedRows = await kx(table)
406
+ .modify((q) => applyFilter(q, mergedFilter))
407
+ .update(timestampedUpdate);
408
+ return affectedRows;
409
+ },
410
+ async deleteMany(filter) {
411
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter, ...filter };
412
+ // If soft delete is enabled, update instead of delete
413
+ if (softDeleteEnabled) {
414
+ const affectedRows = await kx(table)
415
+ .modify((q) => applyFilter(q, mergedFilter))
416
+ .update({ [softDeleteField]: new Date() });
417
+ return affectedRows;
418
+ }
419
+ const affectedRows = await kx(table)
420
+ .modify((q) => applyFilter(q, mergedFilter))
421
+ .delete();
422
+ return affectedRows;
423
+ },
424
+ // -----------------------------
425
+ // Advanced Query Operations
426
+ // -----------------------------
427
+ async upsert(filter, data) {
428
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter, ...filter };
429
+ // Try to find existing record
430
+ const qb = kx(table).select("*");
431
+ applyFilter(qb, mergedFilter);
432
+ const existing = await qb.first();
433
+ if (existing) {
434
+ // Update existing record
435
+ const timestampedUpdate = addUpdatedAt(data);
436
+ const updateQb = kx(table).where({ [pk]: existing[pk] });
437
+ const [row] = await updateQb.update(timestampedUpdate).returning("*");
438
+ return row;
439
+ }
440
+ else {
441
+ // Insert new record
442
+ const timestampedData = addCreatedAt({ ...filter, ...data });
443
+ const [row] = await kx(table).insert(timestampedData).returning("*");
444
+ return row;
445
+ }
446
+ },
447
+ async distinct(field, filter = {}) {
448
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter, ...filter };
449
+ const qb = kx(table)
450
+ .distinct(String(field))
451
+ .modify((q) => applyFilter(q, mergedFilter));
452
+ const rows = await qb;
453
+ return rows.map((row) => row[String(field)]);
454
+ },
455
+ async select(filter, fields) {
456
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter, ...filter };
457
+ const qb = kx(table)
458
+ .select(fields.map(String))
459
+ .modify((q) => applyFilter(q, mergedFilter));
460
+ const rows = await qb;
461
+ return rows;
462
+ },
463
+ // -----------------------------
464
+ // Soft Delete Operations
465
+ // -----------------------------
466
+ softDelete: softDeleteEnabled
467
+ ? async (id) => {
468
+ const mergedFilter = { ...baseFilter, ...notDeletedFilter };
469
+ const qb = kx(table).where({ [pk]: id });
470
+ applyFilter(qb, mergedFilter);
471
+ const affectedRows = await qb.update({
472
+ [softDeleteField]: new Date(),
473
+ });
474
+ return affectedRows > 0;
475
+ }
476
+ : undefined,
477
+ softDeleteMany: softDeleteEnabled
478
+ ? async (filter) => {
479
+ const mergedFilter = {
480
+ ...baseFilter,
481
+ ...notDeletedFilter,
482
+ ...filter,
483
+ };
484
+ const affectedRows = await kx(table)
485
+ .modify((q) => applyFilter(q, mergedFilter))
486
+ .update({ [softDeleteField]: new Date() });
487
+ return affectedRows;
488
+ }
489
+ : undefined,
490
+ restore: softDeleteEnabled
491
+ ? async (id) => {
492
+ const deletedFilter = { [softDeleteField]: { isNotNull: true } };
493
+ const mergedFilter = { ...baseFilter, ...deletedFilter };
494
+ const qb = kx(table).where({ [pk]: id });
495
+ applyFilter(qb, mergedFilter);
496
+ const [row] = await qb
497
+ .update({ [softDeleteField]: null })
498
+ .returning("*");
499
+ return row || null;
500
+ }
501
+ : undefined,
502
+ restoreMany: softDeleteEnabled
503
+ ? async (filter) => {
504
+ const deletedFilter = { [softDeleteField]: { isNotNull: true } };
505
+ const mergedFilter = { ...baseFilter, ...deletedFilter, ...filter };
506
+ const affectedRows = await kx(table)
507
+ .modify((q) => applyFilter(q, mergedFilter))
508
+ .update({ [softDeleteField]: null });
509
+ return affectedRows;
510
+ }
511
+ : undefined,
512
+ findAllWithDeleted: softDeleteEnabled
513
+ ? async (filter = {}) => {
514
+ // Ignore soft delete filter, include all records
515
+ const mergedFilter = { ...baseFilter, ...filter };
516
+ const qb = kx(table).select("*");
517
+ applyFilter(qb, mergedFilter);
518
+ const rows = await qb;
519
+ return rows;
520
+ }
521
+ : undefined,
522
+ findDeleted: softDeleteEnabled
523
+ ? async (filter = {}) => {
524
+ // Only find deleted records
525
+ const deletedFilter = { [softDeleteField]: { isNotNull: true } };
526
+ const mergedFilter = { ...baseFilter, ...deletedFilter, ...filter };
527
+ const qb = kx(table).select("*");
528
+ applyFilter(qb, mergedFilter);
529
+ const rows = await qb;
530
+ return rows;
531
+ }
532
+ : undefined,
220
533
  };
221
534
  return repo;
222
535
  }
536
+ /**
537
+ * Executes a callback within a PostgreSQL transaction.
538
+ * All database operations within the callback are atomic.
539
+ *
540
+ * @param callback - Function to execute within the transaction
541
+ * @param options - Transaction options including isolation level
542
+ * @returns Result of the callback function
543
+ * @throws Error if transaction fails after all retries
544
+ *
545
+ * @example
546
+ * ```typescript
547
+ * const result = await postgresAdapter.withTransaction(async (ctx) => {
548
+ * const usersRepo = ctx.createRepository<User>({ table: 'users' });
549
+ * const ordersRepo = ctx.createRepository<Order>({ table: 'orders' });
550
+ *
551
+ * const [user] = await usersRepo.create({ name: 'John' });
552
+ * const [order] = await ordersRepo.create({ user_id: user.id, total: 100 });
553
+ *
554
+ * return { user, order };
555
+ * }, { isolationLevel: 'serializable' });
556
+ * ```
557
+ */
558
+ async withTransaction(callback, options = {}) {
559
+ const { isolationLevel = "read committed", retries = 0, timeout = database_contracts_1.DATABASE_KIT_CONSTANTS.DEFAULT_TRANSACTION_TIMEOUT, } = options;
560
+ const kx = this.getKnex();
561
+ let lastError;
562
+ for (let attempt = 0; attempt <= retries; attempt++) {
563
+ try {
564
+ const result = await kx.transaction(async (trx) => {
565
+ // Set statement timeout for the transaction
566
+ await trx.raw(`SET LOCAL statement_timeout = ${timeout}`);
567
+ const context = {
568
+ transaction: trx,
569
+ createRepository: (config) => this.createRepository(config, trx),
570
+ };
571
+ return await callback(context);
572
+ }, { isolationLevel });
573
+ this.logger.debug(`Transaction committed successfully (attempt ${attempt + 1})`);
574
+ return result;
575
+ }
576
+ catch (error) {
577
+ lastError = error;
578
+ this.logger.warn(`Transaction failed (attempt ${attempt + 1}/${retries + 1}): ${lastError.message}`);
579
+ // Check if error is retryable
580
+ const isRetryable = this.isRetryableError(error);
581
+ if (!isRetryable || attempt >= retries) {
582
+ throw lastError;
583
+ }
584
+ // Exponential backoff before retry
585
+ const backoffMs = Math.min(100 * Math.pow(2, attempt), 3000);
586
+ await this.sleep(backoffMs);
587
+ }
588
+ }
589
+ throw lastError || new Error("Transaction failed");
590
+ }
591
+ /**
592
+ * Checks if a PostgreSQL error is retryable.
593
+ */
594
+ isRetryableError(error) {
595
+ if (error && typeof error === "object") {
596
+ const pgError = error;
597
+ // PostgreSQL serialization failure codes
598
+ const retryableCodes = [
599
+ "40001", // serialization_failure
600
+ "40P01", // deadlock_detected
601
+ "55P03", // lock_not_available
602
+ "57P01", // admin_shutdown
603
+ "57014", // query_canceled (timeout)
604
+ ];
605
+ if (pgError.code && retryableCodes.includes(pgError.code)) {
606
+ return true;
607
+ }
608
+ }
609
+ return false;
610
+ }
611
+ /**
612
+ * Simple sleep utility for retry backoff.
613
+ */
614
+ sleep(ms) {
615
+ return new Promise((resolve) => setTimeout(resolve, ms));
616
+ }
223
617
  };
224
618
  exports.PostgresAdapter = PostgresAdapter;
225
619
  exports.PostgresAdapter = PostgresAdapter = PostgresAdapter_1 = __decorate([