stabilize-orm 1.1.7 → 1.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.
package/repository.ts CHANGED
@@ -20,7 +20,12 @@ import {
20
20
  ValidatorKey,
21
21
  RelationKey,
22
22
  SoftDeleteKey,
23
+ VersionedKey,
23
24
  } from "./decorators";
25
+ import { getHooks, type HookType } from "./hooks";
26
+
27
+ type VersionOperation = "insert" | "update" | "delete";
28
+
24
29
 
25
30
  /**
26
31
  * Provides a generic repository for a model `T`.
@@ -46,6 +51,8 @@ export class Repository<T> {
46
51
  >;
47
52
  private softDeleteField: string | null;
48
53
  private logger: Logger;
54
+ private versioned: boolean;
55
+ private historyTable: string;
49
56
 
50
57
  /**
51
58
  * Creates an instance of Repository.
@@ -69,6 +76,8 @@ export class Repository<T> {
69
76
  this.softDeleteField =
70
77
  Reflect.getMetadata(SoftDeleteKey, model.prototype) || null;
71
78
  this.logger = logger;
79
+ this.versioned = !!Reflect.getMetadata(VersionedKey, model);
80
+ this.historyTable = `${this.table}_history`;
72
81
  }
73
82
 
74
83
  /**
@@ -102,6 +111,17 @@ export class Repository<T> {
102
111
  }
103
112
  }
104
113
 
114
+ /**
115
+ * Runs lifecycle hooks of a given type for the entity.
116
+ * @param entity The entity instance.
117
+ * @param type The hook type (e.g., 'beforeCreate').
118
+ */
119
+ private async runHooks(entity: any, type: HookType): Promise<void> {
120
+ for (const hook of getHooks(entity, type)) {
121
+ await hook();
122
+ }
123
+ }
124
+
105
125
  /**
106
126
  * Creates a new `QueryBuilder` instance for the repository's table.
107
127
  * Automatically adds a `WHERE` clause to exclude soft-deleted records if applicable.
@@ -152,6 +172,136 @@ export class Repository<T> {
152
172
  return results[0] || null;
153
173
  }
154
174
 
175
+ /**
176
+ * Snapshot query: get record as it was at a point in time.
177
+ */
178
+ async asOf(
179
+ id: number | string,
180
+ asOfDate: Date,
181
+ _client?: DBClient
182
+ ): Promise<T | null> {
183
+ if (!this.versioned) throw new StabilizeError("Model is not versioned", "VERSIONING_ERROR");
184
+ const client = _client || this.client;
185
+ const rows = await client.query<T>(
186
+ `SELECT * FROM ${this.historyTable} WHERE id = ? AND valid_from <= ? AND (valid_to IS NULL OR valid_to > ?) ORDER BY version DESC LIMIT 1`,
187
+ [id, asOfDate, asOfDate]
188
+ );
189
+ return rows[0] || null;
190
+ }
191
+
192
+ /**
193
+ * Get all history for a record.
194
+ */
195
+ async history(
196
+ id: number | string,
197
+ _client?: DBClient
198
+ ): Promise<T[]> {
199
+ if (!this.versioned) throw new StabilizeError("Model is not versioned", "VERSIONING_ERROR");
200
+ const client = _client || this.client;
201
+ return client.query<T>(
202
+ `SELECT * FROM ${this.historyTable} WHERE id = ? ORDER BY version ASC`,
203
+ [id]
204
+ );
205
+ }
206
+
207
+
208
+
209
+ /**
210
+ * Rollback a record to a previous version.
211
+ */
212
+ async rollback(
213
+ id: number | string,
214
+ version: number,
215
+ _client?: DBClient
216
+ ): Promise<T> {
217
+ if (!this.versioned) throw new StabilizeError("Model is not versioned", "VERSIONING_ERROR");
218
+ const client = _client || this.client;
219
+ return client.transaction(async (txClient) => {
220
+ const rows = await txClient.query<T>(
221
+ `SELECT * FROM ${this.historyTable} WHERE id = ? AND version = ? LIMIT 1`,
222
+ [id, version]
223
+ );
224
+ if (!rows.length) throw new StabilizeError("Version not found", "ROLLBACK_ERROR");
225
+
226
+ const entity = rows[0];
227
+ const columns = Object.keys(this.columns).filter((c) => c !== "id");
228
+ const setClause = columns.map((c) => `${this.columns[c]!.name} = ?`).join(", ");
229
+ const params = columns.map((c) => (entity as any)[c]);
230
+
231
+ await txClient.query(
232
+ `UPDATE ${this.table} SET ${setClause} WHERE id = ?`,
233
+ [...params, id]
234
+ );
235
+ await this.writeHistory({ ...entity, version: version + 1 }, "update", txClient);
236
+ return this.findOne(id, {}, txClient) as Promise<T>;
237
+ });
238
+ }
239
+
240
+
241
+ /**
242
+ * Writes a versioned history row for the entity to the history table.
243
+ *
244
+ * This method maps entity property keys to their corresponding SQL column names
245
+ * (as defined in metadata) to ensure that inserts match the schema for all supported
246
+ * databases (Postgres, MySQL, SQLite).
247
+ *
248
+ * This function works for Postgres, MySQL, and SQLite, and uses positional parameters
249
+ * (properly formatted for the target database) for safety and compatibility.
250
+ *
251
+ * @param entity - The entity object being versioned
252
+ * @param operation - The operation performed ("insert", "update", "delete")
253
+ * @param client - The database client to use for the insert
254
+ * @param user - The user/system responsible for the change (default: "system")
255
+ */
256
+ private async writeHistory(
257
+ entity: any,
258
+ operation: VersionOperation,
259
+ client: DBClient,
260
+ user?: string
261
+ ) {
262
+ if (!this.versioned) return;
263
+
264
+ // Get property keys and corresponding SQL column names
265
+ const propertyKeys = Object.keys(this.columns);
266
+ const sqlColumnNames = propertyKeys.map((k) => this.columns[k]!.name);
267
+
268
+ // Build historyColumns using SQL column names
269
+ const historyColumns = [
270
+ ...sqlColumnNames,
271
+ "operation",
272
+ "version",
273
+ "valid_from",
274
+ "valid_to",
275
+ "modified_by",
276
+ "modified_at"
277
+ ];
278
+
279
+ // Map values from entity using property keys
280
+ const values = propertyKeys.map((k) => entity[k]);
281
+
282
+ const params = [
283
+ ...values,
284
+ operation,
285
+ entity.version || 1,
286
+ new Date(),
287
+ null,
288
+ user || "system",
289
+ new Date()
290
+ ];
291
+ // Database-agnostic placeholder formatting
292
+ let placeholders: string;
293
+ if (client.config.type === DBType.Postgres) {
294
+ placeholders = params.map((_, i) => `$${i + 1}`).join(", ");
295
+ } else {
296
+ placeholders = params.map(() => "?").join(", ");
297
+ }
298
+
299
+ await client.query(
300
+ `INSERT INTO ${this.historyTable} (${historyColumns.join(", ")}) VALUES (${placeholders})`,
301
+ params
302
+ );
303
+ }
304
+
155
305
  /**
156
306
  * Creates a new record in the database within a transaction.
157
307
  * @param entity The data for the new record.
@@ -166,11 +316,22 @@ export class Repository<T> {
166
316
  entity: Partial<T>,
167
317
  options: { relations?: string[] } = {},
168
318
  ): Promise<T> {
169
- return this.client.transaction((txClient) =>
170
- this._create(entity, options, txClient),
171
- );
172
- }
319
+ return this.client.transaction(async (txClient) => {
320
+ const instance = new (Object.getPrototypeOf(entity).constructor || Object)();
321
+ Object.assign(instance, entity);
322
+
323
+ await this.runHooks(instance, "beforeCreate");
324
+ await this.runHooks(instance, "beforeSave");
325
+
326
+ const result = await this._create(entity, options, txClient);
173
327
 
328
+ await this.runHooks(result, "afterCreate");
329
+ await this.runHooks(result, "afterSave");
330
+
331
+ await this.writeHistory(result, "insert", txClient);
332
+ return result;
333
+ });
334
+ }
174
335
  /**
175
336
  * @internal
176
337
  * The private implementation for creating a record, executed within a transaction.
@@ -227,7 +388,7 @@ export class Repository<T> {
227
388
  );
228
389
  return result;
229
390
  }
230
-
391
+
231
392
  /**
232
393
  * Creates multiple records in the database in batches.
233
394
  * @param entities An array of entities to create.
@@ -245,9 +406,30 @@ export class Repository<T> {
245
406
  entities: Partial<T>[],
246
407
  options: { relations?: string[]; batchSize?: number } = {},
247
408
  ): Promise<T[]> {
248
- return this.client.transaction((txClient) =>
249
- this._bulkCreate(entities, options, txClient),
250
- );
409
+ return this.client.transaction(async (txClient) => {
410
+ // Prepare entity instances for hooks
411
+ const preparedEntities = entities.map(data => {
412
+ const instance = new (this as any).model();
413
+ Object.assign(instance, data);
414
+ return instance;
415
+ });
416
+
417
+ for (const entity of preparedEntities) {
418
+ await this.runHooks(entity, "beforeCreate");
419
+ await this.runHooks(entity, "beforeSave");
420
+ }
421
+
422
+ const results = await this._bulkCreate(entities, options, txClient);
423
+
424
+ for (const result of results) {
425
+ await this.runHooks(result, "afterCreate");
426
+ await this.runHooks(result, "afterSave");
427
+ if (this.versioned) {
428
+ await this.writeHistory(result, "insert", txClient);
429
+ }
430
+ }
431
+ return results;
432
+ });
251
433
  }
252
434
 
253
435
  /**
@@ -275,47 +457,72 @@ export class Repository<T> {
275
457
  const batch = entities.slice(i, i + batchSize);
276
458
  const keys = Object.keys(batch[0]!).filter((k) => this.columns[k]);
277
459
  const columnNames = keys.map((k) => this.columns[k]?.name).join(", ");
278
- const placeholders = `(${keys.map(() => "?").join(", ")})`;
279
- let query = `INSERT INTO ${this.table} (${columnNames}) VALUES ${batch.map(() => placeholders).join(", ")}`;
280
- const params = batch.flatMap((entity) =>
460
+
461
+ let query: string;
462
+ let params: any[] = batch.flatMap((entity) =>
281
463
  keys.map((k) => (entity as any)[k]),
282
464
  );
283
465
 
284
- let batchResults: T[] = [];
285
- let ids: (number | string)[] = [];
286
-
287
466
  if (dbType === DBType.Postgres) {
288
- query += " RETURNING *";
289
- batchResults = await client.query<T>(query, params);
290
- ids = batchResults.map((r) => (r as any).id);
467
+ // PostgreSQL: numbered placeholders ($1, $2, ...)
468
+ let paramIdx = 1;
469
+ const valuePlaceholders = batch
470
+ .map(
471
+ () =>
472
+ `(${keys.map(() => `$${paramIdx++}`).join(", ")})`
473
+ )
474
+ .join(", ");
475
+ query = `INSERT INTO ${this.table} (${columnNames}) VALUES ${valuePlaceholders} RETURNING *`;
476
+ const batchResults = await client.query<T>(query, params);
477
+ const ids = batchResults.map((r) => (r as any).id);
478
+
479
+ // Handle relation loading if needed
480
+ let finalResults = batchResults;
481
+ if (ids.length > 0 && batchResults.length === 0) {
482
+ const queryBuilder = this.find().where(
483
+ `id IN (${ids.map(() => "?").join(", ")})`,
484
+ ...ids,
485
+ );
486
+ if (options.relations) {
487
+ for (const rel of options.relations) {
488
+ await this.loadRelation(queryBuilder, rel);
489
+ }
490
+ }
491
+ finalResults = await queryBuilder.execute(client);
492
+ }
493
+ results.push(...finalResults);
494
+
291
495
  } else {
496
+ // SQLite/MySQL: ? placeholders
497
+ const placeholders = `(${keys.map(() => "?").join(", ")})`;
498
+ query = `INSERT INTO ${this.table} (${columnNames}) VALUES ${batch.map(() => placeholders).join(", ")}`;
292
499
  await client.query(query, params);
293
- ids = (
500
+ const ids = (
294
501
  await client.query<{ id: number }>(
295
502
  `SELECT id FROM ${this.table} ORDER BY id DESC LIMIT ?`,
296
503
  [batch.length],
297
504
  )
298
505
  ).map((row) => row.id);
299
- }
300
506
 
301
- if (ids.length > 0 && batchResults.length === 0) {
302
- const queryBuilder = this.find().where(
303
- `id IN (${ids.map(() => "?").join(", ")})`,
304
- ...ids,
305
- );
306
- if (options.relations) {
307
- for (const rel of options.relations) {
308
- await this.loadRelation(queryBuilder, rel);
507
+ let batchResults: T[] = [];
508
+ if (ids.length > 0) {
509
+ const queryBuilder = this.find().where(
510
+ `id IN (${ids.map(() => "?").join(", ")})`,
511
+ ...ids,
512
+ );
513
+ if (options.relations) {
514
+ for (const rel of options.relations) {
515
+ await this.loadRelation(queryBuilder, rel);
516
+ }
309
517
  }
518
+ batchResults = await queryBuilder.execute(client);
310
519
  }
311
- batchResults = await queryBuilder.execute(client);
520
+ results.push(...batchResults);
312
521
  }
313
-
314
- results.push(...batchResults);
315
522
  }
316
-
523
+
317
524
  if (this.cache) await this.cache.invalidatePattern(`find:${this.table}:*`);
318
-
525
+
319
526
  this.logger.logDebug(
320
527
  `Bulk created ${results.length} ${this.table} entities in ${(performance.now() - start).toFixed(2)}ms`,
321
528
  );
@@ -333,9 +540,27 @@ export class Repository<T> {
333
540
  * ```
334
541
  */
