@adobe/spacecat-shared-data-access 3.62.0 → 3.64.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 +18 -0
- package/README.md +19 -0
- package/docker-compose.test.yml +1 -1
- package/package.json +5 -5
- package/src/models/plg-onboarding/plg-onboarding.schema.js +1 -0
- package/src/models/site/config.js +33 -0
- package/src/models/suggestion/index.js +8 -0
- package/src/models/suggestion/suggestion.data-schemas.js +91 -8
- package/src/models/token/token.collection.js +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## [@adobe/spacecat-shared-data-access-v3.64.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.63.0...@adobe/spacecat-shared-data-access-v3.64.0) (2026-05-15)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* strict schema for data.issues[] with per-issue lifecycle fields ([#1594](https://github.com/adobe/spacecat-shared/issues/1594)) ([d101db4](https://github.com/adobe/spacecat-shared/commit/d101db4e6407cfe9cbb742d54b500439cf62b64f))
|
|
6
|
+
|
|
7
|
+
## [@adobe/spacecat-shared-data-access-v3.63.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.62.0...@adobe/spacecat-shared-data-access-v3.63.0) (2026-05-14)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add rumConfig to Site config for deterministic RUM availability signal ([#1596](https://github.com/adobe/spacecat-shared/issues/1596)) ([3b16cdf](https://github.com/adobe/spacecat-shared/commit/3b16cdf7d66ddddc4b4b62aae4428feba3ef7f04))
|
|
12
|
+
* **data-access:** add preOnboarded flag to PLG onboarding steps schema ([#1601](https://github.com/adobe/spacecat-shared/issues/1601)) ([6f18f71](https://github.com/adobe/spacecat-shared/commit/6f18f71d112d5f409a545a3720ca8b7d9e3a13d7))
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* adds a function to get latest token by createdAt ([#1597](https://github.com/adobe/spacecat-shared/issues/1597)) ([fb4d5f9](https://github.com/adobe/spacecat-shared/commit/fb4d5f91dc7bf674cff4f9855d3f630aa4807b5a))
|
|
17
|
+
* **deps:** update external fixes ([#1533](https://github.com/adobe/spacecat-shared/issues/1533)) ([0a3e2ab](https://github.com/adobe/spacecat-shared/commit/0a3e2abbbc5f58b5320518f7d596d4cef6271fa0))
|
|
18
|
+
|
|
1
19
|
## [@adobe/spacecat-shared-data-access-v3.62.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.61.0...@adobe/spacecat-shared-data-access-v3.62.0) (2026-05-12)
|
|
2
20
|
|
|
3
21
|
### Features
|
package/README.md
CHANGED
|
@@ -172,6 +172,25 @@ The `deliveryConfig` object on a Site stores delivery infrastructure details. It
|
|
|
172
172
|
| `preferContentApi` | boolean | Whether to prefer the Content API for content retrieval |
|
|
173
173
|
| `contentSourcePath` | string | AEM content root path for a site. Used to disambiguate multiple sites that share the same Cloud Manager program and environment. Corresponds to `/content/<site-name>` in the AEM repository. |
|
|
174
174
|
|
|
175
|
+
## Site rumConfig
|
|
176
|
+
|
|
177
|
+
The `rumConfig` object on a Site tracks Real User Monitoring (RUM) domain key availability. It is set automatically on site creation and refreshed weekly by the `rum-config-refresh` audit handler.
|
|
178
|
+
|
|
179
|
+
| Property | Type | Description |
|
|
180
|
+
|----------|------|-------------|
|
|
181
|
+
| `hasDomainKey` | boolean | Whether the site's domain has an active RUM domain key registered |
|
|
182
|
+
| `lastCheckedAt` | string (ISO 8601) | Timestamp of the most recent RUM domain key check |
|
|
183
|
+
|
|
184
|
+
### Config model methods
|
|
185
|
+
|
|
186
|
+
| Method | Returns | Description |
|
|
187
|
+
|--------|---------|-------------|
|
|
188
|
+
| `getRumConfig()` | `{ hasDomainKey, lastCheckedAt } \| undefined` | Returns the current rumConfig, or `undefined` if not yet set |
|
|
189
|
+
| `hasRumDomainKey()` | `boolean` | Returns `true` if a RUM domain key is active, `false` otherwise |
|
|
190
|
+
| `updateRumConfig(hasDomainKey)` | `void` | Sets `hasDomainKey` and updates `lastCheckedAt` to the current time |
|
|
191
|
+
|
|
192
|
+
Sites created before the `rum-config-refresh` handler ran will have no `rumConfig` key — callers must treat `undefined` as "unknown" rather than "not set up".
|
|
193
|
+
|
|
175
194
|
## Architecture
|
|
176
195
|
|
|
177
196
|
```
|
package/docker-compose.test.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/spacecat-shared-data-access",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.64.0",
|
|
4
4
|
"description": "Shared modules of the Spacecat Services - Data Access",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -42,18 +42,18 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@adobe/fetch": "^4.2.3",
|
|
44
44
|
"@adobe/spacecat-shared-utils": "1.105.0",
|
|
45
|
-
"@supabase/postgrest-js": "2.
|
|
45
|
+
"@supabase/postgrest-js": "2.105.4",
|
|
46
46
|
"@aws-sdk/client-s3": "^3.940.0",
|
|
47
47
|
"@types/joi": "17.2.3",
|
|
48
48
|
"aws-xray-sdk": "3.12.0",
|
|
49
|
-
"joi": "18.1
|
|
49
|
+
"joi": "18.2.1",
|
|
50
50
|
"pluralize": "8.0.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"chai": "6.2.2",
|
|
54
54
|
"chai-as-promised": "8.0.2",
|
|
55
|
-
"nock": "14.0.
|
|
56
|
-
"sinon": "21.
|
|
55
|
+
"nock": "14.0.15",
|
|
56
|
+
"sinon": "21.1.2",
|
|
57
57
|
"sinon-chai": "4.0.1"
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -68,6 +68,7 @@ const schema = new SchemaBuilder(PlgOnboarding, PlgOnboardingCollection)
|
|
|
68
68
|
entitlementCreated: { type: 'boolean' },
|
|
69
69
|
entitlementFailed: { type: 'boolean' },
|
|
70
70
|
orgResolutionFailed: { type: 'boolean' },
|
|
71
|
+
preOnboarded: { type: 'boolean' },
|
|
71
72
|
},
|
|
72
73
|
})
|
|
73
74
|
.addAttribute('error', {
|
|
@@ -407,6 +407,10 @@ export const configSchema = Joi.object({
|
|
|
407
407
|
startTime: Joi.number().optional(),
|
|
408
408
|
})).optional(),
|
|
409
409
|
}).optional(),
|
|
410
|
+
rumConfig: Joi.object({
|
|
411
|
+
hasDomainKey: Joi.boolean().required(),
|
|
412
|
+
lastCheckedAt: Joi.string().isoDate().required(),
|
|
413
|
+
}).optional(),
|
|
410
414
|
commerceLlmoConfig: Joi.object().pattern(
|
|
411
415
|
Joi.string(),
|
|
412
416
|
Joi.object({
|
|
@@ -542,6 +546,18 @@ export const Config = (data = {}) => {
|
|
|
542
546
|
self.getEdgeOptimizeConfig = () => state?.edgeOptimizeConfig;
|
|
543
547
|
self.getOnboardConfig = () => state?.onboardConfig;
|
|
544
548
|
self.getCommerceLlmoConfig = () => state?.commerceLlmoConfig;
|
|
549
|
+
/**
|
|
550
|
+
* Returns the RUM configuration for the site, or undefined if not set.
|
|
551
|
+
* Returns a shallow copy to prevent callers from mutating internal state.
|
|
552
|
+
* @returns {{ hasDomainKey: boolean, lastCheckedAt: string } | undefined}
|
|
553
|
+
*/
|
|
554
|
+
self.getRumConfig = () => (state?.rumConfig ? { ...state.rumConfig } : undefined);
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Returns true if RUM data collection is confirmed active for this site.
|
|
558
|
+
* @returns {boolean}
|
|
559
|
+
*/
|
|
560
|
+
self.hasRumDomainKey = () => state?.rumConfig?.hasDomainKey === true;
|
|
545
561
|
const AUDIT_TARGET_SOURCES = ['manual', 'moneyPages'];
|
|
546
562
|
const auditTargetEntrySchema = Joi.object({
|
|
547
563
|
url: Joi.string().uri().required(),
|
|
@@ -955,6 +971,22 @@ export const Config = (data = {}) => {
|
|
|
955
971
|
state.commerceLlmoConfig = commerceLlmoConfig;
|
|
956
972
|
};
|
|
957
973
|
|
|
974
|
+
/**
|
|
975
|
+
* Records the outcome of a RUM domain-key check and updates the timestamp.
|
|
976
|
+
* @param {boolean} hasDomainKey - Whether the site has an active RUM domain key.
|
|
977
|
+
* @param {Date} [now=new Date()] - Timestamp for lastCheckedAt; injectable for tests.
|
|
978
|
+
* @throws {Error} if hasDomainKey is not a boolean.
|
|
979
|
+
*/
|
|
980
|
+
self.updateRumConfig = (hasDomainKey, now = new Date()) => {
|
|
981
|
+
if (typeof hasDomainKey !== 'boolean') {
|
|
982
|
+
throw new TypeError(`updateRumConfig: hasDomainKey must be a boolean, got ${typeof hasDomainKey}`);
|
|
983
|
+
}
|
|
984
|
+
state.rumConfig = {
|
|
985
|
+
hasDomainKey,
|
|
986
|
+
lastCheckedAt: now.toISOString(),
|
|
987
|
+
};
|
|
988
|
+
};
|
|
989
|
+
|
|
958
990
|
return Object.freeze(self);
|
|
959
991
|
};
|
|
960
992
|
|
|
@@ -974,6 +1006,7 @@ Config.toDynamoItem = (config) => ({
|
|
|
974
1006
|
edgeOptimizeConfig: config.getEdgeOptimizeConfig(),
|
|
975
1007
|
onboardConfig: config.getOnboardConfig?.(),
|
|
976
1008
|
commerceLlmoConfig: config.getCommerceLlmoConfig?.(),
|
|
1009
|
+
rumConfig: config.getRumConfig?.(),
|
|
977
1010
|
enableMoneyPageUrls: config.isMoneyPageUrlsEnabled?.() === false ? false : undefined,
|
|
978
1011
|
auditTargetURLs: config.getAuditTargetURLsConfig?.(),
|
|
979
1012
|
});
|
|
@@ -20,3 +20,11 @@ export {
|
|
|
20
20
|
|
|
21
21
|
// Export DATA_SCHEMAS for api-service to reference
|
|
22
22
|
export const { DATA_SCHEMAS } = Suggestion;
|
|
23
|
+
|
|
24
|
+
// Re-export per-issue lifecycle constants so consumers (audit-worker, autofix-worker,
|
|
25
|
+
// api-service, Mystique) can validate per-issue values without duplicating enum lists.
|
|
26
|
+
export {
|
|
27
|
+
CWV_METRIC_TYPES,
|
|
28
|
+
ISSUE_STATUSES,
|
|
29
|
+
ISSUE_SKIP_REASONS,
|
|
30
|
+
} from './suggestion.data-schemas.js';
|
|
@@ -26,6 +26,85 @@
|
|
|
26
26
|
import Joi from 'joi';
|
|
27
27
|
import { OPPORTUNITY_TYPES } from '@adobe/spacecat-shared-utils';
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Status and skip-reason values for per-issue lifecycle inside `data.issues[]`.
|
|
31
|
+
*
|
|
32
|
+
* Exported for consumers that need to construct or validate per-issue values
|
|
33
|
+
* (audit-worker, autofix-worker, api-service controllers, Mystique).
|
|
34
|
+
*
|
|
35
|
+
* Duplicated from `Suggestion.STATUSES` / `Suggestion.SKIP_REASONS` (suggestion.model.js)
|
|
36
|
+
* to avoid a circular import — the model already imports DATA_SCHEMAS from this file.
|
|
37
|
+
* Keep in sync if either set ever changes.
|
|
38
|
+
*/
|
|
39
|
+
export const ISSUE_STATUSES = [
|
|
40
|
+
'NEW',
|
|
41
|
+
'APPROVED',
|
|
42
|
+
'IN_PROGRESS',
|
|
43
|
+
'SKIPPED',
|
|
44
|
+
'FIXED',
|
|
45
|
+
'ERROR',
|
|
46
|
+
'OUTDATED',
|
|
47
|
+
'PENDING_VALIDATION',
|
|
48
|
+
'REJECTED',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export const ISSUE_SKIP_REASONS = [
|
|
52
|
+
'ALREADY_IMPLEMENTED',
|
|
53
|
+
'INACCURATE_OR_INCOMPLETE',
|
|
54
|
+
'TOO_RISKY',
|
|
55
|
+
'NO_REASON',
|
|
56
|
+
'OTHER',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Allowed CWV metric types for `data.issues[].type`.
|
|
61
|
+
* Exported so audit-worker / Mystique / api-service can reuse the same source of truth.
|
|
62
|
+
*/
|
|
63
|
+
export const CWV_METRIC_TYPES = ['lcp', 'cls', 'inp'];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Strict per-issue schema for `data.issues[]` on CWV suggestions.
|
|
67
|
+
*
|
|
68
|
+
* All fields are optional initially so existing prod rows (issues without `id` / `type` /
|
|
69
|
+
* `status`) keep validating. Tighten to required after a backfill populates `id` and `type`
|
|
70
|
+
* on every existing issue.
|
|
71
|
+
*
|
|
72
|
+
* Field ownership:
|
|
73
|
+
* - `id`, `type`, `value`, `title`, `cwvValue`, `patchContent`, `isCodeChangeAvailable`,
|
|
74
|
+
* initial `status: 'NEW'` — written by Mystique (guidance + code-fix tasks).
|
|
75
|
+
* - `status` transitions (APPROVED/REJECTED/SKIPPED/IN_PROGRESS) — written by api-service
|
|
76
|
+
* via the existing PATCH /suggestions/:id endpoint (UI does read-modify-write of the
|
|
77
|
+
* whole `data` payload).
|
|
78
|
+
* - `status: FIXED | ERROR` — written by autofix-worker after PR creation.
|
|
79
|
+
* - `status: OUTDATED` — written by audit-worker `handleOutdatedSuggestions` on re-audit.
|
|
80
|
+
* - `fixEntityId` — written by autofix-worker (or Mystique `_create_fix_entity`).
|
|
81
|
+
* - `jiraLink` — written by whichever service files the Jira ticket (today: UI or api-service).
|
|
82
|
+
* - `skipReason`, `skipDetail` — written by api-service when `status` transitions to SKIPPED.
|
|
83
|
+
*/
|
|
84
|
+
const CWV_ISSUE_SCHEMA = Joi.object({
|
|
85
|
+
// Identity
|
|
86
|
+
id: Joi.string().optional(),
|
|
87
|
+
|
|
88
|
+
// Semantic
|
|
89
|
+
type: Joi.string().valid(...CWV_METRIC_TYPES).optional(),
|
|
90
|
+
title: Joi.string().optional(),
|
|
91
|
+
value: Joi.string().optional(),
|
|
92
|
+
cwvValue: Joi.number().optional(),
|
|
93
|
+
|
|
94
|
+
// Patch
|
|
95
|
+
patchContent: Joi.string().allow('').optional(),
|
|
96
|
+
isCodeChangeAvailable: Joi.boolean().optional(),
|
|
97
|
+
|
|
98
|
+
// Lifecycle
|
|
99
|
+
status: Joi.string().valid(...ISSUE_STATUSES).optional(),
|
|
100
|
+
skipReason: Joi.string().valid(...ISSUE_SKIP_REASONS).optional(),
|
|
101
|
+
skipDetail: Joi.string().max(1000).optional(),
|
|
102
|
+
|
|
103
|
+
// Linkage
|
|
104
|
+
jiraLink: Joi.string().uri().allow(null).optional(),
|
|
105
|
+
fixEntityId: Joi.string().uuid().optional(),
|
|
106
|
+
}).unknown(true);
|
|
107
|
+
|
|
29
108
|
/**
|
|
30
109
|
* Custom Joi validator that accepts malformed HTTP/HTTPS URLs and relative paths
|
|
31
110
|
* while rejecting dangerous URI schemes (javascript:, data:, blob:, etc.).
|
|
@@ -148,22 +227,26 @@ export const DATA_SCHEMAS = {
|
|
|
148
227
|
url: Joi.string().uri().optional(),
|
|
149
228
|
pageviews: Joi.number().optional(),
|
|
150
229
|
organic: Joi.number().optional(),
|
|
230
|
+
// RUM metrics are nullable in practice. INP requires user interaction events,
|
|
231
|
+
// so devices/pages without interactions report `inp: null`. The *Count fields
|
|
232
|
+
// similarly are null when the source RUM bundle has no sample for that metric.
|
|
233
|
+
// Mirror the existing `.allow(null)` already on lcp/ttfb/cls.
|
|
151
234
|
metrics: Joi.array().items(
|
|
152
235
|
Joi.object({
|
|
153
236
|
deviceType: Joi.string().optional(),
|
|
154
|
-
pageviews: Joi.number().optional(),
|
|
155
|
-
clsCount: Joi.number().optional(),
|
|
156
|
-
ttfbCount: Joi.number().optional(),
|
|
237
|
+
pageviews: Joi.number().allow(null).optional(),
|
|
238
|
+
clsCount: Joi.number().allow(null).optional(),
|
|
239
|
+
ttfbCount: Joi.number().allow(null).optional(),
|
|
157
240
|
lcp: Joi.number().allow(null).optional(),
|
|
158
|
-
inpCount: Joi.number().optional(),
|
|
159
|
-
inp: Joi.number().optional(),
|
|
241
|
+
inpCount: Joi.number().allow(null).optional(),
|
|
242
|
+
inp: Joi.number().allow(null).optional(),
|
|
160
243
|
ttfb: Joi.number().allow(null).optional(),
|
|
161
244
|
cls: Joi.number().allow(null).optional(),
|
|
162
|
-
lcpCount: Joi.number().optional(),
|
|
163
|
-
organic: Joi.number().optional(),
|
|
245
|
+
lcpCount: Joi.number().allow(null).optional(),
|
|
246
|
+
organic: Joi.number().allow(null).optional(),
|
|
164
247
|
}).unknown(true),
|
|
165
248
|
).required(),
|
|
166
|
-
issues: Joi.array().items(
|
|
249
|
+
issues: Joi.array().items(CWV_ISSUE_SCHEMA).optional().default([]),
|
|
167
250
|
jiraLink: Joi.string().uri().allow(null).optional(),
|
|
168
251
|
aggregationKey: Joi.string().allow(null).optional(),
|
|
169
252
|
}).unknown(true),
|
|
@@ -42,6 +42,21 @@ class TokenCollection extends BaseCollection {
|
|
|
42
42
|
* @returns {Promise<import('./token.model.js').default|null>} Token instance
|
|
43
43
|
* (existing or newly created), or null when none exists and createIfNotFound is false.
|
|
44
44
|
*/
|
|
45
|
+
/**
|
|
46
|
+
* Finds the most recently created Token for a given siteId and tokenType,
|
|
47
|
+
* regardless of cycle. Returns null if no token exists.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} siteId - Site ID (UUID).
|
|
50
|
+
* @param {string} tokenType - Token type (e.g. grant_cwv, grant_broken_backlinks).
|
|
51
|
+
* @returns {Promise<import('./token.model.js').default|null>} The last created Token, or null.
|
|
52
|
+
*/
|
|
53
|
+
async findLastCreatedBySiteIdAndTokenType(siteId, tokenType) {
|
|
54
|
+
if (!hasText(siteId) || !hasText(tokenType)) {
|
|
55
|
+
throw new DataAccessError('TokenCollection.findLastCreatedBySiteIdAndTokenType: siteId and tokenType are required');
|
|
56
|
+
}
|
|
57
|
+
return this.findByIndexKeys({ siteId, tokenType }, { order: 'desc' });
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
async findBySiteIdAndTokenType(siteId, tokenType, options = {}) {
|
|
46
61
|
if (!hasText(siteId) || !hasText(tokenType)) {
|
|
47
62
|
throw new DataAccessError('TokenCollection.findBySiteIdAndTokenType: siteId and tokenType are required');
|