@adobe/spacecat-shared-data-access 4.8.0 → 4.10.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 +12 -0
- package/package.json +1 -1
- package/src/models/feedback-event/feedback-event.constants.js +31 -12
- package/src/models/feedback-event/index.d.ts +2 -0
- package/src/models/site/config.js +10 -0
- package/src/models/site/index.d.ts +2 -0
- package/src/models/site/site.collection.js +32 -0
- package/src/models/site-enrollment/index.d.ts +2 -0
- package/src/models/site-enrollment/site-enrollment.collection.js +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [@adobe/spacecat-shared-data-access-v4.10.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v4.9.0...@adobe/spacecat-shared-data-access-v4.10.0) (2026-07-20)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* Add allSiteIdsByTier and allByEnrollmentAndTier collection meth… ([#1569](https://github.com/adobe/spacecat-shared/issues/1569)) ([3ac585b](https://github.com/adobe/spacecat-shared/commit/3ac585bd1b3fe93d32b8d0ad027af81cf6e37830))
|
|
6
|
+
|
|
7
|
+
## [@adobe/spacecat-shared-data-access-v4.9.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v4.8.0...@adobe/spacecat-shared-data-access-v4.9.0) (2026-07-15)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **data-access:** guidance + feedback_subject_id + code-patch export gate for feedback ([#1809](https://github.com/adobe/spacecat-shared/issues/1809)) ([941b9f9](https://github.com/adobe/spacecat-shared/commit/941b9f92621df09a9ce887b70307f4c1261bfd37))
|
|
12
|
+
|
|
1
13
|
## [@adobe/spacecat-shared-data-access-v4.8.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v4.7.0...@adobe/spacecat-shared-data-access-v4.8.0) (2026-07-14)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/package.json
CHANGED
|
@@ -79,16 +79,19 @@ export const EXPORT_EXCLUDED_REJECTION_CATEGORIES = Object.freeze([
|
|
|
79
79
|
]);
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
83
|
-
* `training_opt_in = false
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
82
|
+
* HUMAN-authored fields stripped from the JSONL export for organizations with
|
|
83
|
+
* `training_opt_in = false`: the reviewer's written feedback (`detail_markdown`)
|
|
84
|
+
* and their hand-corrected patch (`edited_fix`). The AI-generated fields — the
|
|
85
|
+
* issue context (`guidance_markdown`) and the generated patch (`previous_fix`) —
|
|
86
|
+
* are ALWAYS exported (the Learning Agent needs the issue + generated patch to
|
|
87
|
+
* map a verdict to what was generated), as are verdict / signal / category /
|
|
88
|
+
* identity metadata. snake_case to match the JSONL row shape (and the
|
|
89
|
+
* feedback_event DB columns) — {@link toJsonlRow} uses this list to do the
|
|
90
|
+
* stripping, so the opt-out boundary is single-sourced here (SITES-43974).
|
|
87
91
|
*/
|
|
88
92
|
export const OPT_OUT_STRIPPED_FIELDS = Object.freeze([
|
|
89
|
-
'previous_fix',
|
|
90
|
-
'edited_fix',
|
|
91
93
|
'detail_markdown',
|
|
94
|
+
'edited_fix',
|
|
92
95
|
]);
|
|
93
96
|
|
|
94
97
|
/**
|
|
@@ -163,11 +166,17 @@ export function toReviewView(row, { includePatches = false } = {}) {
|
|
|
163
166
|
rejectionCategory: row.rejection_category ?? null,
|
|
164
167
|
stateTransition: row.state_transition ?? null,
|
|
165
168
|
tier: row.tier,
|
|
169
|
+
// Sub-item id (e.g. a CWV issue) this review is about; the Backoffice groups
|
|
170
|
+
// its per-issue "previous feedback" table on it. Always in the base view (the
|
|
171
|
+
// UI filters on it) — not gated behind includePatches. Null for
|
|
172
|
+
// whole-suggestion reviews.
|
|
173
|
+
feedbackSubjectId: row.feedback_subject_id ?? null,
|
|
166
174
|
};
|
|
167
175
|
|
|
168
176
|
if (includePatches) {
|
|
169
177
|
view.previousFix = row.previous_fix ?? null;
|
|
170
178
|
view.editedFix = row.edited_fix ?? null;
|
|
179
|
+
view.guidanceMarkdown = row.guidance_markdown ?? null;
|
|
171
180
|
}
|
|
172
181
|
|
|
173
182
|
return view;
|
|
@@ -175,14 +184,21 @@ export function toReviewView(row, { includePatches = false } = {}) {
|
|
|
175
184
|
|
|
176
185
|
/**
|
|
177
186
|
* Whether a raw `feedback_event` row should be exported to the Learning Agent
|
|
178
|
-
* corpus.
|
|
179
|
-
*
|
|
187
|
+
* corpus. Two gates (SITES-43974):
|
|
188
|
+
* 1. It must carry a generated code patch (`previous_fix`). The corpus is
|
|
189
|
+
* code-patch feedback; text-guidance-only reviews (no patch) are retained
|
|
190
|
+
* in Postgres but NOT exported — reserved for a future text-guidance corpus.
|
|
191
|
+
* 2. `product_bug` rejections are excluded (routed to Jira). NULL-category rows
|
|
192
|
+
* (approvals / uncategorised) ARE exported. See
|
|
193
|
+
* {@link EXPORT_EXCLUDED_REJECTION_CATEGORIES}.
|
|
180
194
|
*
|
|
181
195
|
* @param {object} row - a raw feedback_event row from PostgREST.
|
|
182
196
|
* @returns {boolean} true if the row should be exported.
|
|
183
197
|
*/
|
|
184
198
|
export function shouldExport(row) {
|
|
185
|
-
|
|
199
|
+
const hasCodePatch = row.previous_fix != null;
|
|
200
|
+
return hasCodePatch
|
|
201
|
+
&& !EXPORT_EXCLUDED_REJECTION_CATEGORIES.includes(row.rejection_category);
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
/**
|
|
@@ -190,8 +206,10 @@ export function shouldExport(row) {
|
|
|
190
206
|
* object — the canonical Learning Agent export contract. The row mirrors the
|
|
191
207
|
* feedback_event columns (snake_case), stamped with {@link SCHEMA_VERSION} and
|
|
192
208
|
* the derived `verdict`. For organizations that have NOT opted into training,
|
|
193
|
-
* the
|
|
194
|
-
*
|
|
209
|
+
* the HUMAN-authored fields in {@link OPT_OUT_STRIPPED_FIELDS} are nulled; the
|
|
210
|
+
* AI-generated issue context (`guidance_markdown`) + generated patch
|
|
211
|
+
* (`previous_fix`) and the verdict / signal / category / identity metadata still
|
|
212
|
+
* ship (§10.5.7).
|
|
195
213
|
*
|
|
196
214
|
* This is the single source of truth for the JSONL row shape — the exporter
|
|
197
215
|
* (spacecat-jobs-dispatcher) calls it rather than re-deriving the contract.
|
|
@@ -218,6 +236,7 @@ export function toJsonlRow(row, { optedIn = false } = {}) {
|
|
|
218
236
|
state_transition: row.state_transition ?? null,
|
|
219
237
|
tier: row.tier,
|
|
220
238
|
detail_markdown: row.detail_markdown ?? null,
|
|
239
|
+
guidance_markdown: row.guidance_markdown ?? null,
|
|
221
240
|
previous_fix: row.previous_fix ?? null,
|
|
222
241
|
edited_fix: row.edited_fix ?? null,
|
|
223
242
|
};
|
|
@@ -53,8 +53,10 @@ export interface ReviewView {
|
|
|
53
53
|
rejectionCategory: string | null;
|
|
54
54
|
stateTransition: string | null;
|
|
55
55
|
tier: string;
|
|
56
|
+
feedbackSubjectId: string | null;
|
|
56
57
|
previousFix?: unknown;
|
|
57
58
|
editedFix?: unknown;
|
|
59
|
+
guidanceMarkdown?: string | null;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/**
|
|
@@ -477,6 +477,16 @@ export const configSchema = Joi.object({
|
|
|
477
477
|
storeCode: Joi.string().required(),
|
|
478
478
|
storeViewCode: Joi.string().required(),
|
|
479
479
|
hostName: Joi.string().optional(),
|
|
480
|
+
catalogFieldConfig: Joi.object({
|
|
481
|
+
name: Joi.object({
|
|
482
|
+
enabled: Joi.boolean().required(),
|
|
483
|
+
maxLength: Joi.number().integer().min(0).optional(),
|
|
484
|
+
}).optional(),
|
|
485
|
+
description: Joi.object({
|
|
486
|
+
enabled: Joi.boolean().required(),
|
|
487
|
+
maxLength: Joi.number().integer().min(0).optional(),
|
|
488
|
+
}).optional(),
|
|
489
|
+
}).optional(),
|
|
480
490
|
}).options({ stripUnknown: true }),
|
|
481
491
|
).optional(),
|
|
482
492
|
contentAiConfig: Joi.object({
|
|
@@ -409,6 +409,8 @@ export interface SiteCollection extends BaseCollection<Site> {
|
|
|
409
409
|
allByProjectName(projectName: string): Promise<Site[]>;
|
|
410
410
|
allByOrganizationIdAndProjectId(organizationId: string, projectId: string): Promise<Site[]>;
|
|
411
411
|
allByOrganizationIdAndProjectName(organizationId: string, projectName: string): Promise<Site[]>;
|
|
412
|
+
allByEnrollmentProductCode(productCode: string, options?: object): Promise<Site[]>;
|
|
413
|
+
allByEnrollmentAndTier(tier: string, productCode?: string, options?: object): Promise<Site[]>;
|
|
412
414
|
allSitesToAudit(): Promise<string[]>;
|
|
413
415
|
allWithLatestAudit(auditType: string, order?: string, deliveryType?: string): Promise<Site[]>;
|
|
414
416
|
findByBaseURL(baseURL: string): Promise<Site | null>;
|
|
@@ -174,6 +174,38 @@ class SiteCollection extends BaseCollection {
|
|
|
174
174
|
return sites;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Returns all sites enrolled at a given entitlement tier (e.g. 'PAID',
|
|
179
|
+
* 'FREE_TRIAL', 'PLG'). Optionally narrows the result to a single product
|
|
180
|
+
* code (e.g. 'LLMO').
|
|
181
|
+
*
|
|
182
|
+
* Uses entityRegistry to chain through SiteEnrollmentCollection, then
|
|
183
|
+
* batch-fetches full Site objects.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} tier - Entitlement tier to filter by.
|
|
186
|
+
* @param {string} [productCode] - Optional product code to further filter by.
|
|
187
|
+
* @param {object} [options] - batchGetByKeys options (e.g. attribute projection).
|
|
188
|
+
* @returns {Promise<Site[]>}
|
|
189
|
+
*/
|
|
190
|
+
async allByEnrollmentAndTier(tier, productCode, options = {}) {
|
|
191
|
+
if (!hasText(tier)) {
|
|
192
|
+
throw new DataAccessError('tier is required', this);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const siteEnrollmentCollection = this.entityRegistry.getCollection('SiteEnrollmentCollection');
|
|
196
|
+
|
|
197
|
+
const siteIds = await siteEnrollmentCollection.allSiteIdsByTier(tier, productCode);
|
|
198
|
+
if (siteIds.length === 0) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const { data: sites } = await this.batchGetByKeys(
|
|
203
|
+
siteIds.map((siteId) => ({ siteId })),
|
|
204
|
+
options,
|
|
205
|
+
);
|
|
206
|
+
return sites;
|
|
207
|
+
}
|
|
208
|
+
|
|
177
209
|
async allByOrganizationIdAndProjectName(organizationId, projectName) {
|
|
178
210
|
if (!hasText(organizationId)) {
|
|
179
211
|
throw new DataAccessError('organizationId is required', this);
|
|
@@ -27,6 +27,8 @@ export interface SiteEnrollmentCollection extends
|
|
|
27
27
|
BaseCollection<SiteEnrollment> {
|
|
28
28
|
allBySiteId(siteId: string): Promise<SiteEnrollment[]>;
|
|
29
29
|
allByEntitlementId(entitlementId: string): Promise<SiteEnrollment[]>;
|
|
30
|
+
allSiteIdsByProductCode(productCode: string): Promise<string[]>;
|
|
31
|
+
allSiteIdsByTier(tier: string, productCode?: string): Promise<string[]>;
|
|
30
32
|
|
|
31
33
|
findBySiteId(siteId: string): Promise<SiteEnrollment | null>;
|
|
32
34
|
findByEntitlementId(entitlementId: string): Promise<SiteEnrollment | null>;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { hasText } from '@adobe/spacecat-shared-utils';
|
|
13
14
|
import BaseCollection from '../base/base.collection.js';
|
|
14
15
|
import DataAccessError from '../../errors/data-access.error.js';
|
|
15
16
|
|
|
@@ -47,6 +48,39 @@ class SiteEnrollmentCollection extends BaseCollection {
|
|
|
47
48
|
return (data || []).map((row) => row.site_id);
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Returns all site IDs enrolled at a given entitlement tier (e.g. 'PAID',
|
|
53
|
+
* 'FREE_TRIAL', 'PLG') in a single JOIN query. Optionally narrows the tier
|
|
54
|
+
* match to a single product code.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} tier - Entitlement tier to filter by.
|
|
57
|
+
* @param {string} [productCode] - Optional product code to further filter by.
|
|
58
|
+
* @returns {Promise<string[]>} Array of siteId strings.
|
|
59
|
+
*/
|
|
60
|
+
async allSiteIdsByTier(tier, productCode) {
|
|
61
|
+
if (!hasText(tier)) {
|
|
62
|
+
throw new DataAccessError('tier is required', { entityName: 'SiteEnrollment', tableName: 'site_enrollments' });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let query = this.postgrestService
|
|
66
|
+
.from(this.tableName)
|
|
67
|
+
.select('site_id, entitlements!inner(tier, product_code)')
|
|
68
|
+
.eq('entitlements.tier', tier);
|
|
69
|
+
|
|
70
|
+
if (productCode) {
|
|
71
|
+
query = query.eq('entitlements.product_code', productCode);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { data, error } = await query;
|
|
75
|
+
|
|
76
|
+
if (error) {
|
|
77
|
+
this.log.error(`[SiteEnrollmentCollection] Failed to query site_enrollments by tier - ${error.message}`, error);
|
|
78
|
+
throw new DataAccessError('Failed to query site_enrollments by tier', { entityName: 'SiteEnrollment', tableName: 'site_enrollments' }, error);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return [...new Set((data || []).map((row) => row.site_id))];
|
|
82
|
+
}
|
|
83
|
+
|
|
50
84
|
async create(item, options = {}) {
|
|
51
85
|
if (item?.siteId && item?.entitlementId) {
|
|
52
86
|
const existing = await this.findByIndexKeys({
|