@adobe/spacecat-shared-data-access 3.25.0 → 3.27.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.27.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.26.0...@adobe/spacecat-shared-data-access-v3.27.0) (2026-03-20)
2
+
3
+ ### Features
4
+
5
+ * add countryCodeIgnoreList to site llmo config ([#1447](https://github.com/adobe/spacecat-shared/issues/1447)) ([6d824f8](https://github.com/adobe/spacecat-shared/commit/6d824f855aeadf623776a31e305270ae15fa1be2))
6
+
7
+ ## [@adobe/spacecat-shared-data-access-v3.26.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.25.0...@adobe/spacecat-shared-data-access-v3.26.0) (2026-03-19)
8
+
9
+ ### Features
10
+
11
+ * add access control helpers for cross-org delegation (Option 2a) ([#1453](https://github.com/adobe/spacecat-shared/issues/1453)) ([960623e](https://github.com/adobe/spacecat-shared/commit/960623ea9e77e849be8b0a5bd7ee047309377cef)), closes [adobe/spacecat-auth-service#503](https://github.com/adobe/spacecat-auth-service/issues/503) [#1448](https://github.com/adobe/spacecat-shared/issues/1448)
12
+
1
13
  ## [@adobe/spacecat-shared-data-access-v3.25.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.24.0...@adobe/spacecat-shared-data-access-v3.25.0) (2026-03-19)
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.25.0",
3
+ "version": "3.27.0",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -585,6 +585,20 @@ class BaseCollection {
585
585
  return this.#queryByIndexKeys(keys, { ...options, limit: 1 });
586
586
  }
587
587
 
588
+ /**
589
+ * Converts a raw PostgREST row (snake_case) to a model instance using the same field mapping
590
+ * pipeline as the internal create/find paths. Intended for use by collections that receive
591
+ * embedded sub-rows from PostgREST resource embedding (e.g. `sites!fkey(*)`), allowing them
592
+ * to return proper model instances rather than raw snake_case objects.
593
+ *
594
+ * @param {object} row - Raw PostgREST row with snake_case column names.
595
+ * @returns {object|null} A model instance, or null if the row is empty/invalid.
596
+ */
597
+ createInstanceFromRow(row) {
598
+ if (!isNonEmptyObject(row)) return null;
599
+ return this.#createInstance(this.#toModelRecord(row));
600
+ }
601
+
588
602
  async findById(id) {
589
603
  guardId(this.idName, id, this.entityName);
590
604
  if (this.entity) {
@@ -343,6 +343,9 @@ export const configSchema = Joi.object({
343
343
  type: Joi.string().valid('include', 'exclude').optional(),
344
344
  }),
345
345
  ).optional(),
346
+ countryCodeIgnoreList: Joi.array().items(
347
+ Joi.string().length(2),
348
+ ).optional(),
346
349
  cdnBucketConfig: Joi.object({
347
350
  bucketName: Joi.string().optional(),
348
351
  orgId: Joi.string().optional(),
@@ -487,6 +490,7 @@ export const Config = (data = {}) => {
487
490
  return llmoConfig?.customerIntent || [];
488
491
  };
489
492
  self.getLlmoCdnlogsFilter = () => state?.llmo?.cdnlogsFilter;
493
+ self.getLlmoCountryCodeIgnoreList = () => state?.llmo?.countryCodeIgnoreList;
490
494
  self.getLlmoCdnBucketConfig = () => state?.llmo?.cdnBucketConfig;
491
495
  self.getTokowakaConfig = () => state?.tokowakaConfig;
492
496
  self.getEdgeOptimizeConfig = () => state?.edgeOptimizeConfig;
@@ -635,6 +639,11 @@ export const Config = (data = {}) => {
635
639
  state.llmo.cdnlogsFilter = cdnlogsFilter;
636
640
  };
637
641
 
642
+ self.updateLlmoCountryCodeIgnoreList = (countryCodeIgnoreList) => {
643
+ state.llmo = state.llmo || {};
644
+ state.llmo.countryCodeIgnoreList = countryCodeIgnoreList;
645
+ };
646
+
638
647
  self.updateLlmoCdnBucketConfig = (cdnBucketConfig) => {
639
648
  state.llmo = state.llmo || {};
640
649
  state.llmo.cdnBucketConfig = cdnBucketConfig;
@@ -35,22 +35,19 @@ export interface SiteImsOrgAccess extends BaseModel {
35
35
  }
36
36
 
37
37
  export interface SiteImsOrgAccessGrantWithTarget {
38
- grant: {
39
- id: string;
40
- siteId: string;
41
- organizationId: string;
42
- targetOrganizationId: string;
43
- productCode: EntitlementProductCode;
44
- role: SiteImsOrgAccessRole;
45
- grantedBy: string | null;
46
- expiresAt: string | null;
47
- };
38
+ grant: SiteImsOrgAccess;
48
39
  targetOrganization: {
49
40
  id: string;
50
41
  imsOrgId: string;
51
42
  };
52
43
  }
53
44
 
45
+ export interface SiteImsOrgAccessGrantWithSite {
46
+ grant: SiteImsOrgAccess;
47
+ /** Site model instance. Null only if the FK is broken (should not occur given ON DELETE CASCADE). */
48
+ site: Site | null;
49
+ }
50
+
54
51
  export interface SiteImsOrgAccessCollection extends
55
52
  BaseCollection<SiteImsOrgAccess> {
56
53
  allBySiteId(siteId: string): Promise<SiteImsOrgAccess[]>;
@@ -58,8 +55,10 @@ export interface SiteImsOrgAccessCollection extends
58
55
  allByTargetOrganizationId(targetOrganizationId: string): Promise<SiteImsOrgAccess[]>;
59
56
  allByOrganizationIdWithTargetOrganization(organizationId: string): Promise<SiteImsOrgAccessGrantWithTarget[]>;
60
57
  allByOrganizationIdsWithTargetOrganization(organizationIds: string[]): Promise<SiteImsOrgAccessGrantWithTarget[]>;
58
+ allByOrganizationIdWithSites(organizationId: string): Promise<SiteImsOrgAccessGrantWithSite[]>;
61
59
 
62
60
  findBySiteId(siteId: string): Promise<SiteImsOrgAccess | null>;
63
61
  findByOrganizationId(organizationId: string): Promise<SiteImsOrgAccess | null>;
64
62
  findByTargetOrganizationId(targetOrganizationId: string): Promise<SiteImsOrgAccess | null>;
63
+ findBySiteIdAndOrganizationIdAndProductCode(siteId: string, organizationId: string, productCode: EntitlementProductCode): Promise<SiteImsOrgAccess | null>;
65
64
  }
@@ -79,11 +79,16 @@ class SiteImsOrgAccessCollection extends BaseCollection {
79
79
  }
80
80
 
81
81
  /**
82
+ * Shared pagination loop for PostgREST embedding queries. Fetches all pages and maps
83
+ * each row using the provided mapper function.
84
+ *
82
85
  * @param {object} query - PostgREST query builder (result of .from(...).select(...))
83
- * @returns {Promise<Array<{grant: object, targetOrganization: object}>>}
86
+ * @param {Function} mapRow - Maps a raw row to the desired return shape
87
+ * @param {string} errorMessage - Used for logging and DataAccessError message
88
+ * @returns {Promise<Array>}
84
89
  * @private
85
90
  */
86
- async #fetchGrantsWithTargetOrg(query) {
91
+ async #fetchPaginatedGrants(query, mapRow, errorMessage) {
87
92
  const allResults = [];
88
93
  let offset = 0;
89
94
  let keepGoing = true;
@@ -94,9 +99,9 @@ class SiteImsOrgAccessCollection extends BaseCollection {
94
99
  const { data, error } = await orderedQuery.range(offset, offset + DEFAULT_PAGE_SIZE - 1);
95
100
 
96
101
  if (error) {
97
- this.log.error(`[SiteImsOrgAccess] Failed to query grants with target org - ${error.message}`, error);
102
+ this.log.error(`[SiteImsOrgAccess] ${errorMessage} - ${error.message}`, error);
98
103
  throw new DataAccessError(
99
- 'Failed to query grants with target organization',
104
+ errorMessage,
100
105
  { entityName: 'SiteImsOrgAccess', tableName: 'site_ims_org_accesses' },
101
106
  error,
102
107
  );
@@ -111,22 +116,23 @@ class SiteImsOrgAccessCollection extends BaseCollection {
111
116
  }
112
117
  }
113
118
 
114
- return allResults.map((row) => ({
115
- grant: {
116
- id: row.id,
117
- siteId: row.site_id,
118
- organizationId: row.organization_id,
119
- targetOrganizationId: row.target_organization_id,
120
- productCode: row.product_code,
121
- role: row.role,
122
- grantedBy: row.granted_by,
123
- expiresAt: row.expires_at,
124
- },
125
- targetOrganization: {
126
- id: row.organizations.id,
127
- imsOrgId: row.organizations.ims_org_id,
128
- },
129
- }));
119
+ return allResults.map(mapRow);
120
+ }
121
+
122
+ /**
123
+ * @param {object} query - PostgREST query builder
124
+ * @returns {Promise<Array<{grant: SiteImsOrgAccess, targetOrganization: object}>>}
125
+ * @private
126
+ */
127
+ async #fetchGrantsWithTargetOrg(query) {
128
+ return this.#fetchPaginatedGrants(
129
+ query,
130
+ (row) => ({
131
+ grant: this.createInstanceFromRow(row),
132
+ targetOrganization: { id: row.organizations.id, imsOrgId: row.organizations.ims_org_id },
133
+ }),
134
+ 'Failed to query grants with target organization',
135
+ );
130
136
  }
131
137
 
132
138
  /**
@@ -185,6 +191,74 @@ class SiteImsOrgAccessCollection extends BaseCollection {
185
191
  this.postgrestService.from('site_ims_org_accesses').select(select).in('organization_id', organizationIds),
186
192
  );
187
193
  }
194
+
195
+ /**
196
+ * Finds a single grant by the compound key (siteId, organizationId, productCode).
197
+ * Used by hasAccess() in the api-service to verify a grant still exists (Path A revocation
198
+ * check) or to perform a direct DB lookup when the JWT list was truncated (Path B).
199
+ *
200
+ * Returns a model instance so callers can use getExpiresAt(), getRole(), etc.
201
+ * Returns null when no matching grant exists.
202
+ *
203
+ * @param {string} siteId - UUID of the site.
204
+ * @param {string} organizationId - UUID of the delegate organization.
205
+ * @param {string} productCode - Product code (e.g. 'LLMO', 'ASO').
206
+ * @returns {Promise<SiteImsOrgAccess|null>}
207
+ */
208
+ async findBySiteIdAndOrganizationIdAndProductCode(siteId, organizationId, productCode) {
209
+ if (!siteId || !organizationId || !productCode) {
210
+ throw new DataAccessError(
211
+ 'siteId, organizationId and productCode are required',
212
+ { entityName: 'SiteImsOrgAccess', tableName: 'site_ims_org_accesses' },
213
+ );
214
+ }
215
+ return this.findByIndexKeys({ siteId, organizationId, productCode });
216
+ }
217
+
218
+ /**
219
+ * Returns all grants for the given delegate organization with the full site row embedded
220
+ * via PostgREST resource embedding (INNER JOIN on site_id FK). This is a single round-trip
221
+ * query — no N+1 — suitable for populating the site dropdown for delegated users.
222
+ *
223
+ * Returns plain objects, not model instances. The `site` field contains the raw PostgREST
224
+ * row for the joined site (snake_case column names). It is null only when the FK is broken,
225
+ * which should not occur given ON DELETE CASCADE on site_id.
226
+ *
227
+ * @param {string} organizationId - UUID of the delegate organization.
228
+ * @returns {Promise<Array<{
229
+ * grant: {id: string, siteId: string, organizationId: string,
230
+ * targetOrganizationId: string, productCode: string, role: string,
231
+ * grantedBy: string|null, expiresAt: string|null},
232
+ * site: object|null
233
+ * }>>}
234
+ */
235
+ async allByOrganizationIdWithSites(organizationId) {
236
+ if (!organizationId) {
237
+ throw new DataAccessError('organizationId is required', { entityName: 'SiteImsOrgAccess', tableName: 'site_ims_org_accesses' });
238
+ }
239
+ // eslint-disable-next-line max-len
240
+ const select = 'id, site_id, organization_id, target_organization_id, product_code, role, granted_by, expires_at, sites!site_ims_org_accesses_site_id_fkey(*)';
241
+ return this.#fetchGrantsWithSite(
242
+ this.postgrestService.from('site_ims_org_accesses').select(select).eq('organization_id', organizationId),
243
+ );
244
+ }
245
+
246
+ /**
247
+ * @param {object} query - PostgREST query builder
248
+ * @returns {Promise<Array<{grant: SiteImsOrgAccess, site: Site|null}>>}
249
+ * @private
250
+ */
251
+ async #fetchGrantsWithSite(query) {
252
+ const siteCollection = this.entityRegistry.getCollection('SiteCollection');
253
+ return this.#fetchPaginatedGrants(
254
+ query,
255
+ (row) => ({
256
+ grant: this.createInstanceFromRow(row),
257
+ site: row.sites ? siteCollection.createInstanceFromRow(row.sites) : null,
258
+ }),
259
+ 'Failed to query grants with site',
260
+ );
261
+ }
188
262
  }
189
263
 
190
264
  export default SiteImsOrgAccessCollection;