@adobe/spacecat-shared-data-access 1.59.2 → 1.60.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +2 -2
  3. package/src/models/site/config.js +1 -1
  4. package/src/service/audits/accessPatterns.js +7 -7
  5. package/src/service/experiments/accessPatterns.js +2 -2
  6. package/src/service/import-job/accessPatterns.js +1 -1
  7. package/src/service/import-url/accessPatterns.js +2 -2
  8. package/src/service/index.js +10 -18
  9. package/src/service/key-events/accessPatterns.js +3 -3
  10. package/src/service/organizations/accessPatterns.js +3 -3
  11. package/src/service/site-candidates/accessPatterns.js +1 -1
  12. package/src/service/sites/accessPatterns.js +11 -11
  13. package/src/v2/models/api-key/api-key.collection.js +26 -0
  14. package/src/v2/models/api-key/api-key.model.js +59 -0
  15. package/src/v2/models/api-key/api-key.schema.js +82 -0
  16. package/src/v2/models/api-key/index.d.ts +37 -0
  17. package/src/v2/models/api-key/index.js +19 -0
  18. package/src/v2/models/audit/audit.collection.js +26 -0
  19. package/src/v2/models/audit/audit.model.js +89 -0
  20. package/src/v2/models/audit/audit.schema.js +66 -0
  21. package/src/v2/models/audit/index.d.ts +40 -0
  22. package/src/v2/models/audit/index.js +19 -0
  23. package/src/v2/models/base/base.collection.js +450 -0
  24. package/src/v2/models/{base.model.js → base/base.model.js} +109 -89
  25. package/src/v2/models/base/constants.js +17 -0
  26. package/src/v2/models/base/entity.registry.js +137 -0
  27. package/src/v2/models/base/index.d.ts +83 -0
  28. package/src/v2/models/base/index.js +27 -0
  29. package/src/v2/models/base/reference.js +159 -0
  30. package/src/v2/models/base/schema.builder.js +420 -0
  31. package/src/v2/models/base/schema.js +283 -0
  32. package/src/v2/models/configuration/configuration.collection.js +39 -0
  33. package/src/v2/models/configuration/configuration.model.js +160 -0
  34. package/src/v2/models/configuration/configuration.schema.js +103 -0
  35. package/src/v2/models/configuration/index.d.ts +111 -0
  36. package/src/v2/models/configuration/index.js +19 -0
  37. package/src/v2/models/experiment/experiment.collection.js +26 -0
  38. package/src/v2/models/experiment/experiment.model.js +28 -0
  39. package/src/v2/models/experiment/experiment.schema.js +70 -0
  40. package/src/v2/models/experiment/index.d.ts +49 -0
  41. package/src/v2/models/experiment/index.js +19 -0
  42. package/src/v2/models/import-job/import-job.collection.js +45 -0
  43. package/src/v2/models/import-job/import-job.model.js +55 -0
  44. package/src/v2/models/import-job/import-job.schema.js +152 -0
  45. package/src/v2/models/import-job/index.d.ts +51 -0
  46. package/src/v2/models/import-job/index.js +19 -0
  47. package/src/v2/models/import-url/import-url.collection.js +26 -0
  48. package/src/v2/models/import-url/import-url.model.js +28 -0
  49. package/src/v2/models/import-url/import-url.schema.js +59 -0
  50. package/src/v2/models/import-url/index.d.ts +35 -0
  51. package/src/v2/models/import-url/index.js +19 -0
  52. package/src/v2/models/index.d.ts +11 -99
  53. package/src/v2/models/index.js +14 -15
  54. package/src/v2/models/key-event/index.d.ts +28 -0
  55. package/src/v2/models/key-event/index.js +19 -0
  56. package/src/v2/models/key-event/key-event.collection.js +26 -0
  57. package/src/v2/models/key-event/key-event.model.js +37 -0
  58. package/src/v2/models/key-event/key-event.schema.js +45 -0
  59. package/src/v2/models/opportunity/index.d.ts +46 -0
  60. package/src/v2/models/opportunity/index.js +19 -0
  61. package/src/v2/models/opportunity/opportunity.collection.js +26 -0
  62. package/src/v2/models/{opportunity.model.js → opportunity/opportunity.model.js} +15 -2
  63. package/src/v2/models/opportunity/opportunity.schema.js +69 -0
  64. package/src/v2/models/organization/index.d.ts +28 -0
  65. package/src/v2/models/organization/index.js +19 -0
  66. package/src/v2/models/organization/organization.collection.js +26 -0
  67. package/src/v2/models/organization/organization.model.js +31 -0
  68. package/src/v2/models/organization/organization.schema.js +51 -0
  69. package/src/v2/models/site/index.d.ts +43 -0
  70. package/src/v2/models/site/index.js +20 -0
  71. package/src/v2/models/site/site.collection.js +28 -0
  72. package/src/v2/models/site/site.model.js +47 -0
  73. package/src/v2/models/site/site.schema.js +91 -0
  74. package/src/v2/models/site-candidate/index.d.ts +38 -0
  75. package/src/v2/models/site-candidate/index.js +19 -0
  76. package/src/v2/models/site-candidate/site-candidate.collection.js +27 -0
  77. package/src/v2/models/site-candidate/site-candidate.model.js +41 -0
  78. package/src/v2/models/site-candidate/site-candidate.schema.js +59 -0
  79. package/src/v2/models/site-top-page/index.d.ts +35 -0
  80. package/src/v2/models/site-top-page/index.js +19 -0
  81. package/src/v2/models/site-top-page/site-top-page.collection.js +44 -0
  82. package/src/v2/models/site-top-page/site-top-page.model.js +28 -0
  83. package/src/v2/models/site-top-page/site-top-page.schema.js +65 -0
  84. package/src/v2/models/suggestion/index.d.ts +34 -0
  85. package/src/v2/models/suggestion/index.js +19 -0
  86. package/src/v2/models/suggestion/suggestion.collection.js +55 -0
  87. package/src/v2/models/{suggestion.model.js → suggestion/suggestion.model.js} +16 -1
  88. package/src/v2/models/suggestion/suggestion.schema.js +53 -0
  89. package/src/v2/readme.md +201 -256
  90. package/src/v2/util/accessor.utils.js +158 -0
  91. package/src/v2/util/guards.d.ts +7 -0
  92. package/src/v2/util/guards.js +21 -4
  93. package/src/v2/util/index.js +1 -0
  94. package/src/v2/util/patcher.js +54 -25
  95. package/src/v2/util/util.js +84 -0
  96. package/src/v2/models/base.collection.js +0 -275
  97. package/src/v2/models/model.factory.js +0 -74
  98. package/src/v2/models/opportunity.collection.js +0 -74
  99. package/src/v2/models/suggestion.collection.js +0 -104
  100. package/src/v2/schema/opportunity.schema.js +0 -159
  101. package/src/v2/schema/suggestion.schema.js +0 -132
  102. package/src/v2/util/reference.js +0 -41
