@adobe/spacecat-shared-data-access 1.60.2 → 1.61.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 (43) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +5 -5
  3. package/src/v2/errors/data-access.error.js +24 -0
  4. package/src/v2/errors/index.d.ts +5 -1
  5. package/src/v2/errors/index.js +11 -1
  6. package/src/v2/errors/reference.error.js +22 -0
  7. package/src/v2/{models/base/constants.js → errors/schema-validation.error.js} +4 -6
  8. package/src/v2/errors/schema.builder.error.js +27 -0
  9. package/src/v2/errors/schema.error.js +19 -0
  10. package/src/v2/errors/validation.error.js +3 -1
  11. package/src/v2/models/api-key/index.d.ts +15 -2
  12. package/src/v2/models/audit/audit.collection.js +25 -1
  13. package/src/v2/models/audit/audit.schema.js +3 -0
  14. package/src/v2/models/audit/index.d.ts +16 -11
  15. package/src/v2/models/base/base.collection.js +148 -85
  16. package/src/v2/models/base/base.model.js +73 -14
  17. package/src/v2/models/base/entity.registry.js +7 -2
  18. package/src/v2/models/base/index.d.ts +30 -11
  19. package/src/v2/models/base/reference.js +81 -28
  20. package/src/v2/models/base/schema.builder.js +96 -24
  21. package/src/v2/models/base/schema.js +78 -10
  22. package/src/v2/models/configuration/index.d.ts +24 -90
  23. package/src/v2/models/experiment/index.d.ts +11 -3
  24. package/src/v2/models/import-job/index.d.ts +10 -3
  25. package/src/v2/models/import-url/index.d.ts +6 -3
  26. package/src/v2/models/index.d.ts +3 -0
  27. package/src/v2/models/index.js +1 -0
  28. package/src/v2/models/key-event/index.d.ts +5 -1
  29. package/src/v2/models/latest-audit/index.d.ts +43 -0
  30. package/src/v2/models/latest-audit/index.js +19 -0
  31. package/src/v2/models/latest-audit/latest-audit.collection.js +32 -0
  32. package/src/v2/models/latest-audit/latest-audit.model.js +26 -0
  33. package/src/v2/models/latest-audit/latest-audit.schema.js +72 -0
  34. package/src/v2/models/opportunity/index.d.ts +17 -1
  35. package/src/v2/models/opportunity/opportunity.schema.js +1 -0
  36. package/src/v2/models/organization/index.d.ts +3 -1
  37. package/src/v2/models/site/index.d.ts +43 -6
  38. package/src/v2/models/site/site.model.js +0 -6
  39. package/src/v2/models/site/site.schema.js +2 -0
  40. package/src/v2/models/site-candidate/index.d.ts +5 -7
  41. package/src/v2/models/site-top-page/index.d.ts +16 -2
  42. package/src/v2/models/suggestion/index.d.ts +2 -0
  43. package/src/v2/util/patcher.js +11 -0
