@adobe/spacecat-shared-data-access 2.88.9 → 2.90.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.
@@ -16,6 +16,7 @@ import { collectionNameToEntityName, decapitalize } from '../../util/util.js';
16
16
  import ApiKeyCollection from '../api-key/api-key.collection.js';
17
17
  import AsyncJobCollection from '../async-job/async-job.collection.js';
18
18
  import AuditCollection from '../audit/audit.collection.js';
19
+ import AuditUrlCollection from '../audit-url/audit-url.collection.js';
19
20
  import ConfigurationCollection from '../configuration/configuration.collection.js';
20
21
  import ExperimentCollection from '../experiment/experiment.collection.js';
21
22
  import EntitlementCollection from '../entitlement/entitlement.collection.js';
@@ -45,7 +46,7 @@ import PageCitabilityCollection from '../page-citability/page-citability.collect
45
46
  import ApiKeySchema from '../api-key/api-key.schema.js';
46
47
  import AsyncJobSchema from '../async-job/async-job.schema.js';
47
48
  import AuditSchema from '../audit/audit.schema.js';
48
- import ConfigurationSchema from '../configuration/configuration.schema.js';
49
+ import AuditUrlSchema from '../audit-url/audit-url.schema.js';
49
50
  import EntitlementSchema from '../entitlement/entitlement.schema.js';
50
51
  import FixEntitySchema from '../fix-entity/fix-entity.schema.js';
51
52
  import FixEntitySuggestionSchema from '../fix-entity-suggestion/fix-entity-suggestion.schema.js';