@@ -0,0 +1,34 @@
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 type { BaseCollection, BaseModel, Opportunity } from '../index';
14
+
15
+ export interface Suggestion extends BaseModel {
16
+ getData(): object;
17
+ getKpiDeltas(): object;
18
+ getOpportunity(): Promise<Opportunity>;
19
+ getOpportunityId(): string;
20
+ getRank(): number;
21
+ getStatus(): string;
22
+ getType(): string;
23
+ setData(data: object): Suggestion;
24
+ setKpiDeltas(kpiDeltas: object): Suggestion;
25
+ setOpportunityId(opportunityId: string): Suggestion;
26
+ setRank(rank: number): Suggestion;
27
+ setStatus(status: string): Suggestion;
28
+ }
29
+
30
+ export interface SuggestionCollection extends BaseCollection<Suggestion> {
31
+ allByOpportunityId(opportunityId: string): Promise<Suggestion[]>;
32
+ allByOpportunityIdAndStatus(opportunityId: string, status: string): Promise<Suggestion[]>;
33
+ bulkUpdateStatus(suggestions: Suggestion[], status: string): Promise<Suggestion[]>;
34
+ }
@@ -0,0 +1,19 @@
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 Suggestion from './suggestion.model.js';
14
+ import SuggestionCollection from './suggestion.collection.js';
15
+
16
+ export {
17
+ Suggestion,
18
+ SuggestionCollection,
19
+ };
@@ -0,0 +1,55 @@
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 BaseCollection from '../base/base.collection.js';
14
+ import { STATUSES } from './suggestion.model.js';
15
+
16
+ /**
17
+ * SuggestionCollection - A collection class responsible for managing Suggestion entities.
18
+ * Extends the BaseCollection to provide specific methods for interacting with Suggestion records.
19
+ *
20
+ * @class SuggestionCollection
21
+ * @extends BaseCollection
22
+ */
23
+ class SuggestionCollection extends BaseCollection {
24
+ /**
25
+ * Updates the status of multiple given suggestions. The given status must conform
26
+ * to the status enum defined in the Suggestion schema.
27
+ * Saves the updated suggestions to the database automatically.
28
+ * You don't need to call save() on the suggestions after calling this method.
29
+ * @async
30
+ * @param {Suggestion[]} suggestions - An array of Suggestion instances to update.
31
+ * @param {string} status - The new status to set for the suggestions.
32
+ * @return {Promise<*>} - A promise that resolves to the updated suggestions.
33
+ * @throws {Error} - Throws an error if the suggestions are not provided
34
+ * or if the status is invalid.
35
+ */
36
+ async bulkUpdateStatus(suggestions, status) {
37
+ if (!Array.isArray(suggestions)) {
38
+ throw new Error('Suggestions must be an array');
39
+ }
40
+
41
+ if (!Object.values(STATUSES).includes(status)) {
42
+ throw new Error(`Invalid status: ${status}. Must be one of: ${Object.values(STATUSES).join(', ')}`);
43
+ }
44
+
45
+ suggestions.forEach((suggestion) => {
46
+ suggestion.setStatus(status);
47
+ });
48
+
49
+ await this._saveMany(suggestions);
50
+
51
+ return suggestions;
52
+ }
53
+ }
54
+
55
+ export default SuggestionCollection;
@@ -10,7 +10,22 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import BaseModel from './base.model.js';
13
+ import BaseModel from '../base/base.model.js';
14
+
15
+ export const STATUSES = {
16
+ NEW: 'NEW',
17
+ APPROVED: 'APPROVED',
18
+ SKIPPED: 'SKIPPED',
19
+ FIXED: 'FIXED',
20
+ ERROR: 'ERROR',
21
+ };
22
+
23
+ export const TYPES = {
24
+ CODE_CHANGE: 'CODE_CHANGE',
25
+ CONTENT_UPDATE: 'CONTENT_UPDATE',
26
+ REDIRECT_UPDATE: 'REDIRECT_UPDATE',
27
+ METADATA_UPDATE: 'METADATA_UPDATE',
28
+ };
14
29
 
