@adobe/spacecat-shared-data-access 3.72.0 → 3.73.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.73.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.72.1...@adobe/spacecat-shared-data-access-v3.73.0) (2026-05-28)
2
+
3
+ ### Features
4
+
5
+ * **data-access:** rename BrandSemrushProject accessors (LLMO-5190) ([#1635](https://github.com/adobe/spacecat-shared/issues/1635)) ([f97b09d](https://github.com/adobe/spacecat-shared/commit/f97b09df6f15323baa74ede7aa1f431ba7d68293))
6
+
7
+ ## [@adobe/spacecat-shared-data-access-v3.72.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.72.0...@adobe/spacecat-shared-data-access-v3.72.1) (2026-05-28)
8
+
9
+ ### Bug Fixes
10
+
11
+ * (data-access) include all fixes in getAllFixesWithSuggestionsByOpportunityId (SITES-45307) ([#1634](https://github.com/adobe/spacecat-shared/issues/1634)) ([49e073e](https://github.com/adobe/spacecat-shared/commit/49e073e9e79813597fb11a922fc9f561136ce1d8))
12
+
1
13
  ## [@adobe/spacecat-shared-data-access-v3.72.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.71.2...@adobe/spacecat-shared-data-access-v3.72.0) (2026-05-27)
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.72.0",
3
+ "version": "3.73.0",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -22,17 +22,17 @@ class BrandSemrushProjectCollection extends BaseCollection {
22
22
  static COLLECTION_NAME = 'BrandSemrushProjectCollection';
23
23
 
24
24
  /**
25
- * Find the single row for a (brand, semrushLocationId, language) slice, or
26
- * null. Used by spacecat-api-service POST /v2/orgs/.../semrush/projects to
27
- * 409 on a duplicate slice before calling Semrush.
25
+ * Find the single row for a (brand, geoTargetId, languageCode) slice, or
26
+ * null. Used by spacecat-api-service POST /v2/orgs/.../serenity/markets to
27
+ * 409 on a duplicate slice before calling the upstream.
28
28
  *
29
29
  * @param {string} brandId
30
- * @param {number} semrushLocationId
31
- * @param {string} language
30
+ * @param {number} geoTargetId Google Ads Geo Target ID.
31
+ * @param {string} languageCode BCP-47 primary subtag.
32
32
  * @returns {Promise<BrandSemrushProject|null>}
33
33
  */
34
- async findBySlice(brandId, semrushLocationId, language) {
35
- return this.findByIndexKeys({ brandId, semrushLocationId, language });
34
+ async findBySlice(brandId, geoTargetId, languageCode) {
35
+ return this.findByIndexKeys({ brandId, geoTargetId, languageCode });
36
36
  }
37
37
  }
38
38
 
@@ -13,10 +13,12 @@
13
13
  import BaseModel from '../base/base.model.js';
14
14
 
15
15
  /**
16
- * BrandSemrushProject - mapping between an Adobe brand and a Semrush AIO
17
- * project. One row per (brand, semrushLocationId, language) slice. The Semrush
18
- * workspace is reachable via brand -> organization.getSemrushWorkspaceId() and
19
- * is not duplicated on this entity.
16
+ * BrandSemrushProject - mapping between an Adobe brand and an upstream
17
+ * Semrush AIO project. One row per (brand, geoTargetId, languageCode) slice.
18
+ * The Semrush workspace is reachable via
19
+ * brand -> organization.getSemrushWorkspaceId() and is not duplicated on this
20
+ * entity. Entity + table names retain the `Semrush` prefix as internal
21
+ * storage identifiers; the public JS accessors do not.
20
22
  *
21
23
  * @class BrandSemrushProject
22
24
  * @extends BaseModel
@@ -16,11 +16,16 @@ import SchemaBuilder from '../base/schema.builder.js';
16
16
  import BrandSemrushProject from './brand-semrush-project.model.js';
17
17
  import BrandSemrushProjectCollection from './brand-semrush-project.collection.js';
18
18
 
19
+ // "upstream" in the comments below refers to Semrush AIO — the entity name,
20
+ // table name, and `semrushProjectId` accessor stay because the value IS the
21
+ // upstream identifier. The public API surface decouples via the LLMO-5190
22
+ // rename plan (see the schema attributes' `postgrestField` overrides).
23
+
19
24
  // Mirrors the CHECK constraint on brand_to_semrush_projects.language
20
25
  // (db/migrations/20260528000000_brand_to_semrush_projects.sql). The slice
21
26
  // uniqueness gate is an exact DB string match, so inconsistent casing or
22
27
  // format between callers would silently bypass the 409 and bill a duplicate
23
- // Semrush project. Accepts BCP-47-shaped tags: 2/3-letter primary subtag
28
+ // upstream project. Accepts BCP-47-shaped tags: 2/3-letter primary subtag
24
29
  // (`en`, `de`, `zho`) optionally followed by a 2-4-letter region/script
25
30
  // subtag (`de-ch`, `pt-br`, `zh-hant`). All lowercase by contract.
26
31
  const LANGUAGE_TAG_REGEX = /^[a-z]{2,3}(-[a-z]{2,4})?$/;
@@ -45,15 +50,23 @@ const schema = new SchemaBuilder(BrandSemrushProject, BrandSemrushProjectCollect
45
50
  required: true,
46
51
  validate: (value) => hasText(value),
47
52
  })
48
- .addAttribute('semrushLocationId', {
53
+ // Google Ads Geo Target ID (criterion_id for the country). Semrush re-uses
54
+ // it as their location identifier; the value is owned by Google Ads, not
55
+ // Semrush. DB column stays `semrush_location_id` — the table layout is
56
+ // subject to change independently (see LLMO-5190 plan §Context).
57
+ .addAttribute('geoTargetId', {
49
58
  type: 'number',
50
59
  required: true,
51
60
  validate: (value) => Number.isInteger(value) && value > 0,
61
+ postgrestField: 'semrush_location_id',
52
62
  })
53
- .addAttribute('language', {
63
+ // BCP-47 primary subtag (`en`, `de`, `fr`), optionally with a region/script
64
+ // subtag (`de-ch`, `pt-br`, `zh-hant`). DB column stays `language`.
65
+ .addAttribute('languageCode', {
54
66
  type: 'string',
55
67
  required: true,
56
68
  validate: (value) => hasText(value) && LANGUAGE_TAG_REGEX.test(value),
69
+ postgrestField: 'language',
57
70
  })
58
71
  .addAllIndex(['semrushProjectId']);
59
72
 
@@ -22,11 +22,11 @@ import type {
22
22
  export interface BrandSemrushProject extends BaseModel {
23
23
  getBrandId(): string;
24
24
  getSemrushProjectId(): string;
25
- getSemrushLocationId(): number;
26
- getLanguage(): string;
25
+ getGeoTargetId(): number;
26
+ getLanguageCode(): string;
27
27
  setSemrushProjectId(value: string): BrandSemrushProject;
28
- setSemrushLocationId(value: number): BrandSemrushProject;
29
- setLanguage(value: string): BrandSemrushProject;
28
+ setGeoTargetId(value: number): BrandSemrushProject;
29
+ setLanguageCode(value: string): BrandSemrushProject;
30
30
  }
31
31
 
32
32
  export interface BrandSemrushProjectCollection extends
@@ -37,7 +37,7 @@ export interface BrandSemrushProjectCollection extends
37
37
  findBySemrushProjectId(semrushProjectId: string): Promise<BrandSemrushProject | null>;
38
38
  findBySlice(
39
39
  brandId: string,
40
- semrushLocationId: number,
41
- language: string,
40
+ geoTargetId: number,
41
+ languageCode: string,
42
42
  ): Promise<BrandSemrushProject | null>;
43
43
  }
@@ -193,15 +193,15 @@ class FixEntityCollection extends BaseCollection {
193
193
 
194
194
  /**
195
195
  * Gets all fixes with their suggestions for a specific opportunity.
196
- * Fetches all junction records for the opportunity, then batch-loads
197
- * fix entities and suggestions. Actual PostgREST call count is
198
- * 1 + ceil(N/50) + ceil(M/50) due to batchGetByKeys chunking.
196
+ * Fetches fix entities and mapping records in parallel, then batch-loads
197
+ * suggestions. Fixes without mapping records are included with
198
+ * suggestions: [].
199
199
  *
200
200
  * @async
201
201
  * @param {string} opportunityId - The ID of the opportunity.
202
202
  * @returns {Promise<Array>} - A promise that resolves to an array of objects containing:
203
203
  * - fixEntity: The FixEntity model
204
- * - suggestions: Array of associated Suggestion models
204
+ * - suggestions: Array of associated Suggestion models (empty if none mapped)
205
205
  * @throws {DataAccessError} - Throws an error if the query fails.
206
206
  * @throws {ValidationError} - Throws an error if opportunityId is not provided.
207
207
  */
@@ -211,10 +211,12 @@ class FixEntityCollection extends BaseCollection {
211
211
  try {
212
212
  const fixEntitySuggestionCollection = this.entityRegistry.getCollection('FixEntitySuggestionCollection');
213
213
 
214
- const fixEntitySuggestions = await fixEntitySuggestionCollection
215
- .allByIndexKeys({ opportunityId });
214
+ const [allFixEntities, fixEntitySuggestions] = await Promise.all([
215
+ this.allByOpportunityId(opportunityId),
216
+ fixEntitySuggestionCollection.allByIndexKeys({ opportunityId }),
217
+ ]);
216
218
 
217
- return this.#buildFixesWithSuggestions(fixEntitySuggestions);
219
+ return this.#buildAllFixesWithSuggestions(allFixEntities, fixEntitySuggestions);
218
220
  } catch (error) {
219
221
  if (error instanceof DataAccessError) {
220
222
  throw error;
@@ -276,6 +278,44 @@ class FixEntityCollection extends BaseCollection {
276
278
 
277
279
  return result;
278
280
  }
281
+
282
+ async #buildAllFixesWithSuggestions(allFixEntities, fixEntitySuggestions) {
283
+ if (allFixEntities.length === 0) {
284
+ return [];
285
+ }
286
+
287
+ const suggestionIdsByFixEntityId = {};
288
+ for (const junction of fixEntitySuggestions) {
289
+ const fixEntityId = junction.getFixEntityId();
290
+ const suggestionId = junction.getSuggestionId();
291
+ if (!suggestionIdsByFixEntityId[fixEntityId]) {
292
+ suggestionIdsByFixEntityId[fixEntityId] = [];
293
+ }
294
+ suggestionIdsByFixEntityId[fixEntityId].push(suggestionId);
295
+ }
296
+
297
+ const allSuggestionIds = Object.values(suggestionIdsByFixEntityId).flat();
298
+ const suggestionsById = {};
299
+
300
+ if (allSuggestionIds.length > 0) {
301
+ const suggestionCollection = this.entityRegistry.getCollection('SuggestionCollection');
302
+ const suggestions = await suggestionCollection.batchGetByKeys(
303
+ allSuggestionIds.map((id) => ({ [suggestionCollection.idName]: id })),
304
+ );
305
+ for (const suggestion of suggestions.data) {
306
+ suggestionsById[suggestion.getId()] = suggestion;
307
+ }
308
+ }
309
+
310
+ return allFixEntities.map((fixEntity) => {
311
+ const fixEntityId = fixEntity.getId();
312
+ const suggestionIds = suggestionIdsByFixEntityId[fixEntityId] || [];
313
+ const suggestions = suggestionIds
314
+ .map((id) => suggestionsById[id])
315
+ .filter(Boolean);
316
+ return { fixEntity, suggestions };
317
+ });
318
+ }
279
319
  }
280
320
 
281
321
  export default FixEntityCollection;