@adobe/spacecat-shared-data-access 1.60.2 → 1.61.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 (43) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +5 -5
  3. package/src/v2/errors/data-access.error.js +24 -0
  4. package/src/v2/errors/index.d.ts +5 -1
  5. package/src/v2/errors/index.js +11 -1
  6. package/src/v2/errors/reference.error.js +22 -0
  7. package/src/v2/{models/base/constants.js → errors/schema-validation.error.js} +4 -6
  8. package/src/v2/errors/schema.builder.error.js +27 -0
  9. package/src/v2/errors/schema.error.js +19 -0
  10. package/src/v2/errors/validation.error.js +3 -1
  11. package/src/v2/models/api-key/index.d.ts +15 -2
  12. package/src/v2/models/audit/audit.collection.js +25 -1
  13. package/src/v2/models/audit/audit.schema.js +3 -0
  14. package/src/v2/models/audit/index.d.ts +16 -11
  15. package/src/v2/models/base/base.collection.js +148 -85
  16. package/src/v2/models/base/base.model.js +73 -14
  17. package/src/v2/models/base/entity.registry.js +7 -2
  18. package/src/v2/models/base/index.d.ts +30 -11
  19. package/src/v2/models/base/reference.js +81 -28
  20. package/src/v2/models/base/schema.builder.js +96 -24
  21. package/src/v2/models/base/schema.js +78 -10
  22. package/src/v2/models/configuration/index.d.ts +24 -90
  23. package/src/v2/models/experiment/index.d.ts +11 -3
  24. package/src/v2/models/import-job/index.d.ts +10 -3
  25. package/src/v2/models/import-url/index.d.ts +6 -3
  26. package/src/v2/models/index.d.ts +3 -0
  27. package/src/v2/models/index.js +1 -0
  28. package/src/v2/models/key-event/index.d.ts +5 -1
  29. package/src/v2/models/latest-audit/index.d.ts +43 -0
  30. package/src/v2/models/latest-audit/index.js +19 -0
  31. package/src/v2/models/latest-audit/latest-audit.collection.js +32 -0
  32. package/src/v2/models/latest-audit/latest-audit.model.js +26 -0
  33. package/src/v2/models/latest-audit/latest-audit.schema.js +72 -0
  34. package/src/v2/models/opportunity/index.d.ts +17 -1
  35. package/src/v2/models/opportunity/opportunity.schema.js +1 -0
  36. package/src/v2/models/organization/index.d.ts +3 -1
  37. package/src/v2/models/site/index.d.ts +43 -6
  38. package/src/v2/models/site/site.model.js +0 -6
  39. package/src/v2/models/site/site.schema.js +2 -0
  40. package/src/v2/models/site-candidate/index.d.ts +5 -7
  41. package/src/v2/models/site-top-page/index.d.ts +16 -2
  42. package/src/v2/models/suggestion/index.d.ts +2 -0
  43. package/src/v2/util/patcher.js +11 -0
@@ -12,7 +12,13 @@
12
12
 
13
13
  import type { ValidationError } from '../../errors';
14
14
 