15
30
  /**
16
31
  * Suggestion - A class representing a Suggestion entity.
@@ -0,0 +1,53 @@
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 } from '@adobe/spacecat-shared-utils';
16
+
17
+ import SchemaBuilder from '../base/schema.builder.js';
18
+ import Suggestion, { STATUSES, TYPES } from './suggestion.model.js';
19
+ import SuggestionCollection from './suggestion.collection.js';
20
+
21
+ /*
22
+ Schema Doc: https://electrodb.dev/en/modeling/schema/
23
+ Attribute Doc: https://electrodb.dev/en/modeling/attributes/
24
+ Indexes Doc: https://electrodb.dev/en/modeling/indexes/
25
+ */
26
+
27
+ const schema = new SchemaBuilder(Suggestion, SuggestionCollection)
28
+ .addReference('belongs_to', 'Opportunity', ['status', 'rank'])
29
+ .addAttribute('type', {
30
+ type: Object.values(TYPES),
31
+ required: true,
32
+ readOnly: true,
33
+ })
34
+ .addAttribute('rank', {
35
+ type: 'number',
36
+ required: true,
37
+ })
38
+ .addAttribute('data', {
39
+ type: 'any',
40
+ required: true,
41
+ validate: (value) => isNonEmptyObject(value),
42
+ })
43
+ .addAttribute('kpiDeltas', {
44
+ type: 'any',
45
+ validate: (value) => !value || isNonEmptyObject(value),
46
+ })
47
+ .addAttribute('status', {
48
+ type: Object.values(STATUSES),
49
+ required: true,
50
+ default: STATUSES.NEW,
51
+ });
52
+
53
+ export default schema.build();
package/src/v2/readme.md CHANGED
@@ -1,272 +1,217 @@
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
- ## 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
-
19
- ## Architecture Overview
20
-
21
- The architecture follows a collection-management pattern with ElectroDB, enabling efficient handling of DynamoDB entities. The architecture is organized into the following layers:
22
-
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.
27
-
28
- ### Architectural Diagram
29
-
30
- ```plaintext
31
- +--------------------+
32
- | Data Layer |
33
- |--------------------|
34
- | DynamoDB + ElectroDB ORM |
35
- +--------------------+
36
-
37
- +--------------------+
38
- | Collection Layer |
39
- |--------------------|
40
- | BaseCollection, |
41
- | OpportunityCollection, |
42
- | SuggestionCollection |
43
- +--------------------+
44
-
45
- +--------------------+
46
- | Model Layer |
47
- |--------------------|
48
- | BaseModel, |
49
- | Opportunity, |
50
- | Suggestion |
51
- +--------------------+
52
-
53
- +--------------------+
54
- | Factory Layer |
55
- |--------------------|
56
- | ModelFactory |
57
- +--------------------+
1
+ # ElectroDB Entity Framework
2
+
3
+ ## Overview
4
+
5
+ This entity framework streamlines the definition, querying, and manipulation of domain entities in a DynamoDB-based application. Built atop [ElectroDB](https://electrodb.dev/), it provides a consistent layer for schema definition, indexing, and robust CRUD operations, while adding conveniences like automatic indexing methods and reference handling.
6
+
7
+ By adhering to this framework’s conventions, you can introduce and manage new entities with minimal boilerplate and complexity.
8
+
9
+ ## Core Concepts
10
+
11
+ ### Entities
12
+ An *entity* represents a domain concept (e.g., `User`, `Organization`, `Order`) persisted in the database. Each entity is defined by a schema, specifying attributes, indexes, and references to other entities. The schema integrates with ElectroDB, ensuring a uniform approach to modeling data.
13
+
14
+ ### Models
15
+ A *Model* is a class representing a single instance of an entity. It provides:
16
+
17
+ - Attribute getters and setters generated based on the schema.
18
+ - Methods for persisting changes (`save()`), and removing entities (`remove()`).
19
+ - Methods to fetch referenced entities (via `belongs_to`, `has_one`, `has_many` references).
20
+
21
+ Models extend `BaseModel`, which handles most of the common logic.
22
+
23
+ ### Collections
24
+ A *Collection* operates on sets of entities. While `Model` focuses on individual records, `Collection` is for batch and query-level operations:
25
+
26
+ - Query methods like `findById()`, `all()`, and index-derived methods.
27
+ - Batch creation and update methods (`createMany`, `_saveMany`).
28
+ - Automatic generation of `allBy...` and `findBy...` convenience methods based on defined indexes.
29
+
30
+ Collections extend `BaseCollection`, which generates query methods at runtime based on your schema definitions.
31
+
32
+ ### Schema Builder
33
+ The `SchemaBuilder` is a fluent API to define an entity’s schema:
34
+
35
+ - **Attributes:** Configure entity fields and their validation.
36
+ - **Indexes:** Specify primary and secondary indexes for common queries.
37
+ - **References:** Define entity relationships (e.g., `User` belongs to `Organization`).
38
+
39
+ The `SchemaBuilder` enforces naming conventions and sets defaults, reducing repetitive configuration.
40
+
41
+ **Note on Indexes:** Add indexes thoughtfully. Every extra index adds cost and complexity. Only create indexes for well-understood, frequently-needed query patterns.
42
+
43
+ ### Entity Registry
44
+ The `EntityRegistry` aggregates all entities, their schemas, and their collections. It ensures consistent lookup and retrieval of any registered entity’s collection. When you add a new entity, you must register it with the `EntityRegistry` so the rest of the application can discover it.
45
+
46
+ ## Default Attributes and Indexes
47
+
48
+ When you create a schema with `SchemaBuilder`, the following attributes are automatically defined:
49
+
50
+ 1. **ID (Primary Key):** A UUID-based primary key (`${entityName}Id`), ensuring unique identification.
51
+ 2. **createdAt:** A timestamp (ISO string) set at entity creation.
52
+ 3. **updatedAt:** A timestamp (ISO string) updated on each modification.
53
+
54
+ A primary index is also set up, keyed by the `${entityName}Id` attribute, guaranteeing a straightforward way to retrieve entities by their unique ID.
55
+
56
+ ## Auto-Generated Methods
57
+
58
+ ### `BaseCollection`
59
+
60
+ `BaseCollection` automatically generates `allBy...` and `findBy...` methods derived from your defined indexes. For example, if your schema defines an index composed of `opportunityId`, `status`, and `createdAt`, `BaseCollection` will generate:
61
+
62
+ - `allByOpportunityId(opportunityId, options?)`
63
+ - `findByOpportunityId(opportunityId, options?)`
64
+ - `allByOpportunityIdAndStatus(opportunityId, status, options?)`
65
+ - `findByOpportunityIdAndStatus(opportunityId, status, options?)`
66
+ - `allByOpportunityIdAndStatusAndCreatedAt(opportunityId, status, createdAt, options?)`
67
+ - `findByOpportunityIdAndStatusAndCreatedAt(opportunityId, status, createdAt, options?)`
68
+
69
+ **allBy...** methods return arrays of matching entities, while **findBy...** methods return a single (or the first matching) entity. Both can accept an optional `options` object for filtering, ordering, attribute selection, and pagination.
70
+
71
+ **Example:**
72
+ ```js
73
+ const Suggestion = dataAccess.Suggestion;
74
+
75
+ // Retrieve all suggestions by `opportunityId`
76
+ const results = await Suggestion.allByOpportunityId('op-12345');
77
+
78
+ // Retrieve a single suggestion by `opportunityId` and `status`
79
+ const single = await Suggestion.findByOpportunityIdAndStatus('op-12345', 'OPEN');
58
80
  ```
59
81
 
60
- ## Entities and Relationships
61
-
62
- - **Opportunity**: Represents a specific issue identified on a website. It includes attributes like `title`, `description`, `siteId`, and `status`.
63
- - **Suggestion**: Represents a proposed fix for an Opportunity. Attributes include `opportunityId`, `type`, `status`, and `rank`.
64
- - **Relationships**: Opportunities have many Suggestions. This relationship is implemented through `OpportunityCollection` and `SuggestionCollection`, which interact via ElectroDB-managed DynamoDB relationships.
65
-
66
- ## Getting Started
67
-
68
- 1. **Install Dependencies**
69
- ```bash
70
- npm install
71
- ```
72
-
73
- 2. **Setup DynamoDB**
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`.
76
-
77
- 3. **Usage Example**
78
- ```javascript
79
- import { createDataAccess } from './index.js';
80
-
81
- const config = { tableNameData: 'YOUR_TABLE_NAME' };
82
- const log = console;
83
- const dao = createDataAccess(config, log);
84
-
85
- // Create a new Opportunity
86
- const opportunityData = { title: 'Broken Links', siteId: 'site123', type: 'broken-backlinks' };
87
- const newOpportunity = await dao.Opportunity.create(opportunityData);
88
- console.log('New Opportunity Created:', newOpportunity);
89
- ```
90
-
91
- 4. **Extending Functionality**
92
- - Add new models by extending `BaseModel` and new collections by extending `BaseCollection`.
93
- - Register new models in the `ModelFactory` for unified access.
94
-
95
- ## Adding a New ElectroDB-Based Entity
96
-
97
- This guide provides a step-by-step overview for adding a new ElectroDB-based entity to the application.
98
-
99
- ### Step 1: Define the Entity Schema
100
-
101
- 1. **Create Entity Schema File**: Define the entity schema in a new file (e.g., `myNewEntity.schema.js`) within the `/schemas/` directory.
102
-
103
- ```javascript
104
- export const MyNewEntitySchema = {
105
- model: {
106
- entity: 'MyNewEntity',
107
- service: 'MyService',
108
- version: '1',
109
- },
110
- attributes: {
111
- myNewEntityId: {
112
- type: 'string',
113
- required: true,
114
- },
115
- name: {
116
- type: 'string',
117
- required: true,
118
- },
119
- status: {
120
- type: 'string',
121
- enum: ['NEW', 'IN_PROGRESS', 'COMPLETED'],
122
- required: true,
123
- },
124
- createdAt: {
125
- type: 'string',
126
- required: true,
127
- default: () => new Date().toISOString(),
128
- },
129
- },
130
- indexes: {
131
- myNewEntityIndex: {
132
- pk: {
133
- field: 'pk',
134
- facets: ['myNewEntityId'],
135
- },
136
- sk: {
137
- field: 'sk',
138
- facets: ['status'],
139
- },
140
- },
141
- },
142
- references: {
143
- belongs_to: [
144
- { type: 'belongs_to', target: 'Opportunity' },
145
- ],
146
- },
147
- };
148
- ```
149
-
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`.
155
-
156
- ```javascript
157
- import BaseModel from './base.model.js';
158
-
159
- class MyNewEntity extends BaseModel {
160
- constructor(electroService, modelFactory, record, log) {
161
- super(electroService, modelFactory, record, log);
162
- }
163
- }
164
-
165
- export default MyNewEntity;
166
- ```
167
-
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.
171
-
172
- ### Automatic Getter and Setter Methods
173
-
174
- The `BaseModel` automatically generates getter and setter methods for each attribute defined in the entity schema:
175
-
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.
177
-
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
190
-
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.
209
-
210
- ```javascript
211
- import BaseCollection from './base.collection.js';
212
- import MyNewEntity from '../models/myNewEntity.model.js';
213
-
214
- class MyNewEntityCollection extends BaseCollection {
215
- constructor(service, modelFactory, log) {
216
- super(service, modelFactory, MyNewEntity, log);
217
- }
218
-
219
- async allByStatus(status) {
220
- return this.findByIndexKeys({ status });
221
- }
222
- }
223
-
224
- export default MyNewEntityCollection;
225
- ```
82
+ ### `BaseModel`
226
83
 
227
- ### Step 4: Integrate the Entity into Model Factory
84
+ `BaseModel` provides methods for CRUD operations and reference handling:
228
85
 
229
- 1. **Update the Model Factory**: Open `model.factory.js` and add the new entity and collection to the `initialize` method.
86
+ - `save()`: Persists changes to the entity.
87
+ - `remove()`: Deletes the entity from the database.
88
+ - `get...()`: Getters for entity attributes.
89
+ - `set...()`: Setters for entity attributes.
230
90
 
231
- ```javascript
232
- import MyNewEntityCollection from './collections/myNewEntity.collection.js';
91
+ Additionally, `BaseModel` generates methods to fetch referenced entities.
92
+ For example, if `User` belongs to `Organization`, `BaseModel` will create:
233
93
 
