@appweaver/core 1.4.0 → 1.5.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.
Files changed (33) hide show
  1. package/export/export-service.js +6 -0
  2. package/factory/create-model.js +43 -6
  3. package/package.json +1 -1
  4. package/prisma/client/internal/class.js +4 -4
  5. package/prisma/client/internal/prismaNamespace.d.ts +1 -0
  6. package/prisma/client/internal/prismaNamespace.js +2 -1
  7. package/prisma/client/internal/prismaNamespaceBrowser.d.ts +1 -0
  8. package/prisma/client/internal/prismaNamespaceBrowser.js +2 -1
  9. package/prisma/client/models/ConnectedAccount.d.ts +6 -1
  10. package/prisma/client/models/File.d.ts +29 -1
  11. package/resource/resource-loader.js +3 -0
  12. package/resource/resource-routes.js +0 -1
  13. package/resource/resource-service.d.ts +35 -8
  14. package/resource/resource-service.js +110 -23
  15. package/resource/utils/delete-util.d.ts +120 -0
  16. package/resource/utils/delete-util.js +385 -0
  17. package/resource/utils/filter-util.js +11 -5
  18. package/resource/utils/index.d.ts +1 -0
  19. package/resource/utils/index.js +1 -0
  20. package/resource/utils/relation-util.js +68 -19
  21. package/security/api-key/api-key-auth.js +4 -3
  22. package/security/oauth2/oauth2-service.js +19 -5
  23. package/security/resources/api-key/model.js +2 -0
  24. package/security/resources/connected-account/model.js +3 -1
  25. package/server/swagger.js +49 -1
  26. package/storage/file-service.d.ts +13 -3
  27. package/storage/file-service.js +24 -14
  28. package/storage/resources/file/model.js +5 -1
  29. package/types/generated.d.ts +1 -0
  30. package/utils/file-util.d.ts +14 -0
  31. package/utils/file-util.js +21 -0
  32. package/utils/model-util.d.ts +24 -0
  33. package/utils/model-util.js +43 -0
@@ -129,9 +129,9 @@ export declare abstract class ResourceService<ReadOne = Resource, ReadMany = Res
129
129
  * the read restrictions applied and checked for access, then updated with the
130
130
  * data merged with the write restrictions inside a single transaction.
131
131
  * Relations missing from the new value are disconnected, or deleted when
132
- * `orphanRemoval` is configured for them. The resource cache is invalidated
133
- * and a resource event carrying both the previous and the current state is
134
- * emitted after a successful update.
132
+ * `orphanRemoval` is configured for them, the same way a delete does. The resource cache is invalidated and a
133
+ * resource event carrying both the previous and the current state is emitted
134
+ * after a successful update.
135
135
  *
136
136
  * @param {ResourceId} id The id of the resource to update.
137
137
  * @param {Object} data The partial data to update the resource with, including
@@ -141,21 +141,31 @@ export declare abstract class ResourceService<ReadOne = Resource, ReadMany = Res
141
141
  * @throws {@link HttpError} 404 if the resource does not exist or is filtered
142
142
  * out by the read restrictions, 403 if the access check denies the action, 400
143
143
  * if an inline relation payload is missing required fields or the relation
144
- * does not accept new records, and 500 on a database error.
144
+ * does not accept new records, 409 if a live record references a soft
145
+ * deleted orphan through a restricting relation, and 500 on a database error.
145
146
  */
146
147
  update(id: ResourceId, data: Update): Promise<ReadOne>;
147
148
  /**
148
149
  * Deletes an existing resource by its id. The current record is loaded with
149
150
  * the read restrictions applied and checked for access before it is deleted
150
- * inside a single transaction. The resource cache is invalidated and a
151
- * resource event is emitted after a successful delete.
151
+ * inside a single transaction.
152
+ *
153
+ * A model with `softDelete` enabled marks the record with the `deletedAt`
154
+ * and `deletedById` columns instead, soft deleting its cascade as well.
155
+ *
156
+ * The stored files of the deleted records follow the `onResourceDeleted`, or
157
+ * for soft deleted records the `onResourceSoftDeleted`, option of each file
158
+ * field: kept files are marked deleted in the transaction and no longer
159
+ * served, the others are removed once it commits. The cache of every affected
160
+ * model is invalidated and a resource event is emitted.
152
161
  *
153
162
  * @param {ResourceId} id The id of the resource to delete.
154
163
  * @returns {Promise<Object>} The deleted resource with its virtual fields and
155
164
  * relation counts projected.
156
165
  * @throws {@link HttpError} 404 if the resource does not exist or is filtered
157
- * out by the read restrictions, 403 if the access check denies the action, and
158
- * 500 on a database error.
166
+ * out by the read restrictions, 403 if the access check denies it, 409 if a
167
+ * restricting relation still references a soft deleted resource, and 500 on a
168
+ * database error, a restricting relation of a removed resource included.
159
169
  */