@@ -18,15 +18,15 @@ import {
18
18
 
19
19
  import { ElectroValidationError } from 'electrodb';
20
20
 
21
- import { createAccessors } from '../../util/accessor.utils.js';
21
+ import DataAccessError from '../../errors/data-access.error.js';
22
22
  import ValidationError from '../../errors/validation.error.js';
23
+ import { createAccessors } from '../../util/accessor.utils.js';
23
24
  import { guardId } from '../../util/guards.js';
24
25
  import {
25
26
  entityNameToAllPKValue,
26
27
  isNonEmptyArray,
27
28
  removeElectroProperties,
28
29
  } from '../../util/util.js';
29
- import { INDEX_TYPES } from './constants.js';
30
30
 
31
31
  function isValidParent(parent, child) {
32
32
  if (!hasText(parent.entityName)) {
@@ -38,30 +38,6 @@ function isValidParent(parent, child) {
38
38
  return child.record?.[foreignKey] === parent.record?.[foreignKey];
39
39
  }
40
40
 
41
- /**
42
- * Finds the index name by the keys provided. The index is searched
43
- * keys to match the combination of partition and sort keys. If no
44
- * index is found, we fall back to the "all" index, then the "primary".
45
- * @param {Schema} schema - The schema to search for the index.
46
- * @param {Object} keys - The keys to search for.
47
- * @return {*|string} - The index name.
48
- */
49
- function findIndexNameByKeys(schema, keys) {
50
- const keyNames = Object.keys(keys);
51
-
52
- const index = schema.findIndexBySortKeys(keyNames);
53
- if (index) {
54
- return index.index;
55
- }
56
-
57
- const allIndex = schema.findIndexByType(INDEX_TYPES.ALL);
58
- if (allIndex) {
59
- return allIndex.index;
60
- }
61
-
62
- return INDEX_TYPES.PRIMARY;
63
- }
64
-
65
41
  /**
66
42
  * BaseCollection - A base class for managing collections of entities in the application.
67
43
  * This class uses ElectroDB to interact with entities and provides common functionality
@@ -93,6 +69,12 @@ class BaseCollection {
93
69
  this.#initializeCollectionMethods();
94
70
  }
95
71
 
72
+ #logAndThrowError(message, cause) {
73
+ const error = new DataAccessError(message, this, cause);
74
+ this.log.error(`Base Collection Error [${this.entityName}]`, error);
75
+ throw error;
76
+ }
77
+
96
78
  /**
97
79
  * Initialize collection methods for each "by..." index defined in the entity schema.
98
80
  * For each index that starts with "by", we:
@@ -146,72 +128,142 @@ class BaseCollection {
146
128
  return records.map((record) => this.#createInstance(record));
147
129
  }
148
130
 
131
+ /**
132
+ * Clears the accessor cache for the entity. This method is called when the entity is
133
+ * updated or removed to ensure that the cache is invalidated.
134
+ * @private
135
+ */
149
136
  #invalidateCache() {
150
137
  this._accessorCache = {};
151
138
  }
152
139
 
140
+ /**
141
+ * Internal on-create handler. This method is called after the create method has successfully
142
+ * created an entity. It will call the on-create handler defined in the subclass and handles
143
+ * any errors that occur.
144
+ * @param {BaseModel} item - The created entity.
145
+ * @return {Promise<void>}
146
+ * @async
147
+ * @private
148
+ */
149
+ async #onCreate(item) {
150
+ try {
151
+ await this._onCreate(item);
152
+ } catch (error) {
153
+ this.log.error('On-create handler failed', error);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Internal on-create-many handler. This method is called after the createMany method has
159
+ * successfully created entities. It will call the on-create-many handler defined in the
160
+ * subclass and handles any errors that occur.
161
+ * @param {Array<BaseModel>} createdItems - The created entities.
162
+ * @param {{ item: Object, error: ValidationError }[]} errorItems - Items that failed validation.
163
+ * @return {Promise<void>}
164
+ * @async
165
+ * @private
166
+ */
167
+ async #onCreateMany({ createdItems, errorItems }) {
168
+ try {
169
+ await this._onCreateMany({ createdItems, errorItems });
170
+ } catch (error) {
171
+ this.log.error('On-create-many handler failed', error);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Handler for the create method. This method is
177
+ * called after the create method has successfully created an entity.
178
+ * @param {BaseModel} item - The created entity.
179
+ * @return {Promise<void>}
180
+ * @async
181
+ * @protected
182
+ */
183
+ // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-unused-vars
184
+ async _onCreate(item) {
185
+ // no-op
186
+ }
187
+
188
+ /**
189
+ * Handler for the createMany method. This method is
190
+ * called after the createMany method has successfully created entities.
191
+ * @param {Array<BaseModel>} createdItems - The created entities.
192
+ * @param {{ item: Object, error: ValidationError }[]} errorItems - Items that failed validation.
193
+ * @return {Promise<void>}
194
+ * @async
195
+ * @protected
196
+ */
197
+ // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-unused-vars
198
+ async _onCreateMany({ createdItems, errorItems }) {
199
+ // no-op
200
+ }
201
+
153
202
  /**
154
203
  * General method to query entities by index keys. This method is used by other
155
204
  * query methods to perform the actual query operation. It will use the index keys
156
205
  * to find the appropriate index and query the entities. The query result will be
157
206
  * transformed into model instances.
207
+ *
158
208
  * @private
209
+ * @async
159
210
  * @param {Object} keys - The index keys to use for the query.
160
211
  * @param {Object} options - Additional options for the query.
161
212
  * @returns {Promise<BaseModel|Array<BaseModel>|null>} - The query result.
213
+ * @throws {DataAccessError} - Throws an error if the keys are not provided,
214
+ * if options are invalid or if the query operation fails.
162
215
  */
163
216
  async #queryByIndexKeys(keys, options = {}) {
164
217
  if (!isNonEmptyObject(keys)) {
165
- const message = `Failed to query [${this.entityName}]: keys are required`;
166
- this.log.error(message);
167
- throw new Error(message);
218
+ return this.#logAndThrowError(`Failed to query [${this.entityName}]: keys are required`);
168
219
  }
169
220
 
170
221
  if (!isObject(options)) {
171
- const message = `Failed to query [${this.entityName}]: options must be an object`;
172
- this.log.error(message);
173
- throw new Error(message);
222
+ return this.#logAndThrowError(`Failed to query [${this.entityName}]: options must be an object`);
174
223
  }
175
224
 
176
- const indexName = options.index || findIndexNameByKeys(this.schema, keys);
225
+ const indexName = options.index || this.schema.findIndexNameByKeys(keys);
177
226
  const index = this.entity.query[indexName];
178
227
 
179
228
  if (!index) {
180
- const message = `Failed to query [${this.entityName}]: query proxy [${indexName}] not found`;
181
- this.log.error(message);
182
- throw new Error(message);
229
+ this.#logAndThrowError(`Failed to query [${this.entityName}]: query proxy [${indexName}] not found`);
183
230
  }
184
231
 
185
- const queryOptions = {
186
- order: options.order || 'desc',
187
- ...options.limit && { limit: options.limit },
188
- ...options.attributes && { attributes: options.attributes },
189
- };
190
-
191
- let query = index(keys);
192
-
193
- if (isObject(options.between)) {
194
- query = query.between(
195
- { [options.between.attribute]: options.between.start },
196
- { [options.between.attribute]: options.between.end },
197
- );
198
- }
232
+ try {
233
+ const queryOptions = {
234
+ order: options.order || 'desc',
235
+ ...options.limit && { limit: options.limit },
236
+ ...options.attributes && { attributes: options.attributes },
237
+ };
238
+
239
+ let query = index(keys);
240
+
241
+ if (isObject(options.between)) {
242
+ query = query.between(
243
+ { [options.between.attribute]: options.between.start },
244
+ { [options.between.attribute]: options.between.end },
245
+ );
246
+ }
199
247
 
200
- const records = await query.go(queryOptions);
248
+ const records = await query.go(queryOptions);
201
249
 
202
- if (options.limit === 1) {
203
- if (records.data?.length === 0) {
204
- return null;
250
+ if (options.limit === 1) {
251
+ if (records.data?.length === 0) {
252
+ return null;
253
+ }
254
+ return this.#createInstance(records.data[0]);
255
+ } else {
256
+ return this.#createInstances(records.data);
205
257
  }
206
- return this.#createInstance(records.data[0]);
207
- } else {
208
- return this.#createInstances(records.data);
258
+ } catch (error) {
259
+ return this.#logAndThrowError('Failed to query', error);
209
260
  }
210
261
  }
211
262
 
212
263
  /**
213
264
  * Finds all entities in the collection. Requires an index named "all" with a partition key
214
265
  * named "pk" with a static value of "ALL_<ENTITYNAME>".
266
+ * @async
215
267
  * @param {Object} [sortKeys] - The sort keys to use for the query.
216
268
  * @param {Object} [options] - Additional options for the query.
217
269
  * @return {Promise<BaseModel|Array<BaseModel>|null>}
@@ -237,18 +289,20 @@ class BaseCollection {
237
289
  }
238
290
 
239
291
  /**
240
- * Finds a single entity from the "all" index. Requires an index named "all" with a partition key
241
- * named "pk" with a static value of "ALL_<ENTITYNAME>".
292
+ * Finds a single entity from the "all" index. Requires an "all" index to be added to the
293
+ * entity schema via the schema builder.
294
+ * @async
242
295
  * @param {Object} [sortKeys] - The sort keys to use for the query.
243
- * @param {{index?: string, attributes?: string[], order?: string}} [options] -
296
+ * @param {QueryOptions} [options] - Additional options for the query.
244
297
  * Additional options for the query.
245
- * @return {Promise<BaseModel|Array<BaseModel>|null>}
298
+ * @return {Promise<BaseModel|null>}
299
+ * @throws {DataAccessError} - Throws an error if the sort keys are not provided.
246
300
  */
247
301
  async findByAll(sortKeys = {}, options = {}) {
248
302
  if (!isObject(sortKeys)) {
249
303
  const message = `Failed to find by all [${this.entityName}]: sort keys must be an object`;
250
304
  this.log.error(message);
251
- throw new Error(message);
305
+ throw new DataAccessError(message);
252
306
  }
253
307
 
254
308
  const keys = { pk: entityNameToAllPKValue(this.entityName), ...sortKeys };
@@ -256,12 +310,13 @@ class BaseCollection {
256
310
  }
257
311
 
258
312
  /**
259
- * Finds an entity by its ID.
313
+ * Finds an entity by its ID. This will only work if the entity's schema
314
+ * did not override the main table primary key via schema builder.
260
315
  * @async
261
316
  * @param {string} id - The unique identifier of the entity to be found.
262
317
  * @returns {Promise<BaseModel|null>} - A promise that resolves to an instance of
263
318
  * the model if found, otherwise null.
264
- * @throws {Error} - Throws an error if the ID is not provided.
319
+ * @throws {ValidationError} - Throws an error if the ID is not provided.
265
320
  */
266
321
  async findById(id) {
267
322
  guardId(this.idName, id, this.entityName);
@@ -276,6 +331,7 @@ class BaseCollection {
276
331
  * @param {Object} keys - The index keys to use for the query.
277
332
  * @param {{index?: string, attributes?: string[]}} [options] - Additional options for the query.
278
333
  * @returns {Promise<BaseModel|null>} - A promise that resolves to the model instance or null.
334
+ * @throws {DataAccessError} - Throws an error if retrieving the entity fails.
279
335
  * @async
280
336
  */
281
337
  async findByIndexKeys(keys, options = {}) {
@@ -289,13 +345,14 @@ class BaseCollection {
289
345
  * @async
290
346
  * @param {Object} item - The data for the entity to be created.
291
347
  * @returns {Promise<BaseModel>} - A promise that resolves to the created model instance.
292
- * @throws {Error} - Throws an error if the data is invalid or if the creation process fails.
348
+ * @throws {DataAccessError} - Throws an error if the data is invalid or if the
349
+ * creation process fails.
293
350
  */
294
351
  async create(item) {
295
352
  if (!isNonEmptyObject(item)) {
296
353
  const message = `Failed to create [${this.entityName}]: data is required`;
297
354
  this.log.error(message);
298
- throw new Error(message);
355
+ throw new DataAccessError(message);
299
356
  }
300
357
 
301
358
  try {
@@ -303,11 +360,11 @@ class BaseCollection {
303
360
  const instance = this.#createInstance(record.data);
304
361
 
305
362
  this.#invalidateCache();
363
+ await this.#onCreate(instance);
306
364
 
307
365
  return instance;
308
366
  } catch (error) {
309
- this.log.error(`Failed to create [${this.entityName}]`, error);
310
- throw error;
367
+ return this.#logAndThrowError('Failed to create', error);
311
368
  }
312
369
  }
313
370
 
@@ -345,14 +402,14 @@ class BaseCollection {
345
402
  * @return {Promise<{ createdItems: BaseModel[],
346
403
  * errorItems: { item: Object, error: ValidationError }[] }>} - A promise that resolves to
347
404
  * an object containing the created items and any items that failed validation.
348
- * @throws {ValidationError} - Throws a validation error if any of the items has validation
349
- * failures.
405
+ * @throws {DataAccessError} - Throws an error if the items are not provided or if the
406
+ * creation process fails.
350
407
  */
351
408
  async createMany(newItems, parent = null) {
352
409
  if (!isNonEmptyArray(newItems)) {
353
410
  const message = `Failed to create many [${this.entityName}]: items must be a non-empty array`;
354
411
  this.log.error(message);
355
- throw new Error(message);
412
+ throw new DataAccessError(message);
356
413
  }
357
414
 
358
415
  try {
@@ -383,10 +440,11 @@ class BaseCollection {
383
440
 
384
441
  this.log.info(`Created ${createdItems.length} items for [${this.entityName}]`);
385
442
 
443
+ await this.#onCreateMany({ createdItems, errorItems });
444
+
386
445
  return { createdItems, errorItems };
387
446
  } catch (error) {
388
- this.log.error(`Failed to create many [${this.entityName}]`, error);
389
- throw error;
447
+ return this.#logAndThrowError('Failed to create many', error);
390
448
  }
391
449
  }
392
450
 
@@ -396,28 +454,29 @@ class BaseCollection {
396
454
  * @async
397
455
  * @param {Array<BaseModel>} items - An array of model instances to be updated.
398
456
  * @return {Promise<void>} - A promise that resolves when the update operation is complete.
399
- * @throws {Error} - Throws an error if the update operation fails.
457
+ * @throws {DataAccessError} - Throws an error if the items are not provided or if the
458
+ * update operation fails.
459
+ *
400
460
  * @protected
401
461
  */
402
462
  async _saveMany(items) {
403
463
  if (!isNonEmptyArray(items)) {
404
464
  const message = `Failed to save many [${this.entityName}]: items must be a non-empty array`;
405
465
  this.log.error(message);
406
- throw new Error(message);
466
+ throw new DataAccessError(message);
407
467
  }
408
468
 
409
469
  try {
410
470
  const updates = items.map((item) => item.record);
411
471
  const response = await this.entity.put(updates).go();
412
472
 
413
- this.#invalidateCache();
414
-
415
473
  if (response.unprocessed) {
416
474
  this.log.error(`Failed to process all items in batch write for [${this.entityName}]: ${JSON.stringify(response.unprocessed)}`);
417
475
  }
476
+
477
+ return this.#invalidateCache();
418
478
  } catch (error) {
419
- this.log.error(`Failed to save many [${this.entityName}]`, error);
420
- throw error;
479
+ return this.#logAndThrowError('Failed to save many', error);
421
480
  }
422
481
  }
423
482
 
@@ -426,22 +485,26 @@ class BaseCollection {
426
485
  * delete operation. This operation does not remove dependent records.
427
486
  * @param {Array<string>} ids - An array of IDs to remove.
428
487
  * @return {Promise<void>} - A promise that resolves when the removal operation is complete.
429
- * @throws {Error} - Throws an error if the IDs are not provided or if the
488
+ * @throws {DataAccessError} - Throws an error if the IDs are not provided or if the
430
489
  * removal operation fails.
431
490
  */
432
491
  async removeByIds(ids) {
433
492
  if (!isNonEmptyArray(ids)) {
434
493
  const message = `Failed to remove [${this.entityName}]: ids must be a non-empty array`;
435
494
  this.log.error(message);
436
- throw new Error(message);
495
+ throw new DataAccessError(message);
437
496
  }
438
497
 
439
- this.log.info(`Removing ${ids.length} items for [${this.entityName}]`);
440
- // todo: consider removing dependent records
498
+ try {
499
+ this.log.info(`Removing ${ids.length} items for [${this.entityName}]`);
500
+ // todo: consider removing dependent records
441
501
 
442
- await this.entity.delete(ids.map((id) => ({ [this.idName]: id }))).go();
502
+ await this.entity.delete(ids.map((id) => ({ [this.idName]: id }))).go();
443
503
 
444
- this.#invalidateCache();
504
+ return this.#invalidateCache();
505
+ } catch (error) {
506
+ return this.#logAndThrowError('Failed to remove by IDs', error);
507
+ }
445
508
  }
446
509
  }
447
510
 
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { isNonEmptyObject } from '@adobe/spacecat-shared-utils';
14
14
 
15
+ import { DataAccessError } from '../../errors/index.js';
15
16
  import { createAccessors } from '../../util/accessor.utils.js';
16
17
  import Patcher from '../../util/patcher.js';
17
18
  import {
@@ -85,6 +86,18 @@ class BaseModel {
85
86
  });
86
87
  }
87
88
 
89
+ /**
90
+ * Initializes the attributes for the current entity. This method is called during the
91
+ * construction of the entity instance to set up the getter and setter methods for
92
+ * accessing and modifying the entity attributes. The getter and setter methods are
93
+ * automatically generated based on the entity schema. If the schema allows updates,
94
+ * setter methods are generated for each attribute that is not read-only.
95
+ *
96
+ * If the attribute is a reference, the setter method will tell the patcher
97
+ * to validate that the value is a valid UUID.
98
+ *
99
+ * @private
100
+ */
88
101
  #initializeAttributes() {
89
102
  const attributes = this.schema.getAttributes();
90
103
 
@@ -95,7 +108,6 @@ class BaseModel {
95
108
  for (const [name, attr] of Object.entries(attributes)) {
96
109
  const capitalized = capitalize(name);
97
110
  const getterMethodName = `get${capitalized}`;
98
- const setterMethodName = `set${capitalized}`;
99
111
  const isReference = this.schema
100
112
  .getReferencesByType(Reference.TYPES.BELONGS_TO)
101
113
  .some((ref) => ref.getTarget() === idNameToEntityName(name));
@@ -104,19 +116,35 @@ class BaseModel {
104
116
  this[getterMethodName] = () => this.record[name];
105
117
  }
106
118
 
107
- if (!this[setterMethodName] && !attr.readOnly) {
108
- this[setterMethodName] = (value) => {
109
- this.patcher.patchValue(name, value, isReference);
110
- return this;
111
- };
119
+ if (this.schema.allowsUpdates()) {
120
+ const setterMethodName = `set${capitalized}`;
121
+
122
+ if (!this[setterMethodName] && !attr.readOnly) {
123
+ this[setterMethodName] = (value) => {
124
+ this.patcher.patchValue(name, value, isReference);
125
+ return this;
126
+ };
127
+ }
112
128
  }
113
129
  }
114
130
  }
115
131
 
132
+ /**
133
+ * Clears the accessor cache for the entity. This method is called when the entity is
134
+ * updated or removed to ensure that the cache is invalidated.
135
+ * @private
136
+ */
116
137
  #invalidateCache() {
117
138
  this._accessorCache = {};
118
139
  }
119
140
 
141
+ /**
142
+ * Fetches the associated entities for the current entity based on the type of relationship.
143
+ * This is used for the remove operation to remove dependent entities associated with the
144
+ * current entity.
145
+ * @return {Promise<Array>}
146
+ * @private
147
+ */
120
148
  async #fetchDependents() {
121
149
  const promises = [];
122
150
 
@@ -184,18 +212,41 @@ class BaseModel {
184
212
  * removeDependentss flag is set to true in the reference definition.
185
213
  *
186
214
  * Dependents are removed by calling the remove method on each dependent entity, which in turn
187
- * will also remove any dependent entities associated with the dependent entity. This may result
188
- * in a cascade effect where multiple entities are removed. Consider the destructive
189
- * and performance implications before using this method.
215
+ * will also remove any dependent entities associated with the dependent entity. For dependent
216
+ * entities the allowRemove flag is ignored.
217
+ *
218
+ * Removal of entities with many dependents can be a costly operation, as each dependent entity
219
+ * will be removed individually. This can result in a large number of database operations, which
220
+ * can impact performance. It is recommended to use this method with caution, especially when
221
+ * removing entities with many dependents.
222
+ *
190
223
  * @async
191
224
  * @returns {Promise<BaseModel>} - A promise that resolves to the current instance of the entity
192
225
  * after it and its dependents have been removed.
193
- * @throws {Error} - Throws an error if the removal fails.
226
+ * @throws {DataAccessError} - Throws an error if the schema does not allow removal
227
+ * or if the removal operation fails.
194
228
  */
195
229
  async remove() {
230
+ if (!this.schema.allowsRemove()) {
231
+ throw new DataAccessError(`The entity ${this.schema.getModelName()} does not allow removal`);
232
+ }
233
+
234
+ return this._remove();
235
+ }
236
+
237
+ /**
238
+ * Internal remove method that removes the current entity from the database and its dependents.
239
+ * This method does not check if the schema allows removal in order to be able to remove
240
+ * dependents even if the schema does not allow removal.
241
+ * @return {Promise<BaseModel>}
242
+ * @throws {DataAccessError} - Throws an error if the removal operation fails.
243
+ * @protected
244
+ */
245
+ async _remove() {
196
246
  try {
197
247
  const dependents = await this.#fetchDependents();
198
- const removePromises = dependents.map((dependent) => dependent.remove());
248
+ // eslint-disable-next-line no-underscore-dangle
249
+ const removePromises = dependents.map((dependent) => dependent._remove());
199
250
  removePromises.push(this.entity.remove({ [this.idName]: this.getId() }).go());
200
251
 
201
252
  this.log.info(`Removing entity ${this.entityName} with ID ${this.getId()} and ${dependents.length} dependents`);
@@ -207,7 +258,11 @@ class BaseModel {
207
258
  return this;
208
259
  } catch (error) {
209
260
  this.log.error('Failed to remove record', error);
210
- throw error;
261
+ throw new DataAccessError(
262
+ `Failed to remove entity ${this.entityName} with ID ${this.getId()}`,
263
+ this,
264
+ error,
265
+ );
211
266
  }
212
267
  }
213
268
 
@@ -217,7 +272,7 @@ class BaseModel {
217
272
  * @async
218
273
  * @returns {Promise<BaseModel>} - A promise that resolves to the current instance of the entity
219
274
  * after it has been saved.
220
- * @throws {Error} - Throws an error if the save operation fails.
275
+ * @throws {DataAccessError} - Throws an error if the save operation fails.
221
276
  */
222
277
  async save() {
223
278
  // todo: validate associations
@@ -230,7 +285,11 @@ class BaseModel {
230
285
  return this;
231
286
  } catch (error) {
232
287
  this.log.error('Failed to save record', error);
233
- throw error;
288
+ throw new DataAccessError(
289
+ `Failed to to save entity ${this.entityName} with ID ${this.getId()}`,
290
+ this,
291
+ error,
292
+ );
234
293
  }
235
294
  }
236
295
 
@@ -10,6 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import { DataAccessError } from '../../errors/index.js';
13
14
  import { collectionNameToEntityName, decapitalize } from '../../util/util.js';
14
15
 
15
16
  import ApiKeyCollection from '../api-key/api-key.collection.js';
@@ -19,6 +20,7 @@ import ExperimentCollection from '../experiment/experiment.collection.js';
19
20
  import ImportJobCollection from '../import-job/import-job.collection.js';
20
21
  import ImportUrlCollection from '../import-url/import-url.collection.js';
21
22
  import KeyEventCollection from '../key-event/key-event.collection.js';
23
+ import LatestAuditCollection from '../latest-audit/latest-audit.collection.js';
22
24
  import OpportunityCollection from '../opportunity/opportunity.collection.js';
23
25
  import OrganizationCollection from '../organization/organization.collection.js';
24
26
  import SiteCandidateCollection from '../site-candidate/site-candidate.collection.js';
@@ -33,6 +35,7 @@ import ExperimentSchema from '../experiment/experiment.schema.js';
33
35
  import ImportJobSchema from '../import-job/import-job.schema.js';
34
36
  import ImportUrlSchema from '../import-url/import-url.schema.js';
35
37
  import KeyEventSchema from '../key-event/key-event.schema.js';
38
+ import LatestAuditSchema from '../latest-audit/latest-audit.schema.js';
36
39
  import OpportunitySchema from '../opportunity/opportunity.schema.js';
37
40
  import OrganizationSchema from '../organization/organization.schema.js';
38
41
  import SiteSchema from '../site/site.schema.js';
@@ -90,12 +93,13 @@ class EntityRegistry {
90
93
  * Gets a collection instance by its name.
91
94
  * @param {string} collectionName - The name of the collection to retrieve.
92
95
  * @returns {Object} - The requested collection instance.
93
- * @throws {Error} - Throws an error if the collection with the specified name is not found.
96
+ * @throws {DataAccessError} - Throws an error if the collection with the
97
+ * specified name is not found.
94
98
  */
95
99
  getCollection(collectionName) {
96
100
  const collection = this.collections.get(collectionName);
97
101
  if (!collection) {
98
- throw new Error(`Collection ${collectionName} not found`);
102
+ throw new DataAccessError(`Collection ${collectionName} not found`, this);
99
103
  }
100
104
  return collection;
101
105
  }
@@ -127,6 +131,7 @@ EntityRegistry.registerEntity(ExperimentSchema, ExperimentCollection);
127
131
  EntityRegistry.registerEntity(ImportJobSchema, ImportJobCollection);
128
132
  EntityRegistry.registerEntity(ImportUrlSchema, ImportUrlCollection);
129
133
  EntityRegistry.registerEntity(KeyEventSchema, KeyEventCollection);
134
+ EntityRegistry.registerEntity(LatestAuditSchema, LatestAuditCollection);
130
135
  EntityRegistry.registerEntity(OpportunitySchema, OpportunityCollection);
131
136
  EntityRegistry.registerEntity(OrganizationSchema, OrganizationCollection);
132
137
  EntityRegistry.registerEntity(SiteSchema, SiteCollection);