234
- class ModelFactory {
235
- initialize() {
236
- const myNewEntityCollection = new MyNewEntityCollection(
237
- this.service,
238
- this,
239
- this.logger,
240
- );
94
+ - `getOrganization()`: Fetch the referenced `Organization` entity.
95
+ - `getOrganizationId()`: Retrieve the `Organization` ID.
96
+ - `setOrganizationId(organizationId)`: Update the `Organization` reference.
241
97
 
242
- this.models.set(MyNewEntityCollection.name, myNewEntityCollection);
243
- }
244
- }
245
- ```
98
+ Conversely, the `Organization` entity will have:
246
99
 
247
- ### Step 5: Write Unit and Integration Tests
100
+ - `getUsers()`: Fetch all `User` entities referencing this `Organization`.
101
+ - And with the `User`-Schema's `belongs_to` reciprocal reference expressing filterable sort keys, e.g. "email", "location":
102
+ - `getUsersByEmail(email)`: Fetch all `User` entities referencing this `Organization` with a specific email."
103
+ - `getUsersByEmailAndLocation(email, location)`: Fetch all `User` entities referencing this `Organization` with a specific email and location.
248
104
 
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.
105
+ **Example:**
106
+ ```js
107
+ const user = await User.findById('usr-abc123');
251
108
 