160
170
  delete(id: ResourceId): Promise<ReadOne>;
161
171
  /**
@@ -252,6 +262,23 @@ export declare abstract class ResourceService<ReadOne = Resource, ReadMany = Res
252
262
  * and the `_count` property removed.
253
263
  */
254
264
  private projectResource;
265
+ /**
266
+ * Lists the conditions hiding the soft deleted resources from a query. It is
267
+ * empty for a model that deletes its records, so its query and the cursors
268
+ * bound to it stay the same.
269
+ *
270
+ * @returns {Object[]} The `deletedAt: null` condition, or no condition.
271
+ */
272
+ private liveFilters;
273
+ /**
274
+ * Cleans up after records were deleted, once the deleting transaction
275
+ * commits: removes the stored files their fields do not keep, since the
276
+ * storage cannot take part in the transaction, and invalidates the cache of
277
+ * every model they belong to.
278
+ *
279
+ * @param {DeletedRecords} deleted The soft deleted and the removed records.
280
+ */
281
+ private cleanupDeletedRecords;
255
282
  /**
256
283
  * Prepares a write payload for the database by removing the virtual fields
257
284
  * that have no column, filling in default values for the hidden required
@@ -6,6 +6,7 @@ const context_1 = require("../context");
6
6
  const utils_1 = require("../utils");
7
7
  const cache_1 = require("../cache");
8
8
  const errors_1 = require("../errors");
9
+ const storage_1 = require("../storage");
9
10
  const utils_2 = require("./utils");
10
11
  class ResourceService {
11
12
  modelName;
@@ -61,7 +62,7 @@ class ResourceService {
61
62
  let resource;
62
63
  try {
63
64
  resource = await this._client.findFirst({
64
- where: { id, ...restrictions },
65
+ where: { id, ...restrictions, ...(0, utils_1.liveRecordFilter)(this._client.name) },
65
66
  include: includeRelations
66
67
  });
67
68
  }
@@ -120,7 +121,9 @@ class ResourceService {
120
121
  const restrictions = await this.readRestrictions('query', filter);
121
122
  const textSearch = this.extractTextSearchQuery(filter);
122
123
  const mappedFilter = (0, utils_2.mapQueryFilter)(filter, this._client.name);
123
- const query = { AND: [mappedFilter, textSearch, restrictions] };
124
+ const query = {
125
+ AND: [mappedFilter, textSearch, restrictions, ...this.liveFilters()]
126
+ };
124
127
  const includeRelations = (0, utils_2.mapRelationInclusions)(this._client.name, 'query');
125
128
  const orderBy = (0, utils_2.mapStableSortValues)(sort, this._client.name, 'query');
126
129
  // Binding to the mapped query rather than the filter also invalidates a
@@ -220,7 +223,9 @@ class ResourceService {
220
223
  const restrictions = await this.readRestrictions('aggregate', filter);
221
224
  const textSearch = this.extractTextSearchQuery(filter);
222
225
  const mappedFilter = (0, utils_2.mapQueryFilter)(filter, this._client.name);
223
- const query = { AND: [mappedFilter, textSearch, restrictions] };
226
+ const query = {
227
+ AND: [mappedFilter, textSearch, restrictions, ...this.liveFilters()]
228
+ };
224
229
  const rangeQuery = (rangeFrom, rangeTo) => ({
225
230
  AND: [query, { [dateField]: { gte: rangeFrom, lt: rangeTo } }]
226
231
  });
@@ -290,6 +295,7 @@ class ResourceService {
290
295
  const sanitizedData = this.sanitizeData('create', createData);
291
296
  const connectRelations = (0, utils_2.mapRelationActions)(this._client.name, 'create', sanitizedData);
292
297
  const includeRelations = (0, utils_2.mapRelationInclusions)(this._client.name, 'create');
298
+ await (0, utils_2.assertLiveRelationTargets)(this._db.client(), this._client.name, connectRelations);
293
299
  let resource;
294
300
  try {
295
301
  resource = await this._client.create({
@@ -315,9 +321,9 @@ class ResourceService {
315
321
  * the read restrictions applied and checked for access, then updated with the
316
322
  * data merged with the write restrictions inside a single transaction.
317
323
  * Relations missing from the new value are disconnected, or deleted when
318
- * `orphanRemoval` is configured for them. The resource cache is invalidated
319
- * and a resource event carrying both the previous and the current state is
320
- * emitted after a successful update.
324
+ * `orphanRemoval` is configured for them, the same way a delete does. The resource cache is invalidated and a
325
+ * resource event carrying both the previous and the current state is emitted
326
+ * after a successful update.
321
327
  *
322
328
  * @param {ResourceId} id The id of the resource to update.
323
329
  * @param {Object} data The partial data to update the resource with, including
@@ -327,7 +333,8 @@ class ResourceService {
327
333
  * @throws {@link HttpError} 404 if the resource does not exist or is filtered
328
334
  * out by the read restrictions, 403 if the access check denies the action, 400
329
335
  * if an inline relation payload is missing required fields or the relation
330
- * does not accept new records, and 500 on a database error.
336
+ * does not accept new records, 409 if a live record references a soft
337
+ * deleted orphan through a restricting relation, and 500 on a database error.
331
338
  */
