@tstdl/base 0.93.146 → 0.93.147
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.
|
@@ -100,7 +100,18 @@ export type DeleteOptions<_T extends BaseEntity> = {
|
|
|
100
100
|
*/
|
|
101
101
|
export type DeleteManyOptions<T extends BaseEntity> = DeleteOptions<T>;
|
|
102
102
|
/**
|
|
103
|
-
* Options for
|
|
103
|
+
* Options for undelete operations.
|
|
104
|
+
* @template T - The entity type.
|
|
105
|
+
*/
|
|
106
|
+
export type UndeleteOptions<_T extends BaseEntity> = Record<never, never>;
|
|
107
|
+
/**
|
|
108
|
+
* Options for undelete many operations.
|
|
109
|
+
* @template T - The entity type.
|
|
110
|
+
*/
|
|
111
|
+
export type UndeleteManyOptions<T extends BaseEntity> = UndeleteOptions<T>;
|
|
112
|
+
/**
|
|
113
|
+
* Represents a single result from a full-text search operation.
|
|
114
|
+
|
|
104
115
|
*/
|
|
105
116
|
export type RankOptions = {
|
|
106
117
|
/**
|
|
@@ -224,7 +235,7 @@ export type SearchResult<T extends BaseEntity> = {
|
|
|
224
235
|
*/
|
|
225
236
|
export type UpdateOptions<T extends BaseEntity> = LoadOptions<T>;
|
|
226
237
|
/** Type definition for updating entity metadata attributes, allowing partial updates and SQL expressions. */
|
|
227
|
-
export type EntityMetadataUpdate = WithSql<Partial<UntaggedDeep<Pick<EntityMetadata, 'attributes'>>>>;
|
|
238
|
+
export type EntityMetadataUpdate = WithSql<Partial<UntaggedDeep<Pick<EntityMetadata, 'attributes' | 'deleteTimestamp'>>>>;
|
|
228
239
|
export type EntityMetadataInsert = Partial<Pick<EntityMetadata, 'attributes'>>;
|
|
229
240
|
/**
|
|
230
241
|
* Represents the data structure for creating a new entity.
|
|
@@ -5,7 +5,7 @@ import { afterResolve, resolveArgumentType, type Resolvable } from '../../inject
|
|
|
5
5
|
import type { DeepPartial, Function, OneOrMany, Record, SimplifyObject, Type, TypedOmit } from '../../types/index.js';
|
|
6
6
|
import { Entity, type BaseEntity, type EntityMetadataAttributes, type EntityType } from '../entity.js';
|
|
7
7
|
import type { ParadeSearchQuery, Query, TrigramSearchQuery, TsVectorSearchQuery } from '../query/index.js';
|
|
8
|
-
import type { CountOptions, DeleteOptions, EntityMetadataUpdate, EntityUpdate, HasOptions, LoadManyOptions, LoadOptions, NewEntity, Order, SearchOptions, SearchResult, TargetColumn, TargetColumnPath, UpdateOptions } from '../repository.types.js';
|
|
8
|
+
import type { CountOptions, DeleteOptions, EntityMetadataUpdate, EntityUpdate, HasOptions, LoadManyOptions, LoadOptions, NewEntity, Order, SearchOptions, SearchResult, TargetColumn, TargetColumnPath, UndeleteOptions, UpdateOptions } from '../repository.types.js';
|
|
9
9
|
import type { Database } from './database.js';
|
|
10
10
|
import type { PgTransaction } from './transaction.js';
|
|
11
11
|
import { Transactional } from './transactional.js';
|
|
@@ -279,16 +279,18 @@ export declare class EntityRepository<T extends BaseEntity = BaseEntity> extends
|
|
|
279
279
|
* Updates multiple entities by their IDs.
|
|
280
280
|
* @param ids An array of entity IDs to update.
|
|
281
281
|
* @param update The update to apply to the entities.
|
|
282
|
+
* @param options Optional update options.
|
|
282
283
|
* @returns A promise that resolves to an array of the updated entities.
|
|
283
284
|
*/
|
|
284
|
-
updateMany(ids: string[], update: EntityUpdate<T>): Promise<T[]>;
|
|
285
|
+
updateMany(ids: string[], update: EntityUpdate<T>, options?: UpdateOptions<T>): Promise<T[]>;
|
|
285
286
|
/**
|
|
286
287
|
* Updates multiple entities matching a query.
|
|
287
288
|
* @param query The query to filter entities.
|
|
288
289
|
* @param update The update to apply to the entities.
|
|
290
|
+
* @param options Optional update options.
|
|
289
291
|
* @returns A promise that resolves to an array of the updated entities.
|
|
290
292
|
*/
|
|
291
|
-
updateManyByQuery(query: Query<T>, update: EntityUpdate<T>): Promise<T[]>;
|
|
293
|
+
updateManyByQuery(query: Query<T>, update: EntityUpdate<T>, options?: UpdateOptions<T>): Promise<T[]>;
|
|
292
294
|
/**
|
|
293
295
|
* Deletes an entity by its ID (soft delete if metadata is available).
|
|
294
296
|
* Throws `NotFoundError` if the entity is not found.
|
|
@@ -343,6 +345,60 @@ export declare class EntityRepository<T extends BaseEntity = BaseEntity> extends
|
|
|
343
345
|
* @returns A promise that resolves to an array of the deleted entities.
|
|
344
346
|
*/
|
|
345
347
|
deleteManyByQuery(query: Query<T>, options?: DeleteOptions<T>, metadataUpdate?: EntityMetadataUpdate): Promise<T[]>;
|
|
348
|
+
/**
|
|
349
|
+
* Undeletes an entity by its ID (restores from soft-deleted state).
|
|
350
|
+
* Throws `NotFoundError` if the entity is not found (or not deleted).
|
|
351
|
+
* @param id The ID of the entity to undelete.
|
|
352
|
+
* @param options Optional undelete options.
|
|
353
|
+
* @param metadataUpdate Optional metadata update to apply during undeletion.
|
|
354
|
+
* @returns A promise that resolves to the undeleted entity.
|
|
355
|
+
* @throws {NotFoundError} If the entity with the given ID is not found (or not deleted).
|
|
356
|
+
*/
|
|
357
|
+
undelete(id: string, options?: UndeleteOptions<T>, metadataUpdate?: EntityMetadataUpdate): Promise<T>;
|
|
358
|
+
/**
|
|
359
|
+
* Tries to undelete an entity by its ID (restores from soft-deleted state).
|
|
360
|
+
* Returns `undefined` if the entity is not found (or not deleted).
|
|
361
|
+
* @param id The ID of the entity to undelete.
|
|
362
|
+
* @param options Optional undelete options.
|
|
363
|
+
* @param metadataUpdate Optional metadata update to apply during undeletion.
|
|
364
|
+
* @returns A promise that resolves to the undeleted entity or `undefined` if not found.
|
|
365
|
+
*/
|
|
366
|
+
tryUndelete(id: string, options?: UndeleteOptions<T>, metadataUpdate?: EntityMetadataUpdate): Promise<T | undefined>;
|
|
367
|
+
/**
|
|
368
|
+
* Undeletes a single entity matching a query (restores from soft-deleted state).
|
|
369
|
+
* Throws `NotFoundError` if no deleted entity matches the query.
|
|
370
|
+
* @param query The query to filter entities.
|
|
371
|
+
* @param options Optional undelete options.
|
|
372
|
+
* @param metadataUpdate Optional metadata update to apply during undeletion.
|
|
373
|
+
* @returns A promise that resolves to the undeleted entity.
|
|
374
|
+
* @throws {NotFoundError} If no deleted entity matches the query.
|
|
375
|
+
*/
|
|
376
|
+
undeleteByQuery(query: Query<T>, options?: UndeleteOptions<T>, metadataUpdate?: EntityMetadataUpdate): Promise<T>;
|
|
377
|
+
/**
|
|
378
|
+
* Tries to undelete a single entity matching a query (restores from soft-deleted state).
|
|
379
|
+
* Returns `undefined` if no deleted entity matches the query.
|
|
380
|
+
* @param query The query to filter entities.
|
|
381
|
+
* @param options Optional undelete options.
|
|
382
|
+
* @param metadataUpdate Optional metadata update to apply during undeletion.
|
|
383
|
+
* @returns A promise that resolves to the undeleted entity or `undefined` if not found.
|
|
384
|
+
*/
|
|
385
|
+
tryUndeleteByQuery(query: Query<T>, options?: UndeleteOptions<T>, metadataUpdate?: EntityMetadataUpdate): Promise<T | undefined>;
|
|
386
|
+
/**
|
|
387
|
+
* Undeletes multiple entities by their IDs (restores from soft-deleted state).
|
|
388
|
+
* @param ids An array of entity IDs to undelete.
|
|
389
|
+
* @param options Optional undelete options.
|
|
390
|
+
* @param metadataUpdate Optional metadata update to apply during undeletion.
|
|
391
|
+
* @returns A promise that resolves to an array of the undeleted entities.
|
|
392
|
+
*/
|
|
393
|
+
undeleteMany(ids: string[], options?: UndeleteOptions<T>, metadataUpdate?: EntityMetadataUpdate): Promise<T[]>;
|
|
394
|
+
/**
|
|
395
|
+
* Undeletes multiple entities matching a query (restores from soft-deleted state).
|
|
396
|
+
* @param query The query to filter entities.
|
|
397
|
+
* @param options Optional undelete options.
|
|
398
|
+
* @param metadataUpdate Optional metadata update to apply during undeletion.
|
|
399
|
+
* @returns A promise that resolves to an array of the undeleted entities.
|
|
400
|
+
*/
|
|
401
|
+
undeleteManyByQuery(query: Query<T>, options?: UndeleteOptions<T>, metadataUpdate?: EntityMetadataUpdate): Promise<T[]>;
|
|
346
402
|
/**
|
|
347
403
|
* Hard deletes an entity by its ID (removes from the database).
|
|
348
404
|
* Throws `NotFoundError` if the entity is not found.
|
|
@@ -536,7 +592,7 @@ export declare class EntityRepository<T extends BaseEntity = BaseEntity> extends
|
|
|
536
592
|
protected _mapToInsertColumns(obj: DeepPartial<T> | NewEntity<T>, transformContext: TransformContext): Promise<PgInsertValue<PgTableFromType>>;
|
|
537
593
|
protected _mapToTableUpdate(update: EntityUpdate<T>, transformContext: TransformContext, table: AnyPgTable, definitions?: ColumnDefinition[]): Promise<PgUpdateSetSource<PgTableFromType>>;
|
|
538
594
|
protected _mapUpdate(update: EntityUpdate<T>, transformContext: TransformContext): Promise<PgUpdateSetSource<PgTableFromType>>;
|
|
539
|
-
protected _getMetadataUpdate(update?:
|
|
595
|
+
protected _getMetadataUpdate(update?: EntityMetadataUpdate): PgUpdateSetSource<PgTableFromType<EntityType<Entity>>> | undefined;
|
|
540
596
|
private prepareSubclassJoins;
|
|
541
597
|
private addSubclassColumnsToSelection;
|
|
542
598
|
private applySubclassJoins;
|
package/orm/server/repository.js
CHANGED
|
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
7
|
};
|
|
8
|
-
import { and, asc, count, desc, eq, getTableName, inArray, isNull as isSqlNull, isSQLWrapper, lte, or, SQL, sql } from 'drizzle-orm';
|
|
8
|
+
import { and, asc, count, desc, eq, getTableName, inArray, isNotNull as isSqlNotNull, isNull as isSqlNull, isSQLWrapper, lte, or, SQL, sql } from 'drizzle-orm';
|
|
9
9
|
import { match, P } from 'ts-pattern';
|
|
10
10
|
import { CancellationSignal } from '../../cancellation/token.js';
|
|
11
11
|
import { NotFoundError } from '../../errors/not-found.error.js';
|
|
@@ -781,7 +781,7 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
781
781
|
? await this._mapToTableUpdate(update, transformContext, table, columnDefinitions)
|
|
782
782
|
: {
|
|
783
783
|
...fromEntries(columnDefinitions.filter((column) => !primaryKeyColumnNames.includes(column.name)).map((column) => [column.name, sql `excluded.${sql.identifier(resolveTargetColumn(column, table, this.#columnDefinitionsMap).name)}`])),
|
|
784
|
-
...((table == this.#baseTable) ? this._getMetadataUpdate(update) : undefined),
|
|
784
|
+
...((table == this.#baseTable) ? this._getMetadataUpdate(update?.metadata) : undefined),
|
|
785
785
|
};
|
|
786
786
|
const targetPaths = toArray(target);
|
|
787
787
|
const isTargetInTable = targetPaths.every((path) => {
|
|
@@ -860,7 +860,7 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
860
860
|
? await this.mapUpdate(update)
|
|
861
861
|
: {
|
|
862
862
|
...this.#upsertManyExcludedMapping,
|
|
863
|
-
...this._getMetadataUpdate(update),
|
|
863
|
+
...this._getMetadataUpdate(update?.metadata),
|
|
864
864
|
};
|
|
865
865
|
const rows = await this.session
|
|
866
866
|
.insert(this.#table)
|
|
@@ -929,9 +929,9 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
929
929
|
return await this.mapToEntity(mergedRow);
|
|
930
930
|
});
|
|
931
931
|
}
|
|
932
|
-
async updateManyCTI(query, update) {
|
|
932
|
+
async updateManyCTI(query, update, options) {
|
|
933
933
|
return await this.transaction(async (transaction) => {
|
|
934
|
-
const ids = await this.withTransaction(transaction).loadManyByQuery(query).then((entities) => entities.map((entity) => entity.id));
|
|
934
|
+
const ids = await this.withTransaction(transaction).loadManyByQuery(query, options).then((entities) => entities.map((entity) => entity.id));
|
|
935
935
|
if (ids.length == 0) {
|
|
936
936
|
return [];
|
|
937
937
|
}
|
|
@@ -945,7 +945,7 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
945
945
|
.where(inArray(table.id, ids));
|
|
946
946
|
}
|
|
947
947
|
}
|
|
948
|
-
return await this.loadMany(ids);
|
|
948
|
+
return await this.withTransaction(transaction).loadMany(ids, options);
|
|
949
949
|
});
|
|
950
950
|
}
|
|
951
951
|
/**
|
|
@@ -1010,35 +1010,37 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
1010
1010
|
}
|
|
1011
1011
|
return await this.mapToEntity(row);
|
|
1012
1012
|
}
|
|
1013
|
-
return await this.transaction(async () => {
|
|
1014
|
-
const id = await this.tryLoadByQuery(query, options).then((entity) => entity?.id);
|
|
1013
|
+
return await this.transaction(async (transaction) => {
|
|
1014
|
+
const id = await this.withTransaction(transaction).tryLoadByQuery(query, options).then((entity) => entity?.id);
|
|
1015
1015
|
if (isUndefined(id)) {
|
|
1016
1016
|
return undefined;
|
|
1017
1017
|
}
|
|
1018
|
-
return await this.tryUpdate(id, update, options);
|
|
1018
|
+
return await this.withTransaction(transaction).tryUpdate(id, update, options);
|
|
1019
1019
|
});
|
|
1020
1020
|
}
|
|
1021
1021
|
/**
|
|
1022
1022
|
* Updates multiple entities by their IDs.
|
|
1023
1023
|
* @param ids An array of entity IDs to update.
|
|
1024
1024
|
* @param update The update to apply to the entities.
|
|
1025
|
+
* @param options Optional update options.
|
|
1025
1026
|
* @returns A promise that resolves to an array of the updated entities.
|
|
1026
1027
|
*/
|
|
1027
|
-
async updateMany(ids, update) {
|
|
1028
|
+
async updateMany(ids, update, options) {
|
|
1028
1029
|
if (ids.length == 0) {
|
|
1029
1030
|
return [];
|
|
1030
1031
|
}
|
|
1031
|
-
return await this.updateManyByQuery(inArray(this.#table.id, ids), update);
|
|
1032
|
+
return await this.updateManyByQuery(inArray(this.#table.id, ids), update, options);
|
|
1032
1033
|
}
|
|
1033
1034
|
/**
|
|
1034
1035
|
* Updates multiple entities matching a query.
|
|
1035
1036
|
* @param query The query to filter entities.
|
|
1036
1037
|
* @param update The update to apply to the entities.
|
|
1038
|
+
* @param options Optional update options.
|
|
1037
1039
|
* @returns A promise that resolves to an array of the updated entities.
|
|
1038
1040
|
*/
|
|
1039
|
-
async updateManyByQuery(query, update) {
|
|
1041
|
+
async updateManyByQuery(query, update, options) {
|
|
1040
1042
|
if (!this.#isChild) {
|
|
1041
|
-
const sqlQuery = this.convertQuery(query);
|
|
1043
|
+
const sqlQuery = this.convertQuery(query, options);
|
|
1042
1044
|
const mappedUpdate = await this.mapUpdate(update);
|
|
1043
1045
|
const rows = await this.session
|
|
1044
1046
|
.update(this.#table)
|
|
@@ -1047,7 +1049,7 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
1047
1049
|
.returning();
|
|
1048
1050
|
return await this.mapManyToEntity(rows);
|
|
1049
1051
|
}
|
|
1050
|
-
return await this.updateManyCTI(query, update);
|
|
1052
|
+
return await this.updateManyCTI(query, update, options);
|
|
1051
1053
|
}
|
|
1052
1054
|
/**
|
|
1053
1055
|
* Deletes an entity by its ID (soft delete if metadata is available).
|
|
@@ -1082,7 +1084,7 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
1082
1084
|
.update(this.#baseTableWithMetadata)
|
|
1083
1085
|
.set({
|
|
1084
1086
|
deleteTimestamp: TRANSACTION_TIMESTAMP,
|
|
1085
|
-
|
|
1087
|
+
...this._getMetadataUpdate(metadataUpdate),
|
|
1086
1088
|
})
|
|
1087
1089
|
.where(sqlQuery)
|
|
1088
1090
|
.returning();
|
|
@@ -1125,7 +1127,7 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
1125
1127
|
.update(this.#baseTableWithMetadata)
|
|
1126
1128
|
.set({
|
|
1127
1129
|
deleteTimestamp: TRANSACTION_TIMESTAMP,
|
|
1128
|
-
|
|
1130
|
+
...this._getMetadataUpdate(metadataUpdate),
|
|
1129
1131
|
})
|
|
1130
1132
|
.where(sqlQuery)
|
|
1131
1133
|
.returning();
|
|
@@ -1163,12 +1165,109 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
1163
1165
|
.update(this.#baseTableWithMetadata)
|
|
1164
1166
|
.set({
|
|
1165
1167
|
deleteTimestamp: TRANSACTION_TIMESTAMP,
|
|
1166
|
-
|
|
1168
|
+
...this._getMetadataUpdate(metadataUpdate),
|
|
1167
1169
|
})
|
|
1168
1170
|
.where(sqlQuery)
|
|
1169
1171
|
.returning();
|
|
1170
1172
|
return await this.mapManyToEntity(rows);
|
|
1171
1173
|
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Undeletes an entity by its ID (restores from soft-deleted state).
|
|
1176
|
+
* Throws `NotFoundError` if the entity is not found (or not deleted).
|
|
1177
|
+
* @param id The ID of the entity to undelete.
|
|
1178
|
+
* @param options Optional undelete options.
|
|
1179
|
+
* @param metadataUpdate Optional metadata update to apply during undeletion.
|
|
1180
|
+
* @returns A promise that resolves to the undeleted entity.
|
|
1181
|
+
* @throws {NotFoundError} If the entity with the given ID is not found (or not deleted).
|
|
1182
|
+
*/
|
|
1183
|
+
async undelete(id, options, metadataUpdate) {
|
|
1184
|
+
const entity = await this.tryUndelete(id, options, metadataUpdate);
|
|
1185
|
+
if (isUndefined(entity)) {
|
|
1186
|
+
throw new NotFoundError(`${this.typeName} ${id} not found.`);
|
|
1187
|
+
}
|
|
1188
|
+
return entity;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Tries to undelete an entity by its ID (restores from soft-deleted state).
|
|
1192
|
+
* Returns `undefined` if the entity is not found (or not deleted).
|
|
1193
|
+
* @param id The ID of the entity to undelete.
|
|
1194
|
+
* @param options Optional undelete options.
|
|
1195
|
+
* @param metadataUpdate Optional metadata update to apply during undeletion.
|
|
1196
|
+
* @returns A promise that resolves to the undeleted entity or `undefined` if not found.
|
|
1197
|
+
*/
|
|
1198
|
+
async tryUndelete(id, options, metadataUpdate) {
|
|
1199
|
+
return await this.tryUndeleteByQuery(eq(this.#baseTable.id, id), options, metadataUpdate);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Undeletes a single entity matching a query (restores from soft-deleted state).
|
|
1203
|
+
* Throws `NotFoundError` if no deleted entity matches the query.
|
|
1204
|
+
* @param query The query to filter entities.
|
|
1205
|
+
* @param options Optional undelete options.
|
|
1206
|
+
* @param metadataUpdate Optional metadata update to apply during undeletion.
|
|
1207
|
+
* @returns A promise that resolves to the undeleted entity.
|
|
1208
|
+
* @throws {NotFoundError} If no deleted entity matches the query.
|
|
1209
|
+
*/
|
|
1210
|
+
async undeleteByQuery(query, options, metadataUpdate) {
|
|
1211
|
+
const entity = await this.tryUndeleteByQuery(query, options, metadataUpdate);
|
|
1212
|
+
if (isUndefined(entity)) {
|
|
1213
|
+
throw new NotFoundError(`${this.typeName} not found.`);
|
|
1214
|
+
}
|
|
1215
|
+
return entity;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Tries to undelete a single entity matching a query (restores from soft-deleted state).
|
|
1219
|
+
* Returns `undefined` if no deleted entity matches the query.
|
|
1220
|
+
* @param query The query to filter entities.
|
|
1221
|
+
* @param options Optional undelete options.
|
|
1222
|
+
* @param metadataUpdate Optional metadata update to apply during undeletion.
|
|
1223
|
+
* @returns A promise that resolves to the undeleted entity or `undefined` if not found.
|
|
1224
|
+
*/
|
|
1225
|
+
async tryUndeleteByQuery(query, options, metadataUpdate) {
|
|
1226
|
+
if (!this.hasMetadata) {
|
|
1227
|
+
return undefined;
|
|
1228
|
+
}
|
|
1229
|
+
const undeleteQuery = and(this.convertQuery(query, { withDeleted: true, ...options }), isSqlNotNull(this.#baseTableWithMetadata.deleteTimestamp));
|
|
1230
|
+
const update = {
|
|
1231
|
+
metadata: {
|
|
1232
|
+
...metadataUpdate,
|
|
1233
|
+
deleteTimestamp: null,
|
|
1234
|
+
},
|
|
1235
|
+
};
|
|
1236
|
+
return await this.tryUpdateByQuery(undeleteQuery, update, { withDeleted: true, ...options });
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Undeletes multiple entities by their IDs (restores from soft-deleted state).
|
|
1240
|
+
* @param ids An array of entity IDs to undelete.
|
|
1241
|
+
* @param options Optional undelete options.
|
|
1242
|
+
* @param metadataUpdate Optional metadata update to apply during undeletion.
|
|
1243
|
+
* @returns A promise that resolves to an array of the undeleted entities.
|
|
1244
|
+
*/
|
|
1245
|
+
async undeleteMany(ids, options, metadataUpdate) {
|
|
1246
|
+
if (ids.length == 0) {
|
|
1247
|
+
return [];
|
|
1248
|
+
}
|
|
1249
|
+
return await this.undeleteManyByQuery(inArray(this.#table.id, ids), options, metadataUpdate);
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Undeletes multiple entities matching a query (restores from soft-deleted state).
|
|
1253
|
+
* @param query The query to filter entities.
|
|
1254
|
+
* @param options Optional undelete options.
|
|
1255
|
+
* @param metadataUpdate Optional metadata update to apply during undeletion.
|
|
1256
|
+
* @returns A promise that resolves to an array of the undeleted entities.
|
|
1257
|
+
*/
|
|
1258
|
+
async undeleteManyByQuery(query, options, metadataUpdate) {
|
|
1259
|
+
if (!this.hasMetadata) {
|
|
1260
|
+
return [];
|
|
1261
|
+
}
|
|
1262
|
+
const undeleteQuery = and(this.convertQuery(query, { withDeleted: true, ...options }), isSqlNotNull(this.#baseTableWithMetadata.deleteTimestamp));
|
|
1263
|
+
const update = {
|
|
1264
|
+
metadata: {
|
|
1265
|
+
...metadataUpdate,
|
|
1266
|
+
deleteTimestamp: null,
|
|
1267
|
+
},
|
|
1268
|
+
};
|
|
1269
|
+
return await this.updateManyByQuery(undeleteQuery, update, { withDeleted: true, ...options });
|
|
1270
|
+
}
|
|
1172
1271
|
/**
|
|
1173
1272
|
* Hard deletes an entity by its ID (removes from the database).
|
|
1174
1273
|
* Throws `NotFoundError` if the entity is not found.
|
|
@@ -1577,20 +1676,22 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
1577
1676
|
}
|
|
1578
1677
|
return {
|
|
1579
1678
|
...mappedUpdate,
|
|
1580
|
-
...((table == this.#baseTable) ? this._getMetadataUpdate(update) : undefined),
|
|
1679
|
+
...((table == this.#baseTable) ? this._getMetadataUpdate(update?.metadata) : undefined),
|
|
1581
1680
|
};
|
|
1582
1681
|
}
|
|
1583
1682
|
async _mapUpdate(update, transformContext) {
|
|
1584
1683
|
return await this._mapToTableUpdate(update, transformContext, this.#table);
|
|
1585
1684
|
}
|
|
1586
1685
|
_getMetadataUpdate(update) {
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
}
|
|
1593
|
-
:
|
|
1686
|
+
if (!this.hasMetadata) {
|
|
1687
|
+
return undefined;
|
|
1688
|
+
}
|
|
1689
|
+
return {
|
|
1690
|
+
attributes: this.getAttributesUpdate(update?.attributes),
|
|
1691
|
+
...(isDefined(update?.deleteTimestamp) ? { deleteTimestamp: update.deleteTimestamp } : {}),
|
|
1692
|
+
revision: sql `${this.#baseTableWithMetadata.revision} + 1`,
|
|
1693
|
+
revisionTimestamp: TRANSACTION_TIMESTAMP,
|
|
1694
|
+
};
|
|
1594
1695
|
}
|
|
1595
1696
|
prepareSubclassJoins(includeSubclassesOption) {
|
|
1596
1697
|
const includeSubclasses = (includeSubclassesOption == true)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
import { sql } from 'drizzle-orm';
|
|
12
|
+
import { beforeAll, describe, expect, test } from 'vitest';
|
|
13
|
+
import { StringProperty } from '../../schema/index.js';
|
|
14
|
+
import { setupIntegrationTest } from '../../testing/index.js';
|
|
15
|
+
import { ChildEntity, Column, Inheritance, Table } from '../decorators.js';
|
|
16
|
+
import { Entity } from '../entity.js';
|
|
17
|
+
import { getRepository } from '../server/index.js';
|
|
18
|
+
describe('ORM Repository Undelete (Integration)', () => {
|
|
19
|
+
let injector;
|
|
20
|
+
let db;
|
|
21
|
+
let repository;
|
|
22
|
+
let baseRepo;
|
|
23
|
+
let subRepo;
|
|
24
|
+
const schema = 'test_orm_undelete';
|
|
25
|
+
let UndeleteEntity = class UndeleteEntity extends Entity {
|
|
26
|
+
name;
|
|
27
|
+
};
|
|
28
|
+
__decorate([
|
|
29
|
+
StringProperty(),
|
|
30
|
+
__metadata("design:type", String)
|
|
31
|
+
], UndeleteEntity.prototype, "name", void 0);
|
|
32
|
+
UndeleteEntity = __decorate([
|
|
33
|
+
Table('undelete_entities', { schema })
|
|
34
|
+
], UndeleteEntity);
|
|
35
|
+
let Base = class Base extends Entity {
|
|
36
|
+
type;
|
|
37
|
+
baseName;
|
|
38
|
+
};
|
|
39
|
+
__decorate([
|
|
40
|
+
StringProperty(),
|
|
41
|
+
Column({ name: 'type' }),
|
|
42
|
+
__metadata("design:type", String)
|
|
43
|
+
], Base.prototype, "type", void 0);
|
|
44
|
+
__decorate([
|
|
45
|
+
StringProperty(),
|
|
46
|
+
__metadata("design:type", String)
|
|
47
|
+
], Base.prototype, "baseName", void 0);
|
|
48
|
+
Base = __decorate([
|
|
49
|
+
Table('bases', { schema }),
|
|
50
|
+
Inheritance({ strategy: 'joined', discriminatorColumn: 'type' })
|
|
51
|
+
], Base);
|
|
52
|
+
let Subtype = class Subtype extends Base {
|
|
53
|
+
subData;
|
|
54
|
+
};
|
|
55
|
+
__decorate([
|
|
56
|
+
StringProperty(),
|
|
57
|
+
__metadata("design:type", String)
|
|
58
|
+
], Subtype.prototype, "subData", void 0);
|
|
59
|
+
Subtype = __decorate([
|
|
60
|
+
Table('subtypes', { schema }),
|
|
61
|
+
ChildEntity('subtype')
|
|
62
|
+
], Subtype);
|
|
63
|
+
beforeAll(async () => {
|
|
64
|
+
({ injector, database: db } = await setupIntegrationTest({ orm: { schema } }));
|
|
65
|
+
repository = injector.resolve(getRepository(UndeleteEntity));
|
|
66
|
+
baseRepo = injector.resolve(getRepository(Base));
|
|
67
|
+
subRepo = injector.resolve(getRepository(Subtype));
|
|
68
|
+
await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
|
|
69
|
+
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
70
|
+
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('subtypes')} CASCADE`);
|
|
71
|
+
await db.execute(sql `DROP TABLE IF EXISTS ${sql.identifier(schema)}.${sql.identifier('bases')} CASCADE`);
|
|
72
|
+
await db.execute(sql `
|
|
73
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} (
|
|
74
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
75
|
+
name TEXT NOT NULL,
|
|
76
|
+
revision INTEGER NOT NULL,
|
|
77
|
+
revision_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
78
|
+
create_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
79
|
+
delete_timestamp TIMESTAMP WITH TIME ZONE,
|
|
80
|
+
attributes JSONB NOT NULL DEFAULT '{}'
|
|
81
|
+
)
|
|
82
|
+
`);
|
|
83
|
+
await db.execute(sql `
|
|
84
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('bases')} (
|
|
85
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
86
|
+
type TEXT NOT NULL,
|
|
87
|
+
base_name TEXT NOT NULL,
|
|
88
|
+
revision INTEGER NOT NULL,
|
|
89
|
+
revision_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
90
|
+
create_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
91
|
+
delete_timestamp TIMESTAMP WITH TIME ZONE,
|
|
92
|
+
attributes JSONB NOT NULL DEFAULT '{}',
|
|
93
|
+
UNIQUE (id, type)
|
|
94
|
+
)
|
|
95
|
+
`);
|
|
96
|
+
await db.execute(sql `
|
|
97
|
+
CREATE TABLE ${sql.identifier(schema)}.${sql.identifier('subtypes')} (
|
|
98
|
+
id UUID PRIMARY KEY,
|
|
99
|
+
type TEXT NOT NULL CHECK (type = 'subtype'),
|
|
100
|
+
sub_data TEXT NOT NULL,
|
|
101
|
+
FOREIGN KEY (id, type) REFERENCES ${sql.identifier(schema)}.${sql.identifier('bases')} (id, type) ON DELETE CASCADE
|
|
102
|
+
)
|
|
103
|
+
`);
|
|
104
|
+
});
|
|
105
|
+
test('should undelete an entity by ID', async () => {
|
|
106
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
107
|
+
const e1 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'E1' }));
|
|
108
|
+
await repository.delete(e1.id);
|
|
109
|
+
expect(await repository.has(e1.id)).toBe(false);
|
|
110
|
+
const undeleted = await repository.undelete(e1.id);
|
|
111
|
+
expect(undeleted.id).toBe(e1.id);
|
|
112
|
+
expect(undeleted.metadata.deleteTimestamp).toBeNull();
|
|
113
|
+
expect(undeleted.metadata.revision).toBe(3); // Insert (1) -> Delete (2) -> Undelete (3)
|
|
114
|
+
expect(await repository.has(e1.id)).toBe(true);
|
|
115
|
+
const loaded = await repository.load(e1.id);
|
|
116
|
+
expect(loaded.name).toBe('E1');
|
|
117
|
+
expect(loaded.metadata.deleteTimestamp).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
test('should undelete multiple entities by IDs', async () => {
|
|
120
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
121
|
+
const e1 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'E1' }));
|
|
122
|
+
const e2 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'E2' }));
|
|
123
|
+
await repository.deleteMany([e1.id, e2.id]);
|
|
124
|
+
const undeleted = await repository.undeleteMany([e1.id, e2.id]);
|
|
125
|
+
expect(undeleted).toHaveLength(2);
|
|
126
|
+
expect(await repository.count()).toBe(2);
|
|
127
|
+
});
|
|
128
|
+
test('should undelete entities by query', async () => {
|
|
129
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
130
|
+
const e1 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'A' }));
|
|
131
|
+
const e2 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'B' }));
|
|
132
|
+
const e3 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'A' }));
|
|
133
|
+
await repository.deleteManyByQuery({ name: 'A' });
|
|
134
|
+
expect(await repository.count()).toBe(1);
|
|
135
|
+
const undeleted = await repository.undeleteManyByQuery({ name: 'A' });
|
|
136
|
+
expect(undeleted).toHaveLength(2);
|
|
137
|
+
expect(await repository.count()).toBe(3);
|
|
138
|
+
});
|
|
139
|
+
test('should ignore already active entities when undeleting', async () => {
|
|
140
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
141
|
+
const e1 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'E1' }));
|
|
142
|
+
const result = await repository.tryUndelete(e1.id);
|
|
143
|
+
expect(result).toBeUndefined();
|
|
144
|
+
expect(await repository.count()).toBe(1);
|
|
145
|
+
});
|
|
146
|
+
test('should support metadata updates during undeletion', async () => {
|
|
147
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
148
|
+
const e1 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'E1' }));
|
|
149
|
+
await repository.delete(e1.id);
|
|
150
|
+
const undeleted = await repository.undelete(e1.id, {}, { attributes: { restored: true } });
|
|
151
|
+
expect(undeleted.metadata.attributes).toEqual({ restored: true });
|
|
152
|
+
const loaded = await repository.load(e1.id);
|
|
153
|
+
expect(loaded.metadata.attributes).toEqual({ restored: true });
|
|
154
|
+
});
|
|
155
|
+
test('should throw NotFoundError if entity does not exist or is not deleted', async () => {
|
|
156
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('undelete_entities')} CASCADE`);
|
|
157
|
+
await expect(repository.undelete('00000000-0000-0000-0000-000000000000')).rejects.toThrow();
|
|
158
|
+
const e1 = await repository.insert(Object.assign(new UndeleteEntity(), { name: 'E1' }));
|
|
159
|
+
await expect(repository.undelete(e1.id)).rejects.toThrow();
|
|
160
|
+
});
|
|
161
|
+
test('should undelete from subtype repository (CTI)', async () => {
|
|
162
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('bases')} CASCADE`);
|
|
163
|
+
const entity = await subRepo.insert(Object.assign(new Subtype(), { baseName: 'B1', subData: 'S1' }));
|
|
164
|
+
await subRepo.delete(entity.id);
|
|
165
|
+
expect(await subRepo.has(entity.id)).toBe(false);
|
|
166
|
+
const undeleted = await subRepo.undelete(entity.id);
|
|
167
|
+
expect(undeleted.id).toBe(entity.id);
|
|
168
|
+
expect(undeleted.metadata.deleteTimestamp).toBeNull();
|
|
169
|
+
expect(undeleted.metadata.revision).toBe(3);
|
|
170
|
+
expect(await subRepo.has(entity.id)).toBe(true);
|
|
171
|
+
const loaded = await subRepo.load(entity.id);
|
|
172
|
+
expect(loaded.baseName).toBe('B1');
|
|
173
|
+
expect(loaded.subData).toBe('S1');
|
|
174
|
+
});
|
|
175
|
+
test('should undelete polymorphically from base repository (CTI)', async () => {
|
|
176
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('bases')} CASCADE`);
|
|
177
|
+
const entity = await subRepo.insert(Object.assign(new Subtype(), { baseName: 'B1', subData: 'S1' }));
|
|
178
|
+
await subRepo.delete(entity.id);
|
|
179
|
+
expect(await baseRepo.has(entity.id)).toBe(false);
|
|
180
|
+
const undeleted = await baseRepo.undelete(entity.id);
|
|
181
|
+
expect(undeleted.id).toBe(entity.id);
|
|
182
|
+
expect(undeleted.metadata.deleteTimestamp).toBeNull();
|
|
183
|
+
expect(await baseRepo.has(entity.id)).toBe(true);
|
|
184
|
+
const loaded = await baseRepo.load(entity.id, { includeSubclasses: true });
|
|
185
|
+
expect(loaded).toBeInstanceOf(Subtype);
|
|
186
|
+
expect(loaded.subData).toBe('S1');
|
|
187
|
+
});
|
|
188
|
+
test('should undelete multiple entities polymorphically (CTI)', async () => {
|
|
189
|
+
await db.execute(sql `TRUNCATE TABLE ${sql.identifier(schema)}.${sql.identifier('bases')} CASCADE`);
|
|
190
|
+
const e1 = await subRepo.insert(Object.assign(new Subtype(), { baseName: 'B1', subData: 'S1' }));
|
|
191
|
+
const e2 = await subRepo.insert(Object.assign(new Subtype(), { baseName: 'B2', subData: 'S2' }));
|
|
192
|
+
await baseRepo.deleteMany([e1.id, e2.id]);
|
|
193
|
+
expect(await baseRepo.count()).toBe(0);
|
|
194
|
+
const undeleted = await baseRepo.undeleteMany([e1.id, e2.id]);
|
|
195
|
+
expect(undeleted).toHaveLength(2);
|
|
196
|
+
expect(await baseRepo.count()).toBe(2);
|
|
197
|
+
const loadedAll = await baseRepo.loadAll({ includeSubclasses: true });
|
|
198
|
+
expect(loadedAll).toHaveLength(2);
|
|
199
|
+
expect(loadedAll.every((e) => e instanceof Subtype)).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tstdl/base",
|
|
3
|
-
"version": "0.93.
|
|
3
|
+
"version": "0.93.147",
|
|
4
4
|
"author": "Patrick Hein",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -152,8 +152,8 @@
|
|
|
152
152
|
"type-fest": "^5.4"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
|
-
"@aws-sdk/client-s3": "^3.
|
|
156
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
155
|
+
"@aws-sdk/client-s3": "^3.1000",
|
|
156
|
+
"@aws-sdk/s3-request-presigner": "^3.1000",
|
|
157
157
|
"@genkit-ai/google-genai": "^1.29",
|
|
158
158
|
"@google-cloud/storage": "^7.19",
|
|
159
159
|
"@toon-format/toon": "^2.1.0",
|