@adobe/spacecat-shared-data-access 1.55.0 → 1.57.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/dto/audit.js +2 -0
- package/src/index.d.ts +6 -0
- package/src/v2/models/base.collection.js +117 -7
- package/src/v2/models/index.d.ts +5 -1
- package/src/v2/models/opportunity.model.js +19 -0
- package/src/v2/models/suggestion.collection.js +32 -1
- package/src/v2/schema/opportunity.schema.js +1 -1
- package/src/v2/util/patcher.js +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-data-access-v1.57.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v1.56.0...@adobe/spacecat-shared-data-access-v1.57.0) (2024-11-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* batch create/update items & open oppty type ([#450](https://github.com/adobe/spacecat-shared/issues/450)) ([642beaf](https://github.com/adobe/spacecat-shared/commit/642beaf3ab1ef9494f00c2148241d3986cca7fe7))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-data-access-v1.56.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v1.55.0...@adobe/spacecat-shared-data-access-v1.56.0) (2024-11-21)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* introduce missing audit id property ([#452](https://github.com/adobe/spacecat-shared/issues/452)) ([c17e447](https://github.com/adobe/spacecat-shared/commit/c17e447b275d9788d587dc44f3043fb60e83c51b))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-data-access-v1.55.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v1.54.0...@adobe/spacecat-shared-data-access-v1.55.0) (2024-11-20)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/spacecat-shared-data-access",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.57.0",
|
|
4
4
|
"description": "Shared modules of the Spacecat Services - Data Access",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@adobe/spacecat-shared-dynamo": "1.3.
|
|
38
|
-
"@adobe/spacecat-shared-utils": "1.
|
|
39
|
-
"@aws-sdk/client-dynamodb": "3.
|
|
40
|
-
"@aws-sdk/lib-dynamodb": "3.
|
|
37
|
+
"@adobe/spacecat-shared-dynamo": "1.3.49",
|
|
38
|
+
"@adobe/spacecat-shared-utils": "1.23.0",
|
|
39
|
+
"@aws-sdk/client-dynamodb": "3.696.0",
|
|
40
|
+
"@aws-sdk/lib-dynamodb": "3.696.0",
|
|
41
41
|
"@types/joi": "17.2.3",
|
|
42
42
|
"aws-xray-sdk": "3.10.2",
|
|
43
43
|
"electrodb": "3.0.1",
|
package/src/dto/audit.js
CHANGED
|
@@ -43,6 +43,7 @@ export const AuditDto = {
|
|
|
43
43
|
} : {};
|
|
44
44
|
|
|
45
45
|
return {
|
|
46
|
+
id: audit.getId(),
|
|
46
47
|
siteId: audit.getSiteId(),
|
|
47
48
|
auditedAt: audit.getAuditedAt(),
|
|
48
49
|
auditResult: audit.getAuditResult(),
|
|
@@ -62,6 +63,7 @@ export const AuditDto = {
|
|
|
62
63
|
*/
|
|
63
64
|
fromDynamoItem: (dynamoItem) => {
|
|
64
65
|
const auditData = {
|
|
66
|
+
id: dynamoItem.id,
|
|
65
67
|
siteId: dynamoItem.siteId,
|
|
66
68
|
auditedAt: dynamoItem.auditedAt,
|
|
67
69
|
auditResult: dynamoItem.auditResult,
|
package/src/index.d.ts
CHANGED
|
@@ -33,6 +33,12 @@ export declare const ImportUrlStatus: {
|
|
|
33
33
|
* Represents an individual audit of a site.
|
|
34
34
|
*/
|
|
35
35
|
export interface Audit {
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Retrieves the ID of the audit.
|
|
39
|
+
* @returns {string} The audit ID.
|
|
40
|
+
*/
|
|
41
|
+
getId: () => string;
|
|
36
42
|
/**
|
|
37
43
|
* Retrieves the site ID associated with this audit.
|
|
38
44
|
* @returns {string} The site ID.
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
import { isNonEmptyObject } from '@adobe/spacecat-shared-utils';
|
|
14
14
|
|
|
15
|
+
import { ElectroValidationError } from 'electrodb';
|
|
16
|
+
|
|
17
|
+
import ValidationError from '../errors/validation.error.js';
|
|
15
18
|
import { guardId } from '../util/guards.js';
|
|
16
19
|
|
|
17
20
|
/**
|
|
@@ -75,6 +78,10 @@ class BaseCollection {
|
|
|
75
78
|
return records.data.map((record) => this._createInstance({ data: record }));
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
_getEnumValues(fieldName) {
|
|
82
|
+
return this.entity.model.schema.attributes[fieldName]?.enumArray;
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
/**
|
|
79
86
|
* Finds an entity by its ID.
|
|
80
87
|
* @async
|
|
@@ -92,27 +99,130 @@ class BaseCollection {
|
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
/**
|
|
95
|
-
* Creates a new entity in the collection.
|
|
102
|
+
* Creates a new entity in the collection and directly persists it to the database.
|
|
103
|
+
* There is no need to call the save method (which is for updates only) after creating
|
|
104
|
+
* the entity.
|
|
96
105
|
* @async
|
|
97
|
-
* @param {Object}
|
|
106
|
+
* @param {Object} item - The data for the entity to be created.
|
|
98
107
|
* @returns {Promise<BaseModel>} - A promise that resolves to the created model instance.
|
|
99
108
|
* @throws {Error} - Throws an error if the data is invalid or if the creation process fails.
|
|
100
109
|
*/
|
|
101
|
-
async create(
|
|
102
|
-
if (!isNonEmptyObject(
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
async create(item) {
|
|
111
|
+
if (!isNonEmptyObject(item)) {
|
|
112
|
+
const message = `Failed to create [${this.entityName}]: data is required`;
|
|
113
|
+
this.log.error(message);
|
|
114
|
+
throw new Error(message);
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
try {
|
|
118
|
+
// todo: catch ElectroDB validation errors and re-throws as ValidationError
|
|
108
119
|
// todo: validate associations
|
|
109
|
-
const record = await this.entity.create(
|
|
120
|
+
const record = await this.entity.create(item).go();
|
|
110
121
|
return this._createInstance(record);
|
|
111
122
|
} catch (error) {
|
|
112
123
|
this.log.error(`Failed to create [${this.entityName}]`, error);
|
|
113
124
|
throw error;
|
|
114
125
|
}
|
|
115
126
|
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates multiple entities in the collection and directly persists them to the database in
|
|
130
|
+
* a batch write operation. Batches are written in parallel and are limited to 25 items per batch.
|
|
131
|
+
*
|
|
132
|
+
* @async
|
|
133
|
+
* @param {Array<Object>} newItems - An array of data for the entities to be created.
|
|
134
|
+
* @return {Promise<{ createdItems: BaseModel[],
|
|
135
|
+
* errorItems: { item: Object, error: ElectroValidationError }[] }>} - A promise that resolves to
|
|
136
|
+
* an object containing the created items and any items that failed validation.
|
|
137
|
+
* @throws {ValidationError} - Throws a validation error if any of the items has validation
|
|
138
|
+
* failures.
|
|
139
|
+
*/
|
|
140
|
+
async createMany(newItems) {
|
|
141
|
+
if (!Array.isArray(newItems) || newItems.length === 0) {
|
|
142
|
+
const message = `Failed to create many [${this.entityName}]: items must be a non-empty array`;
|
|
143
|
+
this.log.error(message);
|
|
144
|
+
throw new Error(message);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const validatedItems = [];
|
|
149
|
+
const errorItems = [];
|
|
150
|
+
const createdItems = [];
|
|
151
|
+
|
|
152
|
+
newItems.forEach((item) => {
|
|
153
|
+
try {
|
|
154
|
+
this.entity.put(item).params();
|
|
155
|
+
validatedItems.push(item);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof ElectroValidationError) {
|
|
158
|
+
errorItems.push({ item, error: new ValidationError(error) });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* ElectroDB does not return the created items in the response for batch write operations.
|
|
165
|
+
* This listener intercepts the batch write requests and extracts the items before they
|
|
166
|
+
* are stored in the database.
|
|
167
|
+
* @param {Object} result - The result of the operation.
|
|
168
|
+
*/
|
|
169
|
+
const requestItemsListener = (result) => {
|
|
170
|
+
if (result?.type !== 'query' || result?.method !== 'batchWrite') {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
result.params?.RequestItems[this.entity.model.table].forEach((putRequest) => {
|
|
175
|
+
createdItems.push(putRequest.PutRequest.Item);
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
let records = [];
|
|
180
|
+
if (validatedItems.length > 0) {
|
|
181
|
+
const response = await this.entity.put(validatedItems).go(
|
|
182
|
+
{ listeners: [requestItemsListener] },
|
|
183
|
+
);
|
|
184
|
+
records = this._createInstances({ data: createdItems });
|
|
185
|
+
|
|
186
|
+
if (Array.isArray(response.unprocessed) && response.unprocessed.length > 0) {
|
|
187
|
+
this.log.error(`Failed to process all items in batch write for [${this.entityName}]: ${JSON.stringify(response.unprocessed)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { createdItems: records, errorItems };
|
|
192
|
+
} catch (error) {
|
|
193
|
+
this.log.error(`Failed to create many [${this.entityName}]`, error);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Updates a collection of entities in the database using a batch write (put) operation.
|
|
200
|
+
*
|
|
201
|
+
* @async
|
|
202
|
+
* @param {Array<BaseModel>} items - An array of model instances to be updated.
|
|
203
|
+
* @return {Promise<void>} - A promise that resolves when the update operation is complete.
|
|
204
|
+
* @throws {Error} - Throws an error if the update operation fails.
|
|
205
|
+
* @protected
|
|
206
|
+
*/
|
|
207
|
+
async _saveMany(items) {
|
|
208
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
209
|
+
const message = `Failed to save many [${this.entityName}]: items must be a non-empty array`;
|
|
210
|
+
this.log.error(message);
|
|
211
|
+
throw new Error(message);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const updates = items.map((item) => item.record);
|
|
216
|
+
const response = await this.entity.put(updates).go();
|
|
217
|
+
|
|
218
|
+
if (response.unprocessed) {
|
|
219
|
+
this.log.error(`Failed to process all items in batch write for [${this.entityName}]: ${JSON.stringify(response.unprocessed)}`);
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
this.log.error(`Failed to save many [${this.entityName}]`, error);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
116
226
|
}
|
|
117
227
|
|
|
118
228
|
export default BaseCollection;
|
package/src/v2/models/index.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface BaseModel {
|
|
|
25
25
|
* Interface representing an Opportunity model, extending BaseModel.
|
|
26
26
|
*/
|
|
27
27
|
export interface Opportunity extends BaseModel {
|
|
28
|
+
// eslint-disable-next-line no-use-before-define
|
|
29
|
+
addSuggestions(suggestions: object[]): Promise<Suggestion[]>;
|
|
28
30
|
// eslint-disable-next-line no-use-before-define
|
|
29
31
|
getSuggestions(): Promise<Suggestion[]>;
|
|
30
32
|
getSiteId(): string;
|
|
@@ -73,7 +75,8 @@ export interface Suggestion extends BaseModel {
|
|
|
73
75
|
*/
|
|
74
76
|
export interface BaseCollection<T extends BaseModel> {
|
|
75
77
|
findById(id: string): Promise<T>;
|
|
76
|
-
create(
|
|
78
|
+
create(item: object): Promise<T>;
|
|
79
|
+
createMany(items: object[]): Promise<T[]>;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
/**
|
|
@@ -90,6 +93,7 @@ export interface OpportunityCollection extends BaseCollection<Opportunity> {
|
|
|
90
93
|
export interface SuggestionCollection extends BaseCollection<Suggestion> {
|
|
91
94
|
allByOpportunityId(opportunityId: string): Promise<Suggestion[]>;
|
|
92
95
|
allByOpportunityIdAndStatus(opportunityId: string, status: string): Promise<Suggestion[]>;
|
|
96
|
+
bulkUpdateStatus(suggestions: Suggestion[], status: string): Promise<Suggestion[]>;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
/**
|
|
@@ -22,6 +22,25 @@ import BaseModel from './base.model.js';
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
class Opportunity extends BaseModel {
|
|
25
|
+
/**
|
|
26
|
+
* Adds the given suggestions to this Opportunity. Sets this opportunity as the parent
|
|
27
|
+
* of each suggestion, as such the opportunity ID does not need to be provided.
|
|
28
|
+
*
|
|
29
|
+
* @async
|
|
30
|
+
* @param {Array<Object>} suggestions - An array of suggestion objects to add.
|
|
31
|
+
* @return {Promise<{ createdItems: BaseModel[],
|
|
32
|
+
* errorItems: { item: Object, error: ValidationError }[] }>} - A promise that
|
|
33
|
+
* resolves to an object containing the created suggestion items and any
|
|
34
|
+
* errors that occurred.
|
|
35
|
+
*/
|
|
36
|
+
async addSuggestions(suggestions) {
|
|
37
|
+
const childSuggestions = suggestions.map((suggestion) => ({
|
|
38
|
+
...suggestion,
|
|
39
|
+
[this.idName]: this.getId(),
|
|
40
|
+
}));
|
|
41
|
+
return this._getAssociation('SuggestionCollection', 'createMany', childSuggestions);
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
/**
|
|
26
45
|
* Retrieves all Suggestion entities associated to this Opportunity.
|
|
27
46
|
* @async
|
|
@@ -38,7 +38,7 @@ class SuggestionCollection extends BaseCollection {
|
|
|
38
38
|
* Retrieves all Suggestion entities by their associated Opportunity ID.
|
|
39
39
|
* @async
|
|
40
40
|
* @param {string} opportunityId - The unique identifier of the associated Opportunity.
|
|
41
|
-
* @returns {Promise<
|
|
41
|
+
* @returns {Promise<Suggestion[]>} - A promise that resolves to an array of Suggestion
|
|
42
42
|
* instances related to the given Opportunity ID.
|
|
43
43
|
* @throws {Error} - Throws an error if the opportunityId is not provided or if the query fails.
|
|
44
44
|
*/
|
|
@@ -75,6 +75,37 @@ class SuggestionCollection extends BaseCollection {
|
|
|
75
75
|
|
|
76
76
|
return this._createInstances(records);
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Updates the status of multiple given suggestions. The given status must conform
|
|
81
|
+
* to the status enum defined in the Suggestion schema.
|
|
82
|
+
* Saves the updated suggestions to the database automatically.
|
|
83
|
+
* You don't need to call save() on the suggestions after calling this method.
|
|
84
|
+
* @async
|
|
85
|
+
* @param {Suggestion[]} suggestions - An array of Suggestion instances to update.
|
|
86
|
+
* @param {string} status - The new status to set for the suggestions.
|
|
87
|
+
* @return {Promise<*>} - A promise that resolves to the updated suggestions.
|
|
88
|
+
* @throws {Error} - Throws an error if the suggestions are not provided
|
|
89
|
+
* or if the status is invalid.
|
|
90
|
+
*/
|
|
91
|
+
async bulkUpdateStatus(suggestions, status) {
|
|
92
|
+
if (!Array.isArray(suggestions)) {
|
|
93
|
+
throw new Error('Suggestions must be an array');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const validStatuses = this._getEnumValues('status');
|
|
97
|
+
if (!validStatuses?.includes(status)) {
|
|
98
|
+
throw new Error(`Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
suggestions.forEach((suggestion) => {
|
|
102
|
+
suggestion.setStatus(status);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await this._saveMany(suggestions);
|
|
106
|
+
|
|
107
|
+
return suggestions;
|
|
108
|
+
}
|
|
78
109
|
}
|
|
79
110
|
|
|
80
111
|
export default SuggestionCollection;
|
package/src/v2/util/patcher.js
CHANGED
|
@@ -45,6 +45,7 @@ class Patcher {
|
|
|
45
45
|
this.model = entity.model;
|
|
46
46
|
this.idName = `${this.model.name.toLowerCase()}Id`;
|
|
47
47
|
this.record = record;
|
|
48
|
+
this.updates = {};
|
|
48
49
|
|
|
49
50
|
this.patchRecord = null;
|
|
50
51
|
}
|
|
@@ -104,6 +105,7 @@ class Patcher {
|
|
|
104
105
|
[propertyName]: value,
|
|
105
106
|
});
|
|
106
107
|
this.record[propertyName] = value;
|
|
108
|
+
this.updates[propertyName] = value;
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
/**
|
|
@@ -175,9 +177,20 @@ class Patcher {
|
|
|
175
177
|
* @throws {Error} - Throws an error if the save operation fails.
|
|
176
178
|
*/
|
|
177
179
|
async save() {
|
|
180
|
+
if (!this.hasUpdates()) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
178
183
|
await this.#getPatchRecord().go();
|
|
179
184
|
this.record.updatedAt = new Date().getTime();
|
|
180
185
|
}
|
|
186
|
+
|
|
187
|
+
getUpdates() {
|
|
188
|
+
return this.updates;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
hasUpdates() {
|
|
192
|
+
return Object.keys(this.updates).length > 0;
|
|
193
|
+
}
|
|
181
194
|
}
|
|
182
195
|
|
|
183
196
|
export default Patcher;
|