@adobe/spacecat-shared-data-access 3.43.0 → 3.45.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,16 @@
1
+ ## [@adobe/spacecat-shared-data-access-v3.45.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.44.0...@adobe/spacecat-shared-data-access-v3.45.0) (2026-04-04)
2
+
3
+ ### Features
4
+
5
+ * Manage a custom list of audit target URLs ([#1484](https://github.com/adobe/spacecat-shared/issues/1484)) ([06c54d8](https://github.com/adobe/spacecat-shared/commit/06c54d89ea2488f6951cc96b9458521ac9d26706))
6
+ * update suggestion data schemas for some of the opportunities ([#1466](https://github.com/adobe/spacecat-shared/issues/1466)) ([a09e968](https://github.com/adobe/spacecat-shared/commit/a09e968bba9235813451b65defe7d133a8956711))
7
+
8
+ ## [@adobe/spacecat-shared-data-access-v3.44.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.43.0...@adobe/spacecat-shared-data-access-v3.44.0) (2026-04-02)
9
+
10
+ ### Features
11
+
12
+ * add ContactSalesLead entity (SITES-42069) ([#1475](https://github.com/adobe/spacecat-shared/issues/1475)) ([d245781](https://github.com/adobe/spacecat-shared/commit/d2457810e7a01900b2da70b0f81f031f95acdffa))
13
+
1
14
  ## [@adobe/spacecat-shared-data-access-v3.43.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.42.0...@adobe/spacecat-shared-data-access-v3.43.0) (2026-04-02)
2
15
 
3
16
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-data-access",
3
- "version": "3.43.0",
3
+ "version": "3.45.0",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -15,6 +15,7 @@ import { collectionNameToEntityName, decapitalize } from '../../util/util.js';
15
15
 
16
16
  import ApiKeyCollection from '../api-key/api-key.collection.js';
17
17
  import AsyncJobCollection from '../async-job/async-job.collection.js';
18
+ import ContactSalesLeadCollection from '../contact-sales-lead/contact-sales-lead.collection.js';
18
19
  import AuditCollection from '../audit/audit.collection.js';
19
20
  import AuditUrlCollection from '../audit-url/audit-url.collection.js';
20
21
  import ConfigurationCollection from '../configuration/configuration.collection.js';
@@ -54,6 +55,7 @@ import SiteImsOrgAccessCollection from '../site-ims-org-access/site-ims-org-acce
54
55
 
55
56
  import ApiKeySchema from '../api-key/api-key.schema.js';
56
57
  import AsyncJobSchema from '../async-job/async-job.schema.js';
58
+ import ContactSalesLeadSchema from '../contact-sales-lead/contact-sales-lead.schema.js';
57
59
  import AuditSchema from '../audit/audit.schema.js';
58
60
  import AuditUrlSchema from '../audit-url/audit-url.schema.js';
59
61
  import ConsumerSchema from '../consumer/consumer.schema.js';
@@ -187,6 +189,7 @@ class EntityRegistry {
187
189
  // Register ElectroDB-based entities only (Configuration is handled separately)
188
190
  EntityRegistry.registerEntity(ApiKeySchema, ApiKeyCollection);
189
191
  EntityRegistry.registerEntity(AsyncJobSchema, AsyncJobCollection);
192
+ EntityRegistry.registerEntity(ContactSalesLeadSchema, ContactSalesLeadCollection);
190
193
  EntityRegistry.registerEntity(AuditSchema, AuditCollection);
191
194
  EntityRegistry.registerEntity(AuditUrlSchema, AuditUrlCollection);
192
195
  EntityRegistry.registerEntity(ConsumerSchema, ConsumerCollection);
@@ -0,0 +1,27 @@
1
+ /*
2
+ * Copyright 2026 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 BaseCollection from '../base/base.collection.js';
14
+
15
+ /**
16
+ * ContactSalesLeadCollection - A collection class responsible for managing
17
+ * ContactSalesLead entities. Extends the BaseCollection to provide specific
18
+ * methods for interacting with ContactSalesLead records.
19
+ *
20
+ * @class ContactSalesLeadCollection
21
+ * @extends BaseCollection
22
+ */
23
+ class ContactSalesLeadCollection extends BaseCollection {
24
+ static COLLECTION_NAME = 'ContactSalesLeadCollection';
25
+ }
26
+
27
+ export default ContactSalesLeadCollection;
@@ -0,0 +1,32 @@
1
+ /*
2
+ * Copyright 2026 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 BaseModel from '../base/base.model.js';
14
+
15
+ /**
16
+ * ContactSalesLead - A class representing a contact sales lead entity.
17
+ * Tracks users who have expressed interest in purchasing via the "Contact Sales" flow.
18
+ *
19
+ * @class ContactSalesLead
20
+ * @extends BaseModel
21
+ */
22
+ class ContactSalesLead extends BaseModel {
23
+ static ENTITY_NAME = 'ContactSalesLead';
24
+
25
+ static STATUSES = {
26
+ NEW: 'NEW',
27
+ CONTACTED: 'CONTACTED',
28
+ CLOSED: 'CLOSED',
29
+ };
30
+ }
31
+
32
+ export default ContactSalesLead;
@@ -0,0 +1,40 @@
1
+ /*
2
+ * Copyright 2026 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 SchemaBuilder from '../base/schema.builder.js';
14
+ import ContactSalesLead from './contact-sales-lead.model.js';
15
+ import ContactSalesLeadCollection from './contact-sales-lead.collection.js';
16
+
17
+ const schema = new SchemaBuilder(ContactSalesLead, ContactSalesLeadCollection)
18
+ .addReference('belongs_to', 'Organization')
19
+ .addReference('belongs_to', 'Site', [], { required: false })
20
+ .addAttribute('name', {
21
+ type: 'string',
22
+ required: true,
23
+ })
24
+ .addAttribute('email', {
25
+ type: 'string',
26
+ required: true,
27
+ })
28
+ .addAttribute('domain', {
29
+ type: 'string',
30
+ })
31
+ .addAttribute('notes', {
32
+ type: 'string',
33
+ })
34
+ .addAttribute('status', {
35
+ type: Object.values(ContactSalesLead.STATUSES),
36
+ required: true,
37
+ default: ContactSalesLead.STATUSES.NEW,
38
+ });
39
+
40
+ export default schema.build();
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Copyright 2026 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 type {
14
+ BaseCollection, BaseModel, Organization, Site,
15
+ } from '../index';
16
+
17
+ export type ContactSalesLeadStatus = 'NEW' | 'CONTACTED' | 'CLOSED';
18
+
19
+ export interface ContactSalesLead extends BaseModel {
20
+ getName(): string;
21
+ getEmail(): string;
22
+ getDomain(): string | null;
23
+ getSiteId(): string | null;
24
+ getNotes(): string | null;
25
+ getStatus(): ContactSalesLeadStatus;
26
+ getOrganization(): Promise<Organization>;
27
+ getSite(): Promise<Site | null>;
28
+ setName(name: string): ContactSalesLead;
29
+ setEmail(email: string): ContactSalesLead;
30
+ setDomain(domain: string): ContactSalesLead;
31
+ setSiteId(siteId: string): ContactSalesLead;
32
+ setNotes(notes: string): ContactSalesLead;
33
+ setStatus(status: ContactSalesLeadStatus): ContactSalesLead;
34
+ }
35
+
36
+ export interface ContactSalesLeadCollection extends BaseCollection<ContactSalesLead> {
37
+ allByOrganizationId(organizationId: string): Promise<ContactSalesLead[]>;
38
+ allBySiteId(siteId: string): Promise<ContactSalesLead[]>;
39
+ findByOrganizationId(organizationId: string): Promise<ContactSalesLead | null>;
40
+ findBySiteId(siteId: string): Promise<ContactSalesLead | null>;
41
+ }
@@ -0,0 +1,19 @@
1
+ /*
2
+ * Copyright 2026 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 ContactSalesLead from './contact-sales-lead.model.js';
14
+ import ContactSalesLeadCollection from './contact-sales-lead.collection.js';
15
+
16
+ export {
17
+ ContactSalesLead,
18
+ ContactSalesLeadCollection,
19
+ };
@@ -13,6 +13,7 @@
13
13
  export * from './access-grant-log/index.js';
14
14
  export * from './api-key/index.js';
15
15
  export * from './async-job/index.js';
16
+ export * from './contact-sales-lead/index.js';
16
17
  export * from './audit/index.js';
17
18
  export * from './audit-url/index.js';
18
19
  export * from './base/index.js';
@@ -400,6 +400,11 @@ export const configSchema = Joi.object({
400
400
  contentAiConfig: Joi.object({
401
401
  index: Joi.string().optional(),
402
402
  }).optional(),
403
+ auditTargetURLs: Joi.object({
404
+ manual: Joi.array().items(Joi.object({
405
+ url: Joi.string().uri().required(),
406
+ })).optional().default([]),
407
+ }).options({ stripUnknown: true }).optional(),
403
408
  handlers: Joi.object().pattern(Joi.string(), Joi.object({
404
409
  mentions: Joi.object().pattern(Joi.string(), Joi.array().items(Joi.string())),
405
410
  excludedURLs: Joi.array().items(Joi.string()),
@@ -510,6 +515,60 @@ export const Config = (data = {}) => {
510
515
  self.getEdgeOptimizeConfig = () => state?.edgeOptimizeConfig;
511
516
  self.getOnboardConfig = () => state?.onboardConfig;
512
517
  self.getCommerceLlmoConfig = () => state?.commerceLlmoConfig;
518
+ const AUDIT_TARGET_SOURCES = ['manual'];
519
+ const auditTargetEntrySchema = Joi.object({
520
+ url: Joi.string().uri().required(),
521
+ });
522
+
523
+ const validateAuditTargetSource = (source) => {
524
+ if (!AUDIT_TARGET_SOURCES.includes(source)) {
525
+ throw new Error(`Invalid audit target source: "${source}". Must be one of: ${AUDIT_TARGET_SOURCES.join(', ')}`);
526
+ }
527
+ };
528
+
529
+ self.getAuditTargetURLsConfig = () => state?.auditTargetURLs;
530
+
531
+ self.getAuditTargetURLs = () => {
532
+ const targets = state?.auditTargetURLs;
533
+ if (!targets) return [];
534
+ return AUDIT_TARGET_SOURCES.flatMap(
535
+ (source) => (targets[source] || []).map((entry) => ({ ...entry, source })),
536
+ );
537
+ };
538
+
539
+ self.getAuditTargetURLsBySource = (source) => {
540
+ validateAuditTargetSource(source);
541
+ return state?.auditTargetURLs?.[source] || [];
542
+ };
543
+
544
+ self.updateAuditTargetURLs = (source, urls) => {
545
+ validateAuditTargetSource(source);
546
+ Joi.assert(urls, Joi.array().items(auditTargetEntrySchema), 'Invalid audit target URLs');
547
+ state.auditTargetURLs = state.auditTargetURLs || {};
548
+ state.auditTargetURLs[source] = urls;
549
+ };
550
+
551
+ self.addAuditTargetURL = (source, urlObj) => {
552
+ validateAuditTargetSource(source);
553
+ Joi.assert(urlObj, auditTargetEntrySchema, 'Invalid audit target URL');
554
+
555
+ state.auditTargetURLs = state.auditTargetURLs || {};
556
+ state.auditTargetURLs[source] = state.auditTargetURLs[source] || [];
557
+ const allUrls = AUDIT_TARGET_SOURCES.flatMap(
558
+ (s) => (state.auditTargetURLs[s] || []).map((e) => e.url),
559
+ );
560
+ if (!allUrls.includes(urlObj.url)) {
561
+ state.auditTargetURLs[source].push(urlObj);
562
+ }
563
+ };
564
+
565
+ self.removeAuditTargetURL = (source, url) => {
566
+ validateAuditTargetSource(source);
567
+ if (!state.auditTargetURLs?.[source]) return;
568
+ state.auditTargetURLs[source] = state.auditTargetURLs[source]
569
+ .filter((t) => t.url !== url);
570
+ };
571
+
513
572
  self.updateSlackConfig = (channel, workspace, invitedUserCount) => {
514
573
  state.slack = {
515
574
  channel,
@@ -864,4 +923,5 @@ Config.toDynamoItem = (config) => ({
864
923
  edgeOptimizeConfig: config.getEdgeOptimizeConfig(),
865
924
  onboardConfig: config.getOnboardConfig?.(),
866
925
  commerceLlmoConfig: config.getCommerceLlmoConfig?.(),
926
+ auditTargetURLs: config.getAuditTargetURLsConfig?.(),
867
927
  });
@@ -99,6 +99,20 @@ export interface LlmoCustomerIntent {
99
99
  value: string;
100
100
  }
101
101
 
102
+ export type AuditTargetSource = 'manual';
103
+
104
+ export interface AuditTargetEntry {
105
+ url: string;
106
+ }
107
+
108
+ export interface AuditTargetEntryWithSource extends AuditTargetEntry {
109
+ source: AuditTargetSource;
110
+ }
111
+
112
+ export interface AuditTargetURLs {
113
+ manual?: AuditTargetEntry[];
114
+ }
115
+
102
116
  export interface SiteConfig {
103
117
  state: {
104
118
  slack?: {
@@ -107,6 +121,7 @@ export interface SiteConfig {
107
121
  invitedUserCount?: number;
108
122
  };
109
123
  imports?: ImportConfig[];
124
+ auditTargetURLs?: AuditTargetURLs;
110
125
  handlers?: Record<string, {
111
126
  mentions?: Record<string, string[]>;
112
127
  excludedURLs?: string[];
@@ -205,6 +220,11 @@ export interface SiteConfig {
205
220
  removeLlmoTag(tag: string): void;
206
221
  getOnboardConfig(): { lastProfile?: string; lastStartTime?: number; forcedOverride?: boolean; history?: Array<{ profile?: string; startTime?: number }> } | undefined;
207
222
  updateOnboardConfig(onboardConfig: { lastProfile?: string; lastStartTime?: number; forcedOverride?: boolean }, options?: { maxHistory?: number }): void;
223
+ getAuditTargetURLs(): AuditTargetEntryWithSource[];
224
+ getAuditTargetURLsBySource(source: AuditTargetSource): AuditTargetEntry[];
225
+ updateAuditTargetURLs(source: AuditTargetSource, urls: AuditTargetEntry[]): void;
226
+ addAuditTargetURL(source: AuditTargetSource, urlObj: AuditTargetEntry): void;
227
+ removeAuditTargetURL(source: AuditTargetSource, url: string): void;
208
228
  }
209
229
 
210
230
  export interface Site extends BaseModel {
@@ -26,6 +26,17 @@
26
26
  import Joi from 'joi';
27
27
  import { OPPORTUNITY_TYPES } from '@adobe/spacecat-shared-utils';
28
28
 
29
+ /**
30
+ * Custom Joi validator that accepts malformed HTTP/HTTPS URLs and relative paths
31
+ * while rejecting dangerous URI schemes (javascript:, data:, blob:, etc.).
32
+ * Used for BROKEN_INTERNAL_LINKS where crawled content may contain malformed URLs.
33
+ */
34
+ const relaxedUrl = Joi.string().min(1).custom((value, helpers) => (
35
+ /^(https?:\/\/|\/)/i.test(value) ? value : helpers.error('string.uriScheme')
36
+ ), 'relaxed URL').messages({
37
+ 'string.uriScheme': '{{#label}} must start with http://, https://, or /',
38
+ });
39
+
29
40
  /**
30
41
  * Data schemas configuration per opportunity type.
31
42
  *
@@ -85,6 +96,8 @@ export const DATA_SCHEMAS = {
85
96
  ).required(),
86
97
  jiraLink: Joi.string().uri().allow(null).optional(),
87
98
  aggregationKey: Joi.string().optional(),
99
+ patchContent: Joi.string().optional(),
100
+ isCodeChangeAvailable: Joi.boolean().optional(),
88
101
  }).unknown(true),
89
102
  projections: {
90
103
  minimal: {
@@ -113,6 +126,8 @@ export const DATA_SCHEMAS = {
113
126
  ).required(),
114
127
  jiraLink: Joi.string().uri().allow(null).optional(),
115
128
  aggregationKey: Joi.string().optional(),
129
+ patchContent: Joi.string().optional(),
130
+ isCodeChangeAvailable: Joi.boolean().optional(),
116
131
  }).unknown(true),
117
132
  projections: {
118
133
  minimal: {
@@ -123,10 +138,14 @@ export const DATA_SCHEMAS = {
123
138
  },
124
139
  },
125
140
  },
141
+ // CWV has two implicit data shapes:
142
+ // 1. Page-level (type='url'): url and issues are present
143
+ // 2. Group-type (type='group'): url is absent, issues may be populated later via update
144
+ // Both shapes share the same schema; url and issues are optional to support both.
126
145
  [OPPORTUNITY_TYPES.CWV]: {
127
146
  schema: Joi.object({
128
147
  type: Joi.string().required(),
129
- url: Joi.string().uri().required(),
148
+ url: Joi.string().uri().optional(),
130
149
  pageviews: Joi.number().optional(),
131
150
  organic: Joi.number().optional(),
132
151
  metrics: Joi.array().items(
@@ -144,7 +163,7 @@ export const DATA_SCHEMAS = {
144
163
  organic: Joi.number().optional(),
145
164
  }).unknown(true),
146
165
  ).required(),
147
- issues: Joi.array().items(Joi.object()).required(),
166
+ issues: Joi.array().items(Joi.object()).optional().default([]),
148
167
  jiraLink: Joi.string().uri().allow(null).optional(),
149
168
  aggregationKey: Joi.string().allow(null).optional(),
150
169
  }).unknown(true),
@@ -163,6 +182,7 @@ export const DATA_SCHEMAS = {
163
182
  Joi.object({
164
183
  isAppropriate: Joi.boolean().optional(),
165
184
  isDecorative: Joi.boolean().optional(),
185
+ hasAltAttribute: Joi.boolean().optional(),
166
186
  xpath: Joi.string().optional(),
167
187
  altText: Joi.string().optional(),
168
188
  imageUrl: Joi.string().uri().optional(),
@@ -376,12 +396,16 @@ export const DATA_SCHEMAS = {
376
396
  [OPPORTUNITY_TYPES.BROKEN_INTERNAL_LINKS]: {
377
397
  schema: Joi.object({
378
398
  // Support both naming conventions (snake_case and camelCase)
379
- url_from: Joi.string().uri().optional(),
380
- urlFrom: Joi.string().uri().optional(),
381
- url_to: Joi.string().uri().optional(),
382
- urlTo: Joi.string().uri().optional(),
399
+ // URL fields use relaxedUrl instead of Joi.string().uri() because
400
+ // internal-links sometimes keeps URL values as-is, including malformed URLs.
401
+ // Unlike BROKEN_BACKLINKS, these accept malformed http/https/relative URLs
402
+ // but reject dangerous schemes (javascript:, data:, blob:, etc.).
403
+ url_from: relaxedUrl.optional(),
404
+ urlFrom: relaxedUrl.optional(),
405
+ url_to: relaxedUrl.optional(),
406
+ urlTo: relaxedUrl.optional(),
383
407
  title: Joi.string().optional(),
384
- urlsSuggested: Joi.array().items(Joi.string().uri()).optional(),
408
+ urlsSuggested: Joi.array().items(relaxedUrl).optional(),
385
409
  aiRationale: Joi.string().optional(),
386
410
  trafficDomain: Joi.number().optional(),
387
411
  priority: Joi.string().optional(),