@adobe/spacecat-shared-data-access 2.99.0 → 2.100.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,10 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-data-access-v2.100.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v2.99.0...@adobe/spacecat-shared-data-access-v2.100.0) (2026-02-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **data-access:** add type-specific data schemas for suggestions [SITES-39183] ([#1289](https://github.com/adobe/spacecat-shared/issues/1289)) ([9824b22](https://github.com/adobe/spacecat-shared/commit/9824b22bc494467849a2ae903ce902ed3f9fde80))
|
|
7
|
+
|
|
1
8
|
# [@adobe/spacecat-shared-data-access-v2.99.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v2.98.0...@adobe/spacecat-shared-data-access-v2.99.0) (2026-01-30)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @fileoverview Type-specific data schemas for suggestion opportunity types.
|
|
15
|
+
* Defines validation schemas and projection configurations.
|
|
16
|
+
*
|
|
17
|
+
* Validation schemas should be used in audit-worker when creating/updating suggestions
|
|
18
|
+
* to ensure data structure consistency before writing to the database.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import Joi from 'joi';
|
|
22
|
+
import { OPPORTUNITY_TYPES } from '@adobe/spacecat-shared-utils';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Data schemas configuration per opportunity type.
|
|
26
|
+
*
|
|
27
|
+
* @typedef {Object} OpportunityTypeSchema
|
|
28
|
+
* @property {import('joi').Schema} schema - Joi validation schema for the data field
|
|
29
|
+
* @property {Object} projections - Projection configurations
|
|
30
|
+
* @property {Object} projections.minimal - Minimal view configuration
|
|
31
|
+
* @property {string[]} projections.minimal.fields - Fields to include in minimal view
|
|
32
|
+
* @property {Object<string, string>} projections.minimal.transformers - Field transformers to apply
|
|
33
|
+
*
|
|
34
|
+
* @type {Object<string, OpportunityTypeSchema>}
|
|
35
|
+
*
|
|
36
|
+
* @example Adding a new opportunity type
|
|
37
|
+
* [OPPORTUNITY_TYPES.YOUR_TYPE]: {
|
|
38
|
+
* schema: Joi.object({
|
|
39
|
+
* url: Joi.string().uri().required(), // Required - in minimal view
|
|
40
|
+
* customField: Joi.string().required(), // Required - in minimal view
|
|
41
|
+
* optionalField: Joi.string().optional() // Optional - not in minimal view
|
|
42
|
+
* }).unknown(true),
|
|
43
|
+
* projections: {
|
|
44
|
+
* minimal: {
|
|
45
|
+
* fields: ['url', 'customField'],
|
|
46
|
+
* transformers: { customField: 'myTransformerName' }
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
*/
|
|
51
|
+
export const DATA_SCHEMAS = {
|
|
52
|
+
[OPPORTUNITY_TYPES.STRUCTURED_DATA]: {
|
|
53
|
+
schema: Joi.object({
|
|
54
|
+
url: Joi.string().uri().required(),
|
|
55
|
+
type: Joi.string().optional(),
|
|
56
|
+
errors: Joi.array().items(Joi.object()).optional(),
|
|
57
|
+
}).unknown(true),
|
|
58
|
+
projections: {
|
|
59
|
+
minimal: {
|
|
60
|
+
fields: ['url'],
|
|
61
|
+
transformers: {},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
[OPPORTUNITY_TYPES.COLOR_CONTRAST]: {
|
|
66
|
+
schema: Joi.object({
|
|
67
|
+
type: Joi.string().optional(),
|
|
68
|
+
url: Joi.string().uri().required(),
|
|
69
|
+
issues: Joi.array().items(
|
|
70
|
+
Joi.object({
|
|
71
|
+
wcagLevel: Joi.string().optional(),
|
|
72
|
+
severity: Joi.string().optional(),
|
|
73
|
+
occurrences: Joi.number().optional(),
|
|
74
|
+
htmlWithIssues: Joi.array().items(Joi.object()).optional(),
|
|
75
|
+
failureSummary: Joi.string().optional(),
|
|
76
|
+
wcagRule: Joi.string().optional(),
|
|
77
|
+
description: Joi.string().optional(),
|
|
78
|
+
type: Joi.string().optional(),
|
|
79
|
+
}).unknown(true),
|
|
80
|
+
).required(),
|
|
81
|
+
jiraLink: Joi.string().uri().allow(null).optional(),
|
|
82
|
+
aggregationKey: Joi.string().optional(),
|
|
83
|
+
}).unknown(true),
|
|
84
|
+
projections: {
|
|
85
|
+
minimal: {
|
|
86
|
+
fields: ['url', 'issues'],
|
|
87
|
+
transformers: {
|
|
88
|
+
issues: 'filterIssuesOccurrences',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
[OPPORTUNITY_TYPES.A11Y_ASSISTIVE]: {
|
|
94
|
+
schema: Joi.object({
|
|
95
|
+
type: Joi.string().optional(),
|
|
96
|
+
url: Joi.string().uri().required(),
|
|
97
|
+
issues: Joi.array().items(
|
|
98
|
+
Joi.object({
|
|
99
|
+
wcagLevel: Joi.string().optional(),
|
|
100
|
+
severity: Joi.string().optional(),
|
|
101
|
+
occurrences: Joi.number().optional(),
|
|
102
|
+
htmlWithIssues: Joi.array().items(Joi.object()).optional(),
|
|
103
|
+
failureSummary: Joi.string().optional(),
|
|
104
|
+
wcagRule: Joi.string().optional(),
|
|
105
|
+
description: Joi.string().optional(),
|
|
106
|
+
type: Joi.string().optional(),
|
|
107
|
+
}).unknown(true),
|
|
108
|
+
).required(),
|
|
109
|
+
jiraLink: Joi.string().uri().allow(null).optional(),
|
|
110
|
+
aggregationKey: Joi.string().optional(),
|
|
111
|
+
}).unknown(true),
|
|
112
|
+
projections: {
|
|
113
|
+
minimal: {
|
|
114
|
+
fields: ['url', 'issues'],
|
|
115
|
+
transformers: {
|
|
116
|
+
issues: 'filterIssuesOccurrences',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
[OPPORTUNITY_TYPES.CWV]: {
|
|
122
|
+
schema: Joi.object({
|
|
123
|
+
type: Joi.string().required(),
|
|
124
|
+
url: Joi.string().uri().required(),
|
|
125
|
+
pageviews: Joi.number().optional(),
|
|
126
|
+
organic: Joi.number().optional(),
|
|
127
|
+
metrics: Joi.array().items(
|
|
128
|
+
Joi.object({
|
|
129
|
+
deviceType: Joi.string().optional(),
|
|
130
|
+
pageviews: Joi.number().optional(),
|
|
131
|
+
clsCount: Joi.number().optional(),
|
|
132
|
+
ttfbCount: Joi.number().optional(),
|
|
133
|
+
lcp: Joi.number().optional(),
|
|
134
|
+
inpCount: Joi.number().optional(),
|
|
135
|
+
inp: Joi.number().optional(),
|
|
136
|
+
ttfb: Joi.number().optional(),
|
|
137
|
+
cls: Joi.number().optional(),
|
|
138
|
+
lcpCount: Joi.number().optional(),
|
|
139
|
+
organic: Joi.number().optional(),
|
|
140
|
+
}).unknown(true),
|
|
141
|
+
).required(),
|
|
142
|
+
issues: Joi.array().items(Joi.object()).required(),
|
|
143
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
144
|
+
}).unknown(true),
|
|
145
|
+
projections: {
|
|
146
|
+
minimal: {
|
|
147
|
+
fields: ['url', 'type', 'metrics', 'issues'],
|
|
148
|
+
transformers: {
|
|
149
|
+
metrics: 'filterCwvMetrics',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
[OPPORTUNITY_TYPES.ALT_TEXT]: {
|
|
155
|
+
schema: Joi.object({
|
|
156
|
+
recommendations: Joi.array().items(
|
|
157
|
+
Joi.object({
|
|
158
|
+
isAppropriate: Joi.boolean().optional(),
|
|
159
|
+
isDecorative: Joi.boolean().optional(),
|
|
160
|
+
xpath: Joi.string().optional(),
|
|
161
|
+
altText: Joi.string().optional(),
|
|
162
|
+
imageUrl: Joi.string().uri().optional(),
|
|
163
|
+
pageUrl: Joi.string().uri().optional(),
|
|
164
|
+
language: Joi.string().optional(),
|
|
165
|
+
id: Joi.string().optional(),
|
|
166
|
+
}).unknown(true),
|
|
167
|
+
).required(),
|
|
168
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
169
|
+
}).unknown(true),
|
|
170
|
+
projections: {
|
|
171
|
+
minimal: {
|
|
172
|
+
fields: ['recommendations'],
|
|
173
|
+
transformers: {
|
|
174
|
+
recommendations: 'extractPageUrlFromRecommendations',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
[OPPORTUNITY_TYPES.SECURITY_PERMISSIONS]: {
|
|
180
|
+
schema: Joi.object({
|
|
181
|
+
principal: Joi.string().optional(),
|
|
182
|
+
path: Joi.string().required(),
|
|
183
|
+
issue: Joi.string().optional(),
|
|
184
|
+
permissions: Joi.array().items(Joi.string()).optional(),
|
|
185
|
+
recommended_permissions: Joi.array().items(Joi.string()).optional(),
|
|
186
|
+
acl: Joi.array().items(Joi.string()).optional(),
|
|
187
|
+
otherPermissions: Joi.array().optional(),
|
|
188
|
+
rationale: Joi.string().optional(),
|
|
189
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
190
|
+
}).unknown(true),
|
|
191
|
+
projections: {
|
|
192
|
+
minimal: {
|
|
193
|
+
fields: ['path'],
|
|
194
|
+
transformers: {},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
[OPPORTUNITY_TYPES.SECURITY_VULNERABILITIES]: {
|
|
199
|
+
schema: Joi.object({
|
|
200
|
+
current_version: Joi.string().optional(),
|
|
201
|
+
library: Joi.string().optional(),
|
|
202
|
+
recommended_version: Joi.string().allow(null).optional(),
|
|
203
|
+
cves: Joi.array().items(
|
|
204
|
+
Joi.object({
|
|
205
|
+
summary: Joi.string().optional(),
|
|
206
|
+
score: Joi.number().optional(),
|
|
207
|
+
score_text: Joi.string().optional(),
|
|
208
|
+
cve_id: Joi.string().optional(),
|
|
209
|
+
url: Joi.string().uri().optional(),
|
|
210
|
+
}).unknown(true),
|
|
211
|
+
).required(),
|
|
212
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
213
|
+
}).unknown(true),
|
|
214
|
+
projections: {
|
|
215
|
+
minimal: {
|
|
216
|
+
fields: ['cves'],
|
|
217
|
+
transformers: {
|
|
218
|
+
cves: 'extractCveUrls',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
[OPPORTUNITY_TYPES.FORM_ACCESSIBILITY]: {
|
|
224
|
+
schema: Joi.object({
|
|
225
|
+
source: Joi.string().required(),
|
|
226
|
+
type: Joi.string().optional(),
|
|
227
|
+
aiGenerated: Joi.boolean().optional(),
|
|
228
|
+
url: Joi.string().uri().required(),
|
|
229
|
+
issues: Joi.array().items(
|
|
230
|
+
Joi.object({
|
|
231
|
+
wcagLevel: Joi.string().optional(),
|
|
232
|
+
severity: Joi.string().optional(),
|
|
233
|
+
occurrences: Joi.number().optional(),
|
|
234
|
+
htmlWithIssues: Joi.array().items(Joi.object()).optional(),
|
|
235
|
+
failureSummary: Joi.string().optional(),
|
|
236
|
+
wcagRule: Joi.string().optional(),
|
|
237
|
+
understandingUrl: Joi.string().uri().optional(),
|
|
238
|
+
description: Joi.string().optional(),
|
|
239
|
+
type: Joi.string().optional(),
|
|
240
|
+
}).unknown(true),
|
|
241
|
+
).required(),
|
|
242
|
+
jiraLink: Joi.string().uri().allow(null).optional(),
|
|
243
|
+
aggregationKey: Joi.string().optional(),
|
|
244
|
+
}).unknown(true),
|
|
245
|
+
projections: {
|
|
246
|
+
minimal: {
|
|
247
|
+
fields: ['url', 'source', 'issues'],
|
|
248
|
+
transformers: {
|
|
249
|
+
issues: 'filterIssuesOccurrences',
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
[OPPORTUNITY_TYPES.CANONICAL]: {
|
|
255
|
+
schema: Joi.object({
|
|
256
|
+
url: Joi.string().uri().required(),
|
|
257
|
+
checkType: Joi.string().optional(),
|
|
258
|
+
type: Joi.string().optional(),
|
|
259
|
+
suggestion: Joi.string().optional(),
|
|
260
|
+
recommendedAction: Joi.string().optional(),
|
|
261
|
+
explanation: Joi.string().optional(),
|
|
262
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
263
|
+
}).unknown(true),
|
|
264
|
+
projections: {
|
|
265
|
+
minimal: {
|
|
266
|
+
fields: ['url'],
|
|
267
|
+
transformers: {},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
[OPPORTUNITY_TYPES.HEADINGS]: {
|
|
272
|
+
schema: Joi.object({
|
|
273
|
+
url: Joi.string().uri().required(),
|
|
274
|
+
type: Joi.string().optional(),
|
|
275
|
+
checkType: Joi.string().optional(),
|
|
276
|
+
explanation: Joi.string().optional(),
|
|
277
|
+
recommendedAction: Joi.string().optional(),
|
|
278
|
+
checkTitle: Joi.string().optional(),
|
|
279
|
+
isAISuggested: Joi.boolean().optional(),
|
|
280
|
+
transformRules: Joi.object().optional(),
|
|
281
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
282
|
+
}).unknown(true),
|
|
283
|
+
projections: {
|
|
284
|
+
minimal: {
|
|
285
|
+
fields: ['url'],
|
|
286
|
+
transformers: {},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
[OPPORTUNITY_TYPES.HREFLANG]: {
|
|
291
|
+
schema: Joi.object({
|
|
292
|
+
url: Joi.string().uri().required(),
|
|
293
|
+
type: Joi.string().optional(),
|
|
294
|
+
checkType: Joi.string().optional(),
|
|
295
|
+
explanation: Joi.string().optional(),
|
|
296
|
+
recommendedAction: Joi.string().optional(),
|
|
297
|
+
suggestion: Joi.string().allow(null).optional(),
|
|
298
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
299
|
+
}).unknown(true),
|
|
300
|
+
projections: {
|
|
301
|
+
minimal: {
|
|
302
|
+
fields: ['url'],
|
|
303
|
+
transformers: {},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
[OPPORTUNITY_TYPES.INVALID_OR_MISSING_METADATA]: {
|
|
308
|
+
schema: Joi.object({
|
|
309
|
+
url: Joi.string().uri().required(),
|
|
310
|
+
tagName: Joi.string().optional(),
|
|
311
|
+
issue: Joi.string().optional(),
|
|
312
|
+
tagContent: Joi.string().allow('', null).optional(),
|
|
313
|
+
rank: Joi.number().optional(),
|
|
314
|
+
seoRecommendation: Joi.string().optional(),
|
|
315
|
+
issueDetails: Joi.string().optional(),
|
|
316
|
+
seoImpact: Joi.string().optional(),
|
|
317
|
+
aiRationale: Joi.string().optional(),
|
|
318
|
+
aiSuggestion: Joi.string().optional(),
|
|
319
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
320
|
+
}).unknown(true),
|
|
321
|
+
projections: {
|
|
322
|
+
minimal: {
|
|
323
|
+
fields: ['url'],
|
|
324
|
+
transformers: {},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
[OPPORTUNITY_TYPES.SITEMAP]: {
|
|
329
|
+
schema: Joi.object({
|
|
330
|
+
sitemapUrl: Joi.string().uri().required(),
|
|
331
|
+
pageUrl: Joi.string().uri().required(),
|
|
332
|
+
type: Joi.string().valid('url', 'error').optional(),
|
|
333
|
+
statusCode: Joi.number().optional(),
|
|
334
|
+
urlsSuggested: Joi.string().uri().optional(),
|
|
335
|
+
recommendedAction: Joi.string().optional(),
|
|
336
|
+
error: Joi.string().optional(),
|
|
337
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
338
|
+
}).unknown(true),
|
|
339
|
+
projections: {
|
|
340
|
+
minimal: {
|
|
341
|
+
fields: ['sitemapUrl', 'pageUrl'],
|
|
342
|
+
transformers: {},
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
[OPPORTUNITY_TYPES.BROKEN_BACKLINKS]: {
|
|
347
|
+
schema: Joi.object({
|
|
348
|
+
url_from: Joi.string().uri().required(),
|
|
349
|
+
url_to: Joi.string().uri().required(),
|
|
350
|
+
title: Joi.string().optional(),
|
|
351
|
+
traffic_domain: Joi.number().optional(),
|
|
352
|
+
aiRationale: Joi.string().optional(),
|
|
353
|
+
urlsSuggested: Joi.array().items(Joi.string().uri()).optional(),
|
|
354
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
355
|
+
}).unknown(true),
|
|
356
|
+
projections: {
|
|
357
|
+
minimal: {
|
|
358
|
+
fields: ['url_from', 'url_to'],
|
|
359
|
+
transformers: {},
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
[OPPORTUNITY_TYPES.BROKEN_INTERNAL_LINKS]: {
|
|
364
|
+
schema: Joi.object({
|
|
365
|
+
urlFrom: Joi.string().uri().required(),
|
|
366
|
+
urlTo: Joi.string().uri().required(),
|
|
367
|
+
title: Joi.string().optional(),
|
|
368
|
+
urlsSuggested: Joi.array().items(Joi.string().uri()).optional(),
|
|
369
|
+
aiRationale: Joi.string().optional(),
|
|
370
|
+
trafficDomain: Joi.number().optional(),
|
|
371
|
+
priority: Joi.string().optional(),
|
|
372
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
373
|
+
}).unknown(true),
|
|
374
|
+
projections: {
|
|
375
|
+
minimal: {
|
|
376
|
+
fields: ['urlFrom', 'urlTo'],
|
|
377
|
+
transformers: {},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
[OPPORTUNITY_TYPES.PRERENDER]: {
|
|
382
|
+
schema: Joi.object({
|
|
383
|
+
url: Joi.string().uri().required(),
|
|
384
|
+
contentGainRatio: Joi.number().optional(),
|
|
385
|
+
wordCountBefore: Joi.number().optional(),
|
|
386
|
+
wordCountAfter: Joi.number().optional(),
|
|
387
|
+
originalHtmlKey: Joi.string().optional(),
|
|
388
|
+
prerenderedHtmlKey: Joi.string().optional(),
|
|
389
|
+
organicTraffic: Joi.number().optional(),
|
|
390
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
391
|
+
}).unknown(true),
|
|
392
|
+
projections: {
|
|
393
|
+
minimal: {
|
|
394
|
+
fields: ['url'],
|
|
395
|
+
transformers: {},
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
[OPPORTUNITY_TYPES.HIGH_ORGANIC_LOW_CTR]: {
|
|
400
|
+
schema: Joi.object({
|
|
401
|
+
url: Joi.string().uri().optional(),
|
|
402
|
+
clicks: Joi.number().optional(),
|
|
403
|
+
impressions: Joi.number().optional(),
|
|
404
|
+
ctr: Joi.number().optional(),
|
|
405
|
+
position: Joi.number().optional(),
|
|
406
|
+
variations: Joi.array().items(
|
|
407
|
+
Joi.object({
|
|
408
|
+
id: Joi.string().optional(),
|
|
409
|
+
name: Joi.string().optional(),
|
|
410
|
+
screenshotUrl: Joi.string().uri().optional(),
|
|
411
|
+
variationPageUrl: Joi.string().uri().optional(),
|
|
412
|
+
variationEditPageUrl: Joi.string().uri().allow(null).optional(),
|
|
413
|
+
variationMdPageUrl: Joi.string().uri().allow(null).optional(),
|
|
414
|
+
previewImage: Joi.string().uri().optional(),
|
|
415
|
+
explanation: Joi.string().allow(null).optional(),
|
|
416
|
+
projectedImpact: Joi.number().optional(),
|
|
417
|
+
changes: Joi.array().optional(),
|
|
418
|
+
variationChanges: Joi.object({
|
|
419
|
+
changes: Joi.object({
|
|
420
|
+
type: Joi.string().optional(),
|
|
421
|
+
mdUrl: Joi.string().uri().optional(),
|
|
422
|
+
md: Joi.string().optional(),
|
|
423
|
+
}).unknown(true).optional(),
|
|
424
|
+
}).unknown(true).optional(),
|
|
425
|
+
}).unknown(true),
|
|
426
|
+
).optional(),
|
|
427
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
428
|
+
}).unknown(true),
|
|
429
|
+
projections: {
|
|
430
|
+
minimal: {
|
|
431
|
+
fields: [],
|
|
432
|
+
transformers: {},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
[OPPORTUNITY_TYPES.LLM_BLOCKED]: {
|
|
437
|
+
schema: Joi.object({
|
|
438
|
+
affectedUserAgents: Joi.array().items(Joi.string()).optional(),
|
|
439
|
+
lineNumber: Joi.number().optional(),
|
|
440
|
+
items: Joi.array().items(
|
|
441
|
+
Joi.object({
|
|
442
|
+
url: Joi.string().uri().optional(),
|
|
443
|
+
agent: Joi.string().optional(),
|
|
444
|
+
}).unknown(true),
|
|
445
|
+
).optional(),
|
|
446
|
+
robotsTxtHash: Joi.string().optional(),
|
|
447
|
+
url: Joi.string().uri().optional(),
|
|
448
|
+
pattern: Joi.string().optional(),
|
|
449
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
450
|
+
}).unknown(true),
|
|
451
|
+
projections: {
|
|
452
|
+
minimal: {
|
|
453
|
+
fields: [],
|
|
454
|
+
transformers: {},
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
// ========== SCHEMAS NEED VALIDATION ==========
|
|
459
|
+
// The following schemas exist (taken from audit-worker structure) but have NOT been
|
|
460
|
+
// validated against actual suggestion data yet.
|
|
461
|
+
//
|
|
462
|
+
// TODO: When these opportunity types generate suggestions:
|
|
463
|
+
// 1. Validate the schema against real suggestion data
|
|
464
|
+
// 2. Update minimal projection fields to match actual requirements
|
|
465
|
+
// 3. Add Suggestion.validateData() call in audit-worker when creating suggestions
|
|
466
|
+
// 4. Make required fields properly marked based on minimal projection
|
|
467
|
+
//
|
|
468
|
+
// Schemas needing validation:
|
|
469
|
+
// - SITEMAP_PRODUCT_COVERAGE
|
|
470
|
+
// - REDIRECT_CHAINS
|
|
471
|
+
[OPPORTUNITY_TYPES.SITEMAP_PRODUCT_COVERAGE]: {
|
|
472
|
+
schema: Joi.object({
|
|
473
|
+
locale: Joi.string().optional(),
|
|
474
|
+
url: Joi.string().uri().optional(),
|
|
475
|
+
recommendedAction: Joi.string().optional(),
|
|
476
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
477
|
+
}).unknown(true),
|
|
478
|
+
projections: {
|
|
479
|
+
minimal: {
|
|
480
|
+
fields: ['url'],
|
|
481
|
+
transformers: {},
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
[OPPORTUNITY_TYPES.REDIRECT_CHAINS]: {
|
|
486
|
+
schema: Joi.object({
|
|
487
|
+
key: Joi.string().optional(),
|
|
488
|
+
fixType: Joi.string().optional(),
|
|
489
|
+
fix: Joi.string().optional(),
|
|
490
|
+
canApplyFixAutomatically: Joi.boolean().optional(),
|
|
491
|
+
redirectsFile: Joi.string().optional(),
|
|
492
|
+
redirectCount: Joi.number().optional(),
|
|
493
|
+
httpStatusCode: Joi.number().optional(),
|
|
494
|
+
sourceUrl: Joi.string().uri().optional(),
|
|
495
|
+
sourceUrlFull: Joi.string().uri().optional(),
|
|
496
|
+
destinationUrl: Joi.string().uri().optional(),
|
|
497
|
+
destinationUrlFull: Joi.string().uri().optional(),
|
|
498
|
+
finalUrl: Joi.string().uri().optional(),
|
|
499
|
+
finalUrlFull: Joi.string().uri().optional(),
|
|
500
|
+
ordinalDuplicate: Joi.number().optional(),
|
|
501
|
+
redirectChain: Joi.array().items(Joi.object()).optional(),
|
|
502
|
+
errorMsg: Joi.string().optional(),
|
|
503
|
+
aggregationKey: Joi.string().allow(null).optional(),
|
|
504
|
+
}).unknown(true),
|
|
505
|
+
projections: {
|
|
506
|
+
minimal: {
|
|
507
|
+
fields: ['sourceUrl', 'destinationUrl', 'finalUrl'],
|
|
508
|
+
transformers: {},
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
// ========== SCHEMAS TO BE ADDED ==========
|
|
514
|
+
// TODO: The following opportunity types need schemas to be added.
|
|
515
|
+
// Research actual suggestion data for these types and add schemas following the pattern:
|
|
516
|
+
// 1. Get real suggestion data examples
|
|
517
|
+
// 2. Define schema with proper field types
|
|
518
|
+
// 3. Make minimal projection fields required (urls used for filtering on ASO UI)
|
|
519
|
+
// 4. Validate against actual data
|
|
520
|
+
//
|
|
521
|
+
// Opportunity types pending schema implementation:
|
|
522
|
+
// - ACCESSIBILITY (parent type - may use A11Y_ASSISTIVE and COLOR_CONTRAST schemas)
|
|
523
|
+
// - NOTFOUND (404 pages)
|
|
524
|
+
// - RAGECLICK
|
|
525
|
+
// - HIGH_INORGANIC_HIGH_BOUNCE_RATE
|
|
526
|
+
// - HIGH_FORM_VIEWS_LOW_CONVERSIONS
|
|
527
|
+
// - HIGH_PAGE_VIEWS_LOW_FORM_NAV
|
|
528
|
+
// - HIGH_PAGE_VIEWS_LOW_FORM_VIEWS
|
|
529
|
+
// - DETECT_GEO_BRAND_PRESENCE
|
|
530
|
+
// - DETECT_GEO_BRAND_PRESENCE_DAILY
|
|
531
|
+
// - GEO_BRAND_PRESENCE_TRIGGER_REFRESH
|
|
532
|
+
// - GUIDANCE_GEO_FAQ
|
|
533
|
+
// - SECURITY_CSP (data.findings[].url - for URL filtering)
|
|
534
|
+
// - SECURITY_XSS (data.link - for URL filtering,
|
|
535
|
+
// may use SECURITY_CSP schema or need separate schema)
|
|
536
|
+
// - SECURITY_PERMISSIONS_REDUNDANT (may use SECURITY_PERMISSIONS schema
|
|
537
|
+
// or need separate schema)
|
|
538
|
+
// - GENERIC_OPPORTUNITY
|
|
539
|
+
// - PAID_COOKIE_CONSENT
|
|
540
|
+
// - WIKIPEDIA_ANALYSIS
|
|
541
|
+
};
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import BaseModel from '../base/base.model.js';
|
|
14
|
+
import { DATA_SCHEMAS } from './suggestion.data-schemas.js';
|
|
15
|
+
import { FIELD_TRANSFORMERS, FALLBACK_PROJECTION } from './suggestion.projection-utils.js';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Suggestion - A class representing a Suggestion entity.
|
|
@@ -44,6 +46,71 @@ class Suggestion extends BaseModel {
|
|
|
44
46
|
CONFIG_UPDATE: 'CONFIG_UPDATE',
|
|
45
47
|
};
|
|
46
48
|
|
|
49
|
+
// Import schemas from external file for maintainability
|
|
50
|
+
static FIELD_TRANSFORMERS = FIELD_TRANSFORMERS;
|
|
51
|
+
|
|
52
|
+
static DATA_SCHEMAS = DATA_SCHEMAS;
|
|
53
|
+
|
|
54
|
+
static FALLBACK_PROJECTION = FALLBACK_PROJECTION;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gets the projection configuration for a given opportunity type and view.
|
|
58
|
+
* Falls back to FALLBACK_PROJECTION if no schema is defined for the type.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} opportunityType - The opportunity type from OPPORTUNITY_TYPES enum
|
|
61
|
+
* @param {string} [viewName='minimal'] - The view name (e.g., 'minimal', 'summary')
|
|
62
|
+
* @returns {Object} Projection configuration with fields and transformers
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const projection = Suggestion.getProjection('cwv', 'minimal');
|
|
66
|
+
* // Returns: { fields: ['url', 'type', 'metrics', 'issues'],
|
|
67
|
+
* // transformers: { metrics: 'filterCwvMetrics' } }
|
|
68
|
+
*/
|
|
69
|
+
static getProjection(opportunityType, viewName = 'minimal') {
|
|
70
|
+
const schemaConfig = this.DATA_SCHEMAS[opportunityType];
|
|
71
|
+
|
|
72
|
+
if (schemaConfig?.projections?.[viewName]) {
|
|
73
|
+
return schemaConfig.projections[viewName];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fallback for unknown types
|
|
77
|
+
return this.FALLBACK_PROJECTION[viewName] || this.FALLBACK_PROJECTION.minimal;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validates suggestion data against the Joi schema for the given opportunity type.
|
|
82
|
+
* If no schema is defined, validation is skipped (graceful fallback).
|
|
83
|
+
*
|
|
84
|
+
* **Usage:** Call this in audit-worker before creating/updating suggestions to ensure
|
|
85
|
+
* data structure consistency across services.
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} data - Suggestion data to validate
|
|
88
|
+
* @param {string} opportunityType - The opportunity type from OPPORTUNITY_TYPES enum
|
|
89
|
+
* @throws {Error} If validation fails with details
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* // In audit-worker before creating a suggestion:
|
|
93
|
+
* try {
|
|
94
|
+
* Suggestion.validateData({ url: 'https://example.com' }, 'structured-data');
|
|
95
|
+
* // Proceed with creating suggestion
|
|
96
|
+
* } catch (error) {
|
|
97
|
+
* log.error('Invalid suggestion data:', error.message);
|
|
98
|
+
* }
|
|
99
|
+
*/
|
|
100
|
+
static validateData(data, opportunityType) {
|
|
101
|
+
const schemaConfig = this.DATA_SCHEMAS[opportunityType];
|
|
102
|
+
|
|
103
|
+
if (!schemaConfig?.schema) {
|
|
104
|
+
// No schema defined, skip validation
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { error } = schemaConfig.schema.validate(data);
|
|
109
|
+
if (error) {
|
|
110
|
+
throw new Error(`Invalid data for opportunity type ${opportunityType}: ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
47
114
|
// add your customized method here
|
|
48
115
|
}
|
|
49
116
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @fileoverview Utility functions and configurations for suggestion data projections.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reusable field transformation functions for projecting suggestion data.
|
|
19
|
+
* Referenced by name in DATA_SCHEMAS transformer configurations.
|
|
20
|
+
*
|
|
21
|
+
* @type {Object<string, Function>}
|
|
22
|
+
*
|
|
23
|
+
* @example Usage in DATA_SCHEMAS
|
|
24
|
+
* projections: {
|
|
25
|
+
* minimal: {
|
|
26
|
+
* fields: ['issues'],
|
|
27
|
+
* transformers: {
|
|
28
|
+
* // References FIELD_TRANSFORMERS['filterIssuesOccurrences']
|
|
29
|
+
* issues: 'filterIssuesOccurrences'
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
*/
|
|
34
|
+
export const FIELD_TRANSFORMERS = {
|
|
35
|
+
/**
|
|
36
|
+
* Filters issues array to only include occurrences count.
|
|
37
|
+
* Used for accessibility-related opportunity types.
|
|
38
|
+
*/
|
|
39
|
+
filterIssuesOccurrences: (issues) => {
|
|
40
|
+
if (!Array.isArray(issues)) return issues;
|
|
41
|
+
return issues.map((issue) => ({
|
|
42
|
+
occurrences: issue.occurrences,
|
|
43
|
+
}));
|
|
44
|
+
},
|
|
45
|
+
/**
|
|
46
|
+
* Filters metrics array to only include essential CWV fields.
|
|
47
|
+
* Used for Core Web Vitals opportunity type.
|
|
48
|
+
*/
|
|
49
|
+
filterCwvMetrics: (metrics) => {
|
|
50
|
+
if (!Array.isArray(metrics)) return metrics;
|
|
51
|
+
return metrics.map((metric) => ({
|
|
52
|
+
deviceType: metric.deviceType,
|
|
53
|
+
lcp: metric.lcp,
|
|
54
|
+
inp: metric.inp,
|
|
55
|
+
cls: metric.cls,
|
|
56
|
+
}));
|
|
57
|
+
},
|
|
58
|
+
/**
|
|
59
|
+
* Extracts pageUrl from recommendations array.
|
|
60
|
+
* Used for alt-text opportunity type.
|
|
61
|
+
*/
|
|
62
|
+
extractPageUrlFromRecommendations: (recommendations) => {
|
|
63
|
+
if (!Array.isArray(recommendations)) return recommendations;
|
|
64
|
+
return recommendations.map((rec) => ({
|
|
65
|
+
pageUrl: rec.pageUrl,
|
|
66
|
+
}));
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* Extracts URLs from CVEs array.
|
|
70
|
+
* Used for security vulnerability opportunity types.
|
|
71
|
+
*/
|
|
72
|
+
extractCveUrls: (cves) => {
|
|
73
|
+
if (!Array.isArray(cves)) return cves;
|
|
74
|
+
return cves.map((cve) => ({
|
|
75
|
+
url: cve.url,
|
|
76
|
+
}));
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Default projection configuration for opportunity types without explicit schemas.
|
|
82
|
+
* Conservative fallback that only includes common URL-related fields.
|
|
83
|
+
* Forces engineers to define explicit schemas if they need specific data fields.
|
|
84
|
+
*
|
|
85
|
+
* @type {Object}
|
|
86
|
+
* @property {Object} minimal - Minimal view configuration
|
|
87
|
+
* @property {string[]} minimal.fields - Common URL fields only (13 fields)
|
|
88
|
+
* @property {Object} minimal.transformers - No transformers applied by default
|
|
89
|
+
*/
|
|
90
|
+
export const FALLBACK_PROJECTION = {
|
|
91
|
+
minimal: {
|
|
92
|
+
fields: [
|
|
93
|
+
'url',
|
|
94
|
+
'urls',
|
|
95
|
+
'urlFrom',
|
|
96
|
+
'urlTo',
|
|
97
|
+
'url_from',
|
|
98
|
+
'url_to',
|
|
99
|
+
'urlsSuggested',
|
|
100
|
+
'pageUrl',
|
|
101
|
+
'sitemapUrl',
|
|
102
|
+
'pattern',
|
|
103
|
+
'link',
|
|
104
|
+
'path',
|
|
105
|
+
'sourceUrl',
|
|
106
|
+
'destinationUrl',
|
|
107
|
+
],
|
|
108
|
+
transformers: {},
|
|
109
|
+
},
|
|
110
|
+
};
|