@adobe/spacecat-shared-data-access 3.60.0 → 3.62.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,15 @@
1
+ ## [@adobe/spacecat-shared-data-access-v3.62.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.61.0...@adobe/spacecat-shared-data-access-v3.62.0) (2026-05-12)
2
+
3
+ ### Features
4
+
5
+ * **data-access:** add semrushWorkspaceId attribute to Organization ([#1602](https://github.com/adobe/spacecat-shared/issues/1602)) ([79a5d82](https://github.com/adobe/spacecat-shared/commit/79a5d825c2a3cb436d894f5bea90a0a4e230167b))
6
+
7
+ ## [@adobe/spacecat-shared-data-access-v3.61.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.60.0...@adobe/spacecat-shared-data-access-v3.61.0) (2026-05-12)
8
+
9
+ ### Features
10
+
11
+ * **data-access:** add scopeType and scopeId to Opportunity model ([#1576](https://github.com/adobe/spacecat-shared/issues/1576)) ([3f0f8f2](https://github.com/adobe/spacecat-shared/commit/3f0f8f25c058c5c885fd1f553dd43e4ed07b3d67)), closes [#32-42](https://github.com/adobe/spacecat-shared/issues/32-42)
12
+
1
13
  ## [@adobe/spacecat-shared-data-access-v3.60.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.59.0...@adobe/spacecat-shared-data-access-v3.60.0) (2026-05-11)
2
14
 
3
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-data-access",
3
- "version": "3.60.0",
3
+ "version": "3.62.0",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -21,27 +21,31 @@ export interface Opportunity extends BaseModel {
21
21
  getAuditId(): string;
22
22
  getData(): object;
23
23
  getDescription(): string;
24
+ getFixEntities(): Promise<FixEntity[]>;
24
25
  getGuidance(): string;
26
+ getLastAuditedAt(): string;
25
27
  getOrigin(): string;
26
28
  getRunbook(): string;
29
+ getScopeId(): string | undefined;
30
+ getScopeType(): string | undefined;
27
31
  getSite(): Promise<Site>;
28
32
  getSiteId(): string;
29
33
  getStatus(): string;
30
- getFixEntities(): Promise<FixEntity[]>;
31
34
  getSuggestions(): Promise<Suggestion[]>;
32
35
  getSuggestionsByStatus(status: string): Promise<Suggestion[]>;
33
36
  getSuggestionsByStatusAndRank(status: string, rank: string): Promise<Suggestion[]>;
34
- getLastAuditedAt(): string;
35
37
  getTags(): string[];
36
38
  getTitle(): string;
37
39
  getType(): string;
38
40
  setAuditId(auditId: string): Opportunity;
39
41
  setData(data: object): Opportunity;
40
42
  setDescription(description: string): Opportunity;
41
- setLastAuditedAt(lastAuditedAt: string): Opportunity;
42
43
  setGuidance(guidance: string): Opportunity;
44
+ setLastAuditedAt(lastAuditedAt: string): Opportunity;
43
45
  setOrigin(origin: string): Opportunity;
44
46
  setRunbook(runbook: string): Opportunity;
47
+ setScopeId(scopeId: string | null | undefined): Opportunity;
48
+ setScopeType(scopeType: 'brand' | null | undefined): Opportunity;
45
49
  setSiteId(siteId: string): Opportunity;
46
50
  setStatus(status: string): Opportunity;
47
51
  setTags(tags: string[]): Opportunity;
@@ -49,6 +53,7 @@ export interface Opportunity extends BaseModel {
49
53
  }
50
54
 
51
55
  export interface OpportunityCollection extends BaseCollection<Opportunity> {
56
+ allByScope(scopeType: string, scopeId: string): Promise<Opportunity[]>;
52
57
  allByAuditId(auditId: string): Promise<Opportunity[]>;
53
58
  allByAuditIdAndUpdatedAt(auditId: string, updatedAt: string): Promise<Opportunity[]>;
54
59
  allBySiteId(siteId: string): Promise<Opportunity[]>;
@@ -10,6 +10,9 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import { hasText } from '@adobe/spacecat-shared-utils';
14
+
15
+ import { ValidationError } from '../../errors/index.js';
13
16
  import BaseCollection from '../base/base.collection.js';
14
17
 
15
18
  /**
@@ -22,7 +25,91 @@ import BaseCollection from '../base/base.collection.js';
22
25
  class OpportunityCollection extends BaseCollection {
23
26
  static COLLECTION_NAME = 'OpportunityCollection';
24
27
 
25
- // add custom methods here
28
+ /**
29
+ * Validates and creates a new Opportunity. Enforces that scopeType and scopeId
30
+ * must both be present or both be absent — a half-scoped record is invalid.
31
+ *
32
+ * @param {object} item - The opportunity data.
33
+ * @param {object} [options] - Optional create options (e.g. { upsert: true }).
34
+ * @returns {Promise<Opportunity>} The created opportunity instance.
35
+ */
36
+ async create(item, options) {
37
+ const { scopeType, scopeId } = item || {};
38
+ if (hasText(scopeType) !== hasText(scopeId)) {
39
+ throw new ValidationError('scopeType and scopeId must both be set or both be absent', this);
40
+ }
41
+ return super.create(item, options);
42
+ }
43
+
44
+ /**
45
+ * Validates and bulk-creates Opportunities. Enforces the scopeType/scopeId
46
+ * co-presence invariant on every item — half-scoped records are invalid.
47
+ *
48
+ * Overrides BaseCollection.createMany() which would otherwise bypass the
49
+ * single-item create() guard and persist invalid scope tuples directly.
50
+ *
51
+ * @param {object[]} items - The opportunity items to create.
52
+ * @param {...*} rest - Additional arguments forwarded to BaseCollection.createMany.
53
+ * @returns {Promise<*>} The result from BaseCollection.createMany.
54
+ */
55
+ async createMany(items, ...rest) {
56
+ if (Array.isArray(items)) {
57
+ for (const item of items) {
58
+ const { scopeType, scopeId } = item || {};
59
+ if (hasText(scopeType) !== hasText(scopeId)) {
60
+ throw new ValidationError('scopeType and scopeId must both be set or both be absent', this);
61
+ }
62
+ }
63
+ }
64
+ return super.createMany(items, ...rest);
65
+ }
66
+
67
+ /**
68
+ * Validates and updates an Opportunity by its keys. Enforces the scopeType/scopeId
69
+ * co-presence invariant: if either field appears in the update payload, both must
70
+ * be set or both must be absent — a half-scoped update is invalid.
71
+ *
72
+ * Note: this guard enforces co-presence but allows scope re-attribution
73
+ * (moving an opportunity from one scopeId to another within the same scopeType).
74
+ * scopeId is the tenant boundary; callers must verify authorization before mutating it.
75
+ *
76
+ * @param {object} keys - The key attributes identifying the record.
77
+ * @param {object} updates - The fields to update.
78
+ * @returns {Promise<void>}
79
+ */
80
+ async updateByKeys(keys, updates) {
81
+ const hasScopeType = updates != null && 'scopeType' in updates;
82
+ const hasScopeId = updates != null && 'scopeId' in updates;
83
+ // If either field is included in the update, both must be included together,
84
+ // and their combined values must satisfy co-presence (both set or both absent).
85
+ if (hasScopeType !== hasScopeId) {
86
+ throw new ValidationError('scopeType and scopeId must both be set or both be absent', this);
87
+ }
88
+ if (hasScopeType && hasScopeId) {
89
+ const { scopeType, scopeId } = updates;
90
+ if (hasText(scopeType) !== hasText(scopeId)) {
91
+ throw new ValidationError('scopeType and scopeId must both be set or both be absent', this);
92
+ }
93
+ }
94
+ return super.updateByKeys(keys, updates);
95
+ }
96
+
97
+ /**
98
+ * Returns all opportunities matching a given scope type and scope ID.
99
+ *
100
+ * @param {string} scopeType - The scope type (e.g. 'brand').
101
+ * @param {string} scopeId - The scope entity UUID.
102
+ * @returns {Promise<Opportunity[]>} The matching opportunities.
103
+ */
104
+ async allByScope(scopeType, scopeId) {
105
+ if (!hasText(scopeType)) {
106
+ throw new Error('allByScope: scopeType is required');
107
+ }
108
+ if (!hasText(scopeId)) {
109
+ throw new Error('allByScope: scopeId is required');
110
+ }
111
+ return this.allByIndexKeys({ scopeType, scopeId });
112
+ }
26
113
  }
27
114
 
28
115
  export default OpportunityCollection;
@@ -10,6 +10,9 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import { hasText } from '@adobe/spacecat-shared-utils';
14
+
15
+ import { ValidationError } from '../../errors/index.js';
13
16
  import BaseModel from '../base/base.model.js';
14
17
 
15
18
  /**
@@ -37,6 +40,27 @@ class Opportunity extends BaseModel {
37
40
  RESOLVED: 'RESOLVED',
38
41
  };
39
42
 
43
+ static SCOPE_TYPES = {
44
+ BRAND: 'brand',
45
+ };
46
+
47
+ /**
48
+ * Overrides BaseModel.save() to enforce the co-presence invariant: scopeType and scopeId
49
+ * must both be set or both be absent. Guards the setter+save path in addition to create().
50
+ *
51
+ * @returns {Promise<Opportunity>} The saved opportunity.
52
+ * @throws {ValidationError} When only one of scopeType / scopeId is set.
53
+ */
54
+ async save() {
55
+ if (hasText(this.getScopeType()) !== hasText(this.getScopeId())) {
56
+ throw new ValidationError(
57
+ 'scopeType and scopeId must both be set or both be absent',
58
+ this,
59
+ );
60
+ }
61
+ return super.save();
62
+ }
63
+
40
64
  /**
41
65
  * Adds the given suggestions to this Opportunity. Sets this opportunity as the parent
42
66
  * of each suggestion, as such the opportunity ID does not need to be provided.
@@ -12,7 +12,9 @@
12
12
 
13
13
  /* c8 ignore start */
14
14
 
15
- import { isIsoDate, isNonEmptyObject, isValidUrl } from '@adobe/spacecat-shared-utils';
15
+ import {
16
+ isIsoDate, isNonEmptyObject, isValidUrl, isValidUUID,
17
+ } from '@adobe/spacecat-shared-utils';
16
18
 
17
19
  import SchemaBuilder from '../base/schema.builder.js';
18
20
  import Opportunity from './opportunity.model.js';
@@ -64,6 +66,15 @@ const schema = new SchemaBuilder(Opportunity, OpportunityCollection)
64
66
  .addAttribute('lastAuditedAt', {
65
67
  type: 'string',
66
68
  validate: (value) => !value || isIsoDate(value),
67
- });
69
+ })
70
+ .addAttribute('scopeType', {
71
+ type: 'string',
72
+ validate: (value) => !value || Object.values(Opportunity.SCOPE_TYPES).includes(value),
73
+ })
74
+ .addAttribute('scopeId', {
75
+ type: 'string',
76
+ validate: (value) => !value || isValidUUID(value),
77
+ })
78
+ .addIndex({ composite: ['scopeId'] }, { composite: ['scopeType'] });
68
79
 
69
80
  export default schema.build();
@@ -19,6 +19,7 @@ export interface Organization extends BaseModel {
19
19
  getFulfillableItems(): object;
20
20
  getImsOrgId(): string;
21
21
  getName(): string;
22
+ getSemrushWorkspaceId(): string;
22
23
  getSites(): Promise<Site[]>;
23
24
  getProjects(): Promise<Project[]>;
24
25
  getEntitlements(): Promise<Entitlement[]>;
@@ -28,9 +29,12 @@ export interface Organization extends BaseModel {
28
29
  setFulfillableItems(fulfillableItems: object): Organization;
29
30
  setImsOrgId(imsOrgId: string): Organization;
30
31
  setName(name: string): Organization;
32
+ setSemrushWorkspaceId(semrushWorkspaceId: string): Organization;
31
33
  }
32
34
 
33
35
  export interface OrganizationCollection extends BaseCollection<Organization> {
34
36
  allByImsOrgId(imsOrgId: string): Promise<Organization[]>;
37
+ allBySemrushWorkspaceId(semrushWorkspaceId: string): Promise<Organization[]>;
35
38
  findByImsOrgId(imsOrgId: string): Promise<Organization | null>;
39
+ findBySemrushWorkspaceId(semrushWorkspaceId: string): Promise<Organization | null>;
36
40
  }
@@ -12,7 +12,7 @@
12
12
 
13
13
  /* c8 ignore start */
14
14
 
15
- import { isNonEmptyObject } from '@adobe/spacecat-shared-utils';
15
+ import { hasText, isNonEmptyObject } from '@adobe/spacecat-shared-utils';
16
16
 
17
17
  import { Config, DEFAULT_CONFIG, validateConfiguration } from '../site/config.js';
18
18
  import SchemaBuilder from '../base/schema.builder.js';
@@ -48,6 +48,19 @@ const schema = new SchemaBuilder(Organization, OrganizationCollection)
48
48
  type: 'any',
49
49
  validate: (value) => !value || isNonEmptyObject(value),
50
50
  })
51
- .addAllIndex(['imsOrgId']);
51
+ .addAttribute('semrushWorkspaceId', {
52
+ type: 'string',
53
+ // Minimum guard: reject empty / whitespace-only strings. Full format
54
+ // validation deferred until Semrush confirms the workspace-ID format.
55
+ // Use value == null (loose) so undefined and null short-circuit, but
56
+ // empty string ('') falls through to hasText() which rejects it.
57
+ validate: (value) => value == null || hasText(value),
58
+ })
59
+ .addAllIndex(['imsOrgId'])
60
+ // Uniqueness is enforced at the DB level via the UNIQUE constraint on
61
+ // organizations.semrush_workspace_id (mysticat-data-service migration
62
+ // 20260525000000), so findBySemrushWorkspaceId is semantically guaranteed
63
+ // to return at most one row.
64
+ .addAllIndex(['semrushWorkspaceId']);
52
65
 
53
66
  export default schema.build();