@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.
- package/CHANGELOG.md +14 -0
- package/package.json +7 -4
- package/src/index.d.ts +6 -0
- package/src/index.js +5 -0
- package/src/service/index.js +52 -0
- package/src/v2/errors/index.d.ts +13 -0
- package/src/v2/errors/index.js +15 -0
- package/src/v2/errors/validation.error.js +13 -0
- package/src/v2/index.d.ts +15 -0
- package/src/v2/index.js +15 -0
- package/src/v2/models/base.collection.js +118 -0
- package/src/v2/models/base.model.js +127 -0
- package/src/v2/models/index.d.ts +100 -0
- package/src/v2/models/index.js +27 -0
- package/src/v2/models/model.factory.js +74 -0
- package/src/v2/models/opportunity.collection.js +78 -0
- package/src/v2/models/opportunity.model.js +238 -0
- package/src/v2/models/suggestion.collection.js +80 -0
- package/src/v2/models/suggestion.model.js +138 -0
- package/src/v2/readme.md +260 -0
- package/src/v2/schema/opportunity.schema.js +145 -0
- package/src/v2/schema/suggestion.schema.js +122 -0
- package/src/v2/util/guards.d.ts +62 -0
- package/src/v2/util/guards.js +165 -0
- package/src/v2/util/index.d.ts +13 -0
- package/src/v2/util/index.js +22 -0
- package/src/v2/util/patcher.js +183 -0
package/src/v2/readme.md
ADDED
|
@@ -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;
|