335
542
  async update(id: number | string, entity: Partial<T>): Promise<T> {
336
- return this.client.transaction((txClient) =>
337
- this._update(id, entity, txClient),
338
- );
543
+ return this.client.transaction(async (txClient) => {
544
+ const before = await this.findOne(id, {}, txClient);
545
+ if (!before) throw new StabilizeError("Not found", "UPDATE_ERROR");
546
+ const instance = new (Object.getPrototypeOf(before).constructor || Object)();
547
+ Object.assign(instance, before, entity);
548
+
549
+ await this.runHooks(instance, "beforeUpdate");
550
+ await this.runHooks(instance, "beforeSave");
551
+
552
+ const result = await this._update(id, entity, txClient);
553
+
554
+ await this.runHooks(result, "afterUpdate");
555
+ await this.runHooks(result, "afterSave");
556
+
557
+ await this.writeHistory(
558
+ { ...before, ...entity, version: (before as any).version ? (before as any).version + 1 : 1 },
559
+ "update",
560
+ txClient
561
+ );
562
+ return result;
563
+ });
339
564
  }
340
565
 
341
566
  /**
@@ -417,14 +642,48 @@ export class Repository<T> {
417
642
  for (let i = 0; i < updates.length; i += batchSize) {
418
643
  const batch = updates.slice(i, i + batchSize);
419
644
  for (const update of batch) {
420
- const keys = Object.keys(update.set).filter((k) => this.columns[k]);
421
- const setClause = keys.map((k) => `${this.columns[k]?.name} = ?`).join(", ");
422
- const query = `UPDATE ${this.table} SET ${setClause} WHERE ${update.where.condition}${this.softDeleteField ? ` AND ${this.softDeleteField} IS NULL` : ""}`;
423
- const params = [
424
- ...keys.map((k) => (update.set as any)[k]),
425
- ...update.where.params,
426
- ];
427
- await client.query(query, params);
645
+ // Find all IDs matching the where clause
646
+ const rows = await client.query<{ id: number | string }>(
647
+ `SELECT id FROM ${this.table} WHERE ${update.where.condition}${this.softDeleteField ? ` AND ${this.softDeleteField} IS NULL` : ""}`,
648
+ update.where.params,
649
+ );
650
+ for (const { id } of rows) {
651
+ // Fetch record before update for versioning
652
+ const before = await this.findOne(id, {}, client);
653
+ if (!before) continue;
654
+
655
+ // Prepare instance for hooks
656
+ const instance = new ((this as any).model || Object)();
657
+ Object.assign(instance, before, update.set);
658
+
659
+ await this.runHooks(instance, "beforeUpdate");
660
+ await this.runHooks(instance, "beforeSave");
661
+
662
+ const keys = Object.keys(update.set).filter((k) => this.columns[k]);
663
+ const setClause = keys.map((k) => `${this.columns[k]?.name} = ?`).join(", ");
664
+ const query = `UPDATE ${this.table} SET ${setClause} WHERE id = ?${this.softDeleteField ? ` AND ${this.softDeleteField} IS NULL` : ""}`;
665
+ const params = [
666
+ ...keys.map((k) => (update.set as any)[k]),
667
+ id,
668
+ ];
669
+ await client.query(query, params);
670
+
671
+ const after = await this.findOne(id, {}, client);
672
+ if (after) {
673
+ await this.runHooks(after, "afterUpdate");
674
+ await this.runHooks(after, "afterSave");
675
+ if (this.versioned) {
676
+ await this.writeHistory(
677
+ {
678
+ ...after,
679
+ version: (before as any).version ? (before as any).version + 1 : 1
680
+ },
681
+ "update",
682
+ client
683
+ );
684
+ }
685
+ }
686
+ }
428
687
  }
429
688
  }
430
689
 
@@ -434,7 +693,6 @@ export class Repository<T> {
434
693
  `Bulk updated ${updates.length} ${this.table} entities in ${(performance.now() - start).toFixed(2)}ms`,
435
694
  );
436
695
  }
437
-
438
696
  /**
439
697
  * Performs an "update or insert" operation based on a set of unique keys.
440
698
  * @param entity The entity to upsert.
@@ -479,6 +737,32 @@ export class Repository<T> {
479
737
  const insertParams = columns.map((k) => (entity as any)[k]);
480
738
  let params = [...insertParams, ...updateParams];
481
739
 
740
+ // Try to find the record before upsert
741
+ let before: T | null = null;
742
+ let isUpdate = false;
743
+ if (this.versioned && keys.length > 0) {
744
+ const whereClause = keys.map((k) => `${this.columns[k]?.name} = ?`).join(" AND ");
745
+ const whereParams = keys.map((k) => (entity as any)[k]);
746
+ const found = await client.query<T>(
747
+ `SELECT * FROM ${this.table} WHERE ${whereClause} LIMIT 1`,
748
+ whereParams
749
+ );
750
+ before = found[0] || null;
751
+ isUpdate = !!before;
752
+ }
753
+
754
+ // Prepare instance for hooks
755
+ const instance = new ((this as any).model || Object)();
756
+ Object.assign(instance, before || {}, entity);
757
+
758
+ if (isUpdate) {
759
+ await this.runHooks(instance, "beforeUpdate");
760
+ await this.runHooks(instance, "beforeSave");
761
+ } else {
762
+ await this.runHooks(instance, "beforeCreate");
763
+ await this.runHooks(instance, "beforeSave");
764
+ }
765
+
482
766
  if (dbType === DBType.SQLite) {
483
767
  query = `INSERT INTO ${this.table} (${columnNames}) VALUES (${placeholders}) ON CONFLICT(${keys.map((k) => this.columns[k]!.name).join(", ")}) DO UPDATE SET ${updateClause}`;
484
768
  } else if (dbType === DBType.MySQL) {
@@ -495,7 +779,7 @@ export class Repository<T> {
495
779
  let id: number | string | undefined = (results[0] as any)?.id || (entity as any).id;
496
780
 
497
781
  if (!id && dbType !== DBType.Postgres) {
498
- if (dbType === DBType.SQLite) {
782
+ if (dbType === DBType.SQLite) {
499
783
  id = (await client.query<{ id: number }>("SELECT last_insert_rowid() as id"))[0]?.id;
500
784
  } else if (dbType === DBType.MySQL) {
501
785
  const result = await client.query<{ "LAST_INSERT_ID()": number }>("SELECT LAST_INSERT_ID()");
@@ -507,19 +791,34 @@ export class Repository<T> {
507
791
 
508
792
  const result = results[0] ?? ((await this.findOne(id, {}, client)) as T);
509
793
 
794
+ if (isUpdate) {
795
+ await this.runHooks(result, "afterUpdate");
796
+ await this.runHooks(result, "afterSave");
797
+ } else {
798
+ await this.runHooks(result, "afterCreate");
799
+ await this.runHooks(result, "afterSave");
800
+ }
801
+
510
802
  if (this.cache) {
511
803
  await this.cache.invalidatePattern(`find:${this.table}:*`);
512
804
  if (this.cache.getStrategy() === "write-through") {
513
805
  await this.cache.set(`findOne:${this.table}:${id}`, [result], 60);
514
806
  }
515
807
  }
516
-
808
+
809
+ if (this.versioned) {
810
+ await this.writeHistory(
811
+ { ...result, version: before ? ((before as any).version ? (before as any).version + 1 : 1) : 1 },
812
+ before ? "update" : "insert",
813
+ client
814
+ );
815
+ }
816
+
517
817
  this.logger.logDebug(
518
818
  `Upserted ${this.table} with ID ${id} in ${(performance.now() - start).toFixed(2)}ms`,
519
819
  );
520
820
  return result;
521
821
  }
522
-
523
822
  /**
524
823
  * Deletes a record by its ID. Performs a soft delete if enabled on the model.
525
824
  * @param id The ID of the record to delete.
@@ -530,7 +829,16 @@ export class Repository<T> {
530
829
  * ```
531
830
  */
