@adobe/spacecat-shared-data-access 3.25.0 → 3.26.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,9 @@
|
|
|
1
|
+
## [@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)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* 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)
|
|
6
|
+
|
|
1
7
|
## [@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
8
|
|
|
3
9
|
### Features
|
package/package.json
CHANGED
|
@@ -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) {
|
|
@@ -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
|
-
* @
|
|
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 #
|
|
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]
|
|
102
|
+
this.log.error(`[SiteImsOrgAccess] ${errorMessage} - ${error.message}`, error);
|
|
98
103
|
throw new DataAccessError(
|
|
99
|
-
|
|
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(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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;
|