@adobe/spacecat-shared-data-access 1.53.0 → 1.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/package.json +7 -4
- package/src/index.d.ts +6 -0
- package/src/index.js +5 -0
- package/src/service/index.js +52 -0
- package/src/v2/errors/index.d.ts +13 -0
- package/src/v2/errors/index.js +15 -0
- package/src/v2/errors/validation.error.js +13 -0
- package/src/v2/index.d.ts +15 -0
- package/src/v2/index.js +15 -0
- package/src/v2/models/base.collection.js +118 -0
- package/src/v2/models/base.model.js +127 -0
- package/src/v2/models/index.d.ts +100 -0
- package/src/v2/models/index.js +27 -0
- package/src/v2/models/model.factory.js +74 -0
- package/src/v2/models/opportunity.collection.js +78 -0
- package/src/v2/models/opportunity.model.js +238 -0
- package/src/v2/models/suggestion.collection.js +80 -0
- package/src/v2/models/suggestion.model.js +138 -0
- package/src/v2/readme.md +260 -0
- package/src/v2/schema/opportunity.schema.js +145 -0
- package/src/v2/schema/suggestion.schema.js +122 -0
- package/src/v2/util/guards.d.ts +62 -0
- package/src/v2/util/guards.js +165 -0
- package/src/v2/util/index.d.ts +13 -0
- package/src/v2/util/index.js +22 -0
- package/src/v2/util/patcher.js +183 -0
|
@@ -0,0 +1,165 @@
|
|
|
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 { hasText, isNumber, isObject } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
import { validate as validateUUID } from 'uuid';
|
|
15
|
+
|
|
16
|
+
import { ValidationError } from '../errors/index.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Checks if a value is nullable and if the value is null or undefined.
|
|
20
|
+
* @param {any} value - The value to check.
|
|
21
|
+
* @param {boolean} nullable - Whether the value is nullable.
|
|
22
|
+
* @return {boolean} True if the value is nullable and null or undefined, false otherwise.
|
|
23
|
+
*/
|
|
24
|
+
const checkNullable = (value, nullable) => nullable && (value === null || value === undefined);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a value is of a given type.
|
|
28
|
+
* Supported types are 'string', 'number', 'boolean', 'object', and 'uuid'.
|
|
29
|
+
* @param {any} value
|
|
30
|
+
* @param {string} type
|
|
31
|
+
* @return {boolean} True if the value is of the given type, false otherwise.
|
|
32
|
+
*/
|
|
33
|
+
const checkType = (value, type) => {
|
|
34
|
+
switch (type) {
|
|
35
|
+
case 'string':
|
|
36
|
+
return typeof value === 'string';
|
|
37
|
+
case 'number':
|
|
38
|
+
return typeof value === 'number';
|
|
39
|
+
case 'boolean':
|
|
40
|
+
return typeof value === 'boolean';
|
|
41
|
+
case 'object':
|
|
42
|
+
return isObject(value);
|
|
43
|
+
default:
|
|
44
|
+
throw new ValidationError(`Unsupported type: ${type}`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validates that a given property of any type is not null or undefined.
|
|
50
|
+
* @param {String} propertyName - Name of the property being validated.
|
|
51
|
+
* @param {any} value - The value to validate.
|
|
52
|
+
* @param {String} entityName - Name of the entity containing this property.
|
|
53
|
+
* @param {boolean} [nullable] - Whether the value is nullable. Defaults to false.
|
|
54
|
+
* @throws Will throw an error if the value is null or undefined.
|
|
55
|
+
*/
|
|
56
|
+
export const guardAny = (propertyName, value, entityName, nullable = false) => {
|
|
57
|
+
if (!checkNullable(value, nullable) && (value === undefined || value === null)) {
|
|
58
|
+
throw new ValidationError(`Validation failed in ${entityName}: ${propertyName} is required`);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const guardArray = (propertyName, value, entityName, type = 'string', nullable = false) => {
|
|
63
|
+
if (checkNullable(value, nullable)) return;
|
|
64
|
+
if (!Array.isArray(value)) {
|
|
65
|
+
throw new ValidationError(`Validation failed in ${entityName}: ${propertyName} must be an array`);
|
|
66
|
+
}
|
|
67
|
+
if (!value.every((v) => checkType(v, type))) {
|
|
68
|
+
throw new ValidationError(`Validation failed in ${entityName}: ${propertyName} must contain items of type ${type}`);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validates that a given property is a set (unique array) of a given type (defaults to string).
|
|
74
|
+
* @param {String} propertyName - Name of the property being validated.
|
|
75
|
+
* @param {any} value - The value to validate.
|
|
76
|
+
* @param {String} entityName - Name of the entity containing this property.
|
|
77
|
+
* @param {String} [type] - The type of the items in the set. Defaults to 'string'.
|
|
78
|
+
* @param {boolean} [nullable] - Whether the value is nullable. Defaults to false.
|
|
79
|
+
* @throws Will throw an error if the value is not a valid set (unique array) of a given type.
|
|
80
|
+
*/
|
|
81
|
+
export const guardSet = (propertyName, value, entityName, type = 'string', nullable = false) => {
|
|
82
|
+
if (checkNullable(value, nullable)) return;
|
|
83
|
+
if (!Array.isArray(value) || new Set(value).size !== value.length) {
|
|
84
|
+
throw new ValidationError(`Validation failed in ${entityName}: ${propertyName} must be a unique array (set)`);
|
|
85
|
+
}
|
|
86
|
+
if (!value.every((v) => checkType(v, type))) {
|
|
87
|
+
throw new ValidationError(`Validation failed in ${entityName}: ${propertyName} must contain items of type ${type}`);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validates that a given property is a string.
|
|
93
|
+
* @param {String} propertyName - Name of the property being validated.
|
|
94
|
+
* @param {any} value - The value to validate.
|
|
95
|
+
* @param {String} entityName - Name of the entity containing this property.
|
|
96
|
+
* @param {boolean} [nullable] - Whether the value is nullable. Defaults to false.
|
|
97
|
+
* @throws Will throw an error if the value is not a valid string.
|
|
98
|
+
*/
|
|
99
|
+
export const guardString = (propertyName, value, entityName, nullable = false) => {
|
|
100
|
+
if (checkNullable(value, nullable)) return;
|
|
101
|
+
if (!hasText(value)) {
|
|
102
|
+
throw new ValidationError(`Validation failed in ${entityName}: ${propertyName} is required`);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validates that a given property is of an enum type.
|
|
108
|
+
* @param {String} propertyName - Name of the property being validated.
|
|
109
|
+
* @param {any} value - The value to validate.
|
|
110
|
+
* @param {Array<String>} enumValues - Allowed enum values.
|
|
111
|
+
* @param {String} entityName - Name of the entity containing this property.
|
|
112
|
+
* @param {boolean} [nullable] - Whether the value is nullable. Defaults to false.
|
|
113
|
+
* @throws Will throw an error if the value is not a valid enum value.
|
|
114
|
+
*/
|
|
115
|
+
export const guardEnum = (propertyName, value, enumValues, entityName, nullable = false) => {
|
|
116
|
+
if (checkNullable(value, nullable)) return;
|
|
117
|
+
if (!enumValues.includes(value)) {
|
|
118
|
+
throw new ValidationError(`Validation failed in ${entityName}: ${propertyName} must be one of ${enumValues}`);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validates that a given property is a valid ID.
|
|
124
|
+
* @param {String} propertyName - Name of the property being validated.
|
|
125
|
+
* @param {any} value - The value to validate.
|
|
126
|
+
* @param {String} entityName - Name of the entity containing this property.
|
|
127
|
+
* @param {boolean} [nullable] - Whether the value is nullable. Defaults to false.
|
|
128
|
+
* @throws Will throw an error if the value is not a valid ID.
|
|
129
|
+
*/
|
|
130
|
+
export const guardId = (propertyName, value, entityName, nullable = false) => {
|
|
131
|
+
if (checkNullable(value, nullable)) return;
|
|
132
|
+
if (!validateUUID(value)) {
|
|
133
|
+
throw new ValidationError(`Validation failed in ${entityName}: ${propertyName} must be a valid UUID`);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Validates that a given property is a map (object).
|
|
139
|
+
* @param {String} propertyName - Name of the property being validated.
|
|
140
|
+
* @param {any} value - The value to validate.
|
|
141
|
+
* @param {String} entityName - Name of the entity containing this property.
|
|
142
|
+
* @param {boolean} [nullable] - Whether the value is nullable. Defaults to false.
|
|
143
|
+
* @throws Will throw an error if the value is not a valid map (object).
|
|
144
|
+
*/
|
|
145
|
+
export const guardMap = (propertyName, value, entityName, nullable = false) => {
|
|
146
|
+
if (checkNullable(value, nullable)) return;
|
|
147
|
+
if (!isObject(value)) {
|
|
148
|
+
throw new ValidationError(`Validation failed in ${entityName}: ${propertyName} must be an object`);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validates that a given property is a number.
|
|
154
|
+
* @param {String} propertyName - Name of the property being validated.
|
|
155
|
+
* @param {any} value - The value to validate.
|
|
156
|
+
* @param {String} entityName - Name of the entity containing this property.
|
|
157
|
+
* @param {boolean} [nullable] - Whether the value is nullable. Defaults to false.
|
|
158
|
+
* @throws Will throw an error if the value is not a valid number.
|
|
159
|
+
*/
|
|
160
|
+
export const guardNumber = (propertyName, value, entityName, nullable = false) => {
|
|
161
|
+
if (checkNullable(value, nullable)) return;
|
|
162
|
+
if (!isNumber(value)) {
|
|
163
|
+
throw new ValidationError(`Validation failed in ${entityName}: ${propertyName} must be a number`);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type * from './guards.d.ts';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
guardAny,
|
|
15
|
+
guardArray,
|
|
16
|
+
guardEnum,
|
|
17
|
+
guardId,
|
|
18
|
+
guardMap,
|
|
19
|
+
guardNumber,
|
|
20
|
+
guardSet,
|
|
21
|
+
guardString,
|
|
22
|
+
} from './guards.js';
|
|
@@ -0,0 +1,183 @@
|
|
|
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 { isObject } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
|
|
15
|
+
import ValidationError from '../errors/validation.error.js';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
guardAny,
|
|
19
|
+
guardArray,
|
|
20
|
+
guardEnum,
|
|
21
|
+
guardId,
|
|
22
|
+
guardMap,
|
|
23
|
+
guardNumber,
|
|
24
|
+
guardSet,
|
|
25
|
+
guardString,
|
|
26
|
+
} from './index.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Checks if a property is read-only and throws an error if it is.
|
|
30
|
+
* @param {string} propertyName - The name of the property to check.
|
|
31
|
+
* @param {Object} attribute - The attribute to check.
|
|
32
|
+
* @throws {Error} - Throws an error if the property is read-only.
|
|
33
|
+
* @private
|
|
34
|
+
*/
|
|
35
|
+
const checkReadOnly = (propertyName, attribute) => {
|
|
36
|
+
if (attribute.readOnly) {
|
|
37
|
+
throw new ValidationError(`The property ${propertyName} is read-only and cannot be updated.`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
class Patcher {
|
|
42
|
+
constructor(entity, record) {
|
|
43
|
+
this.entity = entity;
|
|
44
|
+
this.entityName = this.entity.model.name.toLowerCase();
|
|
45
|
+
this.model = entity.model;
|
|
46
|
+
this.idName = `${this.model.name.toLowerCase()}Id`;
|
|
47
|
+
this.record = record;
|
|
48
|
+
|
|
49
|
+
this.patchRecord = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Checks if a property is nullable.
|
|
54
|
+
* @param {string} propertyName - The name of the property to check.
|
|
55
|
+
* @return {boolean} True if the property is nullable, false otherwise.
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
#isAttributeNullable(propertyName) {
|
|
59
|
+
return !this.model.schema.attributes[propertyName]?.required;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Gets the composite values for a given key from the entity schema.
|
|
64
|
+
* Composite keys have to be provided to ElectroDB in order to update a record across
|
|
65
|
+
* multiple indexes.
|
|
66
|
+
* @param {Object} record - The record to get the composite values from.
|
|
67
|
+
* @param {string} key - The key to get the composite values for.
|
|
68
|
+
* @return {{}} - An object containing the composite values for the given key.
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
#getCompositeValuesForKey(record, key) {
|
|
72
|
+
const { indexes } = this.model;
|
|
73
|
+
const result = {};
|
|
74
|
+
|
|
75
|
+
const processComposite = (index, compositeType) => {
|
|
76
|
+
const compositeArray = index[compositeType]?.facets;
|
|
77
|
+
if (Array.isArray(compositeArray) && compositeArray.includes(key)) {
|
|
78
|
+
compositeArray.forEach((compositeKey) => {
|
|
79
|
+
if (record[compositeKey] !== undefined) {
|
|
80
|
+
result[compositeKey] = record[compositeKey];
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
Object.values(indexes).forEach((index) => {
|
|
87
|
+
processComposite(index, 'pk');
|
|
88
|
+
processComposite(index, 'sk');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Sets a property on the record and updates the patch record.
|
|
96
|
+
* @param {string} propertyName - The name of the property to set.
|
|
97
|
+
* @param {any} value - The value to set for the property.
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
#set(propertyName, value) {
|
|
101
|
+
const compositeValues = this.#getCompositeValuesForKey(this.record, propertyName);
|
|
102
|
+
this.patchRecord = this.#getPatchRecord().set({
|
|
103
|
+
...compositeValues,
|
|
104
|
+
[propertyName]: value,
|
|
105
|
+
});
|
|
106
|
+
this.record[propertyName] = value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Gets the patch record for the entity. If it does not exist, it will be created.
|
|
111
|
+
* @return {Object} - The patch record for the entity.
|
|
112
|
+
* @private
|
|
113
|
+
*/
|
|
114
|
+
#getPatchRecord() {
|
|
115
|
+
if (!this.patchRecord) {
|
|
116
|
+
this.patchRecord = this.entity.patch({ [this.idName]: this.record[this.idName] });
|
|
117
|
+
}
|
|
118
|
+
return this.patchRecord;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Patches a value for a given property on the entity. This method will validate the value
|
|
123
|
+
* against the schema and throw an error if the value is invalid. If the value is declared as
|
|
124
|
+
* a reference, it will validate the ID format.
|
|
125
|
+
* @param {string} propertyName - The name of the property to patch.
|
|
126
|
+
* @param {any} value - The value to patch.
|
|
127
|
+
* @param {boolean} [isReference=false] - Whether the value is a reference to another entity.
|
|
128
|
+
*/
|
|
129
|
+
patchValue(propertyName, value, isReference = false) {
|
|
130
|
+
const attribute = this.model.schema?.attributes[propertyName];
|
|
131
|
+
if (!isObject(attribute)) {
|
|
132
|
+
throw new ValidationError(`Property ${propertyName} does not exist on entity ${this.entityName}.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
checkReadOnly(propertyName, attribute);
|
|
136
|
+
|
|
137
|
+
const nullable = this.#isAttributeNullable(propertyName);
|
|
138
|
+
|
|
139
|
+
if (isReference) {
|
|
140
|
+
guardId(propertyName, value, this.entityName, nullable);
|
|
141
|
+
} else {
|
|
142
|
+
switch (attribute.type) {
|
|
143
|
+
case 'any':
|
|
144
|
+
guardAny(propertyName, value, this.entityName, nullable);
|
|
145
|
+
break;
|
|
146
|
+
case 'enum':
|
|
147
|
+
guardEnum(propertyName, value, attribute.enumArray, this.entityName, nullable);
|
|
148
|
+
break;
|
|
149
|
+
case 'list':
|
|
150
|
+
guardArray(propertyName, value, this.entityName, attribute.items?.type, nullable);
|
|
151
|
+
break;
|
|
152
|
+
case 'map':
|
|
153
|
+
guardMap(propertyName, value, this.entityName, nullable);
|
|
154
|
+
break;
|
|
155
|
+
case 'number':
|
|
156
|
+
guardNumber(propertyName, value, this.entityName, nullable);
|
|
157
|
+
break;
|
|
158
|
+
case 'set':
|
|
159
|
+
guardSet(propertyName, value, this.entityName, attribute.items?.type, nullable);
|
|
160
|
+
break;
|
|
161
|
+
case 'string':
|
|
162
|
+
guardString(propertyName, value, this.entityName, nullable);
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
throw new ValidationError(`Unsupported type for property ${propertyName}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.#set(propertyName, value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Saves the current state of the entity to the database.
|
|
174
|
+
* @return {Promise<void>}
|
|
175
|
+
* @throws {Error} - Throws an error if the save operation fails.
|
|
176
|
+
*/
|
|
177
|
+
async save() {
|
|
178
|
+
await this.#getPatchRecord().go();
|
|
179
|
+
this.record.updatedAt = new Date().getTime();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export default Patcher;
|