stabilize-orm 1.1.8 → 1.3.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/CHANGELOG.md +33 -5
- package/README.md +253 -143
- package/bun.lock +0 -3
- package/client.ts +7 -82
- package/hooks.ts +76 -0
- package/index.ts +8 -36
- package/migrations.ts +64 -74
- package/model.ts +115 -0
- package/package.json +3 -9
- package/repository.ts +330 -47
- package/decorators.ts +0 -165
package/package.json
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stabilize-orm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
-
"bin": {
|
|
8
|
-
"stabilize": "dist/cli/stabilize-cli.js"
|
|
9
|
-
},
|
|
10
7
|
"scripts": {
|
|
11
|
-
"build": "bun build src/index.ts --outdir dist --target bun --minify
|
|
8
|
+
"build": "bun build src/index.ts --outdir dist --target bun --minify",
|
|
12
9
|
"prepublishOnly": "bun run build && bun run test",
|
|
13
|
-
"
|
|
14
|
-
"format": "bunx prettier --write .",
|
|
15
|
-
"lint": "eslint src tests cli examples",
|
|
16
|
-
"cli": "bun run cli/stabilize-cli.ts"
|
|
10
|
+
"format": "bunx prettier --write ."
|
|
17
11
|
},
|
|
18
12
|
"keywords": [
|
|
19
13
|
"orm",
|
package/repository.ts
CHANGED
|
@@ -9,18 +9,16 @@ import { DBClient } from "./client";
|
|
|
9
9
|
import { ConsoleLogger, type Logger } from "./logger";
|
|
10
10
|
import { QueryBuilder } from "./query-builder";
|
|
11
11
|
import {
|
|
12
|
+
DataTypes,
|
|
12
13
|
DBType,
|
|
13
14
|
RelationType,
|
|
14
15
|
StabilizeError,
|
|
15
16
|
type CacheConfig,
|
|
16
17
|
} from "./types";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
RelationKey,
|
|
22
|
-
SoftDeleteKey,
|
|
23
|
-
} from "./decorators";
|
|
18
|
+
import { MetadataStorage } from "./model";
|
|
19
|
+
import { getHooks, type HookType } from "./hooks";
|
|
20
|
+
|
|
21
|
+
type VersionOperation = "insert" | "update" | "delete";
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
24
|
* Provides a generic repository for a model `T`.
|
|
@@ -46,11 +44,13 @@ export class Repository<T> {
|
|
|
46
44
|
>;
|
|
47
45
|
private softDeleteField: string | null;
|
|
48
46
|
private logger: Logger;
|
|
47
|
+
private versioned: boolean;
|
|
48
|
+
private historyTable: string;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Creates an instance of Repository.
|
|
52
52
|
* @param client The database client instance for executing queries.
|
|
53
|
-
* @param model The model class constructor,
|
|
53
|
+
* @param model The model class constructor, defined with `defineModel`.
|
|
54
54
|
* @param cacheConfig Optional configuration for caching.
|
|
55
55
|
* @param logger A logger instance for logging messages.
|
|
56
56
|
*/
|
|
@@ -62,13 +62,31 @@ export class Repository<T> {
|
|
|
62
62
|
) {
|
|
63
63
|
this.client = client;
|
|
64
64
|
this.cache = cacheConfig.enabled ? new Cache(cacheConfig, logger) : null;
|
|
65
|
-
this.table =
|
|
66
|
-
this.columns =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
this.table = MetadataStorage.getTableName(model);
|
|
66
|
+
this.columns = Object.fromEntries(
|
|
67
|
+
Object.entries(MetadataStorage.getColumns(model)).map(([key, col]) => [
|
|
68
|
+
key,
|
|
69
|
+
{ name: col.name ?? key, type: typeof col.type === 'string' ? col.type : DataTypes[col.type] },
|
|
70
|
+
])
|
|
71
|
+
);
|
|
72
|
+
this.relations = Object.fromEntries(
|
|
73
|
+
Object.entries(MetadataStorage.getRelations(model)).map(([key, rel]) => [
|
|
74
|
+
key,
|
|
75
|
+
{
|
|
76
|
+
type: rel.type,
|
|
77
|
+
targetModel: rel.target,
|
|
78
|
+
foreignKey: rel.foreignKey,
|
|
79
|
+
inverseKey: rel.inverseKey,
|
|
80
|
+
joinTable: rel.joinTable,
|
|
81
|
+
},
|
|
82
|
+
])
|
|
83
|
+
);
|
|
84
|
+
this.validators = MetadataStorage.getValidators(model);
|
|
85
|
+
|
|
86
|
+
this.softDeleteField = MetadataStorage.getSoftDeleteField(model);
|
|
71
87
|
this.logger = logger;
|
|
88
|
+
this.versioned = MetadataStorage.isVersioned(model);
|
|
89
|
+
this.historyTable = `${this.table}_history`;
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
/**
|
|
@@ -84,7 +102,7 @@ export class Repository<T> {
|
|
|
84
102
|
|
|
85
103
|
/**
|
|
86
104
|
* @internal
|
|
87
|
-
* Validates an entity against the 'required' constraints defined in
|
|
105
|
+
* Validates an entity against the 'required' constraints defined in the model configuration.
|
|
88
106
|
* @param entity The partial entity to validate.
|
|
89
107
|
*/
|
|
90
108
|
private validate(entity: Partial<T>) {
|
|
@@ -102,6 +120,17 @@ export class Repository<T> {
|
|
|
102
120
|
}
|
|
103
121
|
}
|
|
104
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Runs lifecycle hooks of a given type for the entity.
|
|
125
|
+
* @param entity The entity instance.
|
|
126
|
+
* @param type The hook type (e.g., 'beforeCreate').
|
|
127
|
+
*/
|
|
128
|
+
private async runHooks(entity: any, type: HookType): Promise<void> {
|
|
129
|
+
for (const hook of getHooks(entity, type)) {
|
|
130
|
+
await hook.callback(entity);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
105
134
|
/**
|
|
106
135
|
* Creates a new `QueryBuilder` instance for the repository's table.
|
|
107
136
|
* Automatically adds a `WHERE` clause to exclude soft-deleted records if applicable.
|
|
@@ -152,6 +181,121 @@ export class Repository<T> {
|
|
|
152
181
|
return results[0] || null;
|
|
153
182
|
}
|
|
154
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Snapshot query: get record as it was at a point in time.
|
|
186
|
+
*/
|
|
187
|
+
async asOf(
|
|
188
|
+
id: number | string,
|
|
189
|
+
asOfDate: Date,
|
|
190
|
+
_client?: DBClient
|
|
191
|
+
): Promise<T | null> {
|
|
192
|
+
if (!this.versioned) throw new StabilizeError("Model is not versioned", "VERSIONING_ERROR");
|
|
193
|
+
const client = _client || this.client;
|
|
194
|
+
const rows = await client.query<T>(
|
|
195
|
+
`SELECT * FROM ${this.historyTable} WHERE id = ? AND valid_from <= ? AND (valid_to IS NULL OR valid_to > ?) ORDER BY version DESC LIMIT 1`,
|
|
196
|
+
[id, asOfDate, asOfDate]
|
|
197
|
+
);
|
|
198
|
+
return rows[0] || null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get all history for a record.
|
|
203
|
+
*/
|
|
204
|
+
async history(
|
|
205
|
+
id: number | string,
|
|
206
|
+
_client?: DBClient
|
|
207
|
+
): Promise<T[]> {
|
|
208
|
+
if (!this.versioned) throw new StabilizeError("Model is not versioned", "VERSIONING_ERROR");
|
|
209
|
+
const client = _client || this.client;
|
|
210
|
+
return client.query<T>(
|
|
211
|
+
`SELECT * FROM ${this.historyTable} WHERE id = ? ORDER BY version ASC`,
|
|
212
|
+
[id]
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Rollback a record to a previous version.
|
|
218
|
+
*/
|
|
219
|
+
async rollback(
|
|
220
|
+
id: number | string,
|
|
221
|
+
version: number,
|
|
222
|
+
_client?: DBClient
|
|
223
|
+
): Promise<T> {
|
|
224
|
+
if (!this.versioned) throw new StabilizeError("Model is not versioned", "VERSIONING_ERROR");
|
|
225
|
+
const client = _client || this.client;
|
|
226
|
+
return client.transaction(async (txClient) => {
|
|
227
|
+
const rows = await txClient.query<T>(
|
|
228
|
+
`SELECT * FROM ${this.historyTable} WHERE id = ? AND version = ? LIMIT 1`,
|
|
229
|
+
[id, version]
|
|
230
|
+
);
|
|
231
|
+
if (!rows.length) throw new StabilizeError("Version not found", "ROLLBACK_ERROR");
|
|
232
|
+
|
|
233
|
+
const entity = rows[0];
|
|
234
|
+
const columns = Object.keys(this.columns).filter((c) => c !== "id");
|
|
235
|
+
const setClause = columns.map((c) => `${this.columns[c]!.name} = ?`).join(", ");
|
|
236
|
+
const params = columns.map((c) => (entity as any)[c]);
|
|
237
|
+
|
|
238
|
+
await txClient.query(
|
|
239
|
+
`UPDATE ${this.table} SET ${setClause} WHERE id = ?`,
|
|
240
|
+
[...params, id]
|
|
241
|
+
);
|
|
242
|
+
await this.writeHistory({ ...entity, version: version + 1 }, "update", txClient);
|
|
243
|
+
return this.findOne(id, {}, txClient) as Promise<T>;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Writes a versioned history row for the entity to the history table.
|
|
249
|
+
* @param entity The entity object being versioned
|
|
250
|
+
* @param operation The operation performed ("insert", "update", "delete")
|
|
251
|
+
* @param client The database client to use for the insert
|
|
252
|
+
* @param user The user/system responsible for the change (default: "system")
|
|
253
|
+
*/
|
|
254
|
+
private async writeHistory(
|
|
255
|
+
entity: any,
|
|
256
|
+
operation: VersionOperation,
|
|
257
|
+
client: DBClient,
|
|
258
|
+
user?: string
|
|
259
|
+
) {
|
|
260
|
+
if (!this.versioned) return;
|
|
261
|
+
|
|
262
|
+
const propertyKeys = Object.keys(this.columns);
|
|
263
|
+
const sqlColumnNames = propertyKeys.map((k) => this.columns[k]!.name);
|
|
264
|
+
|
|
265
|
+
const historyColumns = [
|
|
266
|
+
...sqlColumnNames,
|
|
267
|
+
"operation",
|
|
268
|
+
"version",
|
|
269
|
+
"valid_from",
|
|
270
|
+
"valid_to",
|
|
271
|
+
"modified_by",
|
|
272
|
+
"modified_at"
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
const values = propertyKeys.map((k) => entity[k]);
|
|
276
|
+
const params = [
|
|
277
|
+
...values,
|
|
278
|
+
operation,
|
|
279
|
+
entity.version || 1,
|
|
280
|
+
new Date(),
|
|
281
|
+
null,
|
|
282
|
+
user || "system",
|
|
283
|
+
new Date()
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
let placeholders: string;
|
|
287
|
+
if (client.config.type === DBType.Postgres) {
|
|
288
|
+
placeholders = params.map((_, i) => `$${i + 1}`).join(", ");
|
|
289
|
+
} else {
|
|
290
|
+
placeholders = params.map(() => "?").join(", ");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await client.query(
|
|
294
|
+
`INSERT INTO ${this.historyTable} (${historyColumns.join(", ")}) VALUES (${placeholders})`,
|
|
295
|
+
params
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
155
299
|
/**
|
|
156
300
|
* Creates a new record in the database within a transaction.
|
|
157
301
|
* @param entity The data for the new record.
|
|
@@ -166,9 +310,21 @@ export class Repository<T> {
|
|
|
166
310
|
entity: Partial<T>,
|
|
167
311
|
options: { relations?: string[] } = {},
|
|
168
312
|
): Promise<T> {
|
|
169
|
-
return this.client.transaction((txClient) =>
|
|
170
|
-
|
|
171
|
-
|
|
313
|
+
return this.client.transaction(async (txClient) => {
|
|
314
|
+
const instance = new (Object.getPrototypeOf(entity).constructor || Object)();
|
|
315
|
+
Object.assign(instance, entity);
|
|
316
|
+
|
|
317
|
+
await this.runHooks(instance, "beforeCreate");
|
|
318
|
+
await this.runHooks(instance, "beforeSave");
|
|
319
|
+
|
|
320
|
+
const result = await this._create(entity, options, txClient);
|
|
321
|
+
|
|
322
|
+
await this.runHooks(result, "afterCreate");
|
|
323
|
+
await this.runHooks(result, "afterSave");
|
|
324
|
+
|
|
325
|
+
await this.writeHistory(result, "insert", txClient);
|
|
326
|
+
return result;
|
|
327
|
+
});
|
|
172
328
|
}
|
|
173
329
|
|
|
174
330
|
/**
|
|
@@ -245,9 +401,29 @@ export class Repository<T> {
|
|
|
245
401
|
entities: Partial<T>[],
|
|
246
402
|
options: { relations?: string[]; batchSize?: number } = {},
|
|
247
403
|
): Promise<T[]> {
|
|
248
|
-
return this.client.transaction((txClient) =>
|
|
249
|
-
|
|
250
|
-
|
|
404
|
+
return this.client.transaction(async (txClient) => {
|
|
405
|
+
const preparedEntities = entities.map(data => {
|
|
406
|
+
const instance = new (this as any).model();
|
|
407
|
+
Object.assign(instance, data);
|
|
408
|
+
return instance;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
for (const entity of preparedEntities) {
|
|
412
|
+
await this.runHooks(entity, "beforeCreate");
|
|
413
|
+
await this.runHooks(entity, "beforeSave");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const results = await this._bulkCreate(entities, options, txClient);
|
|
417
|
+
|
|
418
|
+
for (const result of results) {
|
|
419
|
+
await this.runHooks(result, "afterCreate");
|
|
420
|
+
await this.runHooks(result, "afterSave");
|
|
421
|
+
if (this.versioned) {
|
|
422
|
+
await this.writeHistory(result, "insert", txClient);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return results;
|
|
426
|
+
});
|
|
251
427
|
}
|
|
252
428
|
|
|
253
429
|
/**
|
|
@@ -282,7 +458,6 @@ export class Repository<T> {
|
|
|
282
458
|
);
|
|
283
459
|
|
|
284
460
|
if (dbType === DBType.Postgres) {
|
|
285
|
-
// PostgreSQL: numbered placeholders ($1, $2, ...)
|
|
286
461
|
let paramIdx = 1;
|
|
287
462
|
const valuePlaceholders = batch
|
|
288
463
|
.map(
|
|
@@ -294,7 +469,6 @@ export class Repository<T> {
|
|
|
294
469
|
const batchResults = await client.query<T>(query, params);
|
|
295
470
|
const ids = batchResults.map((r) => (r as any).id);
|
|
296
471
|
|
|
297
|
-
// Handle relation loading if needed
|
|
298
472
|
let finalResults = batchResults;
|
|
299
473
|
if (ids.length > 0 && batchResults.length === 0) {
|
|
300
474
|
const queryBuilder = this.find().where(
|
|
@@ -311,7 +485,6 @@ export class Repository<T> {
|
|
|
311
485
|
results.push(...finalResults);
|
|
312
486
|
|
|
313
487
|
} else {
|
|
314
|
-
// SQLite/MySQL: ? placeholders
|
|
315
488
|
const placeholders = `(${keys.map(() => "?").join(", ")})`;
|
|
316
489
|
query = `INSERT INTO ${this.table} (${columnNames}) VALUES ${batch.map(() => placeholders).join(", ")}`;
|
|
317
490
|
await client.query(query, params);
|
|
@@ -358,9 +531,27 @@ export class Repository<T> {
|
|
|
358
531
|
* ```
|
|
359
532
|
*/
|
|
360
533
|
async update(id: number | string, entity: Partial<T>): Promise<T> {
|
|
361
|
-
return this.client.transaction((txClient) =>
|
|
362
|
-
this.
|
|
363
|
-
|
|
534
|
+
return this.client.transaction(async (txClient) => {
|
|
535
|
+
const before = await this.findOne(id, {}, txClient);
|
|
536
|
+
if (!before) throw new StabilizeError("Not found", "UPDATE_ERROR");
|
|
537
|
+
const instance = new (Object.getPrototypeOf(before).constructor || Object)();
|
|
538
|
+
Object.assign(instance, before, entity);
|
|
539
|
+
|
|
540
|
+
await this.runHooks(instance, "beforeUpdate");
|
|
541
|
+
await this.runHooks(instance, "beforeSave");
|
|
542
|
+
|
|
543
|
+
const result = await this._update(id, entity, txClient);
|
|
544
|
+
|
|
545
|
+
await this.runHooks(result, "afterUpdate");
|
|
546
|
+
await this.runHooks(result, "afterSave");
|
|
547
|
+
|
|
548
|
+
await this.writeHistory(
|
|
549
|
+
{ ...before, ...entity, version: (before as any).version ? (before as any).version + 1 : 1 },
|
|
550
|
+
"update",
|
|
551
|
+
txClient
|
|
552
|
+
);
|
|
553
|
+
return result;
|
|
554
|
+
});
|
|
364
555
|
}
|
|
365
556
|
|
|
366
557
|
/**
|
|
@@ -442,14 +633,45 @@ export class Repository<T> {
|
|
|
442
633
|
for (let i = 0; i < updates.length; i += batchSize) {
|
|
443
634
|
const batch = updates.slice(i, i + batchSize);
|
|
444
635
|
for (const update of batch) {
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
636
|
+
const rows = await client.query<{ id: number | string }>(
|
|
637
|
+
`SELECT id FROM ${this.table} WHERE ${update.where.condition}${this.softDeleteField ? ` AND ${this.softDeleteField} IS NULL` : ""}`,
|
|
638
|
+
update.where.params,
|
|
639
|
+
);
|
|
640
|
+
for (const { id } of rows) {
|
|
641
|
+
const before = await this.findOne(id, {}, client);
|
|
642
|
+
if (!before) continue;
|
|
643
|
+
|
|
644
|
+
const instance = new ((this as any).model || Object)();
|
|
645
|
+
Object.assign(instance, before, update.set);
|
|
646
|
+
|
|
647
|
+
await this.runHooks(instance, "beforeUpdate");
|
|
648
|
+
await this.runHooks(instance, "beforeSave");
|
|
649
|
+
|
|
650
|
+
const keys = Object.keys(update.set).filter((k) => this.columns[k]);
|
|
651
|
+
const setClause = keys.map((k) => `${this.columns[k]?.name} = ?`).join(", ");
|
|
652
|
+
const query = `UPDATE ${this.table} SET ${setClause} WHERE id = ?${this.softDeleteField ? ` AND ${this.softDeleteField} IS NULL` : ""}`;
|
|
653
|
+
const params = [
|
|
654
|
+
...keys.map((k) => (update.set as any)[k]),
|
|
655
|
+
id,
|
|
656
|
+
];
|
|
657
|
+
await client.query(query, params);
|
|
658
|
+
|
|
659
|
+
const after = await this.findOne(id, {}, client);
|
|
660
|
+
if (after) {
|
|
661
|
+
await this.runHooks(after, "afterUpdate");
|
|
662
|
+
await this.runHooks(after, "afterSave");
|
|
663
|
+
if (this.versioned) {
|
|
664
|
+
await this.writeHistory(
|
|
665
|
+
{
|
|
666
|
+
...after,
|
|
667
|
+
version: (before as any).version ? (before as any).version + 1 : 1
|
|
668
|
+
},
|
|
669
|
+
"update",
|
|
670
|
+
client
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
453
675
|
}
|
|
454
676
|
}
|
|
455
677
|
|
|
@@ -495,6 +717,7 @@ export class Repository<T> {
|
|
|
495
717
|
const columns = Object.keys(entity).filter((k) => this.columns[k]);
|
|
496
718
|
const columnNames = columns.map((k) => this.columns[k]?.name).join(", ");
|
|
497
719
|
const placeholders = columns.map(() => "?").join(", ");
|
|
720
|
+
|
|
498
721
|
const updateClause = columns
|
|
499
722
|
.filter((c) => !keys.includes(c))
|
|
500
723
|
.map((c) => `${this.columns[c]?.name} = ?`).join(", ");
|
|
@@ -504,11 +727,35 @@ export class Repository<T> {
|
|
|
504
727
|
const insertParams = columns.map((k) => (entity as any)[k]);
|
|
505
728
|
let params = [...insertParams, ...updateParams];
|
|
506
729
|
|
|
730
|
+
let before: T | null = null;
|
|
731
|
+
let isUpdate = false;
|
|
732
|
+
if (this.versioned && keys.length > 0) {
|
|
733
|
+
const whereClause = keys.map((k) => `${this.columns[k]?.name} = ?`).join(" AND ");
|
|
734
|
+
const whereParams = keys.map((k) => (entity as any)[k]);
|
|
735
|
+
const found = await client.query<T>(
|
|
736
|
+
`SELECT * FROM ${this.table} WHERE ${whereClause} LIMIT 1`,
|
|
737
|
+
whereParams
|
|
738
|
+
);
|
|
739
|
+
before = found[0] || null;
|
|
740
|
+
isUpdate = !!before;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const instance = new ((this as any).model || Object)();
|
|
744
|
+
Object.assign(instance, before || {}, entity);
|
|
745
|
+
|
|
746
|
+
if (isUpdate) {
|
|
747
|
+
await this.runHooks(instance, "beforeUpdate");
|
|
748
|
+
await this.runHooks(instance, "beforeSave");
|
|
749
|
+
} else {
|
|
750
|
+
await this.runHooks(instance, "beforeCreate");
|
|
751
|
+
await this.runHooks(instance, "beforeSave");
|
|
752
|
+
}
|
|
753
|
+
|
|
507
754
|
if (dbType === DBType.SQLite) {
|
|
508
755
|
query = `INSERT INTO ${this.table} (${columnNames}) VALUES (${placeholders}) ON CONFLICT(${keys.map((k) => this.columns[k]!.name).join(", ")}) DO UPDATE SET ${updateClause}`;
|
|
509
756
|
} else if (dbType === DBType.MySQL) {
|
|
510
757
|
query = `INSERT INTO ${this.table} (${columnNames}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updateClause}`;
|
|
511
|
-
} else {
|
|
758
|
+
} else {
|
|
512
759
|
const pgUpdateClause = columns
|
|
513
760
|
.filter((c) => !keys.includes(c))
|
|
514
761
|
.map((c) => `${this.columns[c]?.name} = EXCLUDED.${this.columns[c]?.name}`).join(", ");
|
|
@@ -532,6 +779,22 @@ export class Repository<T> {
|
|
|
532
779
|
|
|
533
780
|
const result = results[0] ?? ((await this.findOne(id, {}, client)) as T);
|
|
534
781
|
|
|
782
|
+
if (isUpdate) {
|
|
783
|
+
await this.runHooks(result, "afterUpdate");
|
|
784
|
+
await this.runHooks(result, "afterSave");
|
|
785
|
+
} else {
|
|
786
|
+
await this.runHooks(result, "afterCreate");
|
|
787
|
+
await this.runHooks(result, "afterSave");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (this.versioned) {
|
|
791
|
+
await this.writeHistory(
|
|
792
|
+
{ ...result, version: before ? ((before as any).version ? (before as any).version + 1 : 1) : 1 },
|
|
793
|
+
before ? "update" : "insert",
|
|
794
|
+
client
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
535
798
|
if (this.cache) {
|
|
536
799
|
await this.cache.invalidatePattern(`find:${this.table}:*`);
|
|
537
800
|
if (this.cache.getStrategy() === "write-through") {
|
|
@@ -555,7 +818,16 @@ export class Repository<T> {
|
|
|
555
818
|
* ```
|
|
556
819
|
*/
|
|
557
820
|
async delete(id: number | string): Promise<void> {
|
|
558
|
-
return this.client.transaction((txClient) =>
|
|
821
|
+
return this.client.transaction(async (txClient) => {
|
|
822
|
+
const before = await this.findOne(id, {}, txClient);
|
|
823
|
+
if (!before) throw new StabilizeError("Not found", "DELETE_ERROR");
|
|
824
|
+
await this.runHooks(before, "beforeDelete");
|
|
825
|
+
|
|
826
|
+
await this._delete(id, txClient);
|
|
827
|
+
|
|
828
|
+
await this.runHooks(before, "afterDelete");
|
|
829
|
+
await this.writeHistory(before, "delete", txClient);
|
|
830
|
+
});
|
|
559
831
|
}
|
|
560
832
|
|
|
561
833
|
/**
|
|
@@ -619,14 +891,25 @@ export class Repository<T> {
|
|
|
619
891
|
const batchSize = options.batchSize || 1000;
|
|
620
892
|
for (let i = 0; i < ids.length; i += batchSize) {
|
|
621
893
|
const batch = ids.slice(i, i + batchSize);
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
894
|
+
for (const id of batch) {
|
|
895
|
+
const before = await this.findOne(id, {}, client);
|
|
896
|
+
if (!before) continue;
|
|
897
|
+
|
|
898
|
+
await this.runHooks(before, "beforeDelete");
|
|
899
|
+
|
|
900
|
+
const query = this.softDeleteField
|
|
901
|
+
? `UPDATE ${this.table} SET ${this.softDeleteField} = ? WHERE id = ?`
|
|
902
|
+
: `DELETE FROM ${this.table} WHERE id = ?`;
|
|
903
|
+
const params = this.softDeleteField ? [new Date().toISOString(), id] : [id];
|
|
904
|
+
|
|
905
|
+
await client.query(query, params);
|
|
906
|
+
|
|
907
|
+
await this.runHooks(before, "afterDelete");
|
|
908
|
+
|
|
909
|
+
if (this.versioned) {
|
|
910
|
+
await this.writeHistory(before, "delete", client);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
630
913
|
}
|
|
631
914
|
|
|
632
915
|
if (this.cache) await this.cache.invalidatePattern(`find:${this.table}:*`);
|
|
@@ -721,7 +1004,7 @@ export class Repository<T> {
|
|
|
721
1004
|
"RELATION_ERROR",
|
|
722
1005
|
);
|
|
723
1006
|
|
|
724
|
-
const relatedTable =
|
|
1007
|
+
const relatedTable = MetadataStorage.getTableName(rel.targetModel());
|
|
725
1008
|
if (
|
|
726
1009
|
rel.type === RelationType.OneToOne ||
|
|
727
1010
|
rel.type === RelationType.ManyToOne
|
|
@@ -747,4 +1030,4 @@ export class Repository<T> {
|
|
|
747
1030
|
);
|
|
748
1031
|
}
|
|
749
1032
|
}
|
|
750
|
-
}
|
|
1033
|
+
}
|