15
+ export interface MultiStatusCreateResult<T> {
16
+ createdItems: T[],
17
+ errorItems: { item: object, error: ValidationError }[],
18
+ }
19
+
15
20
  export interface BaseModel {
21
+ _remove(): Promise<this>;
16
22
  getCreatedAt(): string;
17
23
  getId(): string;
18
24
  getUpdatedAt(): string;
@@ -21,11 +27,6 @@ export interface BaseModel {
21
27
  toJSON(): object;
22
28
  }
23
29
 
24
- export interface MultiStatusCreateResult<T> {
25
- createdItems: T[],
26
- errorItems: { item: object, error: ValidationError }[],
27
- }
28
-
29
30
  export interface QueryOptions {
30
31
  index?: string;
31
32
  limit?: number;
@@ -34,12 +35,15 @@ export interface QueryOptions {
34
35
  }
35
36
 
36
37
  export interface BaseCollection<T extends BaseModel> {
38
+ _onCreate(item: T): void;
39
+ _onCreateMany(items: MultiStatusCreateResult<T>): void;
40
+ _saveMany(items: T[]): Promise<T[]>;
37
41
  all(sortKeys?: object, options?: QueryOptions): Promise<T[]>;
38
42
  allByIndexKeys(keys: object, options?: QueryOptions): Promise<T[]>;
39
43
  create(item: object): Promise<T>;
40
- createMany(items: object[]): Promise<MultiStatusCreateResult<T>>;
41
- findByAll(sortKeys?: object, options?: QueryOptions): Promise<T>;
42
- findById(id: string): Promise<T>;
44
+ createMany(items: object[], parent?: T): Promise<MultiStatusCreateResult<T>>;
45
+ findByAll(sortKeys?: object, options?: QueryOptions): Promise<T> | null;
46
+ findById(id: string): Promise<T> | null;
43
47
  findByIndexKeys(indexKeys: object): Promise<T>;
44
48
  removeByIds(ids: string[]): Promise<void>;
45
49
  }
@@ -56,6 +60,7 @@ export interface Reference {
56
60
  getTarget(): string;
57
61
  getType(): string;
58
62
  isRemoveDependents(): boolean;
63
+ toAccessorConfigs(): object[];
59
64
  }
60
65
 
61
66
  export interface IndexAccessor {
@@ -64,27 +69,41 @@ export interface IndexAccessor {
64
69
  }
65
70
 
66
71
  export interface Schema {
72
+ allowsRemove(): boolean;
73
+ allowsUpdates(): boolean;
67
74
  findIndexBySortKeys(sortKeys: string[]): object | null;
68
75
  findIndexByType(type: string): object | null;
76
+ findIndexNameByKeys(keys: object): string;
69
77
  getAttribute(name: string): object;
70
78
  getAttributes(): object;
71
79
  getCollectionName(): string;
72
80
  getEntityName(): string;
73
81
  getIdName(): string;
74
82
  getIndexAccessors(): Array<IndexAccessor>;
75
- getIndexes(): object;
83
+ getIndexByName(indexName: string): object;
76
84
  getIndexKeys(indexName: string): string[];
85
+ getIndexTypes(): string[];
86
+ getIndexes(): object;
77
87
  getModelClass(): object;
78
88
  getModelName(): string;
89
+ getReciprocalReference(registry: EntityRegistry, reference: Reference): Reference | null;
90
+ getReferenceByTypeAndTarget(referenceType: string, target: string): Reference | undefined;
79
91
  getReferences(): Reference[];
80
92
  getReferencesByType(referenceType: string): Reference[];
81
- getReferenceByTypeAndTarget(referenceType: string, target: string): Reference | undefined;
93
+ getServiceName(): string;
94
+ getVersion(): number;
95
+ toAccessorConfigs(): object[];
96
+ toElectroDBSchema(): object;
82
97
  }
83
98
 
84
99
  export interface SchemaBuilder {
85
- addAttribute(name: string, data: object): SchemaBuilder;
86
100
  addAllIndex(sortKeys: string[]): SchemaBuilder;
101
+ addAttribute(name: string, data: object): SchemaBuilder;
87
102
  addIndex(name: string, partitionKey: object, sortKey: object): SchemaBuilder;
88
103
  addReference(referenceType: string, entityName: string, sortKeys?: string[]): SchemaBuilder;
104
+ allowRemove(allow: boolean): SchemaBuilder;
105
+ allowUpdate(allow: boolean): SchemaBuilder;
89
106
  build(): Schema;
107
+ withPrimaryPartitionKeys(partitionKeys: string[]): SchemaBuilder
108
+ withPrimarySortKeys(sortKeys: string[]): SchemaBuilder;
90
109
  }
@@ -10,7 +10,9 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { hasText } from '@adobe/spacecat-shared-utils';
13
+ import { hasText, isNonEmptyObject } from '@adobe/spacecat-shared-utils';
14
+
15
+ import ReferenceError from '../../errors/reference.error.js';
14
16
  import {
15
17
  entityNameToCollectionName,
16
18
  entityNameToIdName,
@@ -19,6 +21,48 @@ import {
19
21
  referenceToBaseMethodName,
20
22
  } from '../../util/util.js';
21
23
 
24
+ const createSortKeyAccessorConfigs = (
25
+ entity,
26
+ baseConfig,
27
+ baseMethodName,
28
+ target,
29
+ targetCollection,
30
+ foreignKeyName,
31
+ foreignKeyValue,
32
+ log,
33
+ ) => {
34
+ const configs = [];
35
+
36
+ const belongsToRef = targetCollection.schema.getReferenceByTypeAndTarget(
37
+ // eslint-disable-next-line no-use-before-define
38
+ Reference.TYPES.BELONGS_TO,
39
+ entity.schema.getModelName(),
40
+ );
41
+
42
+ if (!belongsToRef) {
43
+ log.warn(`Reciprocal reference not found for ${entity.schema.getModelName()} to ${target}`);
44
+ return configs;
45
+ }
46
+
47
+ const sortKeys = belongsToRef.getSortKeys();
48
+ if (!isNonEmptyArray(sortKeys)) {
49
+ log.debug(`No sort keys defined for ${entity.schema.getModelName()} to ${target}`);
50
+ return configs;
51
+ }
52
+
53
+ for (let i = 1; i <= sortKeys.length; i += 1) {
54
+ const subset = sortKeys.slice(0, i);
55
+ configs.push({
56
+ name: keyNamesToMethodName(subset, `${baseMethodName}By`),
57
+ requiredKeys: subset,
58
+ foreignKey: { name: foreignKeyName, value: foreignKeyValue },
59
+ ...baseConfig,
60
+ });
61
+ }
62
+
63
+ return configs;
64
+ };
65
+
22
66
  class Reference {
23
67
  static TYPES = {
24
68
  BELONGS_TO: 'belongs_to',
@@ -36,11 +80,11 @@ class Reference {
36
80
 
37
81
  constructor(type, target, options = {}) {
38
82
  if (!Reference.isValidType(type)) {
39
- throw new Error(`Invalid reference type: ${type}`);
83
+ throw new ReferenceError(this, `Invalid reference type: ${type}`);
40
84
  }
41
85
 
42
86
  if (!hasText(target)) {
43
- throw new Error('Invalid target');
87
+ throw new ReferenceError(this, 'Invalid target');
44
88
  }
45
89
 
46
90
  this.type = type;
@@ -65,6 +109,14 @@ class Reference {
65
109
  }
66
110
 
67
111
  toAccessorConfigs(registry, entity) {
112
+ if (!isNonEmptyObject(registry)) {
113
+ throw new ReferenceError(this, 'Invalid registry');
114
+ }
115
+
116
+ if (!isNonEmptyObject(entity)) {
117
+ throw new ReferenceError(this, 'Invalid entity');
118
+ }
119
+
68
120
  const { log } = registry;
69
121
  const accessorConfigs = [];
70
122
 
@@ -100,6 +152,20 @@ class Reference {
100
152
  requiredKeys: [],
101
153
  foreignKey: { name: foreignKeyName, value: foreignKeyValue },
102
154
  });
155
+
156
+ accessorConfigs.push(
157
+ ...createSortKeyAccessorConfigs(
158
+ entity,
159
+ {},
160
+ baseMethodName,
161
+ target,
162
+ targetCollection,
163
+ foreignKeyName,
164
+ foreignKeyValue,
165
+ log,
166
+ ),
167
+ );
168
+
103
169
  break;
104
170
  }
105
171
 
@@ -115,37 +181,24 @@ class Reference {
115
181
  foreignKey: { name: foreignKeyName, value: foreignKeyValue },
116
182
  });
117
183
 
118
- const belongsToRef = targetCollection.schema.getReferenceByTypeAndTarget(
119
- Reference.TYPES.BELONGS_TO,
120
- entity.schema.getModelName(),
184
+ accessorConfigs.push(
185
+ ...createSortKeyAccessorConfigs(
186
+ entity,
187
+ { all: true },
188
+ baseMethodName,
189
+ target,
190
+ targetCollection,
191
+ foreignKeyName,
192
+ foreignKeyValue,
193
+ log,
194
+ ),
121
195
  );
122
196
 
123
- if (!belongsToRef) {
124
- log.warn(`Reciprocal reference not found for ${entity.schema.getModelName()} to ${target}`);
125
- break;
126
- }
127
-
128
- const sortKeys = belongsToRef.getSortKeys();
129
- if (!isNonEmptyArray(sortKeys)) {
130
- log.debug(`No sort keys defined for ${entity.schema.getModelName()} to ${target}`);
131
- break;
132
- }
133
-
134
- for (let i = 1; i <= sortKeys.length; i += 1) {
135
- const subset = sortKeys.slice(0, i);
136
- accessorConfigs.push({
137
- name: keyNamesToMethodName(subset, `${baseMethodName}By`),
138
- requiredKeys: subset,
139
- all: true,
140
- foreignKey: { name: foreignKeyName, value: foreignKeyValue },
141
- });
142
- }
143
-
144
197
  break;
145
198
  }
146
199
 
147
200
  default:
148
- throw new Error(`Unsupported reference type: ${type}`);
201
+ throw new ReferenceError(this, `Unsupported reference type: ${type}`);
149
202
  }
150
203
 
151
204
  return accessorConfigs.map((config) => ({
@@ -10,10 +10,13 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { hasText, isInteger, isNonEmptyObject } from '@adobe/spacecat-shared-utils';
13
+ import {
14
+ hasText, isBoolean, isInteger, isNonEmptyObject,
15
+ } from '@adobe/spacecat-shared-utils';
14
16
 
15
17
  import { v4 as uuid, validate as uuidValidate } from 'uuid';
16
18
 
19
+ import { SchemaBuilderError } from '../../errors/index.js';
17
20
  import {
18
21
  decapitalize,
19
22
  entityNameToAllPKValue,
@@ -21,7 +24,6 @@ import {
21
24
  isNonEmptyArray,
22
25
  } from '../../util/util.js';
23
26
 
24
- import { INDEX_TYPES } from './constants.js';
25
27
  import BaseModel from './base.model.js';
26
28
  import BaseCollection from './base.collection.js';
27
29
  import Reference from './reference.js';
@@ -86,21 +88,21 @@ class SchemaBuilder {
86
88
  * @param {BaseModel} modelClass - The model class for this entity.
87
89
  * @param {BaseCollection} collectionClass - The collection class for this entity.
88
90
  * @param {number} schemaVersion - A positive integer representing the schema's version.
89
- * @throws {Error} If entityName is not a non-empty string.
90
- * @throws {Error} If schemaVersion is not a positive integer.
91
- * @throws {Error} If serviceName is not a non-empty string.
91
+ * @throws {SchemaBuilderError} If entityName is not a non-empty string.
92
+ * @throws {SchemaBuilderError} If schemaVersion is not a positive integer.
93
+ * @throws {SchemaBuilderError} If serviceName is not a non-empty string.
92
94
  */
93
95
  constructor(modelClass, collectionClass, schemaVersion = 1) {
94
96
  if (!modelClass || !(modelClass.prototype instanceof BaseModel)) {
95
- throw new Error('modelClass must be a subclass of BaseModel.');
97
+ throw new SchemaBuilderError(this, 'modelClass must be a subclass of BaseModel.');
96
98
  }
97
99
 
98
100
  if (!collectionClass || !(collectionClass.prototype instanceof BaseCollection)) {
99
- throw new Error('collectionClass must be a subclass of BaseCollection.');
101
+ throw new SchemaBuilderError(this, 'collectionClass must be a subclass of BaseCollection.');
100
102
  }
101
103
 
102
104
  if (!isInteger(schemaVersion) || schemaVersion < 1) {
103
- throw new Error('schemaVersion is required and must be a positive integer.');
105
+ throw new SchemaBuilderError(this, 'schemaVersion is required and must be a positive integer.');
104
106
  }
105
107
 
106
108
  this.modelClass = modelClass;
@@ -118,6 +120,7 @@ class SchemaBuilder {
118
120
  other: [],
119
121
  };
120
122
 
123
+ this.options = { allowUpdates: true, allowRemove: true };
121
124
  this.attributes = {};
122
125
 
123
126
  // will be populated by build() from rawIndexes
@@ -153,21 +156,89 @@ class SchemaBuilder {
153
156
  });
154
157
  }
155
158
 
159
+ withPrimaryPartitionKeys(partitionKeys) {
160
+ if (!isNonEmptyArray(partitionKeys)) {
161
+ throw new SchemaBuilderError(this, 'Partition keys are required and must be a non-empty array.');
162
+ }
163
+ this.rawIndexes.primary.pk.composite = partitionKeys;
164
+
165
+ return this;
166
+ }
167
+
168
+ /**
169
+ * Sets the sort keys for the primary index (main table). The given sort keys
170
+ * together with the entity id (partition key) will form the primary key. This will
171
+ * change the behavior of collection methods (like findById) that rely on the main
172
+ * table primary key.
173
+ *
174
+ * This should only be used in special cases.
175
+ *
176
+ * @param {Array<string>} sortKeys - The attributes to form the sort key.
177
+ * @throws {SchemaBuilderError} If sortKeys are not provided or are not a non-empty array.
178
+ * @return {SchemaBuilder}
179
+ */
180
+ withPrimarySortKeys(sortKeys) {
181
+ if (!isNonEmptyArray(sortKeys)) {
182
+ throw new SchemaBuilderError(this, 'Sort keys are required and must be a non-empty array.');
183
+ }
184
+ this.rawIndexes.primary.sk.composite = sortKeys;
185
+
186
+ return this;
187
+ }
188
+
189
+ /**
190
+ * By default a schema allows removes. This method allows
191
+ * to disable removes for this entity. Note that this does
192
+ * not prevent removes at the database level, but rather
193
+ * at the application level. The flag is ignored when
194
+ * remove is called implicitly when the entity is removed
195
+ * as part of parent entity remove (dependents).
196
+ * @param {boolean} allow - Whether to allow removes.
197
+ * @throws {SchemaBuilderError} If allow is not a boolean.
198
+ * @return {SchemaBuilder}
199
+ */
200
+ allowRemove(allow) {
201
+ if (!isBoolean(allow)) {
202
+ throw new SchemaBuilderError(this, 'allow must be a boolean.');
203
+ }
204
+ this.options.allowRemove = allow;
205
+
206
+ return this;
207
+ }
208
+
209
+ /**
210
+ * By default a schema allows updates. This method allows
211
+ * to disable updates for this entity. Note that this does
212
+ * not prevent updates at the database level, but rather
213
+ * at the application level.
214
+ * @param {boolean} allow - Whether to allow updates.
215
+ * @throws {SchemaBuilderError} If allow is not a boolean.
216
+ * @return {SchemaBuilder}
217
+ */
218
+ allowUpdates(allow) {
219
+ if (!isBoolean(allow)) {
220
+ throw new SchemaBuilderError(this, 'allow must be a boolean.');
221
+ }
222
+ this.options.allowUpdates = allow;
223
+
224
+ return this;
225
+ }
226
+
156
227
  /**
157
228
  * Adds a new attribute to the schema definition.
158
229
  *
159
230
  * @param {string} name - The attribute name.
160
231
  * @param {object} data - The attribute definition (type, required, validation, etc.).
161
232
  * @returns {SchemaBuilder} Returns this builder for method chaining.
162
- * @throws {Error} If name is not non-empty or data is not an object.
233
+ * @throws {SchemaBuilderError} If name is not non-empty or data is not an object.
163
234
  */
164
235
  addAttribute(name, data) {
165
236
  if (!hasText(name)) {
166
- throw new Error('Attribute name is required and must be non-empty.');
237
+ throw new SchemaBuilderError(this, 'Attribute name is required and must be non-empty.');
167
238
  }
168
239
 
169
240
  if (!isNonEmptyObject(data)) {
170
- throw new Error(`Attribute data for "${name}" is required and must be a non-empty object.`);
241
+ throw new SchemaBuilderError(this, `Attribute data for "${name}" is required and must be a non-empty object.`);
171
242
  }
172
243
 
173
244
  this.attributes[name] = data;
@@ -182,17 +253,17 @@ class SchemaBuilder {
182
253
  *
183
254
  * @param {Array<string>} sortKeys - The attributes to form the sort key.
184
255
  * @returns {SchemaBuilder} Returns this builder for method chaining.
185
- * @throws {Error} If composite attribute names or template are not provided.
256
+ * @throws {SchemaBuilderError} If composite attribute names or template are not provided.
186
257
  */
187
258
  addAllIndex(sortKeys) {
188
259
  if (!isNonEmptyArray(sortKeys)) {
189
- throw new Error('Sort keys are required and must be a non-empty array.');
260
+ throw new SchemaBuilderError(this, 'Sort keys are required and must be a non-empty array.');
190
261
  }
191
262
 
192
263
  this.#internalAddIndex(
193
264
  { template: entityNameToAllPKValue(this.entityName) },
194
265
  { composite: sortKeys },
195
- INDEX_TYPES.ALL,
266
+ Schema.INDEX_TYPES.ALL,
196
267
  );
197
268
 
198
269
  return this;
@@ -205,18 +276,18 @@ class SchemaBuilder {
205
276
  * (e.g., { composite: [attributeName] }).
206
277
  * @param {object} sortKey - The sort key definition.
207
278
  * @returns {SchemaBuilder} Returns this builder for method chaining.
208
- * @throws {Error} If index name is reserved or pk/sk configs are invalid.
279
+ * @throws {SchemaBuilderError} If index name is reserved or pk/sk configs are invalid.
209
280
  */
210
281
  addIndex(partitionKey, sortKey) {
211
282
  if (!isNonEmptyObject(partitionKey)) {
212
- throw new Error('Partition key configuration (pk) is required and must be a non-empty object.');
283
+ throw new SchemaBuilderError(this, 'Partition key configuration (pk) is required and must be a non-empty object.');
213
284
  }
214
285
 
215
286
  if (!isNonEmptyObject(sortKey)) {
216
- throw new Error('Sort key configuration (sk) is required and must be a non-empty object.');
287
+ throw new SchemaBuilderError(this, 'Sort key configuration (sk) is required and must be a non-empty object.');
217
288
  }
218
289
 
219
- this.#internalAddIndex(partitionKey, sortKey, INDEX_TYPES.OTHER);
290
+ this.#internalAddIndex(partitionKey, sortKey, Schema.INDEX_TYPES.OTHER);
220
291
 
221
292
  return this;
222
293
  }
@@ -226,22 +297,22 @@ class SchemaBuilder {
226
297
  *
227
298
  * @param {string} type - One of Reference.TYPES (BELONGS_TO, HAS_MANY, HAS_ONE).
228
299
  * @param {string} entityName - The referenced entity name.
229
- * @param {Array<string>} [sortKeys=['updatedAt']] - The attributes to form the sort key.
300
+ * @param {Array<string>} [sortKeys=[]] - The attributes to form the sort key.
230
301
  * @param {object} [options] - Additional reference options.
231
302
  * @param {boolean} [options.required=true] - Whether the reference is required. Only applies to
232
303
  * BELONGS_TO references.
233
304
  * @param {boolean} [options.removeDependents=false] - Whether to remove dependent entities
234
305
  * on delete. Only applies to HAS_MANY and HAS_ONE references.
235
306
  * @returns {SchemaBuilder} Returns this builder for method chaining.
236
- * @throws {Error} If type or entityName are invalid.
307
+ * @throws {SchemaBuilderError} If type or entityName are invalid.
237
308
  */
238
309
  addReference(type, entityName, sortKeys = [], options = {}) {
239
310
  if (!Reference.isValidType(type)) {
240
- throw new Error(`Invalid referenceType: "${type}".`);
311
+ throw new SchemaBuilderError(this, `Invalid referenceType: "${type}".`);
241
312
  }
242
313
 
243
314
  if (!hasText(entityName)) {
244
- throw new Error('entityName for reference is required and must be a non-empty string.');
315
+ throw new SchemaBuilderError(this, 'entityName for reference is required and must be a non-empty string.');
245
316
  }
246
317
  const reference = {
247
318
  type,
@@ -274,7 +345,7 @@ class SchemaBuilder {
274
345
  this.#internalAddIndex(
275
346
  { composite: [decapitalize(foreignKeyName)] },
276
347
  { composite: isNonEmptyArray(sortKeys) ? sortKeys : ['updatedAt'] },
277
- INDEX_TYPES.BELONGS_TO,
348
+ Schema.INDEX_TYPES.BELONGS_TO,
278
349
  );
279
350
  }
280
351
 
@@ -303,7 +374,7 @@ class SchemaBuilder {
303
374
  ];
304
375
 
305
376
  if (orderedIndexes.length > 5) {
306
- throw new Error('Cannot have more than 5 indexes.');
377
+ throw new SchemaBuilderError(this, 'Cannot have more than 5 indexes.');
307
378
  }
308
379
 
309
380
  this.indexes = { primary: this.rawIndexes.primary };
@@ -342,6 +413,7 @@ class SchemaBuilder {
342
413
  attributes: this.attributes,
343
414
  indexes: this.indexes,
344
415
  references: this.references,
416
+ options: this.options,
345
417
  },
346
418
  );
347
419
  }
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { hasText, isNonEmptyObject } from '@adobe/spacecat-shared-utils';
14
14
 
15
+ import { SchemaError, SchemaValidationError } from '../../errors/index.js';
15
16
  import {
16
17
  classExtends,
17
18
  entityNameToCollectionName,
@@ -24,10 +25,16 @@ import {
24
25
 
25
26
  import BaseCollection from './base.collection.js';
26
27
  import BaseModel from './base.model.js';
27
- import { INDEX_TYPES } from './constants.js';
28
28
  import Reference from './reference.js';
29
29
 
30
30
  class Schema {
31
+ static INDEX_TYPES = {
32
+ PRIMARY: 'primary',
33
+ ALL: 'all',
34
+ BELONGS_TO: 'belongs_to',
35
+ OTHER: 'other',
36
+ };
37
+
31
38
  /**
32
39
  * Constructs a new Schema instance.
33
40
  * @constructor
@@ -38,6 +45,7 @@ class Schema {
38
45
  * @param {number} rawSchema.schemaVersion - The version of the schema.
39
46
  * @param {object} rawSchema.attributes - The attributes of the schema.
40
47
  * @param {object} rawSchema.indexes - The indexes of the schema.
48
+ * @param {object} rawSchema.options - The options of the schema.
41
49
  * @param {Reference[]} [rawSchema.references] - The references of the schema.
42
50
  */
43
51
  constructor(
@@ -52,6 +60,7 @@ class Schema {
52
60
  this.schemaVersion = rawSchema.schemaVersion;
53
61
  this.attributes = rawSchema.attributes;
54
62
  this.indexes = rawSchema.indexes;
63
+ this.options = rawSchema.options;
55
64
  this.references = rawSchema.references || [];
56
65
 
57
66
  this.#validateSchema();
@@ -59,34 +68,46 @@ class Schema {
59
68
 
60
69
  #validateSchema() {
61
70
  if (!classExtends(this.modelClass, BaseModel)) {
62
- throw new Error('Model class must extend BaseModel');
71
+ throw new SchemaValidationError('Model class must extend BaseModel');
63
72
  }
64
73
 
65
74
  if (!classExtends(this.collectionClass, BaseCollection)) {
66
- throw new Error('Collection class must extend BaseCollection');
75
+ throw new SchemaValidationError('Collection class must extend BaseCollection');
67
76
  }
68
77
 
69
78
  if (!hasText(this.serviceName)) {
70
- throw new Error('Schema must have a service name');
79
+ throw new SchemaValidationError('Schema must have a service name');
71
80
  }
72
81
 
73
82
  if (!isPositiveInteger(this.schemaVersion)) {
74
- throw new Error('Schema version must be a positive integer');
83
+ throw new SchemaValidationError('Schema version must be a positive integer');
75
84
  }
76
85
 
77
86
  if (!isNonEmptyObject(this.attributes)) {
78
- throw new Error('Schema must have attributes');
87
+ throw new SchemaValidationError('Schema must have attributes');
79
88
  }
80
89
 
81
90
  if (!isNonEmptyObject(this.indexes)) {
82
- throw new Error('Schema must have indexes');
91
+ throw new SchemaValidationError('Schema must have indexes');
83
92
  }
84
93
 
85
94
  if (!Array.isArray(this.references)) {
86
- throw new Error('References must be an array');
95
+ throw new SchemaValidationError('References must be an array');
96
+ }
97
+
98
+ if (!isNonEmptyObject(this.options)) {
99
+ throw new SchemaValidationError('Schema must have options');
87
100
  }
88
101
  }
89
102
 
103
+ allowsRemove() {
104
+ return this.options?.allowRemove;
105
+ }
106
+
107
+ allowsUpdates() {
108
+ return this.options?.allowUpdates;
109
+ }
110
+
90
111
  getAttribute(name) {
91
112
  return this.attributes[name];
92
113
  }
@@ -117,7 +138,7 @@ class Schema {
117
138
  * ]
118
139
  */
119
140
  getIndexAccessors() {
120
- const indexes = this.getIndexes([INDEX_TYPES.PRIMARY]);
141
+ const indexes = this.getIndexes([Schema.INDEX_TYPES.PRIMARY]);
121
142
  const result = [];
122
143
 
123
144
  Object.keys(indexes).forEach((indexName) => {
@@ -161,6 +182,36 @@ class Schema {
161
182
  return null;
162
183
  }
163
184
 
185
+ /**
186
+ * Finds the index name by the keys provided. The index is searched
187
+ * keys to match the combination of partition and sort keys. If no
188
+ * index is found, we fall back to the "all" index, then the "primary".
189
+ *
190
+ * @param {Object} keys - The keys to search for.
191
+ * @return {string} - The index name.
192
+ */
193
+ findIndexNameByKeys(keys) {
194
+ const { ALL, PRIMARY } = this.getIndexTypes();
195
+ const keyNames = Object.keys(keys);
196
+
197
+ const index = this.findIndexBySortKeys(keyNames);
198
+ if (index) {
199
+ return index.index || PRIMARY;
200
+ }
201
+
202
+ const allIndex = this.findIndexByType(ALL);
203
+ if (allIndex) {
204
+ return allIndex.index;
205
+ }
206
+
207
+ return PRIMARY;
208
+ }
209
+
210
+ // eslint-disable-next-line class-methods-use-this
211
+ getIndexTypes() {
212
+ return Schema.INDEX_TYPES;
213
+ }
214
+
164
215
  findIndexByType(type) {
165
216
  return Object.values(this.indexes).find((index) => index.indexType === type) || null;
166
217
  }
@@ -252,7 +303,24 @@ class Schema {
252
303
  return this.schemaVersion;
253
304
  }
254
305
 
255
- toAccessorConfigs(entity, log) {
306
+ /**
307
+ * Given an entity, generates accessor configurations for all index-based accessors.
308
+ * This is useful for creating methods on the entity that can be used to fetch data
309
+ * based on the index keys. For example, if we have an index by 'opportunityId' and 'status',
310
+ * this method will generate accessor configurations like allByOpportunityId,
311
+ * findByOpportunityId, etc. The accessor configurations can then be used to create
312
+ * accessor methods on the entity using the createAccessors (accessor utils) method.
313
+ *
314
+ * @param {BaseModel|BaseCollection} entity - The entity for which to generate accessors.
315
+ * @param {Object} [log] - The logger to use for logging information
316
+ * @throws {SchemaError} - Throws an error if the entity is not a BaseModel or BaseCollection.
317
+ * @return {Object[]}
318
+ */
319
+ toAccessorConfigs(entity, log = console) {
320
+ if (!(entity instanceof BaseModel) && !(entity instanceof BaseCollection)) {
321
+ throw new SchemaError(this, 'Entity must extend BaseModel or BaseCollection');
322
+ }
323
+
256
324
  const indexAccessors = this.getIndexAccessors();
257
325
  const accessorConfigs = [];
258
326