@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.
- package/CHANGELOG.md +14 -0
- package/package.json +5 -5
- package/src/v2/errors/data-access.error.js +24 -0
- package/src/v2/errors/index.d.ts +5 -1
- package/src/v2/errors/index.js +11 -1
- package/src/v2/errors/reference.error.js +22 -0
- package/src/v2/{models/base/constants.js → errors/schema-validation.error.js} +4 -6
- package/src/v2/errors/schema.builder.error.js +27 -0
- package/src/v2/errors/schema.error.js +19 -0
- package/src/v2/errors/validation.error.js +3 -1
- package/src/v2/models/api-key/index.d.ts +15 -2
- package/src/v2/models/audit/audit.collection.js +25 -1
- package/src/v2/models/audit/audit.schema.js +3 -0
- package/src/v2/models/audit/index.d.ts +16 -11
- package/src/v2/models/base/base.collection.js +148 -85
- package/src/v2/models/base/base.model.js +73 -14
- package/src/v2/models/base/entity.registry.js +7 -2
- package/src/v2/models/base/index.d.ts +30 -11
- package/src/v2/models/base/reference.js +81 -28
- package/src/v2/models/base/schema.builder.js +96 -24
- package/src/v2/models/base/schema.js +78 -10
- package/src/v2/models/configuration/index.d.ts +24 -90
- package/src/v2/models/experiment/index.d.ts +11 -3
- package/src/v2/models/import-job/index.d.ts +10 -3
- package/src/v2/models/import-url/index.d.ts +6 -3
- package/src/v2/models/index.d.ts +3 -0
- package/src/v2/models/index.js +1 -0
- package/src/v2/models/key-event/index.d.ts +5 -1
- package/src/v2/models/latest-audit/index.d.ts +43 -0
- package/src/v2/models/latest-audit/index.js +19 -0
- package/src/v2/models/latest-audit/latest-audit.collection.js +32 -0
- package/src/v2/models/latest-audit/latest-audit.model.js +26 -0
- package/src/v2/models/latest-audit/latest-audit.schema.js +72 -0
- package/src/v2/models/opportunity/index.d.ts +17 -1
- package/src/v2/models/opportunity/opportunity.schema.js +1 -0
- package/src/v2/models/organization/index.d.ts +3 -1
- package/src/v2/models/site/index.d.ts +43 -6
- package/src/v2/models/site/site.model.js +0 -6
- package/src/v2/models/site/site.schema.js +2 -0
- package/src/v2/models/site-candidate/index.d.ts +5 -7
- package/src/v2/models/site-top-page/index.d.ts +16 -2
- package/src/v2/models/suggestion/index.d.ts +2 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
83
|
+
throw new ReferenceError(this, `Invalid reference type: ${type}`);
|
|
40
84
|
}
|
|
41
85
|
|
|
42
86
|
if (!hasText(target)) {
|
|
43
|
-
throw new
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
90
|
-
* @throws {
|
|
91
|
-
* @throws {
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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=[
|
|
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 {
|
|
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
|
|
311
|
+
throw new SchemaBuilderError(this, `Invalid referenceType: "${type}".`);
|
|
241
312
|
}
|
|
242
313
|
|
|
243
314
|
if (!hasText(entityName)) {
|
|
244
|
-
throw new
|
|
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
|
|
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
|
|
71
|
+
throw new SchemaValidationError('Model class must extend BaseModel');
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
if (!classExtends(this.collectionClass, BaseCollection)) {
|
|
66
|
-
throw new
|
|
75
|
+
throw new SchemaValidationError('Collection class must extend BaseCollection');
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
if (!hasText(this.serviceName)) {
|
|
70
|
-
throw new
|
|
79
|
+
throw new SchemaValidationError('Schema must have a service name');
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
if (!isPositiveInteger(this.schemaVersion)) {
|
|
74
|
-
throw new
|
|
83
|
+
throw new SchemaValidationError('Schema version must be a positive integer');
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
if (!isNonEmptyObject(this.attributes)) {
|
|
78
|
-
throw new
|
|
87
|
+
throw new SchemaValidationError('Schema must have attributes');
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
if (!isNonEmptyObject(this.indexes)) {
|
|
82
|
-
throw new
|
|
91
|
+
throw new SchemaValidationError('Schema must have indexes');
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
if (!Array.isArray(this.references)) {
|
|
86
|
-
throw new
|
|
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
|
-
|
|
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
|
|