@adobe/spacecat-shared-data-access 2.71.1 → 2.72.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 +7 -0
- package/README.md +44 -0
- package/package.json +1 -1
- package/src/models/base/base.collection.js +93 -1
- package/src/models/base/base.model.js +7 -1
- package/src/models/base/entity.registry.js +3 -0
- package/src/models/base/index.d.ts +5 -0
- package/src/models/base/schema.js +3 -1
- package/src/models/fix-entity/fix-entity.collection.js +221 -3
- package/src/models/fix-entity/fix-entity.model.js +6 -0
- package/src/models/fix-entity/fix-entity.schema.js +2 -2
- package/src/models/fix-entity/index.d.ts +3 -3
- package/src/models/fix-entity-suggestion/fix-entity-suggestion.collection.js +45 -0
- package/src/models/fix-entity-suggestion/fix-entity-suggestion.model.js +38 -0
- package/src/models/fix-entity-suggestion/fix-entity-suggestion.schema.js +49 -0
- package/src/models/fix-entity-suggestion/index.d.ts +32 -0
- package/src/models/fix-entity-suggestion/index.js +19 -0
- package/src/models/index.d.ts +1 -0
- package/src/models/index.js +1 -0
- package/src/models/opportunity/opportunity.model.js +136 -7
- package/src/models/suggestion/index.d.ts +2 -4
- package/src/models/suggestion/suggestion.collection.js +42 -1
- package/src/models/suggestion/suggestion.schema.js +1 -1
- package/src/util/util.js +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-data-access-v2.72.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v2.71.1...@adobe/spacecat-shared-data-access-v2.72.0) (2025-10-16)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Sites 34129 adds many to many relationship between Fix Entity and Suggestions ([#993](https://github.com/adobe/spacecat-shared/issues/993)) ([8739d2b](https://github.com/adobe/spacecat-shared/commit/8739d2b891d5df6ce883ba3b319047531419100e))
|
|
7
|
+
|
|
1
8
|
# [@adobe/spacecat-shared-data-access-v2.71.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v2.71.0...@adobe/spacecat-shared-data-access-v2.71.1) (2025-10-13)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -85,6 +85,38 @@ npm install @adobe/spacecat-shared-data-access
|
|
|
85
85
|
- **status** (String): Status of the enrollment. (ACTIVE, SUSPENDED, ENDED)
|
|
86
86
|
- **createdAt** (String): Timestamp of creation.
|
|
87
87
|
|
|
88
|
+
### FixEntity
|
|
89
|
+
- **fixEntityId** (String): Unique identifier for the fix entity.
|
|
90
|
+
- **opportunityId** (String): ID of the associated opportunity.
|
|
91
|
+
- **createdAt** (String): Timestamp of creation.
|
|
92
|
+
- **updatedAt** (String): Timestamp of the last update.
|
|
93
|
+
- **type** (String): Type of the fix entity (from Suggestion.TYPES).
|
|
94
|
+
- **status** (String): Status of the fix entity (PENDING, DEPLOYED, PUBLISHED, FAILED, ROLLED_BACK).
|
|
95
|
+
- **executedBy** (String): Who executed the fix.
|
|
96
|
+
- **executedAt** (String): When the fix was executed.
|
|
97
|
+
- **publishedAt** (String): When the fix was published.
|
|
98
|
+
- **changeDetails** (Object): Details of the changes made.
|
|
99
|
+
|
|
100
|
+
### Suggestion
|
|
101
|
+
- **suggestionId** (String): Unique identifier for the suggestion.
|
|
102
|
+
- **opportunityId** (String): ID of the associated opportunity.
|
|
103
|
+
- **createdAt** (String): Timestamp of creation.
|
|
104
|
+
- **updatedAt** (String): Timestamp of the last update.
|
|
105
|
+
- **status** (String): Status of the suggestion (NEW, APPROVED, IN_PROGRESS, SKIPPED, FIXED, ERROR, OUTDATED).
|
|
106
|
+
- **type** (String): Type of the suggestion (CODE_CHANGE, CONTENT_UPDATE, REDIRECT_UPDATE, METADATA_UPDATE, AI_INSIGHTS, CONFIG_UPDATE).
|
|
107
|
+
- **rank** (Number): Rank/priority of the suggestion.
|
|
108
|
+
- **data** (Object): Data payload for the suggestion.
|
|
109
|
+
- **kpiDeltas** (Object): KPI delta information (optional).
|
|
110
|
+
|
|
111
|
+
### FixEntitySuggestion
|
|
112
|
+
- **suggestionId** (String): ID of the associated suggestion (primary partition key).
|
|
113
|
+
- **fixEntityId** (String): ID of the associated fix entity (primary sort key).
|
|
114
|
+
- **opportunityId** (String): ID of the associated opportunity.
|
|
115
|
+
- **fixEntityCreatedAt** (String): Creation timestamp of the fix entity.
|
|
116
|
+
- **fixEntityCreatedDate** (String): Date portion of fixEntityCreatedAt (auto-generated).
|
|
117
|
+
- **createdAt** (String): Timestamp of creation.
|
|
118
|
+
- **updatedAt** (String): Timestamp of the last update.
|
|
119
|
+
|
|
88
120
|
## DynamoDB Data Model
|
|
89
121
|
|
|
90
122
|
The module is designed to work with the following DynamoDB tables:
|
|
@@ -144,6 +176,18 @@ The module provides the following DAOs:
|
|
|
144
176
|
- `getTopPagesForSite`
|
|
145
177
|
- `addSiteTopPage`
|
|
146
178
|
|
|
179
|
+
### FixEntity Functions
|
|
180
|
+
- `getSuggestionsByFixEntityId` - Gets all suggestions associated with a specific FixEntity
|
|
181
|
+
- `setSuggestionsForFixEntity` - Sets suggestions for a FixEntity by managing junction table relationships
|
|
182
|
+
|
|
183
|
+
### Suggestion Functions
|
|
184
|
+
- `bulkUpdateStatus` - Updates the status of multiple suggestions in bulk
|
|
185
|
+
- `getFixEntitiesBySuggestionId` - Gets all FixEntities associated with a specific Suggestion
|
|
186
|
+
|
|
187
|
+
### FixEntitySuggestion Functions
|
|
188
|
+
- `allBySuggestionId` - Gets all junction records associated with a specific Suggestion
|
|
189
|
+
- `allByFixEntityId` - Gets all junction records associated with a specific FixEntity
|
|
190
|
+
|
|
147
191
|
## Integrating Data Access in AWS Lambda Functions
|
|
148
192
|
|
|
149
193
|
Our `spacecat-shared-data-access` module includes a wrapper that can be easily integrated into AWS Lambda functions using `@adobe/helix-shared-wrap`.
|
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@ import { ElectroValidationError } from 'electrodb';
|
|
|
22
22
|
import DataAccessError from '../../errors/data-access.error.js';
|
|
23
23
|
import ValidationError from '../../errors/validation.error.js';
|
|
24
24
|
import { createAccessors } from '../../util/accessor.utils.js';
|
|
25
|
-
import { guardId } from '../../util/guards.js';
|
|
25
|
+
import { guardId, guardArray } from '../../util/guards.js';
|
|
26
26
|
import {
|
|
27
27
|
entityNameToAllPKValue,
|
|
28
28
|
removeElectroProperties,
|
|
@@ -361,6 +361,53 @@ class BaseCollection {
|
|
|
361
361
|
return isNonEmptyObject(record?.data);
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Retrieves multiple entities by their IDs in a single batch operation.
|
|
366
|
+
* This method is more efficient than calling findById multiple times.
|
|
367
|
+
*
|
|
368
|
+
* @async
|
|
369
|
+
* @param {Array<string>} ids - An array of entity IDs to retrieve.
|
|
370
|
+
* @param {{attributes?: string[]}} [options] - Additional options for the query.
|
|
371
|
+
* @returns {Promise<{data: Array<BaseModel>, unprocessed: Array<string>}>} - A promise that
|
|
372
|
+
* resolves
|
|
373
|
+
* to an object containing:
|
|
374
|
+
* - data: Array of found model instances
|
|
375
|
+
* - unprocessed: Array of IDs that couldn't be processed (due to throttling, etc.)
|
|
376
|
+
* @throws {DataAccessError} - Throws an error if the IDs are not provided or if the batch
|
|
377
|
+
* operation fails.
|
|
378
|
+
*/
|
|
379
|
+
async batchGetByKeys(keys, options = {}) {
|
|
380
|
+
guardArray('keys', keys, this.entityName, 'any');
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const goOptions = {};
|
|
384
|
+
|
|
385
|
+
// Add attributes if specified
|
|
386
|
+
if (options.attributes !== undefined) {
|
|
387
|
+
goOptions.attributes = options.attributes;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const result = await this.entity.get(
|
|
391
|
+
keys,
|
|
392
|
+
).go(goOptions);
|
|
393
|
+
|
|
394
|
+
// Process found entities
|
|
395
|
+
const data = result.data
|
|
396
|
+
.map((record) => this.#createInstance(record))
|
|
397
|
+
.filter((entity) => entity !== null);
|
|
398
|
+
|
|
399
|
+
// Extract unprocessed keys
|
|
400
|
+
const unprocessed = result.unprocessed
|
|
401
|
+
? result.unprocessed.map((item) => item)
|
|
402
|
+
: [];
|
|
403
|
+
|
|
404
|
+
return { data, unprocessed };
|
|
405
|
+
} catch (error) {
|
|
406
|
+
this.log.error(`Failed to batch get by keys [${this.entityName}]`, error);
|
|
407
|
+
throw new DataAccessError('Failed to batch get by keys', this, error);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
364
411
|
/**
|
|
365
412
|
* Finds a single entity by index keys.
|
|
366
413
|
* @param {Object} keys - The index keys to use for the query.
|
|
@@ -550,6 +597,51 @@ class BaseCollection {
|
|
|
550
597
|
return this.#logAndThrowError('Failed to remove by IDs', error);
|
|
551
598
|
}
|
|
552
599
|
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Removes records from the collection using an array of key objects for batch deletion.
|
|
603
|
+
* This method is particularly useful for junction tables in many-to-many relationships
|
|
604
|
+
* where you need to remove multiple records based on their composite keys.
|
|
605
|
+
*
|
|
606
|
+
* Each key object in the array represents a record to be deleted, identified by its
|
|
607
|
+
* key attributes (typically partition key + sort key combinations).
|
|
608
|
+
*
|
|
609
|
+
* @async
|
|
610
|
+
* @param {Array<Object>} keys - Array of key objects to match for deletion.
|
|
611
|
+
* Each object should contain the key attributes that uniquely identify a record.
|
|
612
|
+
* @returns {Promise<void>} A promise that resolves when the deletion is complete.
|
|
613
|
+
* The method also invalidates the cache after successful deletion.
|
|
614
|
+
* @throws {DataAccessError} Throws an error if:
|
|
615
|
+
* - The keys parameter is not a non-empty array
|
|
616
|
+
* - Any key object in the array is empty or invalid
|
|
617
|
+
* - The database operation fails
|
|
618
|
+
*
|
|
619
|
+
* @since 2.64.1
|
|
620
|
+
* @memberof BaseCollection
|
|
621
|
+
*/
|
|
622
|
+
async removeByIndexKeys(keys) {
|
|
623
|
+
if (!isNonEmptyArray(keys)) {
|
|
624
|
+
const message = `Failed to remove by index keys [${this.entityName}]: keys must be a non-empty array`;
|
|
625
|
+
this.log.error(message);
|
|
626
|
+
throw new DataAccessError(message);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
keys.forEach((key) => {
|
|
630
|
+
if (!isNonEmptyObject(key)) {
|
|
631
|
+
const message = `Failed to remove by index keys [${this.entityName}]: key must be a non-empty object`;
|
|
632
|
+
this.log.error(message);
|
|
633
|
+
throw new DataAccessError(message);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
await this.entity.delete(keys).go();
|
|
639
|
+
this.log.info(`Removed ${keys.length} items for [${this.entityName}]`);
|
|
640
|
+
return this.#invalidateCache();
|
|
641
|
+
} catch (error) {
|
|
642
|
+
return this.#logAndThrowError('Failed to remove by index keys', error);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
553
645
|
}
|
|
554
646
|
|
|
555
647
|
export default BaseCollection;
|
|
@@ -241,6 +241,12 @@ class BaseModel {
|
|
|
241
241
|
return this._remove();
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
generateCompositeKeys() {
|
|
245
|
+
return {
|
|
246
|
+
[this.idName]: this.getId(),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
244
250
|
/**
|
|
245
251
|
* Internal remove method that removes the current entity from the database and its dependents.
|
|
246
252
|
* This method does not check if the schema allows removal in order to be able to remove
|
|
@@ -269,7 +275,7 @@ class BaseModel {
|
|
|
269
275
|
|
|
270
276
|
await Promise.all(removePromises);
|
|
271
277
|
|
|
272
|
-
await this.entity.remove(
|
|
278
|
+
await this.entity.remove(this.generateCompositeKeys()).go();
|
|
273
279
|
|
|
274
280
|
this.#invalidateCache();
|
|
275
281
|
|
|
@@ -20,6 +20,7 @@ import ConfigurationCollection from '../configuration/configuration.collection.j
|
|
|
20
20
|
import ExperimentCollection from '../experiment/experiment.collection.js';
|
|
21
21
|
import EntitlementCollection from '../entitlement/entitlement.collection.js';
|
|
22
22
|
import FixEntityCollection from '../fix-entity/fix-entity.collection.js';
|
|
23
|
+
import FixEntitySuggestionCollection from '../fix-entity-suggestion/fix-entity-suggestion.collection.js';
|
|
23
24
|
import ImportJobCollection from '../import-job/import-job.collection.js';
|
|
24
25
|
import ImportUrlCollection from '../import-url/import-url.collection.js';
|
|
25
26
|
import KeyEventCollection from '../key-event/key-event.collection.js';
|
|
@@ -46,6 +47,7 @@ import AuditSchema from '../audit/audit.schema.js';
|
|
|
46
47
|
import ConfigurationSchema from '../configuration/configuration.schema.js';
|
|
47
48
|
import EntitlementSchema from '../entitlement/entitlement.schema.js';
|
|
48
49
|
import FixEntitySchema from '../fix-entity/fix-entity.schema.js';
|
|
50
|
+
import FixEntitySuggestionSchema from '../fix-entity-suggestion/fix-entity-suggestion.schema.js';
|
|
49
51
|
import ExperimentSchema from '../experiment/experiment.schema.js';
|
|
50
52
|
import ImportJobSchema from '../import-job/import-job.schema.js';
|
|
51
53
|
import ImportUrlSchema from '../import-url/import-url.schema.js';
|
|
@@ -142,6 +144,7 @@ EntityRegistry.registerEntity(AuditSchema, AuditCollection);
|
|
|
142
144
|
EntityRegistry.registerEntity(ConfigurationSchema, ConfigurationCollection);
|
|
143
145
|
EntityRegistry.registerEntity(EntitlementSchema, EntitlementCollection);
|
|
144
146
|
EntityRegistry.registerEntity(FixEntitySchema, FixEntityCollection);
|
|
147
|
+
EntityRegistry.registerEntity(FixEntitySuggestionSchema, FixEntitySuggestionCollection);
|
|
145
148
|
EntityRegistry.registerEntity(ExperimentSchema, ExperimentCollection);
|
|
146
149
|
EntityRegistry.registerEntity(ImportJobSchema, ImportJobCollection);
|
|
147
150
|
EntityRegistry.registerEntity(ImportUrlSchema, ImportUrlCollection);
|
|
@@ -43,12 +43,17 @@ export interface QueryOptions {
|
|
|
43
43
|
fetchAllPages?: boolean;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface BatchGetOptions {
|
|
47
|
+
attributes?: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
export interface BaseCollection<T extends BaseModel> {
|
|
47
51
|
_onCreate(item: T): void;
|
|
48
52
|
_onCreateMany(items: MultiStatusCreateResult<T>): void;
|
|
49
53
|
_saveMany(items: T[]): Promise<T[]>;
|
|
50
54
|
all(sortKeys?: object, options?: QueryOptions): Promise<T[]>;
|
|
51
55
|
allByIndexKeys(keys: object, options?: QueryOptions): Promise<T[]>;
|
|
56
|
+
batchGetByKeys(keys: object[], options?: BatchGetOptions): Promise<{ data: T[]; unprocessed: object[] }>;
|
|
52
57
|
create(item: object): Promise<T>;
|
|
53
58
|
createMany(items: object[], parent?: T): Promise<MultiStatusCreateResult<T>>;
|
|
54
59
|
existsById(id: string): Promise<boolean>;
|
|
@@ -170,7 +170,9 @@ class Schema {
|
|
|
170
170
|
const allKeys = [...(pk?.facets || []), ...(sk?.facets || [])];
|
|
171
171
|
|
|
172
172
|
// check if all keys in the index are in the sort keys
|
|
173
|
-
|
|
173
|
+
const pkKeys = Array.isArray(pk?.facets) ? pk.facets : [];
|
|
174
|
+
return pkKeys.every((key) => subKeyNames.includes(key))
|
|
175
|
+
&& subKeyNames.every((key) => allKeys.includes(key));
|
|
174
176
|
});
|
|
175
177
|
|
|
176
178
|
if (isNonEmptyObject(index)) {
|
|
@@ -10,17 +10,235 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
import BaseCollection from '../base/base.collection.js';
|
|
13
|
+
import DataAccessError from '../../errors/data-access.error.js';
|
|
14
|
+
import ValidationError from '../../errors/validation.error.js';
|
|
15
|
+
import { guardId, guardArray, guardString } from '../../util/guards.js';
|
|
16
|
+
import { resolveUpdates } from '../../util/util.js';
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
|
-
*
|
|
19
|
+
* FixEntityCollection - A collection class responsible for managing FixEntities.
|
|
16
20
|
* Extends the BaseCollection to provide specific methods for interacting with
|
|
17
|
-
* FixEntity records.
|
|
21
|
+
* FixEntity records and their relationships with Suggestions.
|
|
22
|
+
*
|
|
23
|
+
* This collection provides methods to:
|
|
24
|
+
* - Retrieve suggestions associated with a specific FixEntity
|
|
25
|
+
* - Set suggestions for a FixEntity by managing junction table relationships
|
|
18
26
|
*
|
|
19
27
|
* @class FixEntityCollection
|
|
20
28
|
* @extends BaseCollection
|
|
21
29
|
*/
|
|
22
30
|
class FixEntityCollection extends BaseCollection {
|
|
23
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Gets all suggestions associated with a specific FixEntity.
|
|
33
|
+
*
|
|
34
|
+
* @async
|
|
35
|
+
* @param {string} fixEntityId - The ID of the FixEntity.
|
|
36
|
+
* @returns {Promise<Array>} - A promise that resolves to an array of Suggestion models
|
|
37
|
+
* @throws {DataAccessError} - Throws an error if the fixEntityId is not provided or if the
|
|
38
|
+
* query fails.
|
|
39
|
+
*/
|
|
40
|
+
async getSuggestionsByFixEntityId(fixEntityId) {
|
|
41
|
+
guardId('fixEntityId', fixEntityId, 'FixEntityCollection');
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const fixEntitySuggestionCollection = this.entityRegistry.getCollection('FixEntitySuggestionCollection');
|
|
45
|
+
|
|
46
|
+
const fixEntitySuggestions = await fixEntitySuggestionCollection
|
|
47
|
+
.allByFixEntityId(fixEntityId);
|
|
48
|
+
|
|
49
|
+
if (fixEntitySuggestions.length === 0) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const suggestionCollection = this.entityRegistry.getCollection('SuggestionCollection');
|
|
54
|
+
const suggestions = await suggestionCollection
|
|
55
|
+
.batchGetByKeys(fixEntitySuggestions
|
|
56
|
+
.map((record) => ({ [suggestionCollection.idName]: record.getSuggestionId() })));
|
|
57
|
+
return suggestions.data;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
this.log.error(`Failed to get suggestions for fix entity: ${fixEntityId}`, error);
|
|
60
|
+
throw new DataAccessError('Failed to get suggestions for fix entity', this, error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Sets suggestions for a specific FixEntity by replacing all existing suggestions with new ones.
|
|
66
|
+
* This method efficiently only removes relationships that are no longer needed and only adds
|
|
67
|
+
* new ones.
|
|
68
|
+
*
|
|
69
|
+
* @async
|
|
70
|
+
* @param {string} opportunityId - The ID of the opportunity.
|
|
71
|
+
* @param {FixEntity} fixEntity - The FixEntity entity.
|
|
72
|
+
* @param {Array<Suggestion>} suggestions - An array of Suggestion entities.
|
|
73
|
+
* @returns {Promise<{createdItems: Array, errorItems: Array, removedCount: number}>} - A promise
|
|
74
|
+
* that resolves to an object containing:
|
|
75
|
+
* - createdItems: Array of created FixEntitySuggestionCollection junction records
|
|
76
|
+
* - errorItems: Array of items that failed validation
|
|
77
|
+
* - removedCount: Number of existing relationships that were removed
|
|
78
|
+
* @throws {DataAccessError} - Throws an error if the entities are not provided or if the
|
|
79
|
+
* operation fails.
|
|
80
|
+
*/
|
|
81
|
+
async setSuggestionsForFixEntity(opportunityId, fixEntity, suggestions) {
|
|
82
|
+
guardId('opportunityId', opportunityId, 'FixEntityCollection');
|
|
83
|
+
guardArray('suggestions', suggestions, 'FixEntityCollection', 'any');
|
|
84
|
+
|
|
85
|
+
// Simple null checks
|
|
86
|
+
if (!fixEntity) {
|
|
87
|
+
throw new ValidationError('fixEntity is required');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Extract IDs and other values from entities
|
|
91
|
+
const fixEntityId = fixEntity.getId();
|
|
92
|
+
const fixEntityCreatedAt = fixEntity.getCreatedAt();
|
|
93
|
+
const suggestionIds = suggestions.map((suggestion) => suggestion.getId());
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const fixEntitySuggestionCollection = this.entityRegistry.getCollection('FixEntitySuggestionCollection');
|
|
97
|
+
|
|
98
|
+
const existingRelationships = await fixEntitySuggestionCollection
|
|
99
|
+
.allByFixEntityId(fixEntityId);
|
|
100
|
+
|
|
101
|
+
// Extract existing suggestion IDs from relationship objects
|
|
102
|
+
const existingSuggestionIds = existingRelationships.map((rel) => rel.getSuggestionId());
|
|
103
|
+
|
|
104
|
+
const { toDelete, toCreate } = resolveUpdates(existingSuggestionIds, suggestionIds);
|
|
105
|
+
|
|
106
|
+
let removePromise;
|
|
107
|
+
let createPromise;
|
|
108
|
+
|
|
109
|
+
if (toDelete.length > 0) {
|
|
110
|
+
removePromise = fixEntitySuggestionCollection.removeByIndexKeys(
|
|
111
|
+
toDelete.map((suggestionId) => (
|
|
112
|
+
{
|
|
113
|
+
suggestionId,
|
|
114
|
+
fixEntityId,
|
|
115
|
+
})),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (toCreate.length > 0) {
|
|
120
|
+
createPromise = fixEntitySuggestionCollection.createMany(toCreate.map((suggestionId) => (
|
|
121
|
+
{
|
|
122
|
+
opportunityId,
|
|
123
|
+
fixEntityCreatedAt,
|
|
124
|
+
fixEntityId,
|
|
125
|
+
suggestionId,
|
|
126
|
+
})));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const [removeResult, createResult] = await Promise.allSettled([removePromise, createPromise]);
|
|
130
|
+
|
|
131
|
+
let removedCount = 0;
|
|
132
|
+
let createdItems = [];
|
|
133
|
+
let errorItems = [];
|
|
134
|
+
if (removeResult.status === 'fulfilled') {
|
|
135
|
+
removedCount = toDelete.length;
|
|
136
|
+
} else {
|
|
137
|
+
this.log.error('Remove operation failed:', removeResult.reason);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (createResult.status === 'fulfilled') {
|
|
141
|
+
createdItems = createResult.value?.createdItems || [];
|
|
142
|
+
errorItems = createResult.value?.errorItems || [];
|
|
143
|
+
} else {
|
|
144
|
+
this.log.error('Create operation failed:', createResult.reason);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.log.info(`Set suggestions for fix entity ${fixEntityId}: removed ${removedCount}, `
|
|
148
|
+
+ `added ${createdItems.length}, failed ${errorItems.length}`);
|
|
149
|
+
|
|
150
|
+
return { createdItems, errorItems, removedCount };
|
|
151
|
+
} catch (error) {
|
|
152
|
+
this.log.error('Failed to set suggestions for fix entity', error);
|
|
153
|
+
throw new DataAccessError('Failed to set suggestions for fix entity', this, error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Gets all fixes with their suggestions for a specific opportunity and created date.
|
|
159
|
+
* This method retrieves all fix entities and their associated suggestions for a given opportunity
|
|
160
|
+
* and creation date.
|
|
161
|
+
*
|
|
162
|
+
* @async
|
|
163
|
+
* @param {string} opportunityId - The ID of the opportunity.
|
|
164
|
+
* @param {string} fixEntityCreatedDate - The creation date to filter by (YYYY-MM-DD format).
|
|
165
|
+
* @returns {Promise<Array>} - A promise that resolves to an array of objects containing:
|
|
166
|
+
* - fixEntity: The FixEntity model
|
|
167
|
+
* - suggestions: Array of associated Suggestion models
|
|
168
|
+
* @throws {DataAccessError} - Throws an error if the query fails.
|
|
169
|
+
* @throws {ValidationError} - Throws an error if opportunityId or
|
|
170
|
+
* fixEntityCreatedDate is not provided.
|
|
171
|
+
*/
|
|
172
|
+
async getAllFixesWithSuggestionByCreatedAt(opportunityId, fixEntityCreatedDate) {
|
|
173
|
+
guardId('opportunityId', opportunityId, 'FixEntityCollection');
|
|
174
|
+
guardString('fixEntityCreatedDate', fixEntityCreatedDate, 'FixEntityCollection');
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const fixEntitySuggestionCollection = this.entityRegistry.getCollection('FixEntitySuggestionCollection');
|
|
178
|
+
const suggestionCollection = this.entityRegistry.getCollection('SuggestionCollection');
|
|
179
|
+
|
|
180
|
+
// Query fix entity suggestions by opportunity ID and created date
|
|
181
|
+
const fixEntitySuggestions = await fixEntitySuggestionCollection
|
|
182
|
+
.allByOpportunityIdAndFixEntityCreatedDate(opportunityId, fixEntityCreatedDate);
|
|
183
|
+
|
|
184
|
+
if (fixEntitySuggestions.length === 0) {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Group suggestions by fix entity ID
|
|
189
|
+
const suggestionsByFixEntityId = {};
|
|
190
|
+
const fixEntityIds = new Set();
|
|
191
|
+
|
|
192
|
+
for (const fixEntitySuggestion of fixEntitySuggestions) {
|
|
193
|
+
const fixEntityId = fixEntitySuggestion.getFixEntityId();
|
|
194
|
+
const suggestionId = fixEntitySuggestion.getSuggestionId();
|
|
195
|
+
|
|
196
|
+
fixEntityIds.add(fixEntityId);
|
|
197
|
+
|
|
198
|
+
if (!suggestionsByFixEntityId[fixEntityId]) {
|
|
199
|
+
suggestionsByFixEntityId[fixEntityId] = [];
|
|
200
|
+
}
|
|
201
|
+
suggestionsByFixEntityId[fixEntityId].push(suggestionId);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Get all fix entities
|
|
205
|
+
const fixEntities = await this.batchGetByKeys(
|
|
206
|
+
Array.from(fixEntityIds).map((id) => ({ [this.idName]: id })),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Get all suggestions
|
|
210
|
+
const allSuggestionIds = Object.values(suggestionsByFixEntityId).flat();
|
|
211
|
+
const suggestions = await suggestionCollection.batchGetByKeys(
|
|
212
|
+
allSuggestionIds.map((id) => ({ [suggestionCollection.idName]: id })),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Create a map of suggestions by ID for quick lookup
|
|
216
|
+
const suggestionsById = {};
|
|
217
|
+
for (const suggestion of suggestions.data) {
|
|
218
|
+
suggestionsById[suggestion.getId()] = suggestion;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Combine fix entities with their suggestions
|
|
222
|
+
const result = [];
|
|
223
|
+
for (const fixEntity of fixEntities.data) {
|
|
224
|
+
const fixEntityId = fixEntity.getId();
|
|
225
|
+
const suggestionIds = suggestionsByFixEntityId[fixEntityId] || [];
|
|
226
|
+
const suggestionsForFixEntity = suggestionIds
|
|
227
|
+
.map((id) => suggestionsById[id])
|
|
228
|
+
.filter(Boolean);
|
|
229
|
+
|
|
230
|
+
result.push({
|
|
231
|
+
fixEntity,
|
|
232
|
+
suggestions: suggestionsForFixEntity,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
this.log.error('Failed to get all fixes with suggestions by created date', error);
|
|
239
|
+
throw new DataAccessError('Failed to get all fixes with suggestions by created date', this, error);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
24
242
|
}
|
|
25
243
|
|
|
26
244
|
export default FixEntityCollection;
|
|
@@ -28,6 +28,12 @@ class FixEntity extends BaseModel {
|
|
|
28
28
|
FAILED: 'FAILED', // failed to apply the fix
|
|
29
29
|
ROLLED_BACK: 'ROLLED_BACK', // the fix has been rolled_back
|
|
30
30
|
};
|
|
31
|
+
|
|
32
|
+
async getSuggestions() {
|
|
33
|
+
const fixEntityCollection = this.entityRegistry.getCollection('FixEntityCollection');
|
|
34
|
+
return fixEntityCollection
|
|
35
|
+
.getSuggestionsByFixEntityId(this.getId());
|
|
36
|
+
}
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export default FixEntity;
|
|
@@ -15,7 +15,7 @@ import { isIsoDate, isNonEmptyObject } from '@adobe/spacecat-shared-utils';
|
|
|
15
15
|
import SchemaBuilder from '../base/schema.builder.js';
|
|
16
16
|
import FixEntity from './fix-entity.model.js';
|
|
17
17
|
import FixEntityCollection from './fix-entity.collection.js';
|
|
18
|
-
import
|
|
18
|
+
import Suggestion from '../suggestion/suggestion.model.js';
|
|
19
19
|
|
|
20
20
|
/*
|
|
21
21
|
Schema Doc: https://electrodb.dev/en/modeling/schema/
|
|
@@ -24,7 +24,7 @@ Indexes Doc: https://electrodb.dev/en/modeling/indexes/
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
const schema = new SchemaBuilder(FixEntity, FixEntityCollection)
|
|
27
|
-
.addReference('has_many', '
|
|
27
|
+
.addReference('has_many', 'FixEntitySuggestion', ['updatedAt'], { removeDependents: true })
|
|
28
28
|
.addReference('belongs_to', 'Opportunity', ['status'])
|
|
29
29
|
.addAttribute('type', {
|
|
30
30
|
type: Object.values(Suggestion.TYPES),
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type {
|
|
14
|
-
BaseCollection, BaseModel, Opportunity, Suggestion,
|
|
14
|
+
BaseCollection, BaseModel, Opportunity, Suggestion, FixEntitySuggestion,
|
|
15
15
|
} from '../index';
|
|
16
16
|
|
|
17
17
|
export interface FixEntity extends BaseModel {
|
|
@@ -28,8 +28,6 @@ export interface FixEntity extends BaseModel {
|
|
|
28
28
|
setPublishedAt(value: string): this;
|
|
29
29
|
getStatus(): string;
|
|
30
30
|
setStatus(value: string): this;
|
|
31
|
-
getSuggestions(): Promise<Suggestion[]>;
|
|
32
|
-
getSuggestionsByUpdatedAt(updatedAt: string): Promise<Suggestion[]>;
|
|
33
31
|
getType(): string;
|
|
34
32
|
}
|
|
35
33
|
|
|
@@ -38,4 +36,6 @@ export interface FixEntityCollection extends BaseCollection<FixEntity> {
|
|
|
38
36
|
allByOpportunityIdAndStatus(opportunityId: string, status: string): Promise<FixEntity[]>;
|
|
39
37
|
findByOpportunityId(opportunityId: string): Promise<FixEntity | null>;
|
|
40
38
|
findByOpportunityIdAndStatus(opportunityId: string, status: string): Promise<FixEntity | null>;
|
|
39
|
+
getSuggestionsByFixEntityId(fixEntityId: string): Promise<{data: Array<Suggestion>, unprocessed: Array<string>}>;
|
|
40
|
+
setSuggestionsForFixEntity(opportunityId: string, fixEntity: FixEntity, suggestions: Array<Suggestion>): Promise<{createdItems: Array<FixEntitySuggestion>, errorItems: Array<FixEntitySuggestion>, removedCount: number}>;
|
|
41
41
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { guardId } from '../../util/guards.js';
|
|
14
|
+
import BaseCollection from '../base/base.collection.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* FixEntitySuggestionCollection - A collection class responsible for managing
|
|
18
|
+
* FixEntitySuggestion junction records. This collection handles the many-to-many
|
|
19
|
+
* relationship between FixEntity and Suggestion entities.
|
|
20
|
+
*
|
|
21
|
+
* This collection provides methods to:
|
|
22
|
+
* - Retrieve junction records by Suggestion ID
|
|
23
|
+
* - Retrieve junction records by FixEntity ID
|
|
24
|
+
*
|
|
25
|
+
* @class FixEntitySuggestionCollection
|
|
26
|
+
* @extends BaseCollection
|
|
27
|
+
*/
|
|
28
|
+
class FixEntitySuggestionCollection extends BaseCollection {
|
|
29
|
+
/**
|
|
30
|
+
* Gets all junction records associated with a specific Suggestion.
|
|
31
|
+
*
|
|
32
|
+
* @async
|
|
33
|
+
* @param {string} suggestionId - The ID of the Suggestion.
|
|
34
|
+
* @param {Object} options - Additional query options.
|
|
35
|
+
* @returns {Promise<Array>} - A promise that resolves to
|
|
36
|
+
* an array of FixEntitySuggestion junction records
|
|
37
|
+
* @throws {Error} - Throws an error if the suggestionId is not provided
|
|
38
|
+
*/
|
|
39
|
+
async allBySuggestionId(suggestionId, options = {}) {
|
|
40
|
+
guardId('suggestionId', suggestionId, 'FixEntitySuggestionCollection');
|
|
41
|
+
return this.allByIndexKeys({ suggestionId }, options);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default FixEntitySuggestionCollection;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import BaseModel from '../base/base.model.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* FixEntitySuggestion - A junction table class representing the many-to-many relationship
|
|
17
|
+
* between FixEntity and Suggestion entities. This allows one fix entity to be associated
|
|
18
|
+
* with multiple suggestions and one suggestion to be associated with multiple fix entities.
|
|
19
|
+
*
|
|
20
|
+
* @class FixEntitySuggestion
|
|
21
|
+
* @extends BaseModel
|
|
22
|
+
*/
|
|
23
|
+
class FixEntitySuggestion extends BaseModel {
|
|
24
|
+
static DEFAULT_UPDATED_BY = 'spacecat';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generates the composite keys for the FixEntitySuggestion model.
|
|
28
|
+
* @returns {Object} - The composite keys.
|
|
29
|
+
*/
|
|
30
|
+
generateCompositeKeys() {
|
|
31
|
+
return {
|
|
32
|
+
suggestionId: this.getSuggestionId(),
|
|
33
|
+
fixEntityId: this.getFixEntityId(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default FixEntitySuggestion;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import SchemaBuilder from '../base/schema.builder.js';
|
|
14
|
+
import FixEntitySuggestion from './fix-entity-suggestion.model.js';
|
|
15
|
+
import FixEntitySuggestionCollection from './fix-entity-suggestion.collection.js';
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
Schema Doc: https://electrodb.dev/en/modeling/schema/
|
|
19
|
+
Attribute Doc: https://electrodb.dev/en/modeling/attributes/
|
|
20
|
+
Indexes Doc: https://electrodb.dev/en/modeling/indexes/
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const schema = new SchemaBuilder(FixEntitySuggestion, FixEntitySuggestionCollection)
|
|
24
|
+
.withPrimaryPartitionKeys(['suggestionId'])
|
|
25
|
+
.withPrimarySortKeys(['fixEntityId'])
|
|
26
|
+
.addReference('belongs_to', 'FixEntity')
|
|
27
|
+
.addReference('belongs_to', 'Suggestion')
|
|
28
|
+
.addAttribute('opportunityId', {
|
|
29
|
+
type: 'string',
|
|
30
|
+
required: true,
|
|
31
|
+
readOnly: true,
|
|
32
|
+
})
|
|
33
|
+
.addAttribute('fixEntityCreatedAt', {
|
|
34
|
+
type: 'string',
|
|
35
|
+
required: true,
|
|
36
|
+
readOnly: true,
|
|
37
|
+
})
|
|
38
|
+
.addAttribute('fixEntityCreatedDate', {
|
|
39
|
+
type: 'string',
|
|
40
|
+
readOnly: true,
|
|
41
|
+
watch: ['fixEntityCreatedAt'],
|
|
42
|
+
set: (_, { fixEntityCreatedAt }) => (fixEntityCreatedAt ? fixEntityCreatedAt.split('T')[0] : undefined),
|
|
43
|
+
})
|
|
44
|
+
.addIndex(
|
|
45
|
+
{ composite: ['opportunityId'] },
|
|
46
|
+
{ composite: ['fixEntityCreatedDate', 'updatedAt'] },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export default schema.build();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { BaseCollection, BaseModel, FixEntity, Suggestion } from '../index';
|
|
14
|
+
|
|
15
|
+
export interface FixEntitySuggestion extends BaseModel {
|
|
16
|
+
getFixEntity(): Promise<FixEntity>;
|
|
17
|
+
getSuggestion(): Promise<Suggestion>;
|
|
18
|
+
getFixEntityId(): string;
|
|
19
|
+
setFixEntityId(value: string): this;
|
|
20
|
+
getSuggestionId(): string;
|
|
21
|
+
setSuggestionId(value: string): this;
|
|
22
|
+
getFixEntityCreatedAt(): string;
|
|
23
|
+
setFixEntityCreatedAt(value: string): this;
|
|
24
|
+
getFixEntityCreatedDate(): string;
|
|
25
|
+
setFixEntityCreatedDate(value: string): this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FixEntitySuggestionCollection extends BaseCollection<FixEntitySuggestion> {
|
|
29
|
+
allBySuggestionId(suggestionId: string): Promise<FixEntitySuggestion[]>;
|
|
30
|
+
allByFixEntityId(fixEntityId: string): Promise<FixEntitySuggestion[]>;
|
|
31
|
+
allByOpportunityIdAndFixEntityCreatedDate(opportunityId: string, fixEntityCreatedDate: string): Promise<FixEntitySuggestion[]>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import FixEntitySuggestion from './fix-entity-suggestion.model.js';
|
|
14
|
+
import FixEntitySuggestionCollection from './fix-entity-suggestion.collection.js';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
FixEntitySuggestion,
|
|
18
|
+
FixEntitySuggestionCollection,
|
|
19
|
+
};
|
package/src/models/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type * from './async-job';
|
|
|
15
15
|
export type * from './configuration';
|
|
16
16
|
export type * from './base';
|
|
17
17
|
export type * from './fix-entity';
|
|
18
|
+
export type * from './fix-entity-suggestion';
|
|
18
19
|
export type * from './experiment';
|
|
19
20
|
export type * from './entitlement';
|
|
20
21
|
export type * from './import-job';
|
package/src/models/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export * from './base/index.js';
|
|
|
17
17
|
export * from './configuration/index.js';
|
|
18
18
|
export * from './entitlement/index.js';
|
|
19
19
|
export * from './fix-entity/index.js';
|
|
20
|
+
export * from './fix-entity-suggestion/index.js';
|
|
20
21
|
export * from './experiment/index.js';
|
|
21
22
|
export * from './import-job/index.js';
|
|
22
23
|
export * from './import-url/index.js';
|
|
@@ -59,22 +59,151 @@ class Opportunity extends BaseModel {
|
|
|
59
59
|
/**
|
|
60
60
|
* Adds the given fixEntities to this Opportunity. Sets this opportunity as the parent
|
|
61
61
|
* of each fixEntity, as such the opportunity ID does not need to be provided.
|
|
62
|
+
* Each fixEntity must contain a suggestions array that will be used to create
|
|
63
|
+
* FixEntitySuggestion records.
|
|
62
64
|
*
|
|
63
65
|
* @async
|
|
64
66
|
* @param {Array<Object>} fixEntities - An array of fixEntities objects to add.
|
|
67
|
+
* Each fixEntity must have a suggestions property with at least one suggestion.
|
|
65
68
|
* @return {Promise<{ createdItems: BaseModel[],
|
|
66
69
|
* errorItems: { item: Object, error: ValidationError }[] }>} - A promise that
|
|
67
70
|
* resolves to an object containing the created fixEntities items and any
|
|
68
71
|
* errors that occurred.
|
|
69
72
|
*/
|
|
70
73
|
async addFixEntities(fixEntities) {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.
|
|
74
|
+
const errorItems = [];
|
|
75
|
+
const opportunityId = this.getId();
|
|
76
|
+
|
|
77
|
+
// Step 1: Input validation - categorize fixEntities into valid and invalid
|
|
78
|
+
const validFixEntities = [];
|
|
79
|
+
fixEntities.forEach((fixEntity) => {
|
|
80
|
+
if (!fixEntity.suggestions) {
|
|
81
|
+
errorItems.push({
|
|
82
|
+
item: fixEntity,
|
|
83
|
+
error: new Error('fixEntity must have a suggestions property'),
|
|
84
|
+
});
|
|
85
|
+
} else if (!Array.isArray(fixEntity.suggestions)) {
|
|
86
|
+
errorItems.push({
|
|
87
|
+
item: fixEntity,
|
|
88
|
+
error: new Error('fixEntity.suggestions must be an array'),
|
|
89
|
+
});
|
|
90
|
+
} else if (fixEntity.suggestions.length === 0) {
|
|
91
|
+
errorItems.push({
|
|
92
|
+
item: fixEntity,
|
|
93
|
+
error: new Error('fixEntity.suggestions cannot be empty'),
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
validFixEntities.push(fixEntity);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// If no valid fixEntities, return early
|
|
101
|
+
if (validFixEntities.length === 0) {
|
|
102
|
+
return { createdItems: [], errorItems };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fixEntityCollection = this.entityRegistry.getCollection('FixEntityCollection');
|
|
106
|
+
const suggestionCollection = this.entityRegistry.getCollection('SuggestionCollection');
|
|
107
|
+
const fixEntitySuggestionCollection = this.entityRegistry
|
|
108
|
+
.getCollection('FixEntitySuggestionCollection');
|
|
109
|
+
|
|
110
|
+
// Step 2: Flatten and fetch all unique suggestion IDs
|
|
111
|
+
const allSuggestionIds = new Set();
|
|
112
|
+
validFixEntities.forEach((fixEntity) => {
|
|
113
|
+
fixEntity.suggestions.forEach((suggestionId) => {
|
|
114
|
+
allSuggestionIds.add(suggestionId);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const suggestionResults = await suggestionCollection.batchGetByKeys(
|
|
119
|
+
Array.from(allSuggestionIds).map((suggestionId) => ({
|
|
120
|
+
[suggestionCollection.idName]: suggestionId,
|
|
121
|
+
})),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Create a map of suggestionId -> suggestion entity for O(1) retrieval
|
|
125
|
+
const suggestionMap = new Map();
|
|
126
|
+
suggestionResults.data.forEach((suggestion) => {
|
|
127
|
+
suggestionMap.set(suggestion.getId(), suggestion);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Step 3: Validate that all suggestion IDs exist and prepare fixEntities to create
|
|
131
|
+
const fixEntitiesToCreate = [];
|
|
132
|
+
validFixEntities.forEach((fixEntity) => {
|
|
133
|
+
const missingSuggestions = fixEntity.suggestions.filter(
|
|
134
|
+
(suggestionId) => !suggestionMap.has(suggestionId),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (missingSuggestions.length > 0) {
|
|
138
|
+
errorItems.push({
|
|
139
|
+
item: fixEntity,
|
|
140
|
+
error: new Error(`Invalid suggestion IDs: ${missingSuggestions.join(', ')}`),
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
fixEntitiesToCreate.push(fixEntity);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// If no valid fixEntities to create, return early
|
|
148
|
+
if (fixEntitiesToCreate.length === 0) {
|
|
149
|
+
return { createdItems: [], errorItems };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Step 4: Create FixEntity records
|
|
153
|
+
const fixEntityCreateResult = await fixEntityCollection.createMany(
|
|
154
|
+
fixEntitiesToCreate.map((fixEntity) => {
|
|
155
|
+
const { suggestions: _, ...fixEntityWithoutSuggestions } = fixEntity;
|
|
156
|
+
return {
|
|
157
|
+
...fixEntityWithoutSuggestions,
|
|
158
|
+
[this.idName]: opportunityId,
|
|
159
|
+
};
|
|
160
|
+
}),
|
|
161
|
+
this,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Add any errors from fix entity creation
|
|
165
|
+
if (fixEntityCreateResult.errorItems && fixEntityCreateResult.errorItems.length > 0) {
|
|
166
|
+
// Match error items back to original fixEntities with suggestions
|
|
167
|
+
fixEntityCreateResult.errorItems.forEach((errorItem) => {
|
|
168
|
+
const originalIndex = fixEntitiesToCreate.findIndex(
|
|
169
|
+
(fe) => fe.type === errorItem.item.type
|
|
170
|
+
&& JSON.stringify(fe.changeDetails) === JSON.stringify(errorItem.item.changeDetails),
|
|
171
|
+
);
|
|
172
|
+
if (originalIndex !== -1) {
|
|
173
|
+
errorItems.push({
|
|
174
|
+
item: fixEntitiesToCreate[originalIndex],
|
|
175
|
+
error: errorItem.error,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Step 5: Create FixEntitySuggestion junction records
|
|
182
|
+
const junctionRecordsToCreate = [];
|
|
183
|
+
fixEntityCreateResult.createdItems.forEach((createdFixEntity, index) => {
|
|
184
|
+
const originalFixEntity = fixEntitiesToCreate[index];
|
|
185
|
+
const fixEntityId = createdFixEntity.getId();
|
|
186
|
+
const fixEntityCreatedAt = createdFixEntity.getCreatedAt();
|
|
187
|
+
|
|
188
|
+
originalFixEntity.suggestions.forEach((suggestionId) => {
|
|
189
|
+
junctionRecordsToCreate.push({
|
|
190
|
+
opportunityId,
|
|
191
|
+
fixEntityId,
|
|
192
|
+
suggestionId,
|
|
193
|
+
fixEntityCreatedAt,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Create all junction records at once
|
|
199
|
+
if (junctionRecordsToCreate.length > 0) {
|
|
200
|
+
await fixEntitySuggestionCollection.createMany(junctionRecordsToCreate);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
createdItems: fixEntityCreateResult.createdItems,
|
|
205
|
+
errorItems,
|
|
206
|
+
};
|
|
78
207
|
}
|
|
79
208
|
}
|
|
80
209
|
|
|
@@ -10,15 +10,13 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { BaseCollection, BaseModel, Opportunity } from '../index';
|
|
13
|
+
import type { BaseCollection, BaseModel, Opportunity, FixEntitySuggestion, FixEntity } from '../index';
|
|
14
14
|
|
|
15
15
|
export interface Suggestion extends BaseModel {
|
|
16
16
|
getData(): object;
|
|
17
17
|
getKpiDeltas(): object;
|
|
18
18
|
getOpportunity(): Promise<Opportunity>;
|
|
19
19
|
getOpportunityId(): string;
|
|
20
|
-
getFixEntityId(): string;
|
|
21
|
-
getFixEntity(): Promise<object>;
|
|
22
20
|
getRank(): number;
|
|
23
21
|
getStatus(): string;
|
|
24
22
|
getType(): string;
|
|
@@ -31,9 +29,9 @@ export interface Suggestion extends BaseModel {
|
|
|
31
29
|
|
|
32
30
|
export interface SuggestionCollection extends BaseCollection<Suggestion> {
|
|
33
31
|
allByOpportunityId(opportunityId: string): Promise<Suggestion[]>;
|
|
34
|
-
allByFixEntityId(fixEntityId: string): Promise<Suggestion[]>;
|
|
35
32
|
allByOpportunityIdAndStatus(opportunityId: string, status: string): Promise<Suggestion[]>;
|
|
36
33
|
bulkUpdateStatus(suggestions: Suggestion[], status: string): Promise<Suggestion[]>;
|
|
37
34
|
findByOpportunityId(opportunityId: string): Promise<Suggestion | null>;
|
|
38
35
|
findByOpportunityIdAndStatus(opportunityId: string, status: string): Promise<Suggestion | null>;
|
|
36
|
+
getFixEntitiesBySuggestionId(suggestionId: string): Promise<{data: Array<FixEntity>, unprocessed: Array<string>}>;
|
|
39
37
|
}
|
|
@@ -11,11 +11,18 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import BaseCollection from '../base/base.collection.js';
|
|
14
|
+
import DataAccessError from '../../errors/data-access.error.js';
|
|
14
15
|
import Suggestion from './suggestion.model.js';
|
|
16
|
+
import { guardId } from '../../util/guards.js';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* SuggestionCollection - A collection class responsible for managing Suggestion entities.
|
|
18
|
-
* Extends the BaseCollection to provide specific methods for interacting with Suggestion records
|
|
20
|
+
* Extends the BaseCollection to provide specific methods for interacting with Suggestion records
|
|
21
|
+
* and their relationships with FixEntities.
|
|
22
|
+
*
|
|
23
|
+
* This collection provides methods to:
|
|
24
|
+
* - Update the status of multiple suggestions in bulk
|
|
25
|
+
* - Retrieve FixEntities associated with a specific Suggestion
|
|
19
26
|
*
|
|
20
27
|
* @class SuggestionCollection
|
|
21
28
|
* @extends BaseCollection
|
|
@@ -50,6 +57,40 @@ class SuggestionCollection extends BaseCollection {
|
|
|
50
57
|
|
|
51
58
|
return suggestions;
|
|
52
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Gets all FixEntities associated with a specific Suggestion.
|
|
63
|
+
*
|
|
64
|
+
* @async
|
|
65
|
+
* @param {string} suggestionId - The ID of the Suggestion.
|
|
66
|
+
* @returns {Promise<Array>} - A promise that resolves to an array of FixEntity models
|
|
67
|
+
* @throws {DataAccessError} - Throws an error if the suggestionId is not provided or if the
|
|
68
|
+
* query fails.
|
|
69
|
+
*/
|
|
70
|
+
async getFixEntitiesBySuggestionId(suggestionId) {
|
|
71
|
+
guardId('suggestionId', suggestionId, 'SuggestionCollection');
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const fixEntitySuggestionCollection = this.entityRegistry.getCollection('FixEntitySuggestionCollection');
|
|
75
|
+
const fixEntityCollection = this.entityRegistry.getCollection('FixEntityCollection');
|
|
76
|
+
|
|
77
|
+
// Get all junction records for this suggestion
|
|
78
|
+
const fixEntitySuggestions = await fixEntitySuggestionCollection
|
|
79
|
+
.allBySuggestionId(suggestionId);
|
|
80
|
+
|
|
81
|
+
if (fixEntitySuggestions.length === 0) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const fixEntityIds = fixEntitySuggestions.map((record) => record.getFixEntityId());
|
|
86
|
+
const result = await fixEntityCollection
|
|
87
|
+
.batchGetByKeys(fixEntityIds.map((id) => ({ [fixEntityCollection.idName]: id })));
|
|
88
|
+
return result.data;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.log.error('Failed to get fix entities for suggestion', error);
|
|
91
|
+
throw new DataAccessError('Failed to get fix entities for suggestion', this, error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
53
94
|
}
|
|
54
95
|
|
|
55
96
|
export default SuggestionCollection;
|
|
@@ -26,7 +26,7 @@ Indexes Doc: https://electrodb.dev/en/modeling/indexes/
|
|
|
26
26
|
|
|
27
27
|
const schema = new SchemaBuilder(Suggestion, SuggestionCollection)
|
|
28
28
|
.addReference('belongs_to', 'Opportunity', ['status', 'rank'])
|
|
29
|
-
.addReference('
|
|
29
|
+
.addReference('has_many', 'FixEntitySuggestion', ['updatedAt'], { removeDependents: true })
|
|
30
30
|
.addAttribute('type', {
|
|
31
31
|
type: Object.values(Suggestion.TYPES),
|
|
32
32
|
required: true,
|
package/src/util/util.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { hasText, isInteger } from '@adobe/spacecat-shared-utils';
|
|
14
14
|
import pluralize from 'pluralize';
|
|
15
|
+
import { guardArray } from './guards.js';
|
|
15
16
|
|
|
16
17
|
const capitalize = (str) => (hasText(str) ? str[0].toUpperCase() + str.slice(1) : '');
|
|
17
18
|
|
|
@@ -94,6 +95,19 @@ const zeroPad = (num, length) => {
|
|
|
94
95
|
: '0'.repeat(length - str.length) + str;
|
|
95
96
|
};
|
|
96
97
|
|
|
98
|
+
const resolveUpdates = (existingItems, newItems) => {
|
|
99
|
+
guardArray('existingItems', existingItems, 'resolveUpdates');
|
|
100
|
+
guardArray('newItems', newItems, 'resolveUpdates');
|
|
101
|
+
|
|
102
|
+
// Deduplicate new items
|
|
103
|
+
const dedupedNew = [...new Set(newItems)];
|
|
104
|
+
|
|
105
|
+
const toDelete = existingItems.filter((item) => !dedupedNew.includes(item));
|
|
106
|
+
const toCreate = dedupedNew.filter((item) => !existingItems.includes(item));
|
|
107
|
+
|
|
108
|
+
return { toDelete, toCreate };
|
|
109
|
+
};
|
|
110
|
+
|
|
97
111
|
export {
|
|
98
112
|
capitalize,
|
|
99
113
|
classExtends,
|
|
@@ -113,4 +127,5 @@ export {
|
|
|
113
127
|
sanitizeIdAndAuditFields,
|
|
114
128
|
sanitizeTimestamps,
|
|
115
129
|
zeroPad,
|
|
130
|
+
resolveUpdates,
|
|
116
131
|
};
|