@adobe/spacecat-shared-data-access 1.57.2 → 1.58.1
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 -4
- package/src/v2/models/base.collection.js +57 -10
- package/src/v2/models/base.model.js +122 -15
- package/src/v2/models/index.d.ts +1 -0
- package/src/v2/models/opportunity.collection.js +2 -6
- package/src/v2/models/opportunity.model.js +3 -213
- package/src/v2/models/suggestion.collection.js +3 -10
- package/src/v2/models/suggestion.model.js +1 -112
- package/src/v2/readme.md +95 -83
- package/src/v2/schema/opportunity.schema.js +23 -9
- package/src/v2/schema/suggestion.schema.js +17 -7
- package/src/v2/util/reference.js +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-data-access-v1.58.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v1.58.0...@adobe/spacecat-shared-data-access-v1.58.1) (2024-11-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **deps:** update external fixes ([#465](https://github.com/adobe/spacecat-shared/issues/465)) ([d8ebb23](https://github.com/adobe/spacecat-shared/commit/d8ebb23fbd3d292479a4118dc6a9fb9931a31694))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-data-access-v1.58.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v1.57.2...@adobe/spacecat-shared-data-access-v1.58.0) (2024-11-26)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* auto references & attribute set/get ([#461](https://github.com/adobe/spacecat-shared/issues/461)) ([2c29683](https://github.com/adobe/spacecat-shared/commit/2c296835bb7e71f2984e5537eab1dfc25bcc6141))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-data-access-v1.57.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v1.57.1...@adobe/spacecat-shared-data-access-v1.57.2) (2024-11-23)
|
|
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.58.1",
|
|
4
4
|
"description": "Shared modules of the Spacecat Services - Data Access",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -34,19 +34,20 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@adobe/spacecat-shared-dynamo": "1.3.
|
|
38
|
-
"@adobe/spacecat-shared-utils": "1.23.
|
|
37
|
+
"@adobe/spacecat-shared-dynamo": "1.3.50",
|
|
38
|
+
"@adobe/spacecat-shared-utils": "1.23.1",
|
|
39
39
|
"@aws-sdk/client-dynamodb": "3.699.0",
|
|
40
40
|
"@aws-sdk/lib-dynamodb": "3.699.0",
|
|
41
41
|
"@types/joi": "17.2.3",
|
|
42
42
|
"aws-xray-sdk": "3.10.2",
|
|
43
43
|
"electrodb": "3.0.1",
|
|
44
44
|
"joi": "17.13.3",
|
|
45
|
+
"pluralize": "8.0.0",
|
|
45
46
|
"uuid": "11.0.3"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"chai": "5.1.2",
|
|
49
|
-
"chai-as-promised": "8.0.
|
|
50
|
+
"chai-as-promised": "8.0.1",
|
|
50
51
|
"dynamo-db-local": "9.3.0",
|
|
51
52
|
"sinon": "19.0.2",
|
|
52
53
|
"sinon-chai": "4.0.0"
|
|
@@ -16,6 +16,7 @@ import { ElectroValidationError } from 'electrodb';
|
|
|
16
16
|
|
|
17
17
|
import ValidationError from '../errors/validation.error.js';
|
|
18
18
|
import { guardId } from '../util/guards.js';
|
|
19
|
+
import { keyNamesToIndexName } from '../util/reference.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* BaseCollection - A base class for managing collections of entities in the application.
|
|
@@ -30,7 +31,7 @@ class BaseCollection {
|
|
|
30
31
|
* @constructor
|
|
31
32
|
* @param {Object} electroService - The ElectroDB service used for managing entities.
|
|
32
33
|
* @param {Object} modelFactory - A factory for creating model instances.
|
|
33
|
-
* @param {
|
|
34
|
+
* @param {BaseModel} clazz - The model class that represents the entity.
|
|
34
35
|
* @param {Object} log - A logger for capturing logging information.
|
|
35
36
|
*/
|
|
36
37
|
constructor(electroService, modelFactory, clazz, log) {
|
|
@@ -45,12 +46,12 @@ class BaseCollection {
|
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Creates an instance of a model from a record.
|
|
48
|
-
* @
|
|
49
|
+
* @private
|
|
49
50
|
* @param {Object} record - The record containing data to create the model instance.
|
|
50
51
|
* @returns {BaseModel|null} - Returns an instance of the model class if the data is valid,
|
|
51
52
|
* otherwise null.
|
|
52
53
|
*/
|
|
53
|
-
|
|
54
|
+
#createInstance(record) {
|
|
54
55
|
if (!isNonEmptyObject(record?.data)) {
|
|
55
56
|
this.log.warn(`Failed to create instance of [${this.entityName}]: record is empty`);
|
|
56
57
|
return null;
|
|
@@ -66,18 +67,25 @@ class BaseCollection {
|
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
69
|
* Creates instances of models from a set of records.
|
|
69
|
-
* @
|
|
70
|
+
* @private
|
|
70
71
|
* @param {Object} records - The records containing data to create the model instances.
|
|
71
72
|
* @returns {Array<BaseModel>} - An array of instances of the model class.
|
|
72
73
|
*/
|
|
73
|
-
|
|
74
|
+
#createInstances(records) {
|
|
74
75
|
if (!Array.isArray(records?.data)) {
|
|
75
76
|
this.log.warn(`Failed to create instances of [${this.entityName}]: records are empty`);
|
|
76
77
|
return [];
|
|
77
78
|
}
|
|
78
|
-
return records.data.map((record) => this
|
|
79
|
+
return records.data.map((record) => this.#createInstance({ data: record }));
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Retrieves the enum values for a field in the entity schema. Useful for validating
|
|
84
|
+
* enum values prior to creating or updating an entity.
|
|
85
|
+
* @param {string} fieldName - The name of the field to retrieve enum values for.
|
|
86
|
+
* @return {string[]} - An array of enum values for the field.
|
|
87
|
+
* @protected
|
|
88
|
+
*/
|
|
81
89
|
_getEnumValues(fieldName) {
|
|
82
90
|
return this.entity.model.schema.attributes[fieldName]?.enumArray;
|
|
83
91
|
}
|
|
@@ -95,7 +103,38 @@ class BaseCollection {
|
|
|
95
103
|
|
|
96
104
|
const record = await this.entity.get({ [this.idName]: id }).go();
|
|
97
105
|
|
|
98
|
-
return this
|
|
106
|
+
return this.#createInstance(record);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Finds entities by a set of index keys. Index keys are used to query entities by
|
|
111
|
+
* a specific index defined in the entity schema. The index keys must match the
|
|
112
|
+
* fields defined in the index.
|
|
113
|
+
* @param {Object} keys - The index keys to use for the query.
|
|
114
|
+
* @return {Promise<Array<BaseModel>>} - A promise that resolves to an array of model instances.
|
|
115
|
+
* @throws {Error} - Throws an error if the index keys are not provided or if the index
|
|
116
|
+
* is not found.
|
|
117
|
+
* @async
|
|
118
|
+
*/
|
|
119
|
+
async findByIndexKeys(keys) {
|
|
120
|
+
if (!isNonEmptyObject(keys)) {
|
|
121
|
+
const message = `Failed to find by index keys [${this.entityName}]: keys are required`;
|
|
122
|
+
this.log.error(message);
|
|
123
|
+
throw new Error(message);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const indexName = keyNamesToIndexName(Object.keys(keys));
|
|
127
|
+
const index = this.entity.query[indexName];
|
|
128
|
+
|
|
129
|
+
if (!index) {
|
|
130
|
+
const message = `Failed to find by index keys [${this.entityName}]: index [${indexName}] not found`;
|
|
131
|
+
this.log.error(message);
|
|
132
|
+
throw new Error(message);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const records = await index(keys).go();
|
|
136
|
+
|
|
137
|
+
return this.#createInstances(records);
|
|
99
138
|
}
|
|
100
139
|
|
|
101
140
|
/**
|
|
@@ -118,7 +157,7 @@ class BaseCollection {
|
|
|
118
157
|
// todo: catch ElectroDB validation errors and re-throws as ValidationError
|
|
119
158
|
// todo: validate associations
|
|
120
159
|
const record = await this.entity.create(item).go();
|
|
121
|
-
return this
|
|
160
|
+
return this.#createInstance(record);
|
|
122
161
|
} catch (error) {
|
|
123
162
|
this.log.error(`Failed to create [${this.entityName}]`, error);
|
|
124
163
|
throw error;
|
|
@@ -131,13 +170,14 @@ class BaseCollection {
|
|
|
131
170
|
*
|
|
132
171
|
* @async
|
|
133
172
|
* @param {Array<Object>} newItems - An array of data for the entities to be created.
|
|
173
|
+
* @param {BaseModel} [parent] - Optional parent entity that these items are associated with.
|
|
134
174
|
* @return {Promise<{ createdItems: BaseModel[],
|
|
135
175
|
* errorItems: { item: Object, error: ElectroValidationError }[] }>} - A promise that resolves to
|
|
136
176
|
* an object containing the created items and any items that failed validation.
|
|
137
177
|
* @throws {ValidationError} - Throws a validation error if any of the items has validation
|
|
138
178
|
* failures.
|
|
139
179
|
*/
|
|
140
|
-
async createMany(newItems) {
|
|
180
|
+
async createMany(newItems, parent = null) {
|
|
141
181
|
if (!Array.isArray(newItems) || newItems.length === 0) {
|
|
142
182
|
const message = `Failed to create many [${this.entityName}]: items must be a non-empty array`;
|
|
143
183
|
this.log.error(message);
|
|
@@ -181,13 +221,20 @@ class BaseCollection {
|
|
|
181
221
|
const response = await this.entity.put(validatedItems).go(
|
|
182
222
|
{ listeners: [requestItemsListener] },
|
|
183
223
|
);
|
|
184
|
-
records = this
|
|
224
|
+
records = this.#createInstances({ data: createdItems });
|
|
185
225
|
|
|
186
226
|
if (Array.isArray(response.unprocessed) && response.unprocessed.length > 0) {
|
|
187
227
|
this.log.error(`Failed to process all items in batch write for [${this.entityName}]: ${JSON.stringify(response.unprocessed)}`);
|
|
188
228
|
}
|
|
189
229
|
}
|
|
190
230
|
|
|
231
|
+
if (parent) {
|
|
232
|
+
records.forEach((record) => {
|
|
233
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
234
|
+
record._cacheReference(parent.entity.model.name, parent);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
191
238
|
return { createdItems: records, errorItems };
|
|
192
239
|
} catch (error) {
|
|
193
240
|
this.log.error(`Failed to create many [${this.entityName}]`, error);
|
|
@@ -10,12 +10,28 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { isNonEmptyObject } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
|
|
13
15
|
import Patcher from '../util/patcher.js';
|
|
16
|
+
import {
|
|
17
|
+
capitalize,
|
|
18
|
+
entityNameToCollectionName,
|
|
19
|
+
entityNameToIdName,
|
|
20
|
+
entityNameToReferenceMethodName, idNameToEntityName,
|
|
21
|
+
} from '../util/reference.js';
|
|
14
22
|
|
|
15
23
|
/**
|
|
16
24
|
* Base - A base class for representing individual entities in the application.
|
|
17
25
|
* Provides common functionality for entity management, including fetching, updating,
|
|
18
|
-
* and deleting records.
|
|
26
|
+
* and deleting records. This class is intended to be extended by specific entity classes
|
|
27
|
+
* that represent individual entities in the application. The BaseModel class provides
|
|
28
|
+
* methods for fetching associated entities based on the type of relationship
|
|
29
|
+
* (belongs_to, has_one, has_many).
|
|
30
|
+
* The fetched references are cached to avoid redundant database queries. If the reference
|
|
31
|
+
* is already cached, it will be returned directly.
|
|
32
|
+
* Attribute values can be accessed and modified using getter and setter methods that are
|
|
33
|
+
* automatically generated based on the entity schema. The BaseModel class also provides
|
|
34
|
+
* methods for removing and saving entities to the database.
|
|
19
35
|
*
|
|
20
36
|
* @class BaseModel
|
|
21
37
|
*/
|
|
@@ -35,31 +51,121 @@ class BaseModel {
|
|
|
35
51
|
this.entity = electroService.entities[this.entityName];
|
|
36
52
|
this.idName = `${this.entityName}Id`;
|
|
37
53
|
this.log = log;
|
|
54
|
+
this.referencesCache = {};
|
|
38
55
|
|
|
39
56
|
this.patcher = new Patcher(this.entity, this.record);
|
|
40
|
-
|
|
57
|
+
|
|
58
|
+
this.#initializeReferences();
|
|
59
|
+
this.#initializeAttributes();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Initializes the references for the current entity.
|
|
64
|
+
* This method is called during the construction of the entity instance
|
|
65
|
+
* to set up the reference methods for fetching associated entities.
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
#initializeReferences() {
|
|
69
|
+
const { references } = this.entity.model.original;
|
|
70
|
+
if (!isNonEmptyObject(references)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const [type, refs] of Object.entries(references)) {
|
|
75
|
+
refs.forEach((ref) => {
|
|
76
|
+
const { target } = ref;
|
|
77
|
+
const methodName = entityNameToReferenceMethodName(target, type);
|
|
78
|
+
|
|
79
|
+
this[methodName] = async () => this._fetchReference(type, target);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#initializeAttributes() {
|
|
85
|
+
const { attributes } = this.entity.model.schema;
|
|
86
|
+
|
|
87
|
+
if (!isNonEmptyObject(attributes)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const [name, attr] of Object.entries(attributes)) {
|
|
92
|
+
const capitalized = capitalize(name);
|
|
93
|
+
const getterMethodName = `get${capitalized}`;
|
|
94
|
+
const setterMethodName = `set${capitalized}`;
|
|
95
|
+
const isReference = this.entity.model.original
|
|
96
|
+
.references?.belongs_to?.some((ref) => ref.target === idNameToEntityName(name));
|
|
97
|
+
|
|
98
|
+
if (!this[getterMethodName] || name === this.idName) {
|
|
99
|
+
this[getterMethodName] = () => this.record[name];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!this[setterMethodName] && !attr.readOnly) {
|
|
103
|
+
this[setterMethodName] = (value) => {
|
|
104
|
+
this.patcher.patchValue(name, value, isReference);
|
|
105
|
+
return this;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Gets a cached reference for the specified entity.
|
|
113
|
+
* @param {string} targetName - The name of the entity to fetch.
|
|
114
|
+
* @return {*}
|
|
115
|
+
*/
|
|
116
|
+
#getCachedReference(targetName) {
|
|
117
|
+
return this.referencesCache[targetName];
|
|
41
118
|
}
|
|
42
119
|
|
|
43
120
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @
|
|
121
|
+
* Caches a reference for the specified entity. This method is used to store
|
|
122
|
+
* fetched references to avoid redundant database queries.
|
|
123
|
+
* @param {string} targetName - The name of the entity to cache.
|
|
124
|
+
* @param {*} reference - The reference to cache.
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
_cacheReference(targetName, reference) {
|
|
128
|
+
this.referencesCache[targetName] = reference;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Fetches a reference for the specified entity. This method is used to fetch
|
|
133
|
+
* associated entities based on the type of relationship (belongs_to, has_one, has_many).
|
|
134
|
+
* The fetched references are cached to avoid redundant database queries. If the reference
|
|
135
|
+
* is already cached, it will be returned directly.
|
|
136
|
+
* References are defined in the entity model and are used to fetch associated entities.
|
|
47
137
|
* @async
|
|
48
|
-
* @param {string}
|
|
49
|
-
* @param {string}
|
|
50
|
-
* @
|
|
51
|
-
*
|
|
138
|
+
* @param {string} type - The type of relationship (belongs_to, has_one, has_many).
|
|
139
|
+
* @param {string} targetName - The name of the entity to fetch.
|
|
140
|
+
* @return {Promise<*|null>} - A promise that resolves to the fetched reference or null if
|
|
141
|
+
* not found.
|
|
142
|
+
* @private
|
|
52
143
|
*/
|
|
53
|
-
async
|
|
54
|
-
|
|
144
|
+
async _fetchReference(type, targetName) { /* eslint-disable no-underscore-dangle */
|
|
145
|
+
let result = this.#getCachedReference(targetName);
|
|
146
|
+
if (result) {
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
55
149
|
|
|
56
|
-
|
|
150
|
+
const collectionName = entityNameToCollectionName(targetName);
|
|
151
|
+
const targetCollection = this.modelFactory.getCollection(collectionName);
|
|
152
|
+
|
|
153
|
+
if (type === 'belongs_to' || type === 'has_one') {
|
|
154
|
+
const foreignKey = entityNameToIdName(targetName);
|
|
155
|
+
const id = this.record[foreignKey];
|
|
156
|
+
if (!id) return null;
|
|
157
|
+
|
|
158
|
+
result = await targetCollection.findById(id);
|
|
159
|
+
} else if (type === 'has_many') {
|
|
160
|
+
const foreignKey = entityNameToIdName(this.entityName);
|
|
161
|
+
result = await targetCollection.findByIndexKeys({ [foreignKey]: this.getId() });
|
|
162
|
+
}
|
|
57
163
|
|
|
58
|
-
if (
|
|
59
|
-
|
|
164
|
+
if (result) {
|
|
165
|
+
await this._cacheReference(targetName, result);
|
|
60
166
|
}
|
|
61
167
|
|
|
62
|
-
return
|
|
168
|
+
return result;
|
|
63
169
|
}
|
|
64
170
|
|
|
65
171
|
/**
|
|
@@ -116,6 +222,7 @@ class BaseModel {
|
|
|
116
222
|
// todo: validate associations
|
|
117
223
|
try {
|
|
118
224
|
await this.patcher.save();
|
|
225
|
+
// todo: in case references are updated, clear or refresh references cache
|
|
119
226
|
return this;
|
|
120
227
|
} catch (error) {
|
|
121
228
|
this.log.error('Failed to save record', error);
|
package/src/v2/models/index.d.ts
CHANGED
|
@@ -81,6 +81,7 @@ export interface Suggestion extends BaseModel {
|
|
|
81
81
|
*/
|
|
82
82
|
export interface BaseCollection<T extends BaseModel> {
|
|
83
83
|
findById(id: string): Promise<T>;
|
|
84
|
+
findByIndexKeys(indexKeys: object): Promise<T[]>;
|
|
84
85
|
create(item: object): Promise<T>;
|
|
85
86
|
createMany(items: object[]): Promise<MultiStatusCreateResult<T>>;
|
|
86
87
|
}
|
|
@@ -46,10 +46,7 @@ class OpportunityCollection extends BaseCollection {
|
|
|
46
46
|
if (!hasText(siteId)) {
|
|
47
47
|
throw new Error('SiteId is required');
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
const records = await this.entity.query.bySiteId({ siteId }).go();
|
|
51
|
-
|
|
52
|
-
return this._createInstances(records);
|
|
49
|
+
return this.findByIndexKeys({ siteId });
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
/**
|
|
@@ -70,8 +67,7 @@ class OpportunityCollection extends BaseCollection {
|
|
|
70
67
|
throw new Error('Status is required');
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
|
|
74
|
-
return this._createInstances(records);
|
|
70
|
+
return this.findByIndexKeys({ siteId, status });
|
|
75
71
|
}
|
|
76
72
|
}
|
|
77
73
|
|
|
@@ -38,219 +38,9 @@ class Opportunity extends BaseModel {
|
|
|
38
38
|
...suggestion,
|
|
39
39
|
[this.idName]: this.getId(),
|
|
40
40
|
}));
|
|
41
|
-
return this.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Retrieves all Suggestion entities associated to this Opportunity.
|
|
46
|
-
* @async
|
|
47
|
-
* @returns {Promise<Array<Suggestion>>} - A promise that resolves to an array of Suggestion
|
|
48
|
-
* instances associated with this Opportunity.
|
|
49
|
-
*/
|
|
50
|
-
async getSuggestions() {
|
|
51
|
-
return this._getAssociation(
|
|
52
|
-
'SuggestionCollection',
|
|
53
|
-
'allByOpportunityId',
|
|
54
|
-
this.getId(),
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Gets the ID of the site associated with this Opportunity.
|
|
60
|
-
* @returns {string} - The unique identifier of the site.
|
|
61
|
-
*/
|
|
62
|
-
getSiteId() {
|
|
63
|
-
return this.record.siteId;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Sets the site ID for this Opportunity (associates it with a site).
|
|
68
|
-
* @param {string} siteId - The unique identifier of the site.
|
|
69
|
-
* @returns {Opportunity} - The current instance of Opportunity for chaining.
|
|
70
|
-
* @throws {Error} - Throws an error if the siteId is not a valid UUID.
|
|
71
|
-
*/
|
|
72
|
-
setSiteId(siteId) {
|
|
73
|
-
this.patcher.patchValue('siteId', siteId, true);
|
|
74
|
-
return this;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Gets the ID of the audit associated with this Opportunity.
|
|
79
|
-
* @returns {string} - The unique identifier of the audit.
|
|
80
|
-
*/
|
|
81
|
-
getAuditId() {
|
|
82
|
-
return this.record.auditId;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Sets the audit ID for this Opportunity (associates it with an audit).
|
|
87
|
-
* @param {string} auditId - The unique identifier of the audit.
|
|
88
|
-
* @returns {Opportunity} - The current instance of Opportunity for chaining.
|
|
89
|
-
* @throws {Error} - Throws an error if the auditId is not a valid UUID.
|
|
90
|
-
*/
|
|
91
|
-
setAuditId(auditId) {
|
|
92
|
-
this.patcher.patchValue('auditId', auditId, true);
|
|
93
|
-
return this;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Gets the runbook URL or reference for this Opportunity.
|
|
98
|
-
* @returns {string} - The runbook reference for this Opportunity.
|
|
99
|
-
*/
|
|
100
|
-
getRunbook() {
|
|
101
|
-
return this.record.runbook;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Sets the runbook URL or reference for this Opportunity.
|
|
106
|
-
* @param {string} runbook - The runbook reference or URL.
|
|
107
|
-
* @returns {Opportunity} - The current instance of Opportunity for chaining.
|
|
108
|
-
* @throws {Error} - Throws an error if the runbook is not a string or empty.
|
|
109
|
-
*/
|
|
110
|
-
setRunbook(runbook) {
|
|
111
|
-
this.patcher.patchValue('runbook', runbook);
|
|
112
|
-
return this;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Gets the guidance information for this Opportunity.
|
|
117
|
-
* @returns {Object} - The guidance object for this Opportunity.
|
|
118
|
-
*/
|
|
119
|
-
getGuidance() {
|
|
120
|
-
return this.record.guidance;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Sets the guidance information for this Opportunity.
|
|
125
|
-
* @param {Object} guidance - The guidance object.
|
|
126
|
-
* @returns {Opportunity} - The current instance of Opportunity for chaining.
|
|
127
|
-
* @throws {Error} - Throws an error if the guidance is not an object or empty.
|
|
128
|
-
*/
|
|
129
|
-
setGuidance(guidance) {
|
|
130
|
-
this.patcher.patchValue('guidance', guidance);
|
|
131
|
-
return this;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Gets the title of this Opportunity.
|
|
136
|
-
* @returns {string} - The title of the Opportunity.
|
|
137
|
-
*/
|
|
138
|
-
getTitle() {
|
|
139
|
-
return this.record.title;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Sets the title of this Opportunity.
|
|
144
|
-
* @param {string} title - The title of the Opportunity.
|
|
145
|
-
* @returns {Opportunity} - The current instance of Opportunity for chaining.
|
|
146
|
-
* @throws {Error} - Throws an error if the title is not a string or empty.
|
|
147
|
-
*/
|
|
148
|
-
setTitle(title) {
|
|
149
|
-
this.patcher.patchValue('title', title);
|
|
150
|
-
return this;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Gets the description of this Opportunity.
|
|
155
|
-
* @returns {string} - The description of the Opportunity.
|
|
156
|
-
*/
|
|
157
|
-
getDescription() {
|
|
158
|
-
return this.record.description;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Sets the description of this Opportunity.
|
|
163
|
-
* @param {string} description - The description of the Opportunity.
|
|
164
|
-
* @returns {Opportunity} - The current instance of Opportunity for chaining.
|
|
165
|
-
* @throws {Error} - Throws an error if the description is not a string or empty.
|
|
166
|
-
*/
|
|
167
|
-
setDescription(description) {
|
|
168
|
-
this.patcher.patchValue('description', description);
|
|
169
|
-
return this;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Gets the type of this Opportunity.
|
|
174
|
-
* @returns {string} - The type of the Opportunity.
|
|
175
|
-
*/
|
|
176
|
-
getType() {
|
|
177
|
-
return this.record.type;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Gets the status of this Opportunity.
|
|
182
|
-
* @returns {string} - The status of the Opportunity.
|
|
183
|
-
*/
|
|
184
|
-
getStatus() {
|
|
185
|
-
return this.record.status;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Sets the status of this Opportunity. Check the schema for valid status values.
|
|
190
|
-
* @param {string} status - The status of the Opportunity.
|
|
191
|
-
* @returns {Opportunity} - The current instance of Opportunity for chaining.
|
|
192
|
-
* @throws {Error} - Throws an error if the status is not a valid value.
|
|
193
|
-
*/
|
|
194
|
-
setStatus(status) {
|
|
195
|
-
this.patcher.patchValue('status', status);
|
|
196
|
-
return this;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Gets the origin of this Opportunity.
|
|
201
|
-
* @returns {string} - The origin of the Opportunity (e.g., ESS_OPS, AI, AUTOMATION).
|
|
202
|
-
*/
|
|
203
|
-
getOrigin() {
|
|
204
|
-
return this.record.origin;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Sets the origin of this Opportunity. Check the schema for valid origin values.
|
|
209
|
-
* @param {string} origin - The origin of the Opportunity.
|
|
210
|
-
* @returns {Opportunity} - The current instance of Opportunity for chaining.
|
|
211
|
-
* @throws {Error} - Throws an error if the origin is not a valid value.
|
|
212
|
-
*/
|
|
213
|
-
setOrigin(origin) {
|
|
214
|
-
this.patcher.patchValue('origin', origin);
|
|
215
|
-
return this;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Gets the tags associated with this Opportunity.
|
|
220
|
-
* @returns {Array<string>} - An array of tags associated with the Opportunity.
|
|
221
|
-
*/
|
|
222
|
-
getTags() {
|
|
223
|
-
return this.record.tags;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Sets the tags for this Opportunity. Duplicate tags are made unique.
|
|
228
|
-
* @param {Array<string>} tags - An array of tags to associate with the Opportunity.
|
|
229
|
-
* @returns {Opportunity} - The current instance of Opportunity for chaining.
|
|
230
|
-
* @throws {Error} - Throws an error if the tags are not an array.
|
|
231
|
-
*/
|
|
232
|
-
setTags(tags) {
|
|
233
|
-
this.patcher.patchValue('tags', tags);
|
|
234
|
-
return this;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Gets additional data associated with this Opportunity.
|
|
239
|
-
* @returns {Object} - The additional data for the Opportunity.
|
|
240
|
-
*/
|
|
241
|
-
getData() {
|
|
242
|
-
return this.record.data;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Sets additional data for this Opportunity.
|
|
247
|
-
* @param {Object} data - The data to set for the Opportunity.
|
|
248
|
-
* @returns {Opportunity} - The current instance of Opportunity for chaining.
|
|
249
|
-
* @throws {Error} - Throws an error if the data is not a valid object.
|
|
250
|
-
*/
|
|
251
|
-
setData(data) {
|
|
252
|
-
this.patcher.patchValue('data', data);
|
|
253
|
-
return this;
|
|
41
|
+
return this.modelFactory
|
|
42
|
+
.getCollection('SuggestionCollection')
|
|
43
|
+
.createMany(childSuggestions, this);
|
|
254
44
|
}
|
|
255
45
|
}
|
|
256
46
|
|
|
@@ -46,17 +46,14 @@ class SuggestionCollection extends BaseCollection {
|
|
|
46
46
|
if (!hasText(opportunityId)) {
|
|
47
47
|
throw new Error('OpportunityId is required');
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
const records = await this.entity.query.byOpportunityId({ opportunityId }).go();
|
|
51
|
-
|
|
52
|
-
return this._createInstances(records);
|
|
49
|
+
return this.findByIndexKeys({ opportunityId });
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
/**
|
|
56
53
|
* Retrieves all Suggestion entities by their associated Opportunity ID and status.
|
|
57
54
|
* @param {string} opportunityId - The unique identifier of the associated Opportunity.
|
|
58
55
|
* @param {string} status - The status of the Suggestion entities
|
|
59
|
-
* @return {Promise<
|
|
56
|
+
* @return {Promise<BaseModel[]>} - A promise that resolves to an array of
|
|
60
57
|
* Suggestion instances.
|
|
61
58
|
* @throws {Error} - Throws an error if the opportunityId or status is not provided.
|
|
62
59
|
*/
|
|
@@ -69,11 +66,7 @@ class SuggestionCollection extends BaseCollection {
|
|
|
69
66
|
throw new Error('Status is required');
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
{ opportunityId, status },
|
|
74
|
-
).go();
|
|
75
|
-
|
|
76
|
-
return this._createInstances(records);
|
|
69
|
+
return this.findByIndexKeys({ opportunityId, status });
|
|
77
70
|
}
|
|
78
71
|
|
|
79
72
|
/**
|
|
@@ -21,118 +21,7 @@ import BaseModel from './base.model.js';
|
|
|
21
21
|
* @extends BaseModel
|
|
22
22
|
*/
|
|
23
23
|
class Suggestion extends BaseModel {
|
|
24
|
-
|
|
25
|
-
* Retrieves the Opportunity entity associated to this Suggestion.
|
|
26
|
-
* @async
|
|
27
|
-
* @returns {Promise<Opportunity>} - A promise that resolves to the Opportunity
|
|
28
|
-
* instance associated with this Suggestion.
|
|
29
|
-
*/
|
|
30
|
-
async getOpportunity() {
|
|
31
|
-
return this._getAssociation('OpportunityCollection', 'findById', this.getOpportunityId());
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Gets the Opportunity ID for this Suggestion.
|
|
36
|
-
* @returns {string} - The unique identifier of the related Opportunity.
|
|
37
|
-
*/
|
|
38
|
-
getOpportunityId() {
|
|
39
|
-
return this.record.opportunityId;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Sets the Opportunity ID for this Suggestion.
|
|
44
|
-
* @param {string} opportunityId - The unique identifier of the related Opportunity.
|
|
45
|
-
* @returns {Suggestion} - The current instance of Suggestion for chaining.
|
|
46
|
-
* @throws {Error} - Throws an error if the opportunityId is not a valid UUID.
|
|
47
|
-
*/
|
|
48
|
-
setOpportunityId(opportunityId) {
|
|
49
|
-
this.patcher.patchValue('opportunityId', opportunityId, true);
|
|
50
|
-
return this;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Gets the type of this Suggestion.
|
|
55
|
-
* @returns {string} - The type of the Suggestion (e.g., CODE_CHANGE, CONTENT_UPDATE).
|
|
56
|
-
*/
|
|
57
|
-
getType() {
|
|
58
|
-
return this.record.type;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Gets the status of this Suggestion.
|
|
63
|
-
* @returns {string} - The status of the Suggestion (e.g., NEW, APPROVED, SKIPPED, FIXED, ERROR).
|
|
64
|
-
*/
|
|
65
|
-
getStatus() {
|
|
66
|
-
return this.record.status;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Sets the status of this Suggestion. Check the schema for possible values.
|
|
71
|
-
* @param {string} status - The new status of the Suggestion.
|
|
72
|
-
* @returns {Suggestion} - The current instance of Suggestion for chaining.
|
|
73
|
-
* @throws {Error} - Throws an error if the status is not a valid value.
|
|
74
|
-
*/
|
|
75
|
-
setStatus(status) {
|
|
76
|
-
this.patcher.patchValue('status', status);
|
|
77
|
-
return this;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Gets the rank of this Suggestion.
|
|
82
|
-
* @returns {number} - The rank of the Suggestion used for sorting or prioritization.
|
|
83
|
-
*/
|
|
84
|
-
getRank() {
|
|
85
|
-
return this.record.rank;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Sets the rank of this Suggestion.
|
|
90
|
-
* @param {number} rank - The rank value to set for this Suggestion.
|
|
91
|
-
* @returns {Suggestion} - The current instance of Suggestion for chaining.
|
|
92
|
-
* @throws {Error} - Throws an error if the rank is not a valid number.
|
|
93
|
-
*/
|
|
94
|
-
setRank(rank) {
|
|
95
|
-
this.patcher.patchValue('rank', rank);
|
|
96
|
-
return this;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Gets additional data associated with this Suggestion.
|
|
101
|
-
* @returns {Object} - The additional data for the Suggestion.
|
|
102
|
-
*/
|
|
103
|
-
getData() {
|
|
104
|
-
return this.record.data;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Sets additional data for this Suggestion.
|
|
109
|
-
* @param {Object} data - The data to set for the Suggestion.
|
|
110
|
-
* @returns {Suggestion} - The current instance of Suggestion for chaining.
|
|
111
|
-
* @throws {Error} - Throws an error if the data is not a valid object.
|
|
112
|
-
*/
|
|
113
|
-
setData(data) {
|
|
114
|
-
this.patcher.patchValue('data', data);
|
|
115
|
-
return this;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Gets the KPI deltas for this Suggestion.
|
|
120
|
-
* @returns {Object} - The key performance indicator deltas that are affected by this Suggestion.
|
|
121
|
-
*/
|
|
122
|
-
getKpiDeltas() {
|
|
123
|
-
return this.record.kpiDeltas;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Sets the KPI deltas for this Suggestion.
|
|
128
|
-
* @param {Object} kpiDeltas - The KPI deltas to set for the Suggestion.
|
|
129
|
-
* @returns {Suggestion} - The current instance of Suggestion for chaining.
|
|
130
|
-
* @throws {Error} - Throws an error if the kpiDeltas is not a valid object.
|
|
131
|
-
*/
|
|
132
|
-
setKpiDeltas(kpiDeltas) {
|
|
133
|
-
this.patcher.patchValue('kpiDeltas', kpiDeltas);
|
|
134
|
-
return this;
|
|
135
|
-
}
|
|
24
|
+
// add your customized methods here
|
|
136
25
|
}
|
|
137
26
|
|
|
138
27
|
export default Suggestion;
|
package/src/v2/readme.md
CHANGED
|
@@ -2,14 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
This repository contains a model framework built using the ElectroDB ORM, designed to manage website improvements in a scalable manner. The system consists of several entities, including Opportunities and Suggestions, which represent potential areas of improvement and the actions to resolve them.
|
|
4
4
|
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Architecture Overview](#architecture-overview)
|
|
8
|
+
2. [Entities and Relationships](#entities-and-relationships)
|
|
9
|
+
3. [Getting Started](#getting-started)
|
|
10
|
+
4. [Adding a New ElectroDB-Based Entity](#adding-a-new-electrodb-based-entity)
|
|
11
|
+
- [Step 1: Define the Entity Schema](#step-1-define-the-entity-schema)
|
|
12
|
+
- [Step 2: Add a Model Class](#step-2-add-a-model-class)
|
|
13
|
+
- [Step 3: Add a Collection Class](#step-3-add-a-collection-class)
|
|
14
|
+
- [Step 4: Integrate the Entity into Model Factory](#step-4-integrate-the-entity-into-model-factory)
|
|
15
|
+
- [Step 5: Write Unit and Integration Tests](#step-5-write-unit-and-integration-tests)
|
|
16
|
+
- [Step 6: Create JSDoc and Update Documentation](#step-6-create-jsdoc-and-update-documentation)
|
|
17
|
+
- [Step 7: Run Tests and Verify](#step-7-run-tests-and-verify)
|
|
18
|
+
|
|
5
19
|
## Architecture Overview
|
|
6
20
|
|
|
7
|
-
The architecture
|
|
21
|
+
The architecture follows a collection-management pattern with ElectroDB, enabling efficient handling of DynamoDB entities. The architecture is organized into the following layers:
|
|
8
22
|
|
|
9
|
-
1. **Data Layer**:
|
|
10
|
-
2. **Model Layer**: The `BaseModel` provides
|
|
11
|
-
3. **Collection Layer**: The `BaseCollection` handles
|
|
12
|
-
4. **Factory Layer**: The `ModelFactory` centralizes
|
|
23
|
+
1. **Data Layer**: Uses DynamoDB with ElectroDB to manage schema definitions and data interactions.
|
|
24
|
+
2. **Model Layer**: The `BaseModel` provides methods like `save`, `remove`, and manages associations. Entity classes such as `Opportunity` and `Suggestion` extend `BaseModel` for specific features.
|
|
25
|
+
3. **Collection Layer**: The `BaseCollection` handles CRUD operations for entities. Specialized collections, like `OpportunityCollection` and `SuggestionCollection`, extend `BaseCollection` with tailored methods for specific entities.
|
|
26
|
+
4. **Factory Layer**: The `ModelFactory` centralizes instantiation of models and collections, providing a unified interface for different entity types.
|
|
13
27
|
|
|
14
28
|
### Architectural Diagram
|
|
15
29
|
|
|
@@ -44,9 +58,10 @@ The architecture is centered around a collection-management pattern with Electro
|
|
|
44
58
|
```
|
|
45
59
|
|
|
46
60
|
## Entities and Relationships
|
|
61
|
+
|
|
47
62
|
- **Opportunity**: Represents a specific issue identified on a website. It includes attributes like `title`, `description`, `siteId`, and `status`.
|
|
48
63
|
- **Suggestion**: Represents a proposed fix for an Opportunity. Attributes include `opportunityId`, `type`, `status`, and `rank`.
|
|
49
|
-
- **
|
|
64
|
+
- **Relationships**: Opportunities have many Suggestions. This relationship is implemented through `OpportunityCollection` and `SuggestionCollection`, which interact via ElectroDB-managed DynamoDB relationships.
|
|
50
65
|
|
|
51
66
|
## Getting Started
|
|
52
67
|
|
|
@@ -56,8 +71,8 @@ The architecture is centered around a collection-management pattern with Electro
|
|
|
56
71
|
```
|
|
57
72
|
|
|
58
73
|
2. **Setup DynamoDB**
|
|
59
|
-
-
|
|
60
|
-
- Configure the DynamoDB table name and related settings in
|
|
74
|
+
- Ensure AWS credentials are configured and a DynamoDB table is set up.
|
|
75
|
+
- Configure the DynamoDB table name and related settings in `index.js`.
|
|
61
76
|
|
|
62
77
|
3. **Usage Example**
|
|
63
78
|
```javascript
|
|
@@ -79,17 +94,12 @@ The architecture is centered around a collection-management pattern with Electro
|
|
|
79
94
|
|
|
80
95
|
## Adding a New ElectroDB-Based Entity
|
|
81
96
|
|
|
82
|
-
This guide provides a step-by-step overview for adding a new ElectroDB-based entity to the
|
|
83
|
-
|
|
84
|
-
## Prerequisites
|
|
97
|
+
This guide provides a step-by-step overview for adding a new ElectroDB-based entity to the application.
|
|
85
98
|
|
|
86
|
-
|
|
87
|
-
- Understanding of the current data model and relationships.
|
|
88
|
-
- Node.js and npm installed on your system.
|
|
99
|
+
### Step 1: Define the Entity Schema
|
|
89
100
|
|
|
90
|
-
|
|
101
|
+
1. **Create Entity Schema File**: Define the entity schema in a new file (e.g., `myNewEntity.schema.js`) within the `/schemas/` directory.
|
|
91
102
|
|
|
92
|
-
1. **Create Entity Schema File**: Start by defining the entity schema in a new file (e.g., `myNewEntity.schema.js`) within the `/entities/` directory. This file should export a simple JavaScript object that defines the schema for the entity (refer to the existing `opportunity.schema.js` for an example).
|
|
93
103
|
```javascript
|
|
94
104
|
export const MyNewEntitySchema = {
|
|
95
105
|
model: {
|
|
@@ -129,45 +139,74 @@ This guide provides a step-by-step overview for adding a new ElectroDB-based ent
|
|
|
129
139
|
},
|
|
130
140
|
},
|
|
131
141
|
},
|
|
142
|
+
references: {
|
|
143
|
+
belongs_to: [
|
|
144
|
+
{ type: 'belongs_to', target: 'Opportunity' },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
132
147
|
};
|
|
133
148
|
```
|
|
134
149
|
|
|
135
|
-
|
|
150
|
+
2. **Declare References**: Use the `references` field to define relationships between entities. This sets up associations for easy fetching and managing of related entities, allowing for automatic generation of reference getter methods.
|
|
151
|
+
|
|
152
|
+
### Step 2: Add a Model Class
|
|
153
|
+
|
|
154
|
+
1. **Create the Model Class**: In the `/models/` directory, add `myNewEntity.model.js`.
|
|
136
155
|
|
|
137
|
-
1. **Create the Model Class**: In the `/models/` directory, add a file named `myNewEntity.model.js`.
|
|
138
156
|
```javascript
|
|
139
157
|
import BaseModel from './base.model.js';
|
|
140
|
-
|
|
158
|
+
|
|
141
159
|
class MyNewEntity extends BaseModel {
|
|
142
160
|
constructor(electroService, modelFactory, record, log) {
|
|
143
161
|
super(electroService, modelFactory, record, log);
|
|
144
162
|
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export default MyNewEntity;
|
|
166
|
+
```
|
|
145
167
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
168
|
+
Note: By using `BaseModel`, entity classes can remain empty unless there is a need to:
|
|
169
|
+
- Override automatically generated getters or setters for specific attributes.
|
|
170
|
+
- Add custom methods specific to the entity.
|
|
149
171
|
|
|
150
|
-
|
|
151
|
-
this.record.name = name;
|
|
152
|
-
return this;
|
|
153
|
-
}
|
|
172
|
+
### Automatic Getter and Setter Methods
|
|
154
173
|
|
|
155
|
-
|
|
156
|
-
return this.record.status;
|
|
157
|
-
}
|
|
174
|
+
The `BaseModel` automatically generates getter and setter methods for each attribute defined in the entity schema:
|
|
158
175
|
|
|
159
|
-
|
|
160
|
-
this.record.status = status;
|
|
161
|
-
return this;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
176
|
+
- **Utility Methods**: `BaseModel` provides `getId()`, `getCreatedAt()`, and `getUpdatedAt()` methods out of the box for accessing common entity information like the unique identifier, creation timestamp, and last update timestamp.
|
|
164
177
|
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
- **Getters**: Follow the convention `get<AttributeName>()` to access attribute values.
|
|
179
|
+
- **Setters**: Follow the convention `set<AttributeName>(value)` to modify entity values, while handling patching.
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
|
|
183
|
+
- If an attribute is named `name`, `BaseModel` will automatically generate:
|
|
184
|
+
- `getName()`: Retrieve the value of `name`.
|
|
185
|
+
- `setName(value)`: Update the value of `name`.
|
|
186
|
+
|
|
187
|
+
This reduces boilerplate and ensures consistency.
|
|
188
|
+
|
|
189
|
+
### Automatic Reference Getter Methods
|
|
167
190
|
|
|
168
|
-
|
|
191
|
+
If references are defined in the schema (e.g., `belongs_to`, `has_many`), `BaseModel` generates reference getter methods:
|
|
192
|
+
|
|
193
|
+
- **References Getter Naming**:
|
|
194
|
+
- Methods are named `get<RelatedEntity>()`, where `<RelatedEntity>` corresponds to the target specified in the `references` field.
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
```javascript
|
|
198
|
+
references: {
|
|
199
|
+
belongs_to: [
|
|
200
|
+
{ type: 'belongs_to', target: 'Opportunity' },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
```
|
|
204
|
+
This results in a `getOpportunity()` method for accessing the related `Opportunity` entity.
|
|
205
|
+
|
|
206
|
+
### Step 3: Add a Collection Class
|
|
207
|
+
|
|
208
|
+
1. **Create the Collection Class**: Add `myNewEntity.collection.js` in the `/collections/` directory.
|
|
169
209
|
|
|
170
|
-
1. **Create the Collection Class**: Add a new file named `myNewEntity.collection.js` in the `/collections/` directory.
|
|
171
210
|
```javascript
|
|
172
211
|
import BaseCollection from './base.collection.js';
|
|
173
212
|
import MyNewEntity from '../models/myNewEntity.model.js';
|
|
@@ -178,83 +217,56 @@ This guide provides a step-by-step overview for adding a new ElectroDB-based ent
|
|
|
178
217
|
}
|
|
179
218
|
|
|
180
219
|
async allByStatus(status) {
|
|
181
|
-
return
|
|
220
|
+
return this.findByIndexKeys({ status });
|
|
182
221
|
}
|
|
183
222
|
}
|
|
184
223
|
|
|
185
224
|
export default MyNewEntityCollection;
|
|
186
225
|
```
|
|
187
226
|
|
|
188
|
-
|
|
227
|
+
### Step 4: Integrate the Entity into Model Factory
|
|
228
|
+
|
|
229
|
+
1. **Update the Model Factory**: Open `model.factory.js` and add the new entity and collection to the `initialize` method.
|
|
189
230
|
|
|
190
|
-
1. **Update the Model Factory**: Open `model.factory.js` and add the newly created entity and collection to the initialize method.
|
|
191
231
|
```javascript
|
|
192
232
|
import MyNewEntityCollection from './collections/myNewEntity.collection.js';
|
|
193
233
|
|
|
194
234
|
class ModelFactory {
|
|
195
235
|
initialize() {
|
|
196
|
-
|
|
197
236
|
const myNewEntityCollection = new MyNewEntityCollection(
|
|
198
237
|
this.service,
|
|
199
238
|
this,
|
|
200
239
|
this.logger,
|
|
201
240
|
);
|
|
202
241
|
|
|
203
|
-
this.models.set(MyNewEntityCollection.name,
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
## Step 5: Write Unit Tests
|
|
209
|
-
|
|
210
|
-
1. **Create Unit Test for the Model Class**: Add a new file in the `/tests/unit/v2/models/` directory named `myNewEntity.model.test.js`.
|
|
211
|
-
|
|
212
|
-
- Follow the existing test structure to test all getters, setters, and interactions for `MyNewEntity`.
|
|
213
|
-
- Use Mocha, Chai, Chai-as-promised, and Sinon for testing.
|
|
214
|
-
|
|
215
|
-
2. **Create Unit Test for the Collection Class**: Add another test named `myNewEntity.collection.test.js`.
|
|
216
|
-
|
|
217
|
-
- Test the methods in `MyNewEntityCollection`, particularly those interacting with ElectroDB services, such as `allByStatus`.
|
|
218
|
-
|
|
219
|
-
## Step 6: Add Guard Methods (if needed)
|
|
220
|
-
|
|
221
|
-
1. **Update Guards if Needed**: If your entity requires new types of validation, add guard methods in `guards.js`. The guards should be generic and not specific to field names—ensure they can be reused for different fields of the same type. Update `index.d.ts` to add TypeScript type definitions for those new guard functions if necessary.
|
|
222
|
-
```javascript
|
|
223
|
-
export function guardStatus(propertyName, value, entityName) {
|
|
224
|
-
const allowedStatuses = ['NEW', 'IN_PROGRESS', 'COMPLETED'];
|
|
225
|
-
if (!allowedStatuses.includes(value)) {
|
|
226
|
-
throw new Error(`${propertyName} must be one of ${allowedStatuses.join(', ')} in ${entityName}`);
|
|
242
|
+
this.models.set(MyNewEntityCollection.name, myNewEntityCollection);
|
|
227
243
|
}
|
|
228
244
|
}
|
|
229
245
|
```
|
|
230
246
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
1. **Update Patcher if Needed**: Update `patcher.js` only if there are new types of data being patched that are not yet covered by the current patch methods (e.g., adding a new type like `Date` that hasn't been handled before).
|
|
234
|
-
- Create methods like `patchString`, `patchEnum`, etc., only if the existing ones do not suffice for your new entity attributes.
|
|
247
|
+
### Step 5: Write Unit and Integration Tests
|
|
235
248
|
|
|
236
|
-
|
|
249
|
+
1. **Create Unit Tests**: Add a file named `myNewEntity.model.test.js` in `/tests/unit/models/` to test all getters, setters, and interactions.
|
|
250
|
+
- Use Mocha, Chai, and Sinon for testing.
|
|
237
251
|
|
|
238
|
-
|
|
239
|
-
- Test
|
|
240
|
-
- Make sure the entity can be retrieved through various service methods and that relationships with other entities are properly maintained.
|
|
252
|
+
2. **Create Collection Tests**: Add `myNewEntity.collection.test.js` to `/tests/unit/collections/`.
|
|
253
|
+
- Test methods interacting with ElectroDB, like `allByStatus`.
|
|
241
254
|
|
|
242
|
-
|
|
255
|
+
3. **Add Integration Tests**: Create an integration test file named `myNewEntity.integration.test.js` in `/tests/integration/` to test the full lifecycle of the entity.
|
|
243
256
|
|
|
244
|
-
|
|
257
|
+
### Step 6: Create JSDoc and Update Documentation
|
|
245
258
|
|
|
246
|
-
|
|
259
|
+
1. **Generate JSDoc for Entity and Collection**: Add JSDoc comments for each function to describe the API.
|
|
260
|
+
2. **Update Type Definitions**: Modify `index.d.ts` to include new interfaces and types for the entity.
|
|
247
261
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
1. **Run All Tests**: Ensure all existing and new unit tests pass using Mocha. Run:
|
|
262
|
+
### Step 7: Run Tests and Verify
|
|
251
263
|
|
|
264
|
+
1. **Run All Tests**:
|
|
252
265
|
```bash
|
|
253
|
-
npm run test
|
|
266
|
+
npm run test && npm run test:it
|
|
254
267
|
```
|
|
255
268
|
|
|
256
|
-
2. **Linter**:
|
|
257
|
-
|
|
269
|
+
2. **Run Linter**: Check for coding standard violations.
|
|
258
270
|
```bash
|
|
259
271
|
npm run lint
|
|
260
272
|
```
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { isNonEmptyObject, isValidUrl } from '@adobe/spacecat-shared-utils';
|
|
16
16
|
|
|
17
|
-
import { v4 as uuid } from 'uuid';
|
|
17
|
+
import { validate as uuidValidate, v4 as uuid } from 'uuid';
|
|
18
18
|
|
|
19
19
|
/*
|
|
20
20
|
Schema Doc: https://electrodb.dev/en/modeling/schema/
|
|
@@ -36,21 +36,21 @@ const OpportunitySchema = {
|
|
|
36
36
|
// https://electrodb.dev/en/modeling/attributes/#default
|
|
37
37
|
default: () => uuid(),
|
|
38
38
|
// https://electrodb.dev/en/modeling/attributes/#attribute-validation
|
|
39
|
-
|
|
39
|
+
validate: (value) => uuidValidate(value),
|
|
40
40
|
},
|
|
41
41
|
siteId: {
|
|
42
42
|
type: 'string',
|
|
43
43
|
required: true,
|
|
44
|
-
|
|
44
|
+
validate: (value) => uuidValidate(value),
|
|
45
45
|
},
|
|
46
46
|
auditId: {
|
|
47
47
|
type: 'string',
|
|
48
48
|
required: true,
|
|
49
|
-
|
|
49
|
+
validate: (value) => uuidValidate(value),
|
|
50
50
|
},
|
|
51
51
|
runbook: {
|
|
52
52
|
type: 'string',
|
|
53
|
-
|
|
53
|
+
validate: (value) => !value || isValidUrl(value),
|
|
54
54
|
},
|
|
55
55
|
type: {
|
|
56
56
|
type: 'string',
|
|
@@ -60,7 +60,7 @@ const OpportunitySchema = {
|
|
|
60
60
|
data: {
|
|
61
61
|
type: 'any',
|
|
62
62
|
required: false,
|
|
63
|
-
|
|
63
|
+
validate: (value) => !value || isNonEmptyObject(value),
|
|
64
64
|
},
|
|
65
65
|
origin: {
|
|
66
66
|
type: ['ESS_OPS', 'AI', 'AUTOMATION'],
|
|
@@ -80,10 +80,9 @@ const OpportunitySchema = {
|
|
|
80
80
|
default: () => 'NEW',
|
|
81
81
|
},
|
|
82
82
|
guidance: {
|
|
83
|
-
type: '
|
|
84
|
-
properties: {},
|
|
83
|
+
type: 'any',
|
|
85
84
|
required: false,
|
|
86
|
-
|
|
85
|
+
validate: (value) => !value || isNonEmptyObject(value),
|
|
87
86
|
},
|
|
88
87
|
tags: {
|
|
89
88
|
type: 'set',
|
|
@@ -142,4 +141,19 @@ const OpportunitySchema = {
|
|
|
142
141
|
},
|
|
143
142
|
};
|
|
144
143
|
|
|
144
|
+
/**
|
|
145
|
+
* References to other entities. This is not part of the standard ElectroDB schema, but is used
|
|
146
|
+
* to define relationships between entities in our data layer API.
|
|
147
|
+
* @type {{belongs_to: [{type: string, target: string}]}}
|
|
148
|
+
*/
|
|
149
|
+
OpportunitySchema.references = {
|
|
150
|
+
has_many: [
|
|
151
|
+
{ type: 'has_many', target: 'Suggestions' },
|
|
152
|
+
],
|
|
153
|
+
belongs_to: [
|
|
154
|
+
{ type: 'belongs_to', target: 'Site' },
|
|
155
|
+
{ type: 'belongs_to', target: 'Audit' },
|
|
156
|
+
],
|
|
157
|
+
};
|
|
158
|
+
|
|
145
159
|
export default OpportunitySchema;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
/* c8 ignore start */
|
|
14
14
|
|
|
15
|
-
import { v4 as uuid } from 'uuid';
|
|
15
|
+
import { v4 as uuid, validate as uuidValidate } from 'uuid';
|
|
16
16
|
import { isNonEmptyObject } from '@adobe/spacecat-shared-utils';
|
|
17
17
|
|
|
18
18
|
/*
|
|
@@ -35,12 +35,12 @@ const SuggestionSchema = {
|
|
|
35
35
|
// https://electrodb.dev/en/modeling/attributes/#default
|
|
36
36
|
default: () => uuid(),
|
|
37
37
|
// https://electrodb.dev/en/modeling/attributes/#attribute-validation
|
|
38
|
-
|
|
38
|
+
validate: (value) => uuidValidate(value),
|
|
39
39
|
},
|
|
40
40
|
opportunityId: {
|
|
41
41
|
type: 'string',
|
|
42
42
|
required: true,
|
|
43
|
-
|
|
43
|
+
validate: (value) => uuidValidate(value),
|
|
44
44
|
},
|
|
45
45
|
type: {
|
|
46
46
|
type: ['CODE_CHANGE', 'CONTENT_UPDATE', 'REDIRECT_UPDATE', 'METADATA_UPDATE'],
|
|
@@ -54,13 +54,12 @@ const SuggestionSchema = {
|
|
|
54
54
|
data: {
|
|
55
55
|
type: 'any',
|
|
56
56
|
required: true,
|
|
57
|
-
|
|
57
|
+
validate: (value) => isNonEmptyObject(value),
|
|
58
58
|
},
|
|
59
59
|
kpiDeltas: {
|
|
60
|
-
type: '
|
|
61
|
-
properties: {},
|
|
60
|
+
type: 'any',
|
|
62
61
|
required: false,
|
|
63
|
-
|
|
62
|
+
validate: (value) => !value || isNonEmptyObject(value),
|
|
64
63
|
},
|
|
65
64
|
status: {
|
|
66
65
|
type: ['NEW', 'APPROVED', 'SKIPPED', 'FIXED', 'ERROR'],
|
|
@@ -119,4 +118,15 @@ const SuggestionSchema = {
|
|
|
119
118
|
},
|
|
120
119
|
};
|
|
121
120
|
|
|
121
|
+
/**
|
|
122
|
+
* References to other entities. This is not part of the standard ElectroDB schema, but is used
|
|
123
|
+
* to define relationships between entities in our data layer API.
|
|
124
|
+
* @type {{belongs_to: [{type: string, target: string}]}}
|
|
125
|
+
*/
|
|
126
|
+
SuggestionSchema.references = {
|
|
127
|
+
belongs_to: [
|
|
128
|
+
{ type: 'belongs_to', target: 'Opportunity' },
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
|
|
122
132
|
export default SuggestionSchema;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 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 pluralize from 'pluralize';
|
|
14
|
+
|
|
15
|
+
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
16
|
+
const entityNameToCollectionName = (entityName) => `${pluralize.singular(entityName)}Collection`;
|
|
17
|
+
const entityNameToIdName = (collectionName) => `${collectionName.charAt(0).toLowerCase() + collectionName.slice(1)}Id`;
|
|
18
|
+
const entityNameToReferenceMethodName = (target, type) => {
|
|
19
|
+
let baseName = target.charAt(0).toUpperCase() + target.slice(1);
|
|
20
|
+
baseName = type === 'has_many'
|
|
21
|
+
? pluralize.plural(baseName)
|
|
22
|
+
: pluralize.singular(baseName);
|
|
23
|
+
|
|
24
|
+
return `get${baseName}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const idNameToEntityName = (idName) => capitalize(pluralize.singular(idName.replace('Id', '')));
|
|
28
|
+
|
|
29
|
+
const keyNamesToIndexName = (keyNames) => {
|
|
30
|
+
const capitalizedKeyNames = keyNames.map((keyName) => capitalize(keyName));
|
|
31
|
+
return `by${capitalizedKeyNames.join('And')}`;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
capitalize,
|
|
36
|
+
entityNameToCollectionName,
|
|
37
|
+
entityNameToIdName,
|
|
38
|
+
entityNameToReferenceMethodName,
|
|
39
|
+
idNameToEntityName,
|
|
40
|
+
keyNamesToIndexName,
|
|
41
|
+
};
|