@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.
- package/CHANGELOG.md +14 -0
- package/package.json +5 -5
- package/src/v2/errors/data-access.error.js +24 -0
- package/src/v2/errors/index.d.ts +5 -1
- package/src/v2/errors/index.js +11 -1
- package/src/v2/errors/reference.error.js +22 -0
- package/src/v2/{models/base/constants.js → errors/schema-validation.error.js} +4 -6
- package/src/v2/errors/schema.builder.error.js +27 -0
- package/src/v2/errors/schema.error.js +19 -0
- package/src/v2/errors/validation.error.js +3 -1
- package/src/v2/models/api-key/index.d.ts +15 -2
- package/src/v2/models/audit/audit.collection.js +25 -1
- package/src/v2/models/audit/audit.schema.js +3 -0
- package/src/v2/models/audit/index.d.ts +16 -11
- package/src/v2/models/base/base.collection.js +148 -85
- package/src/v2/models/base/base.model.js +73 -14
- package/src/v2/models/base/entity.registry.js +7 -2
- package/src/v2/models/base/index.d.ts +30 -11
- package/src/v2/models/base/reference.js +81 -28
- package/src/v2/models/base/schema.builder.js +96 -24
- package/src/v2/models/base/schema.js +78 -10
- package/src/v2/models/configuration/index.d.ts +24 -90
- package/src/v2/models/experiment/index.d.ts +11 -3
- package/src/v2/models/import-job/index.d.ts +10 -3
- package/src/v2/models/import-url/index.d.ts +6 -3
- package/src/v2/models/index.d.ts +3 -0
- package/src/v2/models/index.js +1 -0
- package/src/v2/models/key-event/index.d.ts +5 -1
- package/src/v2/models/latest-audit/index.d.ts +43 -0
- package/src/v2/models/latest-audit/index.js +19 -0
- package/src/v2/models/latest-audit/latest-audit.collection.js +32 -0
- package/src/v2/models/latest-audit/latest-audit.model.js +26 -0
- package/src/v2/models/latest-audit/latest-audit.schema.js +72 -0
- package/src/v2/models/opportunity/index.d.ts +17 -1
- package/src/v2/models/opportunity/opportunity.schema.js +1 -0
- package/src/v2/models/organization/index.d.ts +3 -1
- package/src/v2/models/site/index.d.ts +43 -6
- package/src/v2/models/site/site.model.js +0 -6
- package/src/v2/models/site/site.schema.js +2 -0
- package/src/v2/models/site-candidate/index.d.ts +5 -7
- package/src/v2/models/site-top-page/index.d.ts +16 -2
- package/src/v2/models/suggestion/index.d.ts +2 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
225
|
+
const indexName = options.index || this.schema.findIndexNameByKeys(keys);
|
|
177
226
|
const index = this.entity.query[indexName];
|
|
178
227
|
|
|
179
228
|
if (!index) {
|
|
180
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
248
|
+
const records = await query.go(queryOptions);
|
|
201
249
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
|
241
|
-
*
|
|
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 {
|
|
296
|
+
* @param {QueryOptions} [options] - Additional options for the query.
|
|
244
297
|
* Additional options for the query.
|
|
245
|
-
* @return {Promise<BaseModel|
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
349
|
-
*
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
495
|
+
throw new DataAccessError(message);
|
|
437
496
|
}
|
|
438
497
|
|
|
439
|
-
|
|
440
|
-
|
|
498
|
+
try {
|
|
499
|
+
this.log.info(`Removing ${ids.length} items for [${this.entityName}]`);
|
|
500
|
+
// todo: consider removing dependent records
|
|
441
501
|
|
|
442
|
-
|
|
502
|
+
await this.entity.delete(ids.map((id) => ({ [this.idName]: id }))).go();
|
|
443
503
|
|
|
444
|
-
|
|
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 (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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);
|