@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.
- package/CHANGELOG.md +50 -4
- package/README.md +487 -148
- package/dist/adapters/mongo.adapter.d.ts +53 -3
- package/dist/adapters/mongo.adapter.d.ts.map +1 -1
- package/dist/adapters/mongo.adapter.js +410 -27
- package/dist/adapters/mongo.adapter.js.map +1 -1
- package/dist/adapters/postgres.adapter.d.ts +50 -3
- package/dist/adapters/postgres.adapter.d.ts.map +1 -1
- package/dist/adapters/postgres.adapter.js +439 -45
- package/dist/adapters/postgres.adapter.js.map +1 -1
- package/dist/config/database.config.d.ts +1 -1
- package/dist/config/database.config.d.ts.map +1 -1
- package/dist/config/database.config.js +13 -13
- package/dist/config/database.config.js.map +1 -1
- package/dist/config/database.constants.js +7 -7
- package/dist/contracts/database.contracts.d.ts +283 -6
- package/dist/contracts/database.contracts.d.ts.map +1 -1
- package/dist/contracts/database.contracts.js +6 -1
- package/dist/contracts/database.contracts.js.map +1 -1
- package/dist/database-kit.module.d.ts +2 -2
- package/dist/database-kit.module.d.ts.map +1 -1
- package/dist/database-kit.module.js +1 -2
- package/dist/database-kit.module.js.map +1 -1
- package/dist/filters/database-exception.filter.d.ts +1 -1
- package/dist/filters/database-exception.filter.d.ts.map +1 -1
- package/dist/filters/database-exception.filter.js +43 -43
- package/dist/filters/database-exception.filter.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/database.decorators.d.ts.map +1 -1
- package/dist/middleware/database.decorators.js.map +1 -1
- package/dist/services/database.service.d.ts +83 -5
- package/dist/services/database.service.d.ts.map +1 -1
- package/dist/services/database.service.js +136 -16
- package/dist/services/database.service.js.map +1 -1
- package/dist/services/logger.service.d.ts +1 -1
- package/dist/services/logger.service.d.ts.map +1 -1
- package/dist/services/logger.service.js +1 -1
- package/dist/services/logger.service.js.map +1 -1
- package/dist/utils/pagination.utils.d.ts +2 -2
- package/dist/utils/pagination.utils.d.ts.map +1 -1
- package/dist/utils/pagination.utils.js +9 -6
- package/dist/utils/pagination.utils.js.map +1 -1
- package/dist/utils/validation.utils.d.ts.map +1 -1
- package/dist/utils/validation.utils.js +5 -5
- package/dist/utils/validation.utils.js.map +1 -1
- 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(
|
|
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:
|
|
56
|
+
client: "pg",
|
|
48
57
|
connection: this.config.connectionString,
|
|
49
|
-
pool
|
|
58
|
+
pool,
|
|
59
|
+
acquireConnectionTimeout: (_e = poolConfig.acquireTimeoutMs) !== null && _e !== void 0 ? _e : 60000,
|
|
50
60
|
...overrides,
|
|
51
61
|
});
|
|
52
|
-
this.logger.log(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 ||
|
|
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 ===
|
|
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,
|
|
243
|
+
qb.where(key, ">", ops.gt);
|
|
111
244
|
if (ops.gte !== undefined)
|
|
112
|
-
qb.where(key,
|
|
245
|
+
qb.where(key, ">=", ops.gte);
|
|
113
246
|
if (ops.lt !== undefined)
|
|
114
|
-
qb.where(key,
|
|
247
|
+
qb.where(key, "<", ops.lt);
|
|
115
248
|
if (ops.lte !== undefined)
|
|
116
|
-
qb.where(key,
|
|
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 ===
|
|
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(
|
|
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() ===
|
|
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
|
-
|
|
160
|
-
|
|
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
|
|
164
|
-
|
|
165
|
-
.
|
|
166
|
-
.
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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([
|