@adobe/spacecat-shared-data-access 2.85.1 → 2.86.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 CHANGED
@@ -1,3 +1,10 @@
1
+ # [@adobe/spacecat-shared-data-access-v2.86.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v2.85.1...@adobe/spacecat-shared-data-access-v2.86.0) (2025-11-19)
2
+
3
+
4
+ ### Features
5
+
6
+ * add configuration management APIs ([#1063](https://github.com/adobe/spacecat-shared/issues/1063)) ([d6988a9](https://github.com/adobe/spacecat-shared/commit/d6988a94ad79016c238f3f4d9426c328b52c3131))
7
+
1
8
  # [@adobe/spacecat-shared-data-access-v2.85.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v2.85.0...@adobe/spacecat-shared-data-access-v2.85.1) (2025-11-18)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-data-access",
3
- "version": "2.85.1",
3
+ "version": "2.86.0",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -14,7 +14,6 @@ import { isNonEmptyObject, isNonEmptyArray } from '@adobe/spacecat-shared-utils'
14
14
 
15
15
  import { sanitizeIdAndAuditFields } from '../../util/util.js';
16
16
  import BaseModel from '../base/base.model.js';
17
- import { Audit } from '../audit/index.js';
18
17
  import { Entitlement } from '../entitlement/index.js';
19
18
 
20
19
  /**
@@ -47,6 +46,11 @@ class Configuration extends BaseModel {
47
46
  FORTNIGHTLY_SUNDAY: 'fortnightly-sunday',
48
47
  MONTHLY: 'monthly',
49
48
  };
49
+
50
+ static AUDIT_NAME_REGEX = /^[a-z0-9-]+$/;
51
+
52
+ static AUDIT_NAME_MAX_LENGTH = 37;
53
+
50
54
  // add your custom methods or overrides here
51
55
 
52
56
  getHandler(type) {
@@ -253,23 +257,186 @@ class Configuration extends BaseModel {
253
257
  this.updateHandlerOrgs(type, orgId, false);
254
258
  }
255
259
 
260
+ /**
261
+ * Updates the queue URLs configuration by merging with existing queues.
262
+ * Only the specified queue URLs will be updated; others remain unchanged.
263
+ *
264
+ * @param {object} queues - Queue URLs to update (merged with existing)
265
+ * @throws {Error} If queues object is empty or invalid
266
+ */
267
+ updateQueues(queues) {
268
+ if (!isNonEmptyObject(queues)) {
269
+ throw new Error('Queues configuration cannot be empty');
270
+ }
271
+ const existingQueues = this.getQueues() || {};
272
+ const mergedQueues = { ...existingQueues, ...queues };
273
+ this.setQueues(mergedQueues);
274
+ }
275
+
276
+ /**
277
+ * Updates a job's properties (interval, group).
278
+ *
279
+ * @param {string} type - The job type to update
280
+ * @param {object} properties - Properties to update (interval, group)
281
+ * @throws {Error} If job not found or properties are invalid
282
+ */
283
+ updateJob(type, properties) {
284
+ const jobs = this.getJobs();
285
+ const jobIndex = jobs.findIndex((job) => job.type === type);
286
+
287
+ if (jobIndex === -1) {
288
+ throw new Error(`Job type "${type}" not found in configuration`);
289
+ }
290
+
291
+ if (properties.interval && !Object.values(Configuration.JOB_INTERVALS)
292
+ .includes(properties.interval)) {
293
+ throw new Error(`Invalid interval "${properties.interval}". Must be one of: ${Object.values(Configuration.JOB_INTERVALS).join(', ')}`);
294
+ }
295
+
296
+ if (properties.group && !Object.values(Configuration.JOB_GROUPS).includes(properties.group)) {
297
+ throw new Error(`Invalid group "${properties.group}". Must be one of: ${Object.values(Configuration.JOB_GROUPS).join(', ')}`);
298
+ }
299
+
300
+ jobs[jobIndex] = { ...jobs[jobIndex], ...properties };
301
+ this.setJobs(jobs);
302
+ }
303
+
304
+ /**
305
+ * Updates a handler's properties.
306
+ *
307
+ * @param {string} type - The handler type to update
308
+ * @param {object} properties - Properties to update
309
+ * @throws {Error} If handler not found or properties are invalid
310
+ */
311
+ updateHandlerProperties(type, properties) {
312
+ const handlers = this.getHandlers();
313
+ if (!handlers[type]) {
314
+ throw new Error(`Handler "${type}" not found in configuration`);
315
+ }
316
+
317
+ if (properties.productCodes !== undefined) {
318
+ if (!isNonEmptyArray(properties.productCodes)) {
319
+ throw new Error('productCodes must be a non-empty array');
320
+ }
321
+ const validProductCodes = Object.values(Entitlement.PRODUCT_CODES);
322
+ if (!properties.productCodes.every((pc) => validProductCodes.includes(pc))) {
323
+ throw new Error('Invalid product codes provided');
324
+ }
325
+ }
326
+
327
+ if (isNonEmptyArray(properties.dependencies)) {
328
+ for (const dep of properties.dependencies) {
329
+ if (!handlers[dep.handler]) {
330
+ throw new Error(`Dependency handler "${dep.handler}" does not exist in configuration`);
331
+ }
332
+ }
333
+ }
334
+
335
+ if (properties.movingAvgThreshold !== undefined && properties.movingAvgThreshold < 1) {
336
+ throw new Error('movingAvgThreshold must be greater than or equal to 1');
337
+ }
338
+
339
+ if (properties.percentageChangeThreshold !== undefined && properties
340
+ .percentageChangeThreshold < 1) {
341
+ throw new Error('percentageChangeThreshold must be greater than or equal to 1');
342
+ }
343
+
344
+ handlers[type] = { ...handlers[type], ...properties };
345
+ this.setHandlers(handlers);
346
+ }
347
+
348
+ /**
349
+ * Updates the configuration by merging changes into existing sections.
350
+ * This is a flexible update method that allows updating one or more sections at once.
351
+ * Changes are merged, not replaced - existing data is preserved.
352
+ *
353
+ * @param {object} data - Configuration data to update
354
+ * @param {object} [data.handlers] - Handlers to merge (adds new, updates existing)
355
+ * @param {Array} [data.jobs] - Jobs to merge (updates matching jobs by type)
356
+ * @param {object} [data.queues] - Queues to merge (updates specific queue URLs)
357
+ * @throws {Error} If validation fails
358
+ */
359
+ updateConfiguration(data) {
360
+ if (!isNonEmptyObject(data)) {
361
+ throw new Error('Configuration data cannot be empty');
362
+ }
363
+
364
+ if (data.handlers !== undefined) {
365
+ if (!isNonEmptyObject(data.handlers)) {
366
+ throw new Error('Handlers must be a non-empty object if provided');
367
+ }
368
+ const existingHandlers = this.getHandlers() || {};
369
+ const mergedHandlers = { ...existingHandlers };
370
+
371
+ Object.keys(data.handlers).forEach((handlerType) => {
372
+ mergedHandlers[handlerType] = {
373
+ ...existingHandlers[handlerType],
374
+ ...data.handlers[handlerType],
375
+ };
376
+ });
377
+
378
+ this.setHandlers(mergedHandlers);
379
+ }
380
+
381
+ if (data.jobs !== undefined) {
382
+ if (!isNonEmptyArray(data.jobs)) {
383
+ throw new Error('Jobs must be a non-empty array if provided');
384
+ }
385
+ const existingJobs = this.getJobs() || [];
386
+ const mergedJobs = [...existingJobs];
387
+
388
+ data.jobs.forEach((newJob) => {
389
+ const existingIndex = mergedJobs.findIndex(
390
+ (job) => job.type === newJob.type && job.group === newJob.group,
391
+ );
392
+
393
+ if (existingIndex !== -1) {
394
+ mergedJobs[existingIndex] = { ...mergedJobs[existingIndex], ...newJob };
395
+ } else {
396
+ mergedJobs.push(newJob);
397
+ }
398
+ });
399
+
400
+ this.setJobs(mergedJobs);
401
+ }
402
+
403
+ if (data.queues !== undefined) {
404
+ if (!isNonEmptyObject(data.queues)) {
405
+ throw new Error('Queues must be a non-empty object if provided');
406
+ }
407
+ const existingQueues = this.getQueues() || {};
408
+ const mergedQueues = { ...existingQueues, ...data.queues };
409
+
410
+ this.setQueues(mergedQueues);
411
+ }
412
+ }
413
+
256
414
  registerAudit(
257
415
  type,
258
416
  enabledByDefault = false,
259
417
  interval = Configuration.JOB_INTERVALS.NEVER,
260
418
  productCodes = [],
261
419
  ) {
262
- // Validate audit type
263
- if (!Object.values(Audit.AUDIT_TYPES).includes(type)) {
264
- throw new Error(`Audit type ${type} is not a valid audit type in the data model`);
420
+ if (!type || typeof type !== 'string' || type.trim() === '') {
421
+ throw new Error('Audit type must be a non-empty string');
422
+ }
423
+
424
+ if (type.length > Configuration.AUDIT_NAME_MAX_LENGTH) {
425
+ throw new Error(`Audit type must not exceed ${Configuration.AUDIT_NAME_MAX_LENGTH} characters`);
426
+ }
427
+ if (!Configuration.AUDIT_NAME_REGEX.test(type)) {
428
+ throw new Error('Audit type can only contain lowercase letters, numbers, and hyphens');
429
+ }
430
+
431
+ const handlers = this.getHandlers();
432
+ if (handlers && handlers[type]) {
433
+ throw new Error(`Audit type "${type}" is already registered`);
265
434
  }
266
435
 
267
- // Validate job interval
268
436
  if (!Object.values(Configuration.JOB_INTERVALS).includes(interval)) {
269
437
  throw new Error(`Invalid interval ${interval}`);
270
438
  }
271
439
 
272
- // Validate product codes
273
440
  if (!isNonEmptyArray(productCodes)) {
274
441
  throw new Error('No product codes provided');
275
442
  }
@@ -277,26 +444,22 @@ class Configuration extends BaseModel {
277
444
  throw new Error('Invalid product codes provided');
278
445
  }
279
446
 
280
- // Add to handlers if not already registered
281
- const handlers = this.getHandlers();
282
- if (!handlers[type]) {
283
- handlers[type] = {
284
- enabledByDefault,
285
- enabled: {
286
- sites: [],
287
- orgs: [],
288
- },
289
- disabled: {
290
- sites: [],
291
- orgs: [],
292
- },
293
- dependencies: [],
294
- productCodes,
295
- };
296
- this.setHandlers(handlers);
297
- }
298
-
299
- // Add to jobs if not already registered
447
+ const updatedHandlers = handlers || {};
448
+ updatedHandlers[type] = {
449
+ enabledByDefault,
450
+ enabled: {
451
+ sites: [],
452
+ orgs: [],
453
+ },
454
+ disabled: {
455
+ sites: [],
456
+ orgs: [],
457
+ },
458
+ dependencies: [],
459
+ productCodes,
460
+ };
461
+ this.setHandlers(updatedHandlers);
462
+
300
463
  const jobs = this.getJobs();
301
464
  const exists = jobs.find((job) => job.group === 'audits' && job.type === type);
302
465
  if (!exists) {
@@ -310,19 +473,18 @@ class Configuration extends BaseModel {
310
473
  }
311
474
 
312
475
  unregisterAudit(type) {
313
- // Validate audit type
314
- if (!Object.values(Audit.AUDIT_TYPES).includes(type)) {
315
- throw new Error(`Audit type ${type} is not a valid audit type in the data model`);
476
+ if (!type || typeof type !== 'string' || type.trim() === '') {
477
+ throw new Error('Audit type must be a non-empty string');
316
478
  }
317
479
 
318
- // Remove from handlers
319
480
  const handlers = this.getHandlers();
320
- if (handlers[type]) {
321
- delete handlers[type];
322
- this.setHandlers(handlers);
481
+ if (!handlers || !handlers[type]) {
482
+ throw new Error(`Audit type "${type}" is not registered`);
323
483
  }
324
484
 
325
- // Remove from jobs
485
+ delete handlers[type];
486
+ this.setHandlers(handlers);
487
+
326
488
  const jobs = this.getJobs();
327
489
  const jobIndex = jobs.findIndex((job) => job.group === 'audits' && job.type === type);
328
490
  if (jobIndex !== -1) {
@@ -40,7 +40,7 @@ const handlerSchema = Joi.object().pattern(Joi.string(), Joi.object(
40
40
  actions: Joi.array().items(Joi.string()),
41
41
  },
42
42
  )),
43
- productCodes: Joi.array().items(Joi.string()),
43
+ productCodes: Joi.array().items(Joi.string()).min(1).required(),
44
44
  },
45
45
  )).unknown(true);
46
46
 
@@ -12,8 +12,10 @@
12
12
 
13
13
  import Configuration from './configuration.model.js';
14
14
  import ConfigurationCollection from './configuration.collection.js';
15
+ import { checkConfiguration } from './configuration.schema.js';
15
16
 
16
17
  export {
17
18
  Configuration,
18
19
  ConfigurationCollection,
20
+ checkConfiguration,
19
21
  };