532
831
  async delete(id: number | string): Promise<void> {
533
- return this.client.transaction((txClient) => this._delete(id, txClient));
832
+ return this.client.transaction(async (txClient) => {
833
+ const before = await this.findOne(id, {}, txClient);
834
+ if (!before) throw new StabilizeError("Not found", "DELETE_ERROR");
835
+ await this.runHooks(before, "beforeDelete");
836
+
837
+ await this._delete(id, txClient);
838
+
839
+ await this.runHooks(before, "afterDelete");
840
+ await this.writeHistory(before, "delete", txClient);
841
+ });
534
842
  }
535
843
 
536
844
  /**
@@ -545,7 +853,7 @@ export class Repository<T> {
545
853
  ? `UPDATE ${this.table} SET ${this.softDeleteField} = ? WHERE id = ?`
546
854
  : `DELETE FROM ${this.table} WHERE id = ?`;
547
855
  const params = this.softDeleteField ? [new Date().toISOString(), id] : [id];
548
-
856
+
549
857
  await client.query(query, params);
550
858
 
551
859
  if (this.cache) {
@@ -594,14 +902,25 @@ export class Repository<T> {
594
902
  const batchSize = options.batchSize || 1000;
595
903
  for (let i = 0; i < ids.length; i += batchSize) {
596
904
  const batch = ids.slice(i, i + batchSize);
597
- const placeholders = batch.map(() => "?").join(", ");
598
- const query = this.softDeleteField
599
- ? `UPDATE ${this.table} SET ${this.softDeleteField} = ? WHERE id IN (${placeholders})`
600
- : `DELETE FROM ${this.table} WHERE id IN (${placeholders})`;
601
- const params = this.softDeleteField
602
- ? [new Date().toISOString(), ...batch]
603
- : batch;
604
- await client.query(query, params);
905
+ for (const id of batch) {
906
+ const before = await this.findOne(id, {}, client);
907
+ if (!before) continue;
908
+
909
+ await this.runHooks(before, "beforeDelete");
910
+
911
+ const query = this.softDeleteField
912
+ ? `UPDATE ${this.table} SET ${this.softDeleteField} = ? WHERE id = ?`
913
+ : `DELETE FROM ${this.table} WHERE id = ?`;
914
+ const params = this.softDeleteField ? [new Date().toISOString(), id] : [id];
915
+
916
+ await client.query(query, params);
917
+
918
+ await this.runHooks(before, "afterDelete");
919
+
920
+ if (this.versioned) {
921
+ await this.writeHistory(before, "delete", client);
922
+ }
923
+ }
605
924
  }
606
925
 
607
926
  if (this.cache) await this.cache.invalidatePattern(`find:${this.table}:*`);
@@ -610,7 +929,6 @@ export class Repository<T> {
610
929
  `Bulk deleted ${ids.length} ${this.table} entities in ${(performance.now() - start).toFixed(2)}ms`,
611
930
  );
612
931
  }
613
-
614
932
  /**
615
933
  * Recovers a soft-deleted record by its ID.
616
934
  * Throws an error if soft delete is not enabled on the model.
@@ -638,7 +956,7 @@ export class Repository<T> {
638
956
  "RECOVER_ERROR",
639
957
  );
640
958
  }
641
-
959
+
642
960
  await client.query(
643
961
  `UPDATE ${this.table} SET ${this.softDeleteField} = NULL WHERE id = ?`,
644
962
  [id],
@@ -653,7 +971,7 @@ export class Repository<T> {
653
971
  `findOne:${this.table}:${id}`,
654
972
  ]);
655
973
  }
656
-
974
+
657
975
  this.logger.logDebug(
658
976
  `Recovered ${this.table} with ID ${id} in ${(performance.now() - start).toFixed(2)}ms`,
659
977
  );