@adobe/spacecat-shared-tokowaka-client 1.5.4 → 1.5.6
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 +15 -0
- package/README.md +0 -316
- package/package.json +1 -1
- package/src/index.js +8 -3
- package/src/mappers/base-mapper.js +1 -4
- package/src/mappers/faq-mapper.js +1 -4
- package/test/index.test.js +165 -1
- package/test/mappers/base-mapper.test.js +0 -74
- package/test/mappers/readability-mapper.test.js +2 -4
- package/test/mappers/toc-mapper.test.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.5.6](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.5...@adobe/spacecat-shared-tokowaka-client-v1.5.6) (2026-01-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* edge apis, ignore scrapedAt prop for lastUpdated calculation ([#1280](https://github.com/adobe/spacecat-shared/issues/1280)) ([05e4bde](https://github.com/adobe/spacecat-shared/commit/05e4bde07fc865e8d0aee71639f799798e68594d))
|
|
7
|
+
* update prerender config ([#1279](https://github.com/adobe/spacecat-shared/issues/1279)) ([4502ba4](https://github.com/adobe/spacecat-shared/commit/4502ba43f6a26e5d774ce0e2e7dfc6e886ab7402))
|
|
8
|
+
|
|
9
|
+
# [@adobe/spacecat-shared-tokowaka-client-v1.5.5](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.4...@adobe/spacecat-shared-tokowaka-client-v1.5.5) (2026-01-21)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* remove details from tokowaka readme ([#1277](https://github.com/adobe/spacecat-shared/issues/1277)) ([14f3aa8](https://github.com/adobe/spacecat-shared/commit/14f3aa82cfbde56c6baebd1ad979980cbe24dd12))
|
|
15
|
+
|
|
1
16
|
# [@adobe/spacecat-shared-tokowaka-client-v1.5.4](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.5.3...@adobe/spacecat-shared-tokowaka-client-v1.5.4) (2026-01-21)
|
|
2
17
|
|
|
3
18
|
|
package/README.md
CHANGED
|
@@ -17,148 +17,6 @@ const tokowakaClient = TokowakaClient.createFrom(context);
|
|
|
17
17
|
const result = await tokowakaClient.deploySuggestions(site, opportunity, suggestions);
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
## API Reference
|
|
21
|
-
|
|
22
|
-
### TokowakaClient.createFrom(context)
|
|
23
|
-
|
|
24
|
-
Creates a client instance from a context object.
|
|
25
|
-
|
|
26
|
-
**Required context properties:**
|
|
27
|
-
- `context.s3.s3Client` (S3Client): AWS S3 client instance
|
|
28
|
-
- `context.log` (Object, optional): Logger instance
|
|
29
|
-
- `context.env.TOKOWAKA_SITE_CONFIG_BUCKET` (string): S3 bucket name for deployed configurations
|
|
30
|
-
- `context.env.TOKOWAKA_PREVIEW_BUCKET` (string): S3 bucket name for preview configurations
|
|
31
|
-
- `context.env.TOKOWAKA_CDN_PROVIDER` (string | string[]): CDN provider(s) for cache invalidation
|
|
32
|
-
- `context.env.TOKOWAKA_CDN_CONFIG` (string): JSON configuration for CDN clients
|
|
33
|
-
- `context.env.TOKOWAKA_EDGE_URL` (string): Tokowaka edge URL for preview HTML fetching
|
|
34
|
-
|
|
35
|
-
## Environment Variables
|
|
36
|
-
|
|
37
|
-
**Required:**
|
|
38
|
-
- `TOKOWAKA_SITE_CONFIG_BUCKET` - S3 bucket name for storing deployed configurations
|
|
39
|
-
- `TOKOWAKA_PREVIEW_BUCKET` - S3 bucket name for storing preview configurations
|
|
40
|
-
|
|
41
|
-
**Optional (for CDN invalidation):**
|
|
42
|
-
- `TOKOWAKA_CDN_PROVIDER` - CDN provider name(s). Can be a single provider (e.g., "cloudfront") or multiple providers as comma-separated string (e.g., "cloudfront,fastly") or array
|
|
43
|
-
- `TOKOWAKA_CDN_CONFIG` - JSON string with CDN-specific configuration for all providers:
|
|
44
|
-
```json
|
|
45
|
-
{
|
|
46
|
-
"cloudfront": {
|
|
47
|
-
"distributionId": "<distribution-id>",
|
|
48
|
-
"region": "us-east-1"
|
|
49
|
-
},
|
|
50
|
-
"fastly": {
|
|
51
|
-
"serviceId": "<service-id>",
|
|
52
|
-
"apiToken": "<api-token>",
|
|
53
|
-
"distributionUrl": "https://<cloudfront-distribution>.cloudfront.net"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
**Note for Fastly**: The `distributionUrl` is required and should be the full CloudFront distribution URL (e.g., `https://deftbrsarcsf4.cloudfront.net`). Fastly uses this to construct full URLs as surrogate keys for cache purging.
|
|
59
|
-
|
|
60
|
-
**Optional (for preview functionality):**
|
|
61
|
-
- `TOKOWAKA_EDGE_URL` - Tokowaka edge URL for fetching HTML content during preview
|
|
62
|
-
|
|
63
|
-
### Main Methods
|
|
64
|
-
|
|
65
|
-
#### `deploySuggestions(site, opportunity, suggestions)`
|
|
66
|
-
|
|
67
|
-
Generates configuration and uploads to S3 **per URL**. **Automatically fetches existing configuration for each URL and merges** new suggestions with it. Invalidates CDN cache after upload. **Updates the metaconfig's `patches` field** to track deployed endpoints.
|
|
68
|
-
|
|
69
|
-
**Architecture Change:** Creates one S3 file per URL instead of a single file with all URLs. This prevents files from growing too large over time.
|
|
70
|
-
|
|
71
|
-
**Metaconfig Tracking:** After successful deployment, the method updates the domain-level metaconfig's `patches` object with the normalized paths of all deployed endpoints (e.g., `{ "/products/item": true }`). This provides a centralized registry of all deployed endpoints for the domain.
|
|
72
|
-
|
|
73
|
-
**Returns:** `Promise<DeploymentResult>` with:
|
|
74
|
-
- `s3Paths` - Array of S3 keys where configs were uploaded (one per URL)
|
|
75
|
-
- `cdnInvalidations` - Array of CDN invalidation results (one per URL per provider)
|
|
76
|
-
- `succeededSuggestions` - Array of deployed suggestions
|
|
77
|
-
- `failedSuggestions` - Array of `{suggestion, reason}` objects for ineligible suggestions
|
|
78
|
-
|
|
79
|
-
#### `rollbackSuggestions(site, opportunity, suggestions)`
|
|
80
|
-
|
|
81
|
-
Rolls back previously deployed suggestions by removing their patches from the configuration. **Automatically fetches existing configuration for each URL** and removes patches matching the provided suggestions. Invalidates CDN cache after upload.
|
|
82
|
-
|
|
83
|
-
**Architecture Change:** Updates one S3 file per URL instead of a single file with all URLs.
|
|
84
|
-
|
|
85
|
-
**Mapper-Specific Rollback Behavior:**
|
|
86
|
-
- Each opportunity mapper handles its own rollback logic via `rollbackPatches()` method
|
|
87
|
-
- **FAQ:** Automatically removes the "FAQs" heading patch if no FAQ suggestions remain for that URL
|
|
88
|
-
- **Headings/Summarization:** Simple removal by suggestion ID (default behavior)
|
|
89
|
-
|
|
90
|
-
**Returns:** `Promise<RollbackResult>` with:
|
|
91
|
-
- `s3Paths` - Array of S3 keys where configs were uploaded (one per URL)
|
|
92
|
-
- `cdnInvalidations` - Array of CDN invalidation results (one per URL per provider)
|
|
93
|
-
- `succeededSuggestions` - Array of rolled back suggestions
|
|
94
|
-
- `failedSuggestions` - Array of `{suggestion, reason}` objects for ineligible suggestions
|
|
95
|
-
- `removedPatchesCount` - Total number of patches removed across all URLs
|
|
96
|
-
|
|
97
|
-
#### `previewSuggestions(site, opportunity, suggestions, options)`
|
|
98
|
-
|
|
99
|
-
Previews suggestions by uploading to preview S3 path and fetching HTML comparison. **All suggestions must belong to the same URL.**
|
|
100
|
-
|
|
101
|
-
**Returns:** `Promise<PreviewResult>` with:
|
|
102
|
-
- `s3Path` - S3 key where preview config was uploaded
|
|
103
|
-
- `config` - Preview configuration object
|
|
104
|
-
- `cdnInvalidations` - Array of CDN invalidation results (one per provider)
|
|
105
|
-
- `succeededSuggestions` - Array of previewed suggestions
|
|
106
|
-
- `failedSuggestions` - Array of `{suggestion, reason}` objects for ineligible suggestions
|
|
107
|
-
- `html` - Object with `url`, `originalHtml`, and `optimizedHtml`
|
|
108
|
-
|
|
109
|
-
#### `fetchConfig(url, isPreview)`
|
|
110
|
-
|
|
111
|
-
Fetches existing Tokowaka configuration from S3 for a specific URL.
|
|
112
|
-
|
|
113
|
-
**Parameters:**
|
|
114
|
-
- `url` - Full URL (e.g., 'https://www.example.com/products/item')
|
|
115
|
-
- `isPreview` - Whether to fetch from preview path (default: false)
|
|
116
|
-
|
|
117
|
-
**Returns:** `Promise<TokowakaConfig | null>` - Configuration object or null if not found
|
|
118
|
-
|
|
119
|
-
#### `mergeConfigs(existingConfig, newConfig)`
|
|
120
|
-
|
|
121
|
-
Merges existing configuration with new configuration. For each URL path, checks if `opportunityId` + `suggestionId` combination exists and either updates or adds patches accordingly.
|
|
122
|
-
|
|
123
|
-
**Returns:** `TokowakaConfig` - Merged configuration
|
|
124
|
-
|
|
125
|
-
#### `generateConfig(url, opportunity, suggestions)`
|
|
126
|
-
|
|
127
|
-
Generates Tokowaka configuration from opportunity suggestions for a specific URL without uploading.
|
|
128
|
-
|
|
129
|
-
**Parameters:**
|
|
130
|
-
- `url` - Full URL for which to generate config
|
|
131
|
-
- `opportunity` - Opportunity entity
|
|
132
|
-
- `suggestions` - Array of suggestion entities
|
|
133
|
-
|
|
134
|
-
#### `uploadConfig(url, config, isPreview)`
|
|
135
|
-
|
|
136
|
-
Uploads configuration to S3 for a specific URL.
|
|
137
|
-
|
|
138
|
-
**Parameters:**
|
|
139
|
-
- `url` - Full URL (e.g., 'https://www.example.com/products/item')
|
|
140
|
-
- `config` - Tokowaka configuration object
|
|
141
|
-
- `isPreview` - Whether to upload to preview path (default: false)
|
|
142
|
-
|
|
143
|
-
**Returns:** `Promise<string>` - S3 key of uploaded configuration
|
|
144
|
-
|
|
145
|
-
## CDN Cache Invalidation
|
|
146
|
-
|
|
147
|
-
The client invalidates CDN cache after uploading configurations. Failures are logged but don't block deployment.
|
|
148
|
-
|
|
149
|
-
## Site Configuration
|
|
150
|
-
|
|
151
|
-
Sites must have the `tokowakaConfig` field in their site-config to confirm that tokowaka is enabled for that particular site.
|
|
152
|
-
|
|
153
|
-
```javascript
|
|
154
|
-
{
|
|
155
|
-
...
|
|
156
|
-
"tokowakaConfig": {
|
|
157
|
-
"enabled": true
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
20
|
## Supported Opportunity Types
|
|
163
21
|
|
|
164
22
|
### Headings
|
|
@@ -174,177 +32,3 @@ Sites must have the `tokowakaConfig` field in their site-config to confirm that
|
|
|
174
32
|
### Content Summarization
|
|
175
33
|
|
|
176
34
|
**Deployment Eligibility:** Currently all suggestions for `summarization` opportunity can be deployed.
|
|
177
|
-
|
|
178
|
-
## S3 Storage
|
|
179
|
-
|
|
180
|
-
Configurations are now stored **per URL** with domain-level metadata:
|
|
181
|
-
|
|
182
|
-
### Structure
|
|
183
|
-
```
|
|
184
|
-
s3://{TOKOWAKA_SITE_CONFIG_BUCKET}/opportunities/{normalized-domain}/
|
|
185
|
-
├── config (domain-level metaconfig: siteId, prerender)
|
|
186
|
-
├── {base64-encoded-path-1} (URL-specific patches)
|
|
187
|
-
├── {base64-encoded-path-2} (URL-specific patches)
|
|
188
|
-
└── ...
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
For preview configurations:
|
|
192
|
-
```
|
|
193
|
-
s3://{TOKOWAKA_PREVIEW_BUCKET}/preview/opportunities/{normalized-domain}/
|
|
194
|
-
├── config
|
|
195
|
-
├── {base64-encoded-path-1}
|
|
196
|
-
└── ...
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
**Architecture Change:** Each URL has its own configuration file instead of one file per site. Domain-level metaconfig is stored separately to avoid duplication.
|
|
200
|
-
|
|
201
|
-
**URL Normalization:**
|
|
202
|
-
- Domain: Strips `www.` prefix (e.g., `www.example.com` → `example.com`)
|
|
203
|
-
- Path: Removes trailing slash (except for root `/`), ensures starts with `/`, then base64 URL encodes
|
|
204
|
-
|
|
205
|
-
**Example:**
|
|
206
|
-
- URL: `https://www.example.com/products/item`
|
|
207
|
-
- Metaconfig Path: `opportunities/example.com/config`
|
|
208
|
-
- Patch Config Path: `opportunities/example.com/L3Byb2R1Y3RzL2l0ZW0`
|
|
209
|
-
- Where `L3Byb2R1Y3RzL2l0ZW0` is base64 URL encoding of `/products/item`
|
|
210
|
-
|
|
211
|
-
### Metaconfig File Structure
|
|
212
|
-
Domain-level metaconfig (created once per domain, shared by all URLs):
|
|
213
|
-
```json
|
|
214
|
-
{
|
|
215
|
-
"siteId": "abc-123",
|
|
216
|
-
"apiKeys": ["tokowaka-api-key-1"],
|
|
217
|
-
"tokowakaEnabled": true,
|
|
218
|
-
"enhancements": true,
|
|
219
|
-
"prerender": {
|
|
220
|
-
"allowList": [],
|
|
221
|
-
"denyList": ["/*"]
|
|
222
|
-
},
|
|
223
|
-
"patches": {
|
|
224
|
-
"/products/item": true,
|
|
225
|
-
"/about": true,
|
|
226
|
-
"/contact": true
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### Configuration File Structure
|
|
232
|
-
Per-URL configuration (flat structure):
|
|
233
|
-
```json
|
|
234
|
-
{
|
|
235
|
-
"url": "https://example.com/products/item",
|
|
236
|
-
"version": "1.0",
|
|
237
|
-
"forceFail": false,
|
|
238
|
-
"prerender": true,
|
|
239
|
-
"patches": [
|
|
240
|
-
{
|
|
241
|
-
"opportunityId": "abc-123",
|
|
242
|
-
"suggestionId": "xyz-789",
|
|
243
|
-
"prerenderRequired": true,
|
|
244
|
-
"lastUpdated": 1234567890,
|
|
245
|
-
"op": "insertAfter",
|
|
246
|
-
"selector": "main",
|
|
247
|
-
"value": { ... },
|
|
248
|
-
"valueFormat": "hast",
|
|
249
|
-
"target": "ai-bots"
|
|
250
|
-
}
|
|
251
|
-
]
|
|
252
|
-
}
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
**Note:**
|
|
256
|
-
- `siteId` is stored only in domain-level `config` (metaconfig)
|
|
257
|
-
- `prerender` is stored in both metaconfig (domain-level) and patch files (URL-level)
|
|
258
|
-
- The `baseURL` field has been renamed to `url`
|
|
259
|
-
- The `tokowakaOptimizations` nested structure has been removed
|
|
260
|
-
- The `tokowakaForceFail` field has been renamed to `forceFail`
|
|
261
|
-
|
|
262
|
-
## CDN Cache Invalidation
|
|
263
|
-
|
|
264
|
-
The client supports multiple CDN providers for cache invalidation with **intelligent batching** for optimal performance.
|
|
265
|
-
|
|
266
|
-
### Batch Optimization
|
|
267
|
-
|
|
268
|
-
When deploying or rolling back multiple URLs:
|
|
269
|
-
- **Before**: Each URL triggered separate CDN invalidation calls (N URLs = N×2 API calls for 2 providers)
|
|
270
|
-
- **After**: All URLs are collected and invalidated in a single batch call per provider (N URLs = 2 API calls for 2 providers)
|
|
271
|
-
|
|
272
|
-
**Example Performance Improvement:**
|
|
273
|
-
- 10 URLs with CloudFront + Fastly
|
|
274
|
-
- Old: 20 API calls (10 per provider)
|
|
275
|
-
- New: **2 API calls** (1 per provider)
|
|
276
|
-
- **90% reduction in API calls!**
|
|
277
|
-
|
|
278
|
-
### Supported CDN Providers
|
|
279
|
-
|
|
280
|
-
#### CloudFront
|
|
281
|
-
AWS CloudFront CDN invalidation using the AWS SDK.
|
|
282
|
-
|
|
283
|
-
**Configuration:**
|
|
284
|
-
```json
|
|
285
|
-
{
|
|
286
|
-
"cloudfront": {
|
|
287
|
-
"distributionId": "E1234567890ABC",
|
|
288
|
-
"region": "us-east-1"
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
#### Fastly
|
|
294
|
-
Fastly CDN purging using surrogate key purging.
|
|
295
|
-
|
|
296
|
-
**Configuration:**
|
|
297
|
-
```json
|
|
298
|
-
{
|
|
299
|
-
"fastly": {
|
|
300
|
-
"serviceId": "abc123xyz",
|
|
301
|
-
"apiToken": "your-fastly-api-token"
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### Multiple CDN Providers
|
|
307
|
-
|
|
308
|
-
To invalidate cache on multiple CDNs simultaneously:
|
|
309
|
-
|
|
310
|
-
**Environment Variable:**
|
|
311
|
-
```bash
|
|
312
|
-
TOKOWAKA_CDN_PROVIDER="cloudfront,fastly"
|
|
313
|
-
# or as array in code: ["cloudfront", "fastly"]
|
|
314
|
-
|
|
315
|
-
TOKOWAKA_CDN_CONFIG='{"cloudfront":{"distributionId":"...","region":"us-east-1"},"fastly":{"serviceId":"...","apiToken":"...","distributionUrl":"https://xxx.cloudfront.net"}}'
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
**Behavior:**
|
|
319
|
-
- All URLs are batched into a single invalidation request per provider
|
|
320
|
-
- All CDN providers are invalidated sequentially
|
|
321
|
-
- Failures in one CDN don't block others
|
|
322
|
-
- Each CDN returns its own result in the `cdnInvalidations` array
|
|
323
|
-
- Results include status, provider name, and provider-specific details
|
|
324
|
-
|
|
325
|
-
### Fastly Batch Purging
|
|
326
|
-
|
|
327
|
-
The Fastly client uses surrogate key purging with full CloudFront URLs:
|
|
328
|
-
- **Surrogate Keys**: Constructed as full CloudFront URLs (e.g., `https://xxx.cloudfront.net/opportunities/adobe.com/config`)
|
|
329
|
-
- **Batch Purging**: All paths are sent in a single API call using the `Surrogate-Key` header (space-separated URLs)
|
|
330
|
-
- **Configuration**: Requires `distributionUrl` in the Fastly config to construct full URLs
|
|
331
|
-
- Significantly reduces API calls and improves performance for bulk operations
|
|
332
|
-
|
|
333
|
-
**Example Fastly Configuration:**
|
|
334
|
-
```json
|
|
335
|
-
{
|
|
336
|
-
"serviceId": "abc123xyz",
|
|
337
|
-
"apiToken": "your-fastly-api-token",
|
|
338
|
-
"distributionUrl": "https://deftbrsarcsf4.cloudfront.net"
|
|
339
|
-
}
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
This configuration allows Fastly to purge cache entries using URLs like:
|
|
343
|
-
```
|
|
344
|
-
fastly purge --key https://deftbrsarcsf4.cloudfront.net/opportunities/adobe.com/config
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
## Reference Material
|
|
348
|
-
|
|
349
|
-
https://wiki.corp.adobe.com/display/AEMSites/Tokowaka+-+Spacecat+Integration
|
|
350
|
-
https://wiki.corp.adobe.com/display/AEMSites/Tokowaka+Patch+Format
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -355,20 +355,25 @@ class TokowakaClient {
|
|
|
355
355
|
?? existingMetaconfig.forceFail
|
|
356
356
|
?? false;
|
|
357
357
|
|
|
358
|
+
const hasPrerender = isNonEmptyObject(options.prerender)
|
|
359
|
+
|| isNonEmptyObject(existingMetaconfig.prerender);
|
|
360
|
+
const prerender = options.prerender
|
|
361
|
+
?? existingMetaconfig.prerender;
|
|
362
|
+
|
|
358
363
|
const metaconfig = {
|
|
359
|
-
|
|
360
|
-
apiKeys: existingMetaconfig.apiKeys,
|
|
364
|
+
...existingMetaconfig,
|
|
361
365
|
tokowakaEnabled: options.tokowakaEnabled ?? existingMetaconfig.tokowakaEnabled ?? true,
|
|
362
366
|
enhancements: options.enhancements ?? existingMetaconfig.enhancements ?? true,
|
|
363
367
|
patches: isNonEmptyObject(options.patches)
|
|
364
368
|
? options.patches
|
|
365
369
|
: (existingMetaconfig.patches ?? {}),
|
|
366
370
|
...(hasForceFail && { forceFail }),
|
|
371
|
+
...(hasPrerender && { prerender }),
|
|
367
372
|
};
|
|
368
373
|
|
|
369
374
|
const s3Path = await this.uploadMetaconfig(url, metaconfig);
|
|
370
375
|
|
|
371
|
-
this.log.info(`
|
|
376
|
+
this.log.info(`Updated Tokowaka metaconfig for ${normalizedHostName} at ${s3Path}`);
|
|
372
377
|
|
|
373
378
|
return metaconfig;
|
|
374
379
|
}
|
|
@@ -88,10 +88,7 @@ export default class BaseOpportunityMapper {
|
|
|
88
88
|
* @returns {Object} - Base patch object
|
|
89
89
|
*/
|
|
90
90
|
createBasePatch(suggestion, opportunityId) {
|
|
91
|
-
const
|
|
92
|
-
const updatedAt = data?.scrapedAt
|
|
93
|
-
|| data?.transformRules?.scrapedAt
|
|
94
|
-
|| suggestion.getUpdatedAt();
|
|
91
|
+
const updatedAt = suggestion.getUpdatedAt();
|
|
95
92
|
|
|
96
93
|
// Parse timestamp, fallback to Date.now() if invalid
|
|
97
94
|
let lastUpdated = Date.now();
|
|
@@ -111,10 +111,7 @@ export default class FaqMapper extends BaseOpportunityMapper {
|
|
|
111
111
|
// Calculate the most recent lastUpdated from all eligible suggestions
|
|
112
112
|
// The heading patch should have the same timestamp as the newest FAQ
|
|
113
113
|
const maxLastUpdated = Math.max(...eligibleSuggestions.map((suggestion) => {
|
|
114
|
-
const
|
|
115
|
-
const updatedAt = data?.scrapedAt
|
|
116
|
-
|| data?.transformRules?.scrapedAt
|
|
117
|
-
|| suggestion.getUpdatedAt();
|
|
114
|
+
const updatedAt = suggestion.getUpdatedAt();
|
|
118
115
|
|
|
119
116
|
if (updatedAt) {
|
|
120
117
|
const parsed = new Date(updatedAt).getTime();
|
package/test/index.test.js
CHANGED
|
@@ -590,7 +590,7 @@ describe('TokowakaClient', () => {
|
|
|
590
590
|
});
|
|
591
591
|
|
|
592
592
|
it('should update metaconfig with default options', async () => {
|
|
593
|
-
const siteId = 'site-
|
|
593
|
+
const siteId = 'site-456';
|
|
594
594
|
const url = 'https://www.example.com/page1';
|
|
595
595
|
|
|
596
596
|
const result = await client.updateMetaconfig(url, siteId);
|
|
@@ -1144,6 +1144,170 @@ describe('TokowakaClient', () => {
|
|
|
1144
1144
|
|
|
1145
1145
|
expect(result).to.have.property('forceFail', false);
|
|
1146
1146
|
});
|
|
1147
|
+
|
|
1148
|
+
it('should include prerender when provided in options', async () => {
|
|
1149
|
+
const siteId = 'site-789';
|
|
1150
|
+
const url = 'https://example.com';
|
|
1151
|
+
const prerenderConfig = { allowList: ['/*'] };
|
|
1152
|
+
|
|
1153
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: prerenderConfig });
|
|
1154
|
+
|
|
1155
|
+
expect(result).to.have.property('prerender');
|
|
1156
|
+
expect(result.prerender).to.deep.equal(prerenderConfig);
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
it('should preserve existingMetaconfig prerender when options.prerender is undefined', async () => {
|
|
1160
|
+
const existingPrerenderConfig = { allowList: ['/*', '/products/*'] };
|
|
1161
|
+
const configWithPrerender = {
|
|
1162
|
+
siteId: 'site-456',
|
|
1163
|
+
apiKeys: ['existing-api-key-123'],
|
|
1164
|
+
tokowakaEnabled: false,
|
|
1165
|
+
enhancements: false,
|
|
1166
|
+
patches: {},
|
|
1167
|
+
prerender: existingPrerenderConfig,
|
|
1168
|
+
};
|
|
1169
|
+
s3Client.send.onFirstCall().resolves({
|
|
1170
|
+
Body: {
|
|
1171
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithPrerender)),
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
const siteId = 'site-789';
|
|
1176
|
+
const url = 'https://example.com';
|
|
1177
|
+
|
|
1178
|
+
const result = await client.updateMetaconfig(url, siteId);
|
|
1179
|
+
|
|
1180
|
+
expect(result).to.have.property('prerender');
|
|
1181
|
+
expect(result.prerender).to.deep.equal(existingPrerenderConfig);
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
it('should use existingMetaconfig prerender when options.prerender is null', async () => {
|
|
1185
|
+
const existingPrerenderConfig = { allowList: ['/*'] };
|
|
1186
|
+
const configWithPrerender = {
|
|
1187
|
+
siteId: 'site-456',
|
|
1188
|
+
apiKeys: ['existing-api-key-123'],
|
|
1189
|
+
tokowakaEnabled: false,
|
|
1190
|
+
enhancements: false,
|
|
1191
|
+
patches: {},
|
|
1192
|
+
prerender: existingPrerenderConfig,
|
|
1193
|
+
};
|
|
1194
|
+
s3Client.send.onFirstCall().resolves({
|
|
1195
|
+
Body: {
|
|
1196
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithPrerender)),
|
|
1197
|
+
},
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
const siteId = 'site-789';
|
|
1201
|
+
const url = 'https://example.com';
|
|
1202
|
+
|
|
1203
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: null });
|
|
1204
|
+
|
|
1205
|
+
expect(result).to.have.property('prerender');
|
|
1206
|
+
expect(result.prerender).to.deep.equal(existingPrerenderConfig);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it('should override existingMetaconfig prerender when provided in options', async () => {
|
|
1210
|
+
const existingPrerenderConfig = { allowList: ['/blog/*'] };
|
|
1211
|
+
const newPrerenderConfig = { allowList: ['/*', '/products/*'] };
|
|
1212
|
+
const configWithPrerender = {
|
|
1213
|
+
siteId: 'site-456',
|
|
1214
|
+
apiKeys: ['existing-api-key-123'],
|
|
1215
|
+
tokowakaEnabled: false,
|
|
1216
|
+
enhancements: false,
|
|
1217
|
+
patches: {},
|
|
1218
|
+
prerender: existingPrerenderConfig,
|
|
1219
|
+
};
|
|
1220
|
+
s3Client.send.onFirstCall().resolves({
|
|
1221
|
+
Body: {
|
|
1222
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithPrerender)),
|
|
1223
|
+
},
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
const siteId = 'site-789';
|
|
1227
|
+
const url = 'https://example.com';
|
|
1228
|
+
|
|
1229
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: newPrerenderConfig });
|
|
1230
|
+
|
|
1231
|
+
expect(result).to.have.property('prerender');
|
|
1232
|
+
expect(result.prerender).to.deep.equal(newPrerenderConfig);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
it('should not include prerender when neither options nor existingMetaconfig have it', async () => {
|
|
1236
|
+
const siteId = 'site-789';
|
|
1237
|
+
const url = 'https://example.com';
|
|
1238
|
+
|
|
1239
|
+
const result = await client.updateMetaconfig(url, siteId);
|
|
1240
|
+
|
|
1241
|
+
expect(result).to.not.have.property('prerender');
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
it('should not include prerender when both options and existingMetaconfig have empty prerender', async () => {
|
|
1245
|
+
const siteId = 'site-789';
|
|
1246
|
+
const url = 'https://example.com';
|
|
1247
|
+
|
|
1248
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: {} });
|
|
1249
|
+
|
|
1250
|
+
expect(result).to.not.have.property('prerender');
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
it('should include prerender from options when existingMetaconfig does not have it', async () => {
|
|
1254
|
+
const siteId = 'site-789';
|
|
1255
|
+
const url = 'https://example.com';
|
|
1256
|
+
const prerenderConfig = { allowList: ['/*'] };
|
|
1257
|
+
|
|
1258
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: prerenderConfig });
|
|
1259
|
+
|
|
1260
|
+
expect(result).to.have.property('prerender');
|
|
1261
|
+
expect(result.prerender).to.deep.equal(prerenderConfig);
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it('should handle prerender with multiple paths in allowList', async () => {
|
|
1265
|
+
const siteId = 'site-789';
|
|
1266
|
+
const url = 'https://example.com';
|
|
1267
|
+
const prerenderConfig = {
|
|
1268
|
+
allowList: ['/*', '/products/*', '/blog/*', '/about'],
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: prerenderConfig });
|
|
1272
|
+
|
|
1273
|
+
expect(result).to.have.property('prerender');
|
|
1274
|
+
expect(result.prerender).to.deep.equal(prerenderConfig);
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
it('should use options.prerender even when it is an empty object if existingMetaconfig has no prerender', async () => {
|
|
1278
|
+
const siteId = 'site-789';
|
|
1279
|
+
const url = 'https://example.com';
|
|
1280
|
+
|
|
1281
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: {} });
|
|
1282
|
+
|
|
1283
|
+
// Empty object is not null/undefined, so it will be used by nullish coalescing
|
|
1284
|
+
// But hasPrerender will be false, so it won't be included in final metaconfig
|
|
1285
|
+
expect(result).to.not.have.property('prerender');
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
it('should handle case where existingMetaconfig.prerender is undefined and options.prerender is provided', async () => {
|
|
1289
|
+
const configWithoutPrerender = {
|
|
1290
|
+
siteId: 'site-456',
|
|
1291
|
+
apiKeys: ['existing-api-key-123'],
|
|
1292
|
+
tokowakaEnabled: false,
|
|
1293
|
+
enhancements: false,
|
|
1294
|
+
patches: {},
|
|
1295
|
+
};
|
|
1296
|
+
s3Client.send.onFirstCall().resolves({
|
|
1297
|
+
Body: {
|
|
1298
|
+
transformToString: sinon.stub().resolves(JSON.stringify(configWithoutPrerender)),
|
|
1299
|
+
},
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
const siteId = 'site-789';
|
|
1303
|
+
const url = 'https://example.com';
|
|
1304
|
+
const prerenderConfig = { allowList: ['/*'] };
|
|
1305
|
+
|
|
1306
|
+
const result = await client.updateMetaconfig(url, siteId, { prerender: prerenderConfig });
|
|
1307
|
+
|
|
1308
|
+
expect(result).to.have.property('prerender');
|
|
1309
|
+
expect(result.prerender).to.deep.equal(prerenderConfig);
|
|
1310
|
+
});
|
|
1147
1311
|
});
|
|
1148
1312
|
|
|
1149
1313
|
describe('uploadConfig', () => {
|
|
@@ -68,7 +68,6 @@ describe('BaseOpportunityMapper', () => {
|
|
|
68
68
|
const testMapper = new TestMapper(log);
|
|
69
69
|
const suggestion = {
|
|
70
70
|
getId: () => 'test-123',
|
|
71
|
-
getData: () => ({}),
|
|
72
71
|
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
73
72
|
};
|
|
74
73
|
|
|
@@ -94,7 +93,6 @@ describe('BaseOpportunityMapper', () => {
|
|
|
94
93
|
const testMapper = new TestMapper(log);
|
|
95
94
|
const suggestion = {
|
|
96
95
|
getId: () => 'test-no-date',
|
|
97
|
-
getData: () => ({}),
|
|
98
96
|
getUpdatedAt: () => null, // Returns null
|
|
99
97
|
};
|
|
100
98
|
|
|
@@ -109,54 +107,6 @@ describe('BaseOpportunityMapper', () => {
|
|
|
109
107
|
expect(patch.prerenderRequired).to.be.true;
|
|
110
108
|
});
|
|
111
109
|
|
|
112
|
-
it('should prioritize scrapedAt from getData()', () => {
|
|
113
|
-
class TestMapper extends BaseOpportunityMapper {
|
|
114
|
-
getOpportunityType() { return 'test'; }
|
|
115
|
-
|
|
116
|
-
requiresPrerender() { return true; }
|
|
117
|
-
|
|
118
|
-
suggestionsToPatches() { return []; }
|
|
119
|
-
|
|
120
|
-
canDeploy() { return { eligible: true }; }
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const testMapper = new TestMapper(log);
|
|
124
|
-
const scrapedTime = '2025-01-20T15:30:00.000Z';
|
|
125
|
-
const suggestion = {
|
|
126
|
-
getId: () => 'test-scraped',
|
|
127
|
-
getData: () => ({ scrapedAt: scrapedTime }),
|
|
128
|
-
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const patch = testMapper.createBasePatch(suggestion, 'opp-scraped');
|
|
132
|
-
|
|
133
|
-
expect(patch.lastUpdated).to.equal(new Date(scrapedTime).getTime());
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('should use transformRules.scrapedAt when scrapedAt is not available', () => {
|
|
137
|
-
class TestMapper extends BaseOpportunityMapper {
|
|
138
|
-
getOpportunityType() { return 'test'; }
|
|
139
|
-
|
|
140
|
-
requiresPrerender() { return true; }
|
|
141
|
-
|
|
142
|
-
suggestionsToPatches() { return []; }
|
|
143
|
-
|
|
144
|
-
canDeploy() { return { eligible: true }; }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const testMapper = new TestMapper(log);
|
|
148
|
-
const transformScrapedTime = '2025-01-18T12:00:00.000Z';
|
|
149
|
-
const suggestion = {
|
|
150
|
-
getId: () => 'test-transform',
|
|
151
|
-
getData: () => ({ transformRules: { scrapedAt: transformScrapedTime } }),
|
|
152
|
-
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const patch = testMapper.createBasePatch(suggestion, 'opp-transform');
|
|
156
|
-
|
|
157
|
-
expect(patch.lastUpdated).to.equal(new Date(transformScrapedTime).getTime());
|
|
158
|
-
});
|
|
159
|
-
|
|
160
110
|
it('should handle invalid date strings by using Date.now()', () => {
|
|
161
111
|
class TestMapper extends BaseOpportunityMapper {
|
|
162
112
|
getOpportunityType() { return 'test'; }
|
|
@@ -171,7 +121,6 @@ describe('BaseOpportunityMapper', () => {
|
|
|
171
121
|
const testMapper = new TestMapper(log);
|
|
172
122
|
const suggestion = {
|
|
173
123
|
getId: () => 'test-invalid',
|
|
174
|
-
getData: () => ({}),
|
|
175
124
|
getUpdatedAt: () => 'invalid-date-string',
|
|
176
125
|
};
|
|
177
126
|
|
|
@@ -183,29 +132,6 @@ describe('BaseOpportunityMapper', () => {
|
|
|
183
132
|
expect(patch.lastUpdated).to.be.at.least(beforeTime);
|
|
184
133
|
expect(patch.lastUpdated).to.be.at.most(afterTime);
|
|
185
134
|
});
|
|
186
|
-
|
|
187
|
-
it('should handle missing getData() gracefully', () => {
|
|
188
|
-
class TestMapper extends BaseOpportunityMapper {
|
|
189
|
-
getOpportunityType() { return 'test'; }
|
|
190
|
-
|
|
191
|
-
requiresPrerender() { return true; }
|
|
192
|
-
|
|
193
|
-
suggestionsToPatches() { return []; }
|
|
194
|
-
|
|
195
|
-
canDeploy() { return { eligible: true }; }
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const testMapper = new TestMapper(log);
|
|
199
|
-
const suggestion = {
|
|
200
|
-
getId: () => 'test-no-data',
|
|
201
|
-
getData: () => null,
|
|
202
|
-
getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
const patch = testMapper.createBasePatch(suggestion, 'opp-no-data');
|
|
206
|
-
|
|
207
|
-
expect(patch.lastUpdated).to.equal(new Date('2025-01-15T10:00:00.000Z').getTime());
|
|
208
|
-
});
|
|
209
135
|
});
|
|
210
136
|
|
|
211
137
|
describe('rollbackPatches', () => {
|
|
@@ -261,7 +261,6 @@ describe('ReadabilityMapper', () => {
|
|
|
261
261
|
getData: () => ({
|
|
262
262
|
textPreview: 'Lorem ipsum...',
|
|
263
263
|
url: 'https://www.website.com',
|
|
264
|
-
scrapedAt: '2025-09-20T06:21:12.584Z',
|
|
265
264
|
transformRules: {
|
|
266
265
|
value: 'Tech enthusiasts keep up with the latest tech news...',
|
|
267
266
|
op: 'replace',
|
|
@@ -290,14 +289,13 @@ describe('ReadabilityMapper', () => {
|
|
|
290
289
|
expect(patch.lastUpdated).to.be.a('number');
|
|
291
290
|
});
|
|
292
291
|
|
|
293
|
-
it('should create patch with
|
|
292
|
+
it('should create patch with updatedAt timestamp', () => {
|
|
294
293
|
const suggestion = {
|
|
295
294
|
getId: () => 'sugg-456',
|
|
296
|
-
getUpdatedAt: () => '2025-
|
|
295
|
+
getUpdatedAt: () => '2025-09-20T06:21:12.584Z',
|
|
297
296
|
getData: () => ({
|
|
298
297
|
textPreview: 'Original text...',
|
|
299
298
|
url: 'https://www.example.com',
|
|
300
|
-
scrapedAt: '2025-09-20T06:21:12.584Z',
|
|
301
299
|
transformRules: {
|
|
302
300
|
value: 'Improved readability text',
|
|
303
301
|
op: 'replace',
|