@@ -82,11 +83,13 @@ class EntityRegistry {
82
83
  /**
83
84
  * Constructs an instance of EntityRegistry.
84
85
  * @constructor
85
- * @param {Object} service - The ElectroDB service instance used to manage entities.
86
+ * @param {Object} services - Dictionary of services keyed by datastore type.
87
+ * @param {Object} services.dynamo - The ElectroDB service instance for DynamoDB operations.
88
+ * @param {{s3Client: S3Client, s3Bucket: string}|null} [services.s3] - S3 service configuration.
86
89
  * @param {Object} log - A logger for capturing and logging information.
87
90
  */
88
- constructor(service, log) {
89
- this.service = service;
91
+ constructor(services, log) {
92
+ this.services = services;
90
93
  this.log = log;
91
94
  this.collections = new Map();
92
95
 
@@ -96,13 +99,20 @@ class EntityRegistry {
96
99
  /**
97
100
  * Initializes the collections managed by the EntityRegistry.
98
101
  * This method creates instances of each collection and stores them in an internal map.
102
+ * ElectroDB-based collections are initialized with the dynamo service.
103
+ * Configuration is handled specially as it's a standalone S3-based collection.
99
104
  * @private
100
105
  */
101
106
  #initialize() {
107
+ // Initialize ElectroDB-based collections
102
108
  Object.values(EntityRegistry.entities).forEach(({ collection: Collection, schema }) => {
103
- const collection = new Collection(this.service, this, schema, this.log);
109
+ const collection = new Collection(this.services.dynamo, this, schema, this.log);
104
110
  this.collections.set(Collection.COLLECTION_NAME, collection);
105
111
  });
112
+
113
+ // Initialize Configuration collection separately (standalone S3-based)
114
+ const configCollection = new ConfigurationCollection(this.services.s3, this.log);
115
+ this.collections.set(ConfigurationCollection.COLLECTION_NAME, configCollection);
106
116
  }
107
117
 
108
118
  /**
@@ -140,10 +150,11 @@ class EntityRegistry {
140
150
  }
141
151
  }
142
152
 
153
+ // Register ElectroDB-based entities only (Configuration is handled separately)
143
154
  EntityRegistry.registerEntity(ApiKeySchema, ApiKeyCollection);
144
155
  EntityRegistry.registerEntity(AsyncJobSchema, AsyncJobCollection);
145
156
  EntityRegistry.registerEntity(AuditSchema, AuditCollection);
146
- EntityRegistry.registerEntity(ConfigurationSchema, ConfigurationCollection);
157
+ EntityRegistry.registerEntity(AuditUrlSchema, AuditUrlCollection);
147
158
  EntityRegistry.registerEntity(EntitlementSchema, EntitlementCollection);
148
159
  EntityRegistry.registerEntity(FixEntitySchema, FixEntityCollection);
149
160
  EntityRegistry.registerEntity(FixEntitySuggestionSchema, FixEntitySuggestionCollection);
@@ -10,36 +10,196 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { incrementVersion, sanitizeIdAndAuditFields, zeroPad } from '../../util/util.js';
14
- import BaseCollection from '../base/base.collection.js';
13
+ import {
14
+ GetObjectCommand,
15
+ PutObjectCommand,
16
+ } from '@aws-sdk/client-s3';
17
+
18
+ import DataAccessError from '../../errors/data-access.error.js';
19
+ import { sanitizeIdAndAuditFields } from '../../util/util.js';
20
+ import Configuration from './configuration.model.js';
21
+ import { checkConfiguration } from './configuration.schema.js';
22
+
23
+ const S3_CONFIG_KEY = 'config/spacecat/global-config.json';
15
24
 
16
25
  /**
17
- * ConfigurationCollection - A collection class responsible for managing Configuration entities.
18
- * Extends the BaseCollection to provide specific methods for interacting with
19
- * Configuration records.
26
+ * ConfigurationCollection - A standalone collection class for managing Configuration entities.
27
+ * Unlike other collections, this does not use ElectroDB or DynamoDB.
28
+ * Configuration is stored as a versioned JSON object in S3.
20
29
  *
21
30
  * @class ConfigurationCollection
22
- * @extends BaseCollection
23
31
  */
24
- class ConfigurationCollection extends BaseCollection {
32
+ class ConfigurationCollection {
25
33
  static COLLECTION_NAME = 'ConfigurationCollection';
26
34
 
35
+ /**
36
+ * Constructs an instance of ConfigurationCollection.
37
+ * @constructor
38
+ * @param {{s3Client: S3Client, s3Bucket: string}|null} s3Config - S3 configuration.
39
+ * @param {Object} log - A logger for capturing logging information.
40
+ */
41
+ constructor(s3Config, log) {
42
+ this.log = log;
43
+
44
+ if (s3Config) {
45
+ this.s3Client = s3Config.s3Client;
46
+ this.s3Bucket = s3Config.s3Bucket;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Validates that S3 is configured. Throws an error if not.
52
+ * @private
53
+ * @throws {DataAccessError} If S3 is not configured.
54
+ */
55
+ #requireS3() {
56
+ if (!this.s3Client || !this.s3Bucket) {
57
+ throw new DataAccessError(
58
+ 'S3 configuration is required for Configuration storage. '
59
+ + 'Ensure S3_CONFIG_BUCKET environment variable is set.',
60
+ this,
61
+ );
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Creates an instance of the Configuration model from data and versionId.
67
+ * @private
68
+ * @param {Object} data - The configuration data.
69
+ * @param {string} versionId - The S3 VersionId.
70
+ * @returns {Configuration} - The Configuration model instance.
71
+ */
72
+ #createInstance(data, versionId) {
73
+ return new Configuration(data, versionId, this, this.log);
74
+ }
75
+
76
+ /**
77
+ * Creates a new configuration version and stores it in S3.
78
+ * S3 versioning handles the version history automatically.
79
+ * The S3 VersionId is used as the configurationId.
80
+ *
81
+ * @param {Object} data - The configuration data to store.
82
+ * @returns {Promise<Configuration>} - The created Configuration instance.
83
+ * @throws {DataAccessError} If S3 is not configured or the operation fails.
84
+ */
27
85
  async create(data) {
28
- const latestConfiguration = await this.findLatest();
29
- const version = latestConfiguration ? incrementVersion(latestConfiguration.getVersion()) : 1;
30
- const sanitizedData = sanitizeIdAndAuditFields('Configuration', data);
86
+ this.#requireS3();
87
+
88
+ try {
89
+ const sanitizedData = sanitizeIdAndAuditFields('Configuration', data);
90
+
91
+ const now = new Date().toISOString();
92
+ const configData = {
93
+ ...sanitizedData,
94
+ createdAt: now,
95
+ updatedAt: now,
96
+ updatedBy: sanitizedData.updatedBy || 'system',
97
+ };
98
+
99
+ // Validate the configuration against the schema
100
+ checkConfiguration(configData);
101
+
102
+ const command = new PutObjectCommand({
103
+ Bucket: this.s3Bucket,
104
+ Key: S3_CONFIG_KEY,
105
+ Body: JSON.stringify(configData),
106
+ ContentType: 'application/json',
107
+ });
31
108
 
32
- sanitizedData.version = version;
109
+ const response = await this.s3Client.send(command);
110
+ const { VersionId: versionId } = response;
33
111
 
34
- return super.create(sanitizedData);
112
+ this.log.info(`Configuration stored in S3 with VersionId ${versionId}`);
113
+
114
+ return this.#createInstance(configData, versionId);
115
+ } catch (error) {
116
+ if (error instanceof DataAccessError) {
117
+ throw error;
118
+ }
119
+ const message = `Failed to create configuration in S3: ${error.message}`;
120
+ this.log.error(message, error);
121
+ throw new DataAccessError(message, this, error);
122
+ }
35
123
  }
36
124
 
125
+ /**
126
+ * Finds a configuration by S3 VersionId.
127
+ *
128
+ * @param {string} version - The S3 VersionId.
129
+ * @returns {Promise<Configuration|null>} - The Configuration instance or null if not found.
130
+ * @throws {DataAccessError} If S3 is not configured or the operation fails.
131
+ */
37
132
  async findByVersion(version) {
38
- return this.findByAll({ versionString: zeroPad(version, 10) });
133
+ this.#requireS3();
134
+
135
+ const versionId = String(version);
136
+
137
+ try {
138
+ const command = new GetObjectCommand({
139
+ Bucket: this.s3Bucket,
140
+ Key: S3_CONFIG_KEY,
141
+ VersionId: versionId,
142
+ });
143
+
144
+ const response = await this.s3Client.send(command);
145
+ const bodyString = await response.Body.transformToString();
146
+ const configData = JSON.parse(bodyString);
147
+
148
+ return this.#createInstance(configData, versionId);
149
+ } catch (error) {
150
+ if (error.name === 'NoSuchKey' || error.name === 'NoSuchVersion') {
151
+ this.log.info(`Configuration with version ${versionId} not found in S3`);
152
+ return null;
153
+ }
154
+
155
+ if (error instanceof DataAccessError) {
156
+ throw error;
157
+ }
158
+
159
+ const message = `Failed to retrieve configuration with version ${versionId} from S3: ${error.message}`;
160
+ this.log.error(message, error);
161
+ throw new DataAccessError(message, this, error);
162
+ }
39
163
  }
40
164
 
165
+ /**
166
+ * Retrieves the latest configuration from S3.
167
+ * S3 automatically returns the latest version when versioning is enabled.
168
+ *
169
+ * @returns {Promise<Configuration|null>} - The latest Configuration instance
170
+ * or null if not found.
171
+ * @throws {DataAccessError} If S3 is not configured or the operation fails.
172
+ */
41
173
  async findLatest() {
42
- return this.findByAll({}, { order: 'desc' });
174
+ this.#requireS3();
175
+
176
+ try {
177
+ const command = new GetObjectCommand({
178
+ Bucket: this.s3Bucket,
179
+ Key: S3_CONFIG_KEY,
180
+ });
181
+
182
+ const response = await this.s3Client.send(command);
183
+ const bodyString = await response.Body.transformToString();
184
+ const configData = JSON.parse(bodyString);
185
+ const { VersionId: versionId } = response;
186
+
187
+ return this.#createInstance(configData, versionId);
188
+ } catch (error) {
189
+ // If the object doesn't exist, return null (first-time setup)
190
+ if (error.name === 'NoSuchKey') {
191
+ this.log.info('No configuration found in S3, returning null');
192
+ return null;
193
+ }
194
+
195
+ if (error instanceof DataAccessError) {
196
+ throw error;
197
+ }
198
+
199
+ const message = `Failed to retrieve configuration from S3: ${error.message}`;
200
+ this.log.error(message, error);
201
+ throw new DataAccessError(message, this, error);
202
+ }
43
203
  }
44
204
  }
45
205
 
@@ -13,17 +13,16 @@
13
13
  import { isNonEmptyObject, isNonEmptyArray } from '@adobe/spacecat-shared-utils';
14
14
 
15
15
  import { sanitizeIdAndAuditFields } from '../../util/util.js';
16
- import BaseModel from '../base/base.model.js';
17
16
  import { Entitlement } from '../entitlement/index.js';
18
17
 
19
18
  /**
20
- * Configuration - A class representing an Configuration entity.
21
- * Provides methods to access and manipulate Configuration-specific data.
19
+ * Configuration - A standalone class representing the global Configuration entity.
20
+ * This is a singleton entity stored in S3 with versioning.
21
+ * Unlike other entities, Configuration does not use ElectroDB or DynamoDB.
22
22
  *
23
23
  * @class Configuration
24
- * @extends BaseModel
25
24
  */
26
- class Configuration extends BaseModel {
25
+ class Configuration {
27
26
  static ENTITY_NAME = 'Configuration';
28
27
 
29
28
  static JOB_GROUPS = {
@@ -53,7 +52,78 @@ class Configuration extends BaseModel {
53
52
 
54
53
  static AUDIT_NAME_MAX_LENGTH = 37;
55
54
 
56
- // add your custom methods or overrides here
55
+ constructor(data, versionId, collection, log) {
56
+ this.handlers = data.handlers;
57
+ this.jobs = data.jobs;
58
+ this.queues = data.queues;
59
+ this.slackRoles = data.slackRoles;
60
+ this.createdAt = data.createdAt;
61
+ this.updatedAt = data.updatedAt;
62
+ this.updatedBy = data.updatedBy;
63
+ this.versionId = versionId;
64
+ this.collection = collection;
65
+ this.log = log;
66
+ }
67
+
68
+ getId() {
69
+ return this.versionId;
70
+ }
71
+
72
+ getConfigurationId() {
73
+ return this.versionId;
74
+ }
75
+
76
+ getVersion() {
77
+ return this.versionId;
78
+ }
79
+
80
+ getCreatedAt() {
81
+ return this.createdAt;
82
+ }
83
+
84
+ getUpdatedAt() {
85
+ return this.updatedAt;
86
+ }
87
+
88
+ getUpdatedBy() {
89
+ return this.updatedBy;
90
+ }
91
+
92
+ setUpdatedBy(updatedBy) {
93
+ this.updatedBy = updatedBy;
94
+ }
95
+
96
+ getHandlers() {
97
+ return this.handlers;
98
+ }
99
+
100
+ setHandlers(handlers) {
101
+ this.handlers = handlers;
102
+ }
103
+
104
+ getJobs() {
105
+ return this.jobs;
106
+ }
107
+
108
+ setJobs(jobs) {
109
+ this.jobs = jobs;
110
+ }
111
+
112
+ getQueues() {
113
+ return this.queues;
114
+ }
115
+
116
+ setQueues(queues) {
117
+ this.queues = queues;
118
+ }
119
+
120
+ getSlackRoles() {
121
+ return this.slackRoles;
122
+ }
123
+
124
+ setSlackRoles(slackRoles) {
125
+ this.slackRoles = slackRoles;
126
+ }
57
127
 
58
128
  getHandler(type) {
59
129
  return this.getHandlers()?.[type];
@@ -161,6 +231,7 @@ class Configuration extends BaseModel {
161
231
  if (enabled) {
162
232
  if (handler.enabledByDefault) {
163
233
  handler.disabled[entityKey] = handler.disabled[entityKey]
234
+ /* c8 ignore next */
164
235
  .filter((id) => id !== entityId) || [];
165
236
  } else {
166
237
  handler.enabled[entityKey] = Array
@@ -170,6 +241,7 @@ class Configuration extends BaseModel {
170
241
  handler.disabled[entityKey] = Array
171
242
  .from(new Set([...(handler.disabled[entityKey] || []), entityId]));
172
243
  } else {
244
+ /* c8 ignore next */
173
245
  handler.enabled[entityKey] = handler.enabled[entityKey].filter((id) => id !== entityId) || [];
174
246
  }
175
247
 
@@ -270,6 +342,7 @@ class Configuration extends BaseModel {
270
342
  if (!isNonEmptyObject(queues)) {
271
343
  throw new Error('Queues configuration cannot be empty');
272
344
  }
345
+ /* c8 ignore next */
273
346
  const existingQueues = this.getQueues() || {};
274
347
  const mergedQueues = { ...existingQueues, ...queues };
275
348
  this.setQueues(mergedQueues);
@@ -496,7 +569,19 @@ class Configuration extends BaseModel {
496
569
  }
497
570
 
498
571
  async save() {
499
- return this.collection.create(sanitizeIdAndAuditFields(this.constructor.name, this.toJSON()));
572
+ return this.collection.create(sanitizeIdAndAuditFields('Configuration', this.toJSON()));
573
+ }
574
+
575
+ toJSON() {
576
+ return {
577
+ handlers: this.handlers,
578
+ jobs: this.jobs,
579
+ queues: this.queues,
580
+ slackRoles: this.slackRoles,
581
+ createdAt: this.createdAt,
582
+ updatedAt: this.updatedAt,
583
+ updatedBy: this.updatedBy,
584
+ };
500
585
  }
501
586
  }
502
587
 
@@ -10,18 +10,21 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- /* c8 ignore start */
14
-
15
- import { isNonEmptyObject } from '@adobe/spacecat-shared-utils';
13
+ /**
14
+ * Configuration Schema
15
+ *
16
+ * Unlike other entities, Configuration does not use ElectroDB and is stored in S3.
17
+ * Validation is handled by Joi schemas defined below.
18
+ */
16
19
 
17
20
  import Joi from 'joi';
18
21
 
19
- import SchemaBuilder from '../base/schema.builder.js';
20
22
  import Configuration from './configuration.model.js';
21
- import ConfigurationCollection from './configuration.collection.js';
22
- import { zeroPad } from '../../util/util.js';
23
23
 
24
- const handlerSchema = Joi.object().pattern(Joi.string(), Joi.object(
24
+ const validJobGroups = Object.values(Configuration.JOB_GROUPS);
25
+ const validJobIntervals = Object.values(Configuration.JOB_INTERVALS);
26
+
27
+ export const handlerSchema = Joi.object().pattern(Joi.string(), Joi.object(
25
28
  {
26
29
  enabled: Joi.object({
27
30
  sites: Joi.array().items(Joi.string()),
@@ -44,17 +47,32 @@ const handlerSchema = Joi.object().pattern(Joi.string(), Joi.object(
44
47
  },
45
48
  )).unknown(true);
46
49
 
47
- const jobsSchema = Joi.array().required();
50
+ export const jobSchema = Joi.object({
51
+ group: Joi.string().valid(...validJobGroups).required(),
52
+ type: Joi.string().required(),
53
+ interval: Joi.string().valid(...validJobIntervals).required(),
54
+ }).unknown(true);
55
+
56
+ export const jobsSchema = Joi.array().items(jobSchema).required();
57
+
58
+ export const queueSchema = Joi.object().min(1).required();
48
59
 
49
- const queueSchema = Joi.object().required();
60
+ export const slackRolesSchema = Joi.object().min(1);
50
61
 
51
- const configurationSchema = Joi.object({
52
- version: Joi.number().required(),
62
+ export const configurationSchema = Joi.object({
53
63
  queues: queueSchema,
54
64
  handlers: handlerSchema,
55
65
  jobs: jobsSchema,
66
+ slackRoles: slackRolesSchema,
56
67
  }).unknown(true);
57
68
 
69
+ /**
70
+ * Validates a configuration object against the schema.
71
+ * @param {object} data - The configuration data to validate.
72
+ * @param {object} [schema=configurationSchema] - The Joi schema to validate against.
73
+ * @returns {object} The validated configuration data.
74
+ * @throws {Error} If validation fails.
75
+ */
58
76
  export const checkConfiguration = (data, schema = configurationSchema) => {
59
77
  const { error, value } = schema.validate(data);
60
78
 
@@ -64,50 +82,3 @@ export const checkConfiguration = (data, schema = configurationSchema) => {
64
82
 
65
83
  return value;
66
84
  };
67
-
68
- /*
69
- Schema Doc: https://electrodb.dev/en/modeling/schema/
70
- Attribute Doc: https://electrodb.dev/en/modeling/attributes/
71
- Indexes Doc: https://electrodb.dev/en/modeling/indexes/
72
- */
73
-
74
- const schema = new SchemaBuilder(Configuration, ConfigurationCollection)
75
- .addAttribute('handlers', {
76
- type: 'any',
77
- validate: (value) => !value || checkConfiguration(value, handlerSchema),
78
- })
79
- .addAttribute('jobs', {
80
- type: 'list',
81
- items: {
82
- type: 'map',
83
- properties: {
84
- group: { type: Object.values(Configuration.JOB_GROUPS), required: true },
85
- type: { type: 'string', required: true },
86
- interval: { type: Object.values(Configuration.JOB_INTERVALS), required: true },
87
- },
88
- },
89
- })
90
- .addAttribute('queues', {
91
- type: 'any',
92
- required: true,
93
- validate: (value) => isNonEmptyObject(value),
94
- })
95
- .addAttribute('slackRoles', {
96
- type: 'any',
97
- validate: (value) => !value || isNonEmptyObject(value),
98
- })
99
- .addAttribute('version', {
100
- type: 'number',
101
- required: true,
102
- readOnly: true,
103
- })
104
- .addAttribute('versionString', { // used for indexing/sorting
105
- type: 'string',
106
- required: true,
107
- readOnly: true,
108
- default: '0', // setting the default forces set() to run, to transform the version number to a string
109
- set: (value, all) => zeroPad(all.version, 10),
110
- })
111
- .addAllIndex(['versionString']);
112
-
113
- export default schema.build();
@@ -10,40 +10,52 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import type {
14
- BaseCollection, BaseModel, Organization, Site,
15
- } from '../index';
13
+ import type { Organization, Site } from '../index';
16
14
 
17
- export interface Configuration extends BaseModel {
15
+ export interface Configuration {
18
16
  addHandler(type: string, handler: object): void;
19
- disableHandlerForOrganization(type: string, organization: Organization): void;
17
+ disableHandlerForOrg(type: string, org: Organization): void;
20
18
  disableHandlerForSite(type: string, site: Site): void;
21
- enableHandlerForOrganization(type: string, organization: Organization): void;
19
+ enableHandlerForOrg(type: string, org: Organization): void;
22
20
  enableHandlerForSite(type: string, site: Site): void;
23
21
  getConfigurationId(): string;
24
- getEnabledSiteIdsForHandler(type: string): string[];
25
- getEnabledAuditsForSite(site: Site): string[];
22
+ getCreatedAt(): string;
26
23
  getDisabledAuditsForSite(site: Site): string[];
24
+ getEnabledAuditsForSite(site: Site): string[];
25
+ getEnabledSiteIdsForHandler(type: string): string[];
27
26
  getHandler(type: string): object | undefined;
28
27
  getHandlers(): object;
29
- getJobs(): object;
28
+ getId(): string;
29
+ getJobs(): object[];
30
30
  getQueues(): object;
31
31
  getSlackRoleMembersByRole(role: string): string[];
32
32
  getSlackRoles(): object;
33
- getVersion(): number;
34
- isHandlerEnabledForOrg(type: string, organization: Organization): boolean;
33
+ getUpdatedAt(): string;
34
+ getUpdatedBy(): string;
35
+ getVersion(): string;
36
+ isHandlerDependencyMetForOrg(type: string, org: Organization): true | string[];
37
+ isHandlerDependencyMetForSite(type: string, site: Site): true | string[];
38
+ isHandlerEnabledForOrg(type: string, org: Organization): boolean;
35
39
  isHandlerEnabledForSite(type: string, site: Site): boolean;
40
+ registerAudit(type: string, enabledByDefault?: boolean, interval?: string, productCodes?: string[]): void;
41
+ save(): Promise<Configuration>;
36
42
  setHandlers(handlers: object): void;
37
- setJobs(jobs: object): void;
43
+ setJobs(jobs: object[]): void;
38
44
  setQueues(queues: object): void;
39
45
  setSlackRoles(slackRoles: object): void;
46
+ setUpdatedBy(updatedBy: string): void;
47
+ toJSON(): object;
48
+ unregisterAudit(type: string): void;
49
+ updateConfiguration(data: object): void;
40
50
  updateHandlerOrgs(type: string, orgId: string, enabled: boolean): void;
51
+ updateHandlerProperties(type: string, properties: object): void;
41
52
  updateHandlerSites(type: string, siteId: string, enabled: boolean): void;
42
- registerAudit(type: string, enabledByDefault?: boolean, interval?: string, productCodes?: string[]): void;
43
- unregisterAudit(type: string): void;
53
+ updateJob(type: string, properties: { interval?: string; group?: string }): void;
54
+ updateQueues(queues: object): void;
44
55
  }
45
56
 
46
- export interface ConfigurationCollection extends BaseCollection<Configuration> {
47
- findByVersion(version: number): Promise<Configuration | null>;
57
+ export interface ConfigurationCollection {
58
+ create(data: object): Promise<Configuration>;
59
+ findByVersion(version: string): Promise<Configuration | null>;
48
60
  findLatest(): Promise<Configuration | null>;
49
61
  }
@@ -13,6 +13,7 @@
13
13
  export * from './api-key/index.js';
14
14
  export * from './async-job/index.js';
15
15
  export * from './audit/index.js';
16
+ export * from './audit-url/index.js';
16
17
  export * from './base/index.js';
17
18
  export * from './configuration/index.js';
18
19
  export * from './entitlement/index.js';
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { DynamoDB } from '@aws-sdk/client-dynamodb';
14
14
  import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
15
+ import { S3Client } from '@aws-sdk/client-s3';
15
16
  import { Service } from 'electrodb';
16
17
 
17
18
  import { instrumentAWSClient } from '@adobe/spacecat-shared-utils';
@@ -65,10 +66,45 @@ const createElectroService = (client, config, log) => {
65
66
  );
66
67
  };
67
68
 
69
+ /**
70
+ * Creates an S3 service configuration if bucket configuration is provided.
71
+ *
72
+ * @param {object} config - Configuration object
73
+ * @param {string} [config.s3Bucket] - S3 bucket name
74
+ * @param {string} [config.region] - AWS region
75
+ * @returns {{s3Client: S3Client, s3Bucket: string}|null} - S3 client and bucket or null
76
+ */
77
+ const createS3Service = (config) => {
78
+ const { s3Bucket, region } = config;
79
+
80
+ if (!s3Bucket) {
81
+ return null;
82
+ }
83
+
84
+ const options = region ? { region } : {};
85
+ const s3Client = instrumentAWSClient(new S3Client(options));
86
+
87
+ return { s3Client, s3Bucket };
88
+ };
89
+
90
+ /**
91
+ * Creates a services dictionary containing all datastore services.
92
+ * Each collection can declare which service it needs via its DATASTORE_TYPE.
93
+ *
94
+ * @param {object} electroService - The ElectroDB service for DynamoDB operations
95
+ * @param {object} config - Configuration object
96
+ * @returns {object} Services dictionary with dynamo and s3 services
97
+ */
98
+ const createServices = (electroService, config) => ({
99
+ dynamo: electroService,
100
+ s3: createS3Service(config),
101
+ });
102
+
68
103
  /**
69
104
  * Creates a data access layer for interacting with DynamoDB using ElectroDB.
70
105
  *
71
- * @param {{tableNameData: string}} config - Configuration object containing table name
106
+ * @param {{tableNameData: string, s3Bucket?: string, region?: string}} config - Configuration
107
+ * object containing table name and optional S3 configuration
72
108
  * @param {object} log - Logger instance, defaults to console
73
109
  * @param {DynamoDB} [client] - Optional custom DynamoDB client instance
74
110
  * @returns {object} Data access collections for interacting with entities
@@ -78,7 +114,8 @@ export const createDataAccess = (config, log = console, client = undefined) => {
78
114
 
79
115
  const rawClient = createRawClient(client);
80
116
  const electroService = createElectroService(rawClient, config, log);
81
- const entityRegistry = new EntityRegistry(electroService, log);
117
+ const services = createServices(electroService, config);
118
+ const entityRegistry = new EntityRegistry(services, log);
82
119
 
83
120
  return entityRegistry.getCollections();
84
121
  };