@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,158 @@
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, isNonEmptyObject, isNumber } from '@adobe/spacecat-shared-utils';
14
+
15
+ import ValidationError from '../errors/validation.error.js';
16
+
17
+ function validateValue(context, keyName, value) {
18
+ const { type } = context.schema.getAttribute(keyName);
19
+ const validator = type === 'number' ? isNumber : hasText;
20
+
21
+ if (!validator(value)) {
22
+ throw new ValidationError(`${keyName} is required`);
23
+ }
24
+ }
25
+
26
+ function parseAccessorArgs(context, requiredKeyNames, args) {
27
+ const keys = {};
28
+ for (let i = 0; i < requiredKeyNames.length; i += 1) {
29
+ const keyName = requiredKeyNames[i];
30
+ const keyValue = args[i];
31
+
32
+ validateValue(context, keyName, keyValue);
33
+
34
+ keys[keyName] = keyValue;
35
+ }
36
+
37
+ let options = {};
38
+
39
+ if (args.length > requiredKeyNames.length) {
40
+ options = args[requiredKeyNames.length];
41
+ }
42
+
43
+ return { keys, options };
44
+ }
45
+
46
+ function validateConfig(config) {
47
+ if (!isNonEmptyObject(config)) {
48
+ throw new Error('Config is required');
49
+ }
50
+
51
+ const {
52
+ collection, context, name, requiredKeys,
53
+ } = config;
54
+
55
+ if (!isNonEmptyObject(collection)) {
56
+ throw new Error('Collection is required');
57
+ }
58
+
59
+ if (!isNonEmptyObject(context)) {
60
+ throw new Error('Context is required');
61
+ }
62
+
63
+ if (!hasText(name)) {
64
+ throw new Error('Name is required');
65
+ }
66
+
67
+ if (!Array.isArray(requiredKeys)) {
68
+ throw new Error('Required keys must be an array');
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Create an accessor for a collection. The accessor can be used to query the collection.
74
+ * @param {object} config - The accessor configuration.
75
+ * @param {boolean} [config.all=false] - Whether to return all items in the collection.
76
+ * @param {boolean} [config.byId=false] - Whether to return an item by ID.
77
+ * @param {object} config.collection - The collection to query.
78
+ * @param {object} config.context - The context to attach the accessor to.
79
+ * @param {object} [config.foreignKey] - The foreign key to use when querying by ID.
80
+ * @param {string} config.name - The name of the accessor.
81
+ * @param {string[]} [config.requiredKeys] - The required keys for the accessor.
82
+ * @throws {Error} - If the configuration is invalid.
83
+ * @returns {void}
84
+ */
85
+ export function createAccessor(config) { /* eslint-disable no-underscore-dangle */
86
+ validateConfig(config);
87
+
88
+ const {
89
+ all = false,
90
+ byId = false,
91
+ collection,
92
+ context,
93
+ foreignKey,
94
+ name,
95
+ requiredKeys = [],
96
+ } = config;
97
+ if (!context._accessorCache) {
98
+ Object.defineProperty(context, '_accessorCache', {
99
+ enumerable: false,
100
+ configurable: true,
101
+ writable: true,
102
+ value: {},
103
+ });
104
+ }
105
+
106
+ const foreignKeys = {
107
+ ...isNonEmptyObject(foreignKey) && { [foreignKey.name]: foreignKey.value },
108
+ };
109
+
110
+ const accessor = async (...args) => {
111
+ const argsKey = args.length > 0 ? JSON.stringify(args) : '_';
112
+ const cacheKey = `${name}:${argsKey}`;
113
+
114
+ if (context._accessorCache[cacheKey] !== undefined) {
115
+ return context._accessorCache[cacheKey];
116
+ }
117
+
118
+ let result;
119
+
120
+ if (byId) {
121
+ if (!hasText(foreignKey.value)) {
122
+ result = null;
123
+ } else {
124
+ result = collection.findById(foreignKey.value);
125
+ }
126
+ } else {
127
+ const { keys, options } = parseAccessorArgs(collection, requiredKeys, args);
128
+ const allKeys = { ...foreignKeys, ...keys };
129
+
130
+ result = all
131
+ ? collection.allByIndexKeys(allKeys, options)
132
+ : collection.findByIndexKeys(allKeys, options);
133
+ }
134
+
135
+ result = await result;
136
+ context._accessorCache[cacheKey] = result;
137
+
138
+ return result;
139
+ };
140
+
141
+ Object.defineProperty(
142
+ context,
143
+ name,
144
+ {
145
+ enumerable: false,
146
+ configurable: false,
147
+ writable: true,
148
+ value: accessor,
149
+ },
150
+ );
151
+ }
152
+
153
+ export function createAccessors(configs, log) {
154
+ configs.forEach((config) => {
155
+ createAccessor(config);
156
+ log.info(`Created accessor ${config.name} for ${config.context.schema.getModelName()} to ${config.collection.schema.getModelName()}`);
157
+ });
158
+ }
@@ -25,6 +25,13 @@ export function guardAny(
25
25
  nullable?: boolean,
26
26
  ): void;
27
27
 
28
+ export function guardBoolean(
29
+ propertyName: string,
30
+ value: never,
31
+ entityName: string,
32
+ nullable?: boolean,
33
+ ): void;
34
+
28
35
  export function guardEnum(
29
36
  propertyName: string,
30
37
  value: never,
@@ -32,14 +32,16 @@ const checkNullable = (value, nullable) => nullable && (value === null || value
32
32
  */
33
33
  const checkType = (value, type) => {
34
34
  switch (type) {
35
+ case 'any':
36
+ return isObject(value);
37
+ case 'boolean':
38
+ return typeof value === 'boolean';
39
+ case 'map':
40
+ return isObject(value);
35
41
  case 'string':
36
42
  return typeof value === 'string';
37
43
  case 'number':
38
44
  return typeof value === 'number';
39
- case 'boolean':
40
- return typeof value === 'boolean';
41
- case 'object':
42
- return isObject(value);
43
45
  default:
44
46
  throw new ValidationError(`Unsupported type: ${type}`);
45
47
  }
@@ -59,6 +61,21 @@ export const guardAny = (propertyName, value, entityName, nullable = false) => {
59
61
  }
60
62
  };
61
63
 
64
+ /**
65
+ * Validates that a given property is a boolean.
66
+ * @param {String} propertyName - Name of the property being validated.
67
+ * @param {any} value - The value to validate.
68
+ * @param {String} entityName - Name of the entity containing this property.
69
+ * @param {boolean} [nullable] - Whether the value is nullable. Defaults to false.
70
+ * @throws Will throw an error if the value is not a valid boolean.
71
+ */
72
+ export const guardBoolean = (propertyName, value, entityName, nullable = false) => {
73
+ if (checkNullable(value, nullable)) return;
74
+ if (typeof value !== 'boolean') {
75
+ throw new ValidationError(`Validation failed in ${entityName}: ${propertyName} must be a boolean`);
76
+ }
77
+ };
78
+
62
79
  export const guardArray = (propertyName, value, entityName, type = 'string', nullable = false) => {
63
80
  if (checkNullable(value, nullable)) return;
64
81
  if (!Array.isArray(value)) {
@@ -13,6 +13,7 @@
13
13
  export {
14
14
  guardAny,
15
15
  guardArray,
16
+ guardBoolean,
16
17
  guardEnum,
17
18
  guardId,
18
19
  guardMap,
@@ -16,6 +16,7 @@ import ValidationError from '../errors/validation.error.js';
16
16
 
17
17
  import {
18
18
  guardAny,
19
+ guardBoolean,
19
20
  guardArray,
20
21
  guardEnum,
21
22
  guardId,
@@ -24,6 +25,7 @@ import {
24
25
  guardSet,
25
26
  guardString,
26
27
  } from './index.js';
28
+ import { isNonEmptyArray } from './util.js';
27
29
 
28
30
  /**
29
31
  * Checks if a property is read-only and throws an error if it is.
@@ -39,12 +41,24 @@ const checkReadOnly = (propertyName, attribute) => {
39
41
  };
40
42
 
41
43
  class Patcher {
42
- constructor(entity, record) {
44
+ /**
45
+ * Creates a new Patcher instance for an entity.
46
+ * @param {object} entity - The entity backing the record.
47
+ * @param {Schema} schema - The schema for the entity.
48
+ * @param {object} record - The record to patch.
49
+ */
50
+ constructor(entity, schema, record) {
43
51
  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
52
  this.record = record;
53
+
54
+ this.entityName = schema.getEntityName();
55
+ this.model = entity.model;
56
+ this.idName = schema.getIdName();
57
+
58
+ // holds the previous value of updated attributes
59
+ this.previous = {};
60
+
61
+ // holds the updates to the attributes
48
62
  this.updates = {};
49
63
 
50
64
  this.patchRecord = null;
@@ -61,24 +75,25 @@ class Patcher {
61
75
  }
62
76
 
63
77
  /**
64
- * Gets the composite values for a given key from the entity schema.
65
78
  * Composite keys have to be provided to ElectroDB in order to update a record across
66
- * multiple indexes.
67
- * @param {Object} record - The record to get the composite values from.
68
- * @param {string} key - The key to get the composite values for.
69
- * @return {{}} - An object containing the composite values for the given key.
79
+ * multiple indexes. This method retrieves the composite values for the entity from
80
+ * the schema indexes and filters out any values that are being updated.
81
+ * @return {{}} - An object containing the composite values for the entity.
70
82
  * @private
71
83
  */
72
- #getCompositeValuesForKey(record, key) {
84
+ #getCompositeValues() {
73
85
  const { indexes } = this.model;
74
86
  const result = {};
75
87
 
76
88
  const processComposite = (index, compositeType) => {
77
89
  const compositeArray = index[compositeType]?.facets;
78
- if (Array.isArray(compositeArray) && compositeArray.includes(key)) {
90
+ if (isNonEmptyArray(compositeArray)) {
79
91
  compositeArray.forEach((compositeKey) => {
80
- if (record[compositeKey] !== undefined) {
81
- result[compositeKey] = record[compositeKey];
92
+ if (
93
+ !Object.keys(this.updates).includes(compositeKey)
94
+ && this.record[compositeKey] !== undefined
95
+ ) {
96
+ result[compositeKey] = this.record[compositeKey];
82
97
  }
83
98
  });
84
99
  }
@@ -94,18 +109,25 @@ class Patcher {
94
109
 
95
110
  /**
96
111
  * Sets a property on the record and updates the patch record.
97
- * @param {string} propertyName - The name of the property to set.
112
+ * @param {string} attribute - The attribute to set.
98
113
  * @param {any} value - The value to set for the property.
99
114
  * @private
100
115
  */
101
- #set(propertyName, value) {
102
- const compositeValues = this.#getCompositeValuesForKey(this.record, propertyName);
103
- this.patchRecord = this.#getPatchRecord().set({
104
- ...compositeValues,
105
- [propertyName]: value,
106
- });
107
- this.record[propertyName] = value;
108
- this.updates[propertyName] = value;
116
+ #set(attribute, value) {
117
+ this.patchRecord = this.#getPatchRecord().set({ [attribute.name]: value });
118
+
119
+ const update = {
120
+ [attribute.name]: {
121
+ previous: this.record[attribute.name],
122
+ current: value,
123
+ },
124
+ };
125
+
126
+ // update the record with the update value for later save
127
+ this.record[attribute.name] = value;
128
+
129
+ // remember the update operation with the previous and current value
130
+ this.updates = { ...this.updates, ...update };
109
131
  }
110
132
 
111
133
  /**
@@ -145,6 +167,9 @@ class Patcher {
145
167
  case 'any':
146
168
  guardAny(propertyName, value, this.entityName, nullable);
147
169
  break;
170
+ case 'boolean':
171
+ guardBoolean(propertyName, value, this.entityName, nullable);
172
+ break;
148
173
  case 'enum':
149
174
  guardEnum(propertyName, value, attribute.enumArray, this.entityName, nullable);
150
175
  break;
@@ -168,7 +193,7 @@ class Patcher {
168
193
  }
169
194
  }
170
195
 
171
- this.#set(propertyName, value);
196
+ this.#set(attribute, value);
172
197
  }
173
198
 
174
199
  /**
@@ -180,8 +205,12 @@ class Patcher {
180
205
  if (!this.hasUpdates()) {
181
206
  return;
182
207
  }
183
- await this.#getPatchRecord().go();
184
- this.record.updatedAt = new Date().getTime();
208
+
209
+ const compositeValues = this.#getCompositeValues();
210
+ await this.#getPatchRecord()
211
+ .composite(compositeValues)
212
+ .go();
213
+ this.record.updatedAt = new Date().toISOString();
185
214
  }
186
215
 
187
216
  getUpdates() {
@@ -0,0 +1,84 @@
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, isInteger } from '@adobe/spacecat-shared-utils';
14
+ import pluralize from 'pluralize';
15
+
16
+ const capitalize = (str) => (hasText(str) ? str[0].toUpperCase() + str.slice(1) : '');
17
+
18
+ const classExtends = (clazz, base) => (typeof clazz === 'function' && clazz.prototype instanceof base);
19
+
20
+ const decapitalize = (str) => (hasText(str) ? str[0].toLowerCase() + str.slice(1) : '');
21
+
22
+ const collectionNameToEntityName = (collectionName) => collectionName.replace('Collection', '');
23
+
24
+ const entityNameToCollectionName = (entityName) => `${capitalize(pluralize.singular(entityName))}Collection`;
25
+
26
+ const entityNameToIdName = (entityName) => `${decapitalize(pluralize.singular(entityName))}Id`;
27
+
28
+ const referenceToBaseMethodName = (reference) => {
29
+ const target = capitalize(reference.getTarget());
30
+ const baseName = reference.getType() === 'has_many'
31
+ ? pluralize.plural(target)
32
+ : pluralize.singular(target);
33
+
34
+ return `get${baseName}`;
35
+ };
36
+
37
+ const entityNameToAllPKValue = (entityName) => `ALL_${pluralize.plural(entityName.toUpperCase())}`;
38
+
39
+ const idNameToEntityName = (idName) => capitalize(pluralize.singular(idName.replace('Id', '')));
40
+
41
+ const isPositiveInteger = (value) => isInteger(value) && value > 0;
42
+
43
+ const keyNamesToIndexName = (keyNames) => `by${keyNames.map(capitalize).join('And')}`;
44
+
45
+ const keyNamesToMethodName = (keyNames, prefix) => prefix + keyNames.map(capitalize).join('And');
46
+
47
+ const modelNameToEntityName = (modelName) => decapitalize(modelName);
48
+
49
+ const sanitizeTimestamps = (data) => {
50
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
51
+ const { createdAt, updatedAt, ...rest } = data;
52
+ return rest;
53
+ };
54
+
55
+ const sanitizeIdAndAuditFields = (entityName, data) => {
56
+ const idName = entityNameToIdName(entityName);
57
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
58
+ const { [idName]: _, ...rest } = data;
59
+ return sanitizeTimestamps(rest);
60
+ };
61
+
62
+ const incrementVersion = (version) => (isInteger(version) ? parseInt(version, 10) + 1 : 1);
63
+
64
+ const isNonEmptyArray = (value) => Array.isArray(value) && value.length > 0;
65
+
66
+ export {
67
+ capitalize,
68
+ classExtends,
69
+ collectionNameToEntityName,
70
+ decapitalize,
71
+ entityNameToAllPKValue,
72
+ entityNameToCollectionName,
73
+ entityNameToIdName,
74
+ idNameToEntityName,
75
+ incrementVersion,
76
+ isNonEmptyArray,
77
+ isPositiveInteger,
78
+ keyNamesToIndexName,
79
+ keyNamesToMethodName,
80
+ modelNameToEntityName,
81
+ referenceToBaseMethodName,
82
+ sanitizeIdAndAuditFields,
83
+ sanitizeTimestamps,
84
+ };