@@ -14,7 +14,6 @@ import { isNonEmptyObject, isNonEmptyArray } from '@adobe/spacecat-shared-utils'
1414
1515import { sanitizeIdAndAuditFields } from '../../util/util.js' ;
1616import BaseModel from '../base/base.model.js' ;
17- import { Audit } from '../audit/index.js' ;
1817import { Entitlement } from '../entitlement/index.js' ;
1918
2019/**
@@ -47,6 +46,11 @@ class Configuration extends BaseModel {
4746 FORTNIGHTLY_SUNDAY : 'fortnightly-sunday' ,
4847 MONTHLY : 'monthly' ,
4948 } ;
49+
50+ static AUDIT_NAME_REGEX = / ^ [ a - z 0 - 9 - ] + $ / ;
51+
52+ static AUDIT_NAME_MAX_LENGTH = 37 ;
53+
5054 // add your custom methods or overrides here
5155
5256 getHandler ( type ) {
@@ -253,50 +257,209 @@ class Configuration extends BaseModel {
253257 this . updateHandlerOrgs ( type , orgId , false ) ;
254258 }
255259
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+
256414 registerAudit (
257415 type ,
258416 enabledByDefault = false ,
259417 interval = Configuration . JOB_INTERVALS . NEVER ,
260418 productCodes = [ ] ,
261419 ) {
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` ) ;
265434 }
266435
267- // Validate job interval
268436 if ( ! Object . values ( Configuration . JOB_INTERVALS ) . includes ( interval ) ) {
269437 throw new Error ( `Invalid interval ${ interval } ` ) ;
270438 }
271439
272- // Validate product codes
273440 if ( ! isNonEmptyArray ( productCodes ) ) {
274441 throw new Error ( 'No product codes provided' ) ;
275442 }
276443 if ( ! productCodes . every ( ( pc ) => Object . values ( Entitlement . PRODUCT_CODES ) . includes ( pc ) ) ) {
277444 throw new Error ( 'Invalid product codes provided' ) ;
278445 }
279446
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+
300463 const jobs = this . getJobs ( ) ;
301464 const exists = jobs . find ( ( job ) => job . group === 'audits' && job . type === type ) ;
302465 if ( ! exists ) {
@@ -310,19 +473,18 @@ class Configuration extends BaseModel {
310473 }
311474
312475 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' ) ;
316478 }
317479
318- // Remove from handlers
319480 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` ) ;
323483 }
324484
325- // Remove from jobs
485+ delete handlers [ type ] ;
486+ this . setHandlers ( handlers ) ;
487+
326488 const jobs = this . getJobs ( ) ;
327489 const jobIndex = jobs . findIndex ( ( job ) => job . group === 'audits' && job . type === type ) ;
328490 if ( jobIndex !== - 1 ) {
0 commit comments