@adobe/spacecat-shared-data-access 1.57.1 → 1.58.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 CHANGED
@@ -1,3 +1,17 @@
1
+ # [@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)
2
+
3
+
4
+ ### Features
5
+
6
+ * auto references & attribute set/get ([#461](https://github.com/adobe/spacecat-shared/issues/461)) ([2c29683](https://github.com/adobe/spacecat-shared/commit/2c296835bb7e71f2984e5537eab1dfc25bcc6141))
7
+
8
+ # [@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)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **deps:** update external fixes ([#454](https://github.com/adobe/spacecat-shared/issues/454)) ([325cf8d](https://github.com/adobe/spacecat-shared/commit/325cf8dded5fcabadaf7d8fdd510d33aeafd08a7))
14
+
1
15
  # [@adobe/spacecat-shared-data-access-v1.57.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v1.57.0...@adobe/spacecat-shared-data-access-v1.57.1) (2024-11-22)
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.57.1",
3
+ "version": "1.58.0",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -34,14 +34,15 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
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",
37
+ "@adobe/spacecat-shared-dynamo": "1.3.50",
38
+ "@adobe/spacecat-shared-utils": "1.23.1",
39
+ "@aws-sdk/client-dynamodb": "3.699.0",
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": {
@@ -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 {Class} clazz - The model class that represents the entity.
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
- * @protected
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
- _createInstance(record) {
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
- * @protected
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
- _createInstances(records) {
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._createInstance({ data: record }));
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._createInstance(record);
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._createInstance(record);
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._createInstances({ data: createdItems });
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
- this.associationsCache = {};
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
- * Gets the association of the model by querying another related model. Retrieved
45
- * associations are cached to avoid redundant queries.
46
- * @protected
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} modelName - The name of the related model.
49
- * @param {string} method - The method to use for querying the related model.
50
- * @param {...*} args - Additional arguments to be passed to the method.
51
- * @returns {Promise<Object>} - A promise that resolves to the associated model instance.
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 _getAssociation(modelName, method, ...args) {
54
- const cache = this.associationsCache;
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
- cache[modelName] = cache[modelName] || {};
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 (!(method in cache[modelName])) {
59
- cache[modelName][method] = await this.modelFactory.getCollection(modelName)[method](...args);
164
+ if (result) {
165
+ await this._cacheReference(targetName, result);
60
166
  }
61
167
 
62
- return cache[modelName][method];
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);
@@ -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
- const records = await this.entity.query.bySiteIdAndStatus({ siteId, status }).go();
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._getAssociation('SuggestionCollection', 'createMany', childSuggestions);
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<Array<BaseModel>>} - A promise that resolves to an array of
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
- const records = await this.entity.query.byOpportunityIdAndStatus(
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 is centered around a collection-management pattern with ElectroDB, enabling efficient management of DynamoDB entities. It uses a layered architecture as follows:
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**: Utilizes DynamoDB as the data store, with ElectroDB for managing schema definitions and data interactions.
10
- 2. **Model Layer**: The `BaseModel` provides common methods like `save`, `remove`, and associations for all entities. Each entity (e.g., `Opportunity`, `Suggestion`) extends `BaseModel` for specific features.
11
- 3. **Collection Layer**: The `BaseCollection` handles entity-specific CRUD operations. `OpportunityCollection` and `SuggestionCollection` extend `BaseCollection` to provide tailored methods for managing Opportunities and Suggestions.
12
- 4. **Factory Layer**: The `ModelFactory` centralizes the instantiation of models and collections, providing a unified interface for interacting with different entity types.
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
- - **Relationship**: Opportunities have many Suggestions. This is implemented through the `OpportunityCollection` and `SuggestionCollection`, which interact via ElectroDB-managed DynamoDB relationships.
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
- - This framework relies on AWS DynamoDB for data storage. Ensure you have AWS credentials configured and a DynamoDB table set up.
60
- - Configure the DynamoDB table name and related settings in the `index.js` configuration.
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 existing application. By following this guide, you will be able to create, integrate, and test a new entity seamlessly.
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
- - Familiarity with ElectroDB and how it models data.
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
- ## Step 1: Define the Entity Schema
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
- ## Step 2: Add a Model Class
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
- getName() {
147
- return this.record.name;
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
- setName(name) {
151
- this.record.name = name;
152
- return this;
153
- }
172
+ ### Automatic Getter and Setter Methods
154
173
 
155
- getStatus() {
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
- setStatus(status) {
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
- export default MyNewEntity;
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
- ## Step 3: Add a Collection Class
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 await this.service.entities.myNewEntity.query.myNewEntityIndex({ status }).go();
220
+ return this.findByIndexKeys({ status });
182
221
  }
183
222
  }
184
223
 
185
224
  export default MyNewEntityCollection;
186
225
  ```
187
226
 
188
- ## Step 4: Integrate the Entity into Model Factory
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, myNewEntityColection);
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
- ## Step 7: Update the Patcher (if needed)
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
- ## Step 8: Add to Integration Tests
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
- 1. **Add Integration Tests**: Update the integration test suite to include the new entity. This will help ensure that the new entity integrates well with the rest of the system. Create an integration test file named `myNewEntity.integration.test.js` in the `/tests/it/v2/` directory.
239
- - Test the full lifecycle of the entity: creation, updating, querying, and deletion.
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
- ## Step 9: Create JSDoc and Update Documentation
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
- 1. **Generate JSDoc for Entity and Collection**: For each function in your model and collection files, ensure JSDoc comments are present for developers to easily understand the API.
257
+ ### Step 6: Create JSDoc and Update Documentation
245
258
 
246
- 2. **Update Type Definitions**: Update the `index.d.ts` file to include new interfaces and types for your new entity, ensuring that IDEs can provide auto-completion and type-checking.
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
- ## Step 10: Run Tests and Verify
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 & npm run test:it
266
+ npm run test && npm run test:it
254
267
  ```
255
268
 
256
- 2. **Linter**: Run ESLint to check for any coding standard violations.
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
- validation: (value) => !uuid.validate(value),
39
+ validate: (value) => uuidValidate(value),
40
40
  },
41
41
  siteId: {
42
42
  type: 'string',
43
43
  required: true,
44
- validation: (value) => !uuid.validate(value),
44
+ validate: (value) => uuidValidate(value),
45
45
  },
46
46
  auditId: {
47
47
  type: 'string',
48
48
  required: true,
49
- validation: (value) => !uuid.validate(value),
49
+ validate: (value) => uuidValidate(value),
50
50
  },
51
51
  runbook: {
52
52
  type: 'string',
53
- validation: (value) => !isValidUrl(value),
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
- validation: (value) => !isNonEmptyObject(value),
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: 'map',
84
- properties: {},
83
+ type: 'any',
85
84
  required: false,
86
- validation: (value) => !isNonEmptyObject(value),
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
- validation: (value) => !uuid.validate(value),
38
+ validate: (value) => uuidValidate(value),
39
39
  },
40
40
  opportunityId: {
41
41
  type: 'string',
42
42
  required: true,
43
- validation: (value) => !uuid.validate(value),
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
- validation: (value) => !isNonEmptyObject(value),
57
+ validate: (value) => isNonEmptyObject(value),
58
58
  },
59
59
  kpiDeltas: {
60
- type: 'map',
61
- properties: {},
60
+ type: 'any',
62
61
  required: false,
63
- validation: (value) => !isNonEmptyObject(value),
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
+ };