@adobe/spacecat-shared-data-access 3.74.0 → 3.74.2

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.74.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.74.1...@adobe/spacecat-shared-data-access-v3.74.2) (2026-06-11)
2
+
3
+ ### Bug Fixes
4
+
5
+ * reset linked issue status to NEW on FixEntity removal ([#1668](https://github.com/adobe/spacecat-shared/issues/1668)) ([46c54b8](https://github.com/adobe/spacecat-shared/commit/46c54b8829ea3b00029a4afa0cb835f3e7983dc9))
6
+
7
+ ## [@adobe/spacecat-shared-data-access-v3.74.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.74.0...@adobe/spacecat-shared-data-access-v3.74.1) (2026-06-09)
8
+
9
+ ### Bug Fixes
10
+
11
+ * site-level handler overrides were ignored, org-level disable always won ([#1590](https://github.com/adobe/spacecat-shared/issues/1590)) ([cfeca81](https://github.com/adobe/spacecat-shared/commit/cfeca81266351c7523d939cb3a2f50831599b927)), closes [#updatedHandler](https://github.com/adobe/spacecat-shared/issues/updatedHandler)
12
+
1
13
  ## [@adobe/spacecat-shared-data-access-v3.74.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.73.3...@adobe/spacecat-shared-data-access-v3.74.0) (2026-06-02)
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.74.0",
3
+ "version": "3.74.2",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -174,6 +174,21 @@ class Configuration {
174
174
  .map((job) => job.type);
175
175
  }
176
176
 
177
+ /**
178
+ * Returns whether a handler is enabled for a given site.
179
+ *
180
+ * Precedence (highest to lowest):
181
+ * 1. disabled.sites includes siteId → false (site explicitly disabled)
182
+ * 2. enabled.sites includes siteId → true (site explicitly enabled; overrides org-level)
183
+ * 3. disabled.orgs includes orgId → false (org disabled, no site-level override present)
184
+ * 4. enabledByDefault → true
185
+ * 5. enabled.orgs includes orgId → true
186
+ * 6. otherwise → false
187
+ *
188
+ * Note: disabled.sites and enabled.sites are assumed disjoint; disabled.sites wins if violated
189
+ * (see #updatedHandler which keeps these lists disjoint on every write).
190
+ * isHandlerEnabledForOrg uses a different (simpler) precedence — see its JSDoc.
191
+ */
177
192
  isHandlerEnabledForSite(type, site) {
178
193
  const handler = this.getHandlers()?.[type];
179
194
  if (!handler) {
@@ -183,26 +198,53 @@ class Configuration {
183
198
  const siteId = site.getId();
184
199
  const orgId = site.getOrganizationId();
185
200
 
186
- if (handler.disabled) {
187
- const sites = handler.disabled.sites || [];
188
- const orgs = handler.disabled.orgs || [];
189
- if (sites.includes(siteId) || orgs.includes(orgId)) {
190
- return false;
201
+ const disabledSites = handler.disabled?.sites || [];
202
+ const enabledSites = handler.enabled?.sites || [];
203
+ const disabledOrgs = handler.disabled?.orgs || [];
204
+ const enabledOrgs = handler.enabled?.orgs || [];
205
+
206
+ // Site-level takes priority over org-level: an explicit site enable/disable
207
+ // overrides the org-level setting, allowing a single site to be upgraded to
208
+ // a paid profile even if the org is in disabled.orgs.
209
+ if (disabledSites.includes(siteId)) {
210
+ return false;
211
+ }
212
+ if (enabledSites.includes(siteId)) {
213
+ if (disabledOrgs.includes(orgId)) {
214
+ // TODO(SITES-44467): demote to debug after 30-day canary window
215
+ this.log.info('[isHandlerEnabledForSite] site-override: site in enabled.sites overrides org in disabled.orgs', { type, siteId, orgId });
191
216
  }
217
+ return true;
192
218
  }
193
219
 
220
+ if (disabledOrgs.includes(orgId)) {
221
+ return false;
222
+ }
194
223
  if (handler.enabledByDefault) {
195
224
  return true;
196
225
  }
197
- if (handler.enabled) {
198
- const sites = handler.enabled.sites || [];
199
- const orgs = handler.enabled.orgs || [];
200
- return sites.includes(siteId) || orgs.includes(orgId);
201
- }
202
-
203
- return false;
226
+ return enabledOrgs.includes(orgId);
204
227
  }
205
228
 
229
+ /**
230
+ * Returns whether a handler is enabled for a given org.
231
+ *
232
+ * Precedence (highest to lowest):
233
+ * 1. disabled.orgs includes orgId → false
234
+ * 2. enabledByDefault → true
235
+ * 3. enabled.orgs includes orgId → true
236
+ * 4. otherwise → false
237
+ *
238
+ * Note: unlike isHandlerEnabledForSite, there is no site-level override here.
239
+ * An org in disabled.orgs is always disabled regardless of site-level entries.
240
+ *
241
+ * Cross-system consequence: callers that fan out on org policy (e.g. org-scoped
242
+ * digests or batch jobs keyed on orgId) use this method as their gate. A site in
243
+ * enabled.sites whose org is in disabled.orgs will pass site-level evaluation
244
+ * (true via isHandlerEnabledForSite) but will NOT pass org-level evaluation
245
+ * (false here). This asymmetry is intentional: org-scoped fan-out follows org
246
+ * policy; site-level overrides do not propagate to org-scoped callers.
247
+ */
206
248
  isHandlerEnabledForOrg(type, org) {
207
249
  const handler = this.getHandlers()?.[type];
208
250
  if (!handler) {
@@ -243,20 +285,21 @@ class Configuration {
243
285
  }
244
286
 
245
287
  if (enabled) {
246
- if (handler.enabledByDefault) {
247
- handler.disabled[entityKey] = handler.disabled[entityKey]
248
- /* c8 ignore next */
249
- .filter((id) => id !== entityId) || [];
250
- } else {
251
- handler.enabled[entityKey] = Array
252
- .from(new Set([...(handler.enabled[entityKey] || []), entityId]));
253
- }
288
+ // Always remove from disabled first (handles re-enabling a previously disabled entry).
289
+ handler.disabled[entityKey] = (handler.disabled[entityKey] || [])
290
+ .filter((id) => id !== entityId);
291
+ // Always add to enabled so isHandlerEnabledForSite can find the site in enabled.sites
292
+ // and return true even when the org is in disabled.orgs (site-level override).
293
+ handler.enabled[entityKey] = Array
294
+ .from(new Set([...(handler.enabled[entityKey] || []), entityId]));
254
295
  } else if (handler.enabledByDefault) {
255
296
  handler.disabled[entityKey] = Array
256
297
  .from(new Set([...(handler.disabled[entityKey] || []), entityId]));
298
+ handler.enabled[entityKey] = (handler.enabled[entityKey] || [])
299
+ .filter((id) => id !== entityId);
257
300
  } else {
258
- /* c8 ignore next */
259
- handler.enabled[entityKey] = handler.enabled[entityKey].filter((id) => id !== entityId) || [];
301
+ handler.enabled[entityKey] = (handler.enabled[entityKey] || [])
302
+ .filter((id) => id !== entityId);
260
303
  }
261
304
 
262
305
  handlers[type] = handler;
@@ -44,6 +44,50 @@ class FixEntity extends BaseModel {
44
44
  return fixEntityCollection
45
45
  .getSuggestionsByFixEntityId(this.getId());
46
46
  }
47
+
48
+ /**
49
+ * Removes this FixEntity. When the fix is bound to a specific issue (granular CWV
50
+ * model — `changeDetails.issueId` is set), also rewinds every linked Suggestion so
51
+ * the issue is reopenable: clears `data.issues[issueId].fixEntityId` and resets
52
+ * that issue's status to NEW. The fix row and its junction rows are deleted by the
53
+ * inherited remove flow.
54
+ *
55
+ * Skipped when the fix has no issueId (legacy / non-CWV fixes) — same behavior as
56
+ * before. Also skipped when this method is reached as a dependent cascade (parent
57
+ * removal goes through `_remove()` directly), since the parent teardown moots the
58
+ * per-issue rewind.
59
+ */
60
+ async remove() {
61
+ const issueId = this.record?.changeDetails?.issueId;
62
+ if (issueId) {
63
+ await this.#resetLinkedIssues(issueId);
64
+ }
65
+ return super.remove();
66
+ }
67
+
68
+ async #resetLinkedIssues(issueId) {
69
+ const suggestions = await this.getSuggestions();
70
+
71
+ await Promise.all(suggestions.map(async (suggestion) => {
72
+ const data = suggestion.getData();
73
+ if (!data || !Array.isArray(data.issues)) {
74
+ return;
75
+ }
76
+ let mutated = false;
77
+ const nextIssues = data.issues.map((issue) => {
78
+ if (issue && issue.id === issueId) {
79
+ mutated = true;
80
+ return { ...issue, status: 'NEW', fixEntityId: null };
81
+ }
82
+ return issue;
83
+ });
84
+ if (!mutated) {
85
+ return;
86
+ }
87
+ suggestion.setData({ ...data, issues: nextIssues });
88
+ await suggestion.save();
89
+ }));
90
+ }
47
91
  }
48
92
 
49
93
  export default FixEntity;