332
339
  async update(id, data) {
333
340
  const readRestrictions = await this.readRestrictions('update', {
@@ -346,13 +353,18 @@ class ResourceService {
346
353
  const includeRelations = (0, utils_2.mapRelationInclusions)(this._client.name, 'update');
347
354
  let updateResource;
348
355
  let resource;
356
+ let orphans;
349
357
  try {
350
- [updateResource, resource] = await this._db
358
+ [updateResource, resource, orphans] = await this._db
351
359
  .client()
352
360
  .$transaction(async (tx) => {
353
361
  const txModel = tx[this._client.name];
354
362
  const current = await txModel.findFirst({
355
- where: { id, ...readRestrictions },
363
+ where: {
364
+ id,
365
+ ...readRestrictions,
366
+ ...(0, utils_1.liveRecordFilter)(this._client.name)
367
+ },
356
368
  include: includeRelations
357
369
  });
358
370
  if (!current || current.id !== id) {
@@ -363,12 +375,18 @@ class ResourceService {
363
375
  throw new errors_1.HttpError(`${this._client.name} update action is forbidden`, 403);
364
376
  }
365
377
  const setRelations = (0, utils_2.mapRelationActions)(this._client.name, 'update', sanitizedData, current);
378
+ await (0, utils_2.assertLiveRelationTargets)(tx, this._client.name, setRelations);
379
+ // Soft deleted orphans are marked before the update reads the
380
+ // relations back, so the response no longer holds them
381
+ const deleteData = (0, utils_2.softDeleteData)();
382
+ const removed = await (0, utils_2.removeOrphans)(tx, this._client.name, setRelations, deleteData);
383
+ await (0, utils_2.retainDeletedFiles)(tx, removed, deleteData);
366
384
  const updated = await txModel.update({
367
385
  where: { id },
368
386
  include: includeRelations,
369
387
  data: { ...sanitizedData, ...setRelations }
370
388
  });
371
- return [current, updated];
389
+ return [current, updated, removed];
372
390
  });
373
391
  }
374
392
  catch (e) {
@@ -378,6 +396,7 @@ class ResourceService {
378
396
  throw new errors_1.HttpError(`${this._client.name} update error`, 500, e);
379
397
  }
380
398
  await this._cacheService.invalidateCache(this._client.name, 'update');
399
+ await this.cleanupDeletedRecords(orphans);
381
400
  this._events.emitResourceEvent(this._client.name, 'update', {
382
401
  previous: updateResource,
383
402
  current: resource
@@ -387,24 +406,43 @@ class ResourceService {
387
406
  /**
388
407
  * Deletes an existing resource by its id. The current record is loaded with
389
408
  * the read restrictions applied and checked for access before it is deleted
390
- * inside a single transaction. The resource cache is invalidated and a
391
- * resource event is emitted after a successful delete.
409
+ * inside a single transaction.
410
+ *
411
+ * A model with `softDelete` enabled marks the record with the `deletedAt`
412
+ * and `deletedById` columns instead, soft deleting its cascade as well.
413
+ *
414
+ * The stored files of the deleted records follow the `onResourceDeleted`, or
415
+ * for soft deleted records the `onResourceSoftDeleted`, option of each file
416
+ * field: kept files are marked deleted in the transaction and no longer
417
+ * served, the others are removed once it commits. The cache of every affected
418
+ * model is invalidated and a resource event is emitted.
392
419
  *
393
420
  * @param {ResourceId} id The id of the resource to delete.
394
421
  * @returns {Promise<Object>} The deleted resource with its virtual fields and
395
422
  * relation counts projected.
396
423
  * @throws {@link HttpError} 404 if the resource does not exist or is filtered
397
- * out by the read restrictions, 403 if the access check denies the action, and
398
- * 500 on a database error.
424
+ * out by the read restrictions, 403 if the access check denies it, 409 if a
425
+ * restricting relation still references a soft deleted resource, and 500 on a
426
+ * database error, a restricting relation of a removed resource included.
399
427
  */
400
428
  async delete(id) {
401
429
  const restrictions = await this.readRestrictions('delete', id);
430
+ const softDelete = (0, common_1.hasSoftDelete)((0, context_1.injectModel)(this._client.name).config);
431
+ const includeRelations = (0, utils_2.mapRelationInclusions)(this._client.name, 'delete');
402
432
  let resource;
433
+ let deleted;
403
434
  try {
404
- resource = await this._db.client().$transaction(async (tx) => {
435
+ [resource, deleted] = await this._db.client().$transaction(async (tx) => {
405
436
  const txModel = tx[this._client.name];
437
+ // A soft deleted record is read with its relations up front, as a
438
+ // database delete returns them before the cascade removes them
406
439
  const current = await txModel.findFirst({
407
- where: { id, ...restrictions }
440
+ where: {
441
+ id,
442
+ ...restrictions,
443
+ ...(0, utils_1.liveRecordFilter)(this._client.name)
444
+ },
445
+ include: softDelete ? includeRelations : undefined
408
446
  });
409
447
  if (!current || current.id !== id) {
410
448
  throw new errors_1.HttpError(`${this._client.name} data not found`, 404);
@@ -413,11 +451,28 @@ class ResourceService {
413
451
  if (!access) {
414
452
  throw new errors_1.HttpError(`${this._client.name} delete action is forbidden`, 403);
415
453
  }
416
- const includeRelations = (0, utils_2.mapRelationInclusions)(this._client.name, 'delete');
417
- return await txModel.delete({
418
- where: { id },
419
- include: includeRelations
420
- });
454
+ const data = (0, utils_2.softDeleteData)();
455
+ let result;
456
+ let records;
457
+ if (softDelete) {
458
+ records = await (0, utils_2.softDeleteCascade)(tx, this._client.name, [id], data);
459
+ const updated = await txModel.update({ where: { id }, data });
460
+ result = { ...current, ...updated };
461
+ }
462
+ else {
463
+ records = await (0, utils_2.cascadedRecords)(tx, this._client.name, [id]);
464
+ result = await txModel.delete({
465
+ where: { id },
466
+ include: includeRelations
467
+ });
468
+ }
469
+ // Merged rather than spread, since a self cascade holds the same model
470
+ (0, utils_2.mergeAffectedRecords)(records, { [this._client.name]: [id] });
471
+ const removed = softDelete
472
+ ? { soft: records, hard: {} }
473
+ : { soft: {}, hard: records };
474
+ await (0, utils_2.retainDeletedFiles)(tx, removed, data);
475
+ return [result, removed];
421
476
  });
422
477
  }
423
478
  catch (e) {
@@ -426,7 +481,7 @@ class ResourceService {
426
481
  }
427
482
  throw new errors_1.HttpError(`${this._client.name} delete error`, 500, e);
428
483
  }
429
- await this._cacheService.invalidateCache(this._client.name, 'delete');
484
+ await this.cleanupDeletedRecords(deleted);
430
485
  this._events.emitResourceEvent(this._client.name, 'delete', {
431
486
  current: resource
432
487
  });
@@ -541,7 +596,7 @@ class ResourceService {
541
596
  * and the `_count` property removed.
542
597
  */
543
598
  projectResource(resource) {
544
- const projectedResource = (0, utils_1.projectVirtualFields)(resource, this._client.name);
599
+ const projectedResource = (0, utils_1.projectVirtualFields)((0, utils_2.hideDeletedRelations)(resource, this._client.name), this._client.name);
545
600
  if (!(0, common_1.isPlainObject)(projectedResource['_count'])) {
546
601
  return projectedResource;
547
602
  }
@@ -552,6 +607,38 @@ class ResourceService {
552
607
  delete projectedResource['_count'];
553
608
  return projectedResource;
554
609
  }
610
+ /**
611
+ * Lists the conditions hiding the soft deleted resources from a query. It is
612
+ * empty for a model that deletes its records, so its query and the cursors
613
+ * bound to it stay the same.
614
+ *
615
+ * @returns {Object[]} The `deletedAt: null` condition, or no condition.
616
+ */
617
+ liveFilters() {
618
+ const liveFilter = (0, utils_1.liveRecordFilter)(this._client.name);
619
+ return Object.keys(liveFilter).length > 0 ? [liveFilter] : [];
620
+ }
621
+ /**
622
+ * Cleans up after records were deleted, once the deleting transaction
623
+ * commits: removes the stored files their fields do not keep, since the
624
+ * storage cannot take part in the transaction, and invalidates the cache of
625
+ * every model they belong to.
626
+ *
627
+ * @param {DeletedRecords} deleted The soft deleted and the removed records.
628
+ */
629
+ async cleanupDeletedRecords(deleted) {
630
+ const fileService = (0, context_1.inject)(storage_1.FileService, false);
631
+ const groups = [
632
+ [true, deleted.soft],
633
+ [false, deleted.hard]
634
+ ];
635
+ for (const [softDeleted, records] of groups) {
636
+ for (const [modelName, ids] of Object.entries(records)) {
637
+ await fileService?.deleteResourcesFiles(modelName, ids, softDeleted);
638
+ await this._cacheService.invalidateCache(modelName, 'delete');
639
+ }
640
+ }
641
+ }
555
642
  /**
556
643
  * Prepares a write payload for the database by removing the virtual fields
557
644
  * that have no column, filling in default values for the hidden required
@@ -0,0 +1,120 @@
1
+ import { ResourceId } from '@appweaver/common';
2
+ import type { RelationActions } from './relation-util';
3
+ /** The records of every model affected by a delete, keyed by model name. */
4
+ export type AffectedRecords = Record<string, ResourceId[]>;
5
+ /** The records deleted by one action, split by how they were deleted. */
6
+ export type DeletedRecords = {
7
+ /** Records marked with the soft delete columns */
8
+ soft: AffectedRecords;
9
+ /** Records removed from the database */
10
+ hard: AffectedRecords;
11
+ };
12
+ /** The columns marking a record as soft deleted. */
13
+ export type SoftDeleteData = {
14
+ deletedAt: Date;
15
+ deletedById?: ResourceId | null;
16
+ };
17
+ /**
18
+ * Restricts a mapped relation filter to the related records that are not soft deleted, so a filter never matches
19
+ * through a deleted record, and a null check treats a deleted related record as missing.
20
+ *
21
+ * @param {*} condition - The mapped database condition of the relation field.
22
+ * @param {string} resourceName - The name of the related model.
23
+ * @param {boolean} isArrayType - Whether the relation is a list (to-many) relation.
24
+ * @return {*} The condition restricted to the live related records, or unchanged if the related model does not soft
25
+ * delete its records.
26
+ */
27
+ export declare function liveRelationFilter(condition: any, resourceName: string, isArrayType: boolean): any;
28
+ /**
29
+ * Builds the inclusion filter of a list relation, so only the related records that are not soft deleted are read.
30
+ *
31
+ * @param {string} [resourceName] - The name of the related model.
32
+ * @return {Object|undefined} The `where` clause of the inclusion, or undefined if the related model does not soft
33
+ * delete its records.
34
+ */
35
+ export declare function liveInclusionFilter(resourceName?: string): {
36
+ where: Record<string, any>;
37
+ } | undefined;
38
+ /**
39
+ * Replaces the soft deleted records in the single relations of a resource with null, recursively. Unlike the list
40
+ * relations, a single relation cannot be filtered when it is read.
41
+ *
42
+ * @param {Object} resource - The resource as returned by the database client.
43
+ * @param {string} resourceName - The name of the model describing the resource.
44
+ * @return {Object} A copy of the resource without the soft deleted related records, or the resource itself if it
45
+ * holds no relations.
46
+ */
47
+ export declare function hideDeletedRelations<T>(resource: T, resourceName: string): T;
48
+ /**
49
+ * Builds the column values marking a record as soft deleted now by the current user, who is only recorded when the
50
+ * application defines an auth model.
51
+ *
52
+ * @return {SoftDeleteData} The soft delete column values.
53
+ */
54
+ export declare function softDeleteData(): SoftDeleteData;
55
+ /**
56
+ * Mirrors the referential actions of a database delete for soft deleted records: cascading records are soft deleted
57
+ * with the same column values, level by level, a restricting relation aborts the delete, and references set to null on
58
+ * delete are kept for a manual restore. The records themselves are not updated.
59
+ *
60
+ * @param {Object} tx - The transaction client the delete runs in.
61
+ * @param {string} resourceName - The name of the model of the deleted records.
62
+ * @param {ResourceId[]} ids - The ids of the deleted records.
63
+ * @param {SoftDeleteData} data - The soft delete column values applied to the cascaded records.
64
+ * @return {Promise<AffectedRecords>} The ids of the soft deleted records per model, excluding the records themselves.
65
+ * @throws {HttpError} 409 if a live record references a deleted record through a restricting relation.
66
+ */
67
+ export declare function softDeleteCascade(tx: any, resourceName: string, ids: ResourceId[], data: SoftDeleteData): Promise<AffectedRecords>;
68
+ /**
69
+ * Collects the records the database cascade removes together with deleted records, so their files and caches can be
70
+ * cleaned up once the delete commits.
71
+ *
72
+ * @param {Object} tx - The transaction client the delete runs in.
73
+ * @param {string} resourceName - The name of the model of the deleted records.
74
+ * @param {ResourceId[]} ids - The ids of the deleted records.
75
+ * @return {Promise<AffectedRecords>} The ids of the cascaded records per model, excluding the records themselves.
76
+ */
77
+ export declare function cascadedRecords(tx: any, resourceName: string, ids: ResourceId[]): Promise<AffectedRecords>;
78
+ /**
79
+ * Removes the orphans an update maps to a nested `delete` action (`orphanRemoval`). Orphans of a soft deleted model are
80
+ * soft deleted with their cascade and keep their references, so their `delete` action is dropped; the others are left
81
+ * to the `delete` action, collecting the records its cascade removes.
82
+ *
83
+ * @param {Object} tx - The transaction client the update runs in.
84
+ * @param {string} resourceName - The name of the model the relation actions belong to.
85
+ * @param {RelationActions} relationActions - The mapped relation write actions, from which the nested `delete` actions
86
+ * of the soft deleted orphans are removed.
87
+ * @param {SoftDeleteData} data - The soft delete column values applied to the soft deleted orphans.
88
+ * @return {Promise<DeletedRecords>} The ids of the soft deleted and the removed records per model.
89
+ * @throws {HttpError} 409 if a live record references a soft deleted orphan through a restricting relation.
90
+ */
91
+ export declare function removeOrphans(tx: any, resourceName: string, relationActions: RelationActions, data: SoftDeleteData): Promise<DeletedRecords>;
92
+ /**
93
+ * Marks the file rows of the files the deleted records keep (`onResourceDeleted` / `onResourceSoftDeleted: 'keep'`) as
94
+ * deleted with the values of the delete, so they stay stored, e.g. for audit, but are no longer served.
95
+ *
96
+ * @param {Object} tx - The transaction client the delete runs in.
97
+ * @param {DeletedRecords} deleted - The soft deleted and the removed records.
98
+ * @param {SoftDeleteData} data - The soft delete column values of the delete.
99
+ * @return {Promise<void>} Resolves when the kept files are marked deleted.
100
+ */
101
+ export declare function retainDeletedFiles(tx: any, deleted: DeletedRecords, data: SoftDeleteData): Promise<void>;
102
+ /**
103
+ * Merges the affected records of several deletes into the target, keeping the ids of every model unique.
104
+ *
105
+ * @param {AffectedRecords} target - The affected records merged into.
106
+ * @param {AffectedRecords[]} sources - The affected records to add.
107
+ * @return {AffectedRecords} The target holding the ids of every source.
108
+ */
109
+ export declare function mergeAffectedRecords(target: AffectedRecords, ...sources: AffectedRecords[]): AffectedRecords;
110
+ /**
111
+ * Rejects the relation write actions pointing at soft deleted records, which the database would otherwise connect or
112
+ * update, since it does not know the record is deleted.
113
+ *
114
+ * @param {Object} client - The database client, or the transaction client the write runs in.
115
+ * @param {string} resourceName - The name of the model the relation actions belong to.
116
+ * @param {RelationActions} relationActions - The mapped relation write actions.
117
+ * @return {Promise<void>} Resolves when no action points at a soft deleted record.
118
+ * @throws {HttpError} 400 if a connect, update or connect-or-create action matches a soft deleted record.
119
+ */
120
+ export declare function assertLiveRelationTargets(client: any, resourceName: string, relationActions: RelationActions): Promise<void>;