@adobe/spacecat-shared-data-access 1.53.0 → 1.54.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.
@@ -0,0 +1,260 @@
1
+ # ElectroDB Model Framework
2
+
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
+
5
+ ## Architecture Overview
6
+
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:
8
+
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.
13
+
14
+ ### Architectural Diagram
15
+
16
+ ```plaintext
17
+ +--------------------+
18
+ | Data Layer |
19
+ |--------------------|
20
+ | DynamoDB + ElectroDB ORM |
21
+ +--------------------+
22
+
23
+ +--------------------+
24
+ | Collection Layer |
25
+ |--------------------|
26
+ | BaseCollection, |
27
+ | OpportunityCollection, |
28
+ | SuggestionCollection |
29
+ +--------------------+
30
+
31
+ +--------------------+
32
+ | Model Layer |
33
+ |--------------------|
34
+ | BaseModel, |
35
+ | Opportunity, |
36
+ | Suggestion |
37
+ +--------------------+
38
+
39
+ +--------------------+
40
+ | Factory Layer |
41
+ |--------------------|
42
+ | ModelFactory |
43
+ +--------------------+
44
+ ```
45
+
46
+ ## Entities and Relationships
47
+ - **Opportunity**: Represents a specific issue identified on a website. It includes attributes like `title`, `description`, `siteId`, and `status`.
48
+ - **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.
50
+
51
+ ## Getting Started
52
+
53
+ 1. **Install Dependencies**
54
+ ```bash
55
+ npm install
56
+ ```
57
+
58
+ 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.
61
+
62
+ 3. **Usage Example**
63
+ ```javascript
64
+ import { createDataAccess } from './index.js';
65
+
66
+ const config = { tableNameData: 'YOUR_TABLE_NAME' };
67
+ const log = console;
68
+ const dao = createDataAccess(config, log);
69
+
70
+ // Create a new Opportunity
71
+ const opportunityData = { title: 'Broken Links', siteId: 'site123', type: 'broken-backlinks' };
72
+ const newOpportunity = await dao.Opportunity.create(opportunityData);
73
+ console.log('New Opportunity Created:', newOpportunity);
74
+ ```
75
+
76
+ 4. **Extending Functionality**
77
+ - Add new models by extending `BaseModel` and new collections by extending `BaseCollection`.
78
+ - Register new models in the `ModelFactory` for unified access.
79
+
80
+ ## Adding a New ElectroDB-Based Entity
81
+
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
85
+
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.
89
+
90
+ ## Step 1: Define the Entity Schema
91
+
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
+ ```javascript
94
+ export const MyNewEntitySchema = {
95
+ model: {
96
+ entity: 'MyNewEntity',
97
+ service: 'MyService',
98
+ version: '1',
99
+ },
100
+ attributes: {
101
+ myNewEntityId: {
102
+ type: 'string',
103
+ required: true,
104
+ },
105
+ name: {
106
+ type: 'string',
107
+ required: true,
108
+ },
109
+ status: {
110
+ type: 'string',
111
+ enum: ['NEW', 'IN_PROGRESS', 'COMPLETED'],
112
+ required: true,
113
+ },
114
+ createdAt: {
115
+ type: 'string',
116
+ required: true,
117
+ default: () => new Date().toISOString(),
118
+ },
119
+ },
120
+ indexes: {
121
+ myNewEntityIndex: {
122
+ pk: {
123
+ field: 'pk',
124
+ facets: ['myNewEntityId'],
125
+ },
126
+ sk: {
127
+ field: 'sk',
128
+ facets: ['status'],
129
+ },
130
+ },
131
+ },
132
+ };
133
+ ```
134
+
135
+ ## Step 2: Add a Model Class
136
+
137
+ 1. **Create the Model Class**: In the `/models/` directory, add a file named `myNewEntity.model.js`.
138
+ ```javascript
139
+ import BaseModel from './base.model.js';
140
+
141
+ class MyNewEntity extends BaseModel {
142
+ constructor(electroService, modelFactory, record, log) {
143
+ super(electroService, modelFactory, record, log);
144
+ }
145
+
146
+ getName() {
147
+ return this.record.name;
148
+ }
149
+
150
+ setName(name) {
151
+ this.record.name = name;
152
+ return this;
153
+ }
154
+
155
+ getStatus() {
156
+ return this.record.status;
157
+ }
158
+
159
+ setStatus(status) {
160
+ this.record.status = status;
161
+ return this;
162
+ }
163
+ }
164
+
165
+ export default MyNewEntity;
166
+ ```
167
+
168
+ ## Step 3: Add a Collection Class
169
+
170
+ 1. **Create the Collection Class**: Add a new file named `myNewEntity.collection.js` in the `/collections/` directory.
171
+ ```javascript
172
+ import BaseCollection from './base.collection.js';
173
+ import MyNewEntity from '../models/myNewEntity.model.js';
174
+
175
+ class MyNewEntityCollection extends BaseCollection {
176
+ constructor(service, modelFactory, log) {
177
+ super(service, modelFactory, MyNewEntity, log);
178
+ }
179
+
180
+ async allByStatus(status) {
181
+ return await this.service.entities.myNewEntity.query.myNewEntityIndex({ status }).go();
182
+ }
183
+ }
184
+
185
+ export default MyNewEntityCollection;
186
+ ```
187
+
188
+ ## Step 4: Integrate the Entity into Model Factory
189
+
190
+ 1. **Update the Model Factory**: Open `model.factory.js` and add the newly created entity and collection to the initialize method.
191
+ ```javascript
192
+ import MyNewEntityCollection from './collections/myNewEntity.collection.js';
193
+
194
+ class ModelFactory {
195
+ initialize() {
196
+
197
+ const myNewEntityCollection = new MyNewEntityCollection(
198
+ this.service,
199
+ this,
200
+ this.logger,
201
+ );
202
+
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}`);
227
+ }
228
+ }
229
+ ```
230
+
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.
235
+
236
+ ## Step 8: Add to Integration Tests
237
+
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.
241
+
242
+ ## Step 9: Create JSDoc and Update Documentation
243
+
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.
245
+
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.
247
+
248
+ ## Step 10: Run Tests and Verify
249
+
250
+ 1. **Run All Tests**: Ensure all existing and new unit tests pass using Mocha. Run:
251
+
252
+ ```bash
253
+ npm run test & npm run test:it
254
+ ```
255
+
256
+ 2. **Linter**: Run ESLint to check for any coding standard violations.
257
+
258
+ ```bash
259
+ npm run lint
260
+ ```
@@ -0,0 +1,145 @@
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
+ /* c8 ignore start */
14
+
15
+ import { isNonEmptyObject, isValidUrl } from '@adobe/spacecat-shared-utils';
16
+
17
+ import { v4 as uuid } from 'uuid';
18
+
19
+ /*
20
+ Schema Doc: https://electrodb.dev/en/modeling/schema/
21
+ Attribute Doc: https://electrodb.dev/en/modeling/attributes/
22
+ Indexes Doc: https://electrodb.dev/en/modeling/indexes/
23
+ */
24
+
25
+ const OpportunitySchema = {
26
+ model: {
27
+ entity: 'Opportunity',
28
+ version: '1',
29
+ service: 'SpaceCat',
30
+ },
31
+ attributes: {
32
+ opportunityId: {
33
+ type: 'string',
34
+ required: true,
35
+ readOnly: true,
36
+ // https://electrodb.dev/en/modeling/attributes/#default
37
+ default: () => uuid(),
38
+ // https://electrodb.dev/en/modeling/attributes/#attribute-validation
39
+ validation: (value) => !uuid.validate(value),
40
+ },
41
+ siteId: {
42
+ type: 'string',
43
+ required: true,
44
+ validation: (value) => !uuid.validate(value),
45
+ },
46
+ auditId: {
47
+ type: 'string',
48
+ required: true,
49
+ validation: (value) => !uuid.validate(value),
50
+ },
51
+ runbook: {
52
+ type: 'string',
53
+ validation: (value) => !isValidUrl(value),
54
+ },
55
+ type: {
56
+ type: ['broken-backlinks', 'broken-internal-links'],
57
+ readOnly: true,
58
+ required: true,
59
+ },
60
+ data: {
61
+ type: 'any',
62
+ required: false,
63
+ validation: (value) => !isNonEmptyObject(value),
64
+ },
65
+ origin: {
66
+ type: ['ESS_OPS', 'AI', 'AUTOMATION'],
67
+ required: true,
68
+ },
69
+ title: {
70
+ type: 'string',
71
+ required: true,
72
+ },
73
+ description: {
74
+ type: 'string',
75
+ required: false,
76
+ },
77
+ status: {
78
+ type: ['NEW', 'IN_PROGRESS', 'IGNORED', 'RESOLVED'],
79
+ required: true,
80
+ default: () => 'NEW',
81
+ },
82
+ guidance: {
83
+ type: 'map',
84
+ properties: {},
85
+ required: false,
86
+ validation: (value) => !isNonEmptyObject(value),
87
+ },
88
+ tags: {
89
+ type: 'set',
90
+ items: 'string',
91
+ required: false,
92
+ },
93
+ createdAt: {
94
+ type: 'number',
95
+ readOnly: true,
96
+ required: true,
97
+ default: () => Date.now(),
98
+ set: () => Date.now(),
99
+ },
100
+ updatedAt: {
101
+ type: 'number',
102
+ watch: '*',
103
+ required: true,
104
+ default: () => Date.now(),
105
+ set: () => Date.now(),
106
+ },
107
+ // todo: add createdBy, updatedBy and auto-set from auth context
108
+ },
109
+ indexes: {
110
+ primary: { // operates on the main table, no 'index' property
111
+ pk: {
112
+ field: 'pk',
113
+ composite: ['opportunityId'],
114
+ },
115
+ sk: {
116
+ field: 'sk',
117
+ composite: [],
118
+ },
119
+ },
120
+ bySiteId: {
121
+ index: 'spacecat-data-opportunity-by-site',
122
+ pk: {
123
+ field: 'gsi1pk',
124
+ composite: ['siteId'],
125
+ },
126
+ sk: {
127
+ field: 'gsi1sk',
128
+ composite: ['opportunityId'],
129
+ },
130
+ },
131
+ bySiteIdAndStatus: {
132
+ index: 'spacecat-data-opportunity-by-site-and-status',
133
+ pk: {
134
+ field: 'gsi2pk',
135
+ composite: ['siteId', 'status'],
136
+ },
137
+ sk: {
138
+ field: 'gsi2sk',
139
+ composite: ['updatedAt'],
140
+ },
141
+ },
142
+ },
143
+ };
144
+
145
+ export default OpportunitySchema;
@@ -0,0 +1,122 @@
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
+ /* c8 ignore start */
14
+
15
+ import { v4 as uuid } from 'uuid';
16
+ import { isNonEmptyObject } from '@adobe/spacecat-shared-utils';
17
+
18
+ /*
19
+ Schema Doc: https://electrodb.dev/en/modeling/schema/
20
+ Attribute Doc: https://electrodb.dev/en/modeling/attributes/
21
+ Indexes Doc: https://electrodb.dev/en/modeling/indexes/
22
+ */
23
+
24
+ const SuggestionSchema = {
25
+ model: {
26
+ entity: 'Suggestion',
27
+ version: '1',
28
+ service: 'SpaceCat',
29
+ },
30
+ attributes: {
31
+ suggestionId: {
32
+ type: 'string',
33
+ required: true,
34
+ readOnly: true,
35
+ // https://electrodb.dev/en/modeling/attributes/#default
36
+ default: () => uuid(),
37
+ // https://electrodb.dev/en/modeling/attributes/#attribute-validation
38
+ validation: (value) => !uuid.validate(value),
39
+ },
40
+ opportunityId: {
41
+ type: 'string',
42
+ required: true,
43
+ validation: (value) => !uuid.validate(value),
44
+ },
45
+ type: {
46
+ type: ['CODE_CHANGE', 'CONTENT_UPDATE', 'REDIRECT_UPDATE', 'METADATA_UPDATE'],
47
+ required: true,
48
+ readOnly: true,
49
+ },
50
+ rank: {
51
+ type: 'number',
52
+ required: true,
53
+ },
54
+ data: {
55
+ type: 'any',
56
+ required: true,
57
+ validation: (value) => !isNonEmptyObject(value),
58
+ },
59
+ kpiDeltas: {
60
+ type: 'map',
61
+ properties: {},
62
+ required: false,
63
+ validation: (value) => !isNonEmptyObject(value),
64
+ },
65
+ status: {
66
+ type: ['NEW', 'APPROVED', 'SKIPPED', 'FIXED', 'ERROR'],
67
+ required: true,
68
+ default: () => 'NEW',
69
+ },
70
+ createdAt: {
71
+ type: 'number',
72
+ readOnly: true,
73
+ required: true,
74
+ default: () => Date.now(),
75
+ set: () => Date.now(),
76
+ },
77
+ updatedAt: {
78
+ type: 'number',
79
+ watch: '*',
80
+ required: true,
81
+ default: () => Date.now(),
82
+ set: () => Date.now(),
83
+ },
84
+ // todo: add createdBy, updatedBy and auto-set from auth context
85
+ },
86
+ indexes: {
87
+ primary: { // operates on the main table, no 'index' property
88
+ pk: {
89
+ field: 'pk',
90
+ composite: ['suggestionId'],
91
+ },
92
+ sk: {
93
+ field: 'sk',
94
+ composite: [],
95
+ },
96
+ },
97
+ byOpportunityId: {
98
+ index: 'spacecat-data-suggestion-by-opportunity',
99
+ pk: {
100
+ field: 'gsi1pk',
101
+ composite: ['opportunityId'],
102
+ },
103
+ sk: {
104
+ field: 'gsi1sk',
105
+ composite: ['suggestionId'],
106
+ },
107
+ },
108
+ byOpportunityIdAndStatus: {
109
+ index: 'spacecat-data-suggestion-by-opportunity-and-status',
110
+ pk: {
111
+ field: 'gsi2pk',
112
+ composite: ['opportunityId'],
113
+ },
114
+ sk: {
115
+ field: 'gsi2sk',
116
+ composite: ['status', 'rank'],
117
+ },
118
+ },
119
+ },
120
+ };
121
+
122
+ export default SuggestionSchema;
@@ -0,0 +1,62 @@
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
+ export function guardSet(
14
+ propertyName: string,
15
+ value: never,
16
+ entityName: string,
17
+ type?: string,
18
+ nullable?: boolean,
19
+ ): void;
20
+
21
+ export function guardAny(
22
+ propertyName: string,
23
+ value: never,
24
+ entityName: string,
25
+ nullable?: boolean,
26
+ ): void;
27
+
28
+ export function guardEnum(
29
+ propertyName: string,
30
+ value: never,
31
+ enumValues: string[],
32
+ entityName: string,
33
+ nullable?: boolean,
34
+ ): void;
35
+
36
+ export function guardId(
37
+ propertyName: string,
38
+ value: never,
39
+ entityName: string,
40
+ nullable?: boolean,
41
+ ): void;
42
+
43
+ export function guardMap(
44
+ propertyName: string,
45
+ value: never,
46
+ entityName: string,
47
+ nullable?: boolean,
48
+ ): void;
49
+
50
+ export function guardNumber(
51
+ propertyName: string,
52
+ value: never,
53
+ entityName: string,
54
+ nullable?: boolean,
55
+ ): void;
56
+
57
+ export function guardString(
58
+ propertyName: string,
59
+ value: never,
60
+ entityName: string,
61
+ nullable?: boolean,
62
+ ): void;