252
- 2. **Create Collection Tests**: Add `myNewEntity.collection.test.js` to `/tests/unit/collections/`.
253
- - Test methods interacting with ElectroDB, like `allByStatus`.
109
+ // Work with attributes
110
+ console.log(user.getEmail()); // e.g. "john@example.com"
111
+ user.setName('John Smith');
112
+ await user.save();
254
113
 
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.
114
+ // Fetch referenced entity
115
+ const org = await user.getOrganization();
116
+ console.log(org.getName());
117
+ ```
256
118
 
257
- ### Step 6: Create JSDoc and Update Documentation
119
+ ## Step-by-Step: Adding a New Entity
258
120
 
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.
121
+ Follow these steps to introduce a new entity into the framework.
261
122
 
262
- ### Step 7: Run Tests and Verify
123
+ ### 1. Define the Schema
124
+ Create `user.schema.js`:
125
+
126
+ ```js
127
+ import SchemaBuilder from '../base/schema.builder.js';
128
+ import User from './user.model.js';
129
+ import UserCollection from './user.collection.js';
130
+
131
+ const userSchema = new SchemaBuilder(User, UserCollection)
132
+ .addAttribute('email', {
133
+ type: 'string',
134
+ required: true,
135
+ validate: (value) => value.includes('@'),
136
+ })
137
+ .addAttribute('name', { type: 'string', required: true })
138
+ .addAllIndexWithComposite('email')
139
+ .addReference('belongs_to', 'Organization') // Adds organizationId and byOrganizationId index
140
+ .build();
141
+
142
+ export default userSchema;
143
+ ```
144
+
145
+ ### 2. Implement the Model
146
+ Create `user.model.js`:
147
+
148
+ ```js
149
+ import BaseModel from '../base/base.model.js';
150
+
151
+ class UserModel extends BaseModel {
152
+ // Additional domain logic methods can be added here if needed.
153
+ }
154
+
155
+ export default UserModel;
156
+ ```
157
+
158
+ ### 3. Implement the Collection
159
+ Create `user.collection.js`:
160
+
161
+ ```js
162
+ import BaseCollection from '../base/base.collection.js';
163
+ import UserModel from './user.model.js';
164
+ import userSchema from './user.schema.js';
165
+
166
+ class UserCollection extends BaseCollection {
167
+ // Additional domain logic collection methods can be added here if needed.
168
+ async findByEmail(email) {
169
+ return this.findByIndexKeys({ email });
170
+ }
171
+ }
172
+
173
+ export default UserCollection;
174
+ ```
175
+
176
+ ### 4. Register the Entity
177
+ In `entity.registry.js` (or equivalent):
178
+
179
+ ```js
180
+ import UserSchema from '../user/user.schema.js';
181
+ import UserCollection from '../user/user.collection.js';
182
+
183
+ EntityRegistry.registerEntity(UserSchema, UserCollection);
184
+ ```
185
+
186
+ ### 5. Update DynamoDB Configuration and `schema.json`
187
+
188
+ After defining indexes in the schema, **manually add these indexes to your DynamoDB table configuration**. DynamoDB does not automatically create GSIs. You must:
189
+
190
+ - Use the AWS Console, CLI, or CloudFormation/Terraform templates to define these GSIs.
191
+ - Update your `schema.json` or another documentation file to reflect the newly created indexes, so the team knows which indexes exist and what query patterns they support.
192
+
193
+ ### 6. Use the Entity
194
+ ```js
195
+ const { User, Organization } = dataAccess;
196
+
197
+ // Create a user
198
+ const newUser = await User.create({ email: 'john@example.com', name: 'John Doe' });
199
+
200
+ // Find user by ID
201
+ const user = await User.findById(newUser.getId());
202
+
203
+ // Get the user organization
204
+ const org = await user.getOrganization();
205
+
206
+ // ...or in reverse
207
+ const anOrg = await Organization.findById(user.getOrganizationId());
208
+ const orgUsers = await anOrg.getUsers();
209
+
210
+ // Update user and save
211
+ user.setName('John X. Doe');
212
+ await user.save();
213
+ ```
263
214
 
264
- 1. **Run All Tests**:
265
- ```bash
266
- npm run test && npm run test:it
267
- ```
215
+ ## Consideration for Indexes
268
216
 
269
- 2. **Run Linter**: Check for coding standard violations.
270
- ```bash
271
- npm run lint
272
- ```
217
+ Indexes cost money and complexity. Do not add indexes lightly. Determine which query patterns you truly need and only then introduce additional indexes.