@adobe/spacecat-shared-tokowaka-client 1.4.6 → 1.4.7

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-tokowaka-client-v1.4.7](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.4.6...@adobe/spacecat-shared-tokowaka-client-v1.4.7) (2026-01-14)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * update tokowaka s3 config with patches ([#1263](https://github.com/adobe/spacecat-shared/issues/1263)) ([59605e9](https://github.com/adobe/spacecat-shared/commit/59605e9b75b7289029bd14e1832e594ea565296d))
7
+
1
8
  # [@adobe/spacecat-shared-tokowaka-client-v1.4.6](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.4.5...@adobe/spacecat-shared-tokowaka-client-v1.4.6) (2026-01-13)
2
9
 
3
10
 
package/README.md CHANGED
@@ -64,10 +64,12 @@ Creates a client instance from a context object.
64
64
 
65
65
  #### `deploySuggestions(site, opportunity, suggestions)`
66
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.
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
68
 
69
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
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
+
71
73
  **Returns:** `Promise<DeploymentResult>` with:
72
74
  - `s3Paths` - Array of S3 keys where configs were uploaded (one per URL)
73
75
  - `cdnInvalidations` - Array of CDN invalidation results (one per URL per provider)
@@ -212,7 +214,17 @@ Domain-level metaconfig (created once per domain, shared by all URLs):
212
214
  {
213
215
  "siteId": "abc-123",
214
216
  "apiKeys": ["tokowaka-api-key-1"],
215
- "prerender": true
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
+ }
216
228
  }
217
229
  ```
218
230
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-tokowaka-client",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
4
4
  "description": "Tokowaka Client for SpaceCat - Edge optimization config management",
5
5
  "type": "module",
6
6
  "engines": {
package/src/index.js CHANGED
@@ -17,7 +17,12 @@ import { v4 as uuidv4 } from 'uuid';
17
17
  import MapperRegistry from './mappers/mapper-registry.js';
18
18
  import CdnClientRegistry from './cdn/cdn-client-registry.js';
19
19
  import { mergePatches } from './utils/patch-utils.js';
20
- import { getTokowakaConfigS3Path, getTokowakaMetaconfigS3Path, getHostName } from './utils/s3-utils.js';
20
+ import {
21
+ getTokowakaConfigS3Path,
22
+ getTokowakaMetaconfigS3Path,
23
+ getHostName,
24
+ normalizePath,
25
+ } from './utils/s3-utils.js';
21
26
  import { groupSuggestionsByUrlPath, filterEligibleSuggestions } from './utils/suggestion-utils.js';
22
27
  import { getEffectiveBaseURL } from './utils/site-utils.js';
23
28
  import { fetchHtmlWithWarmup, calculateForwardedHost } from './utils/custom-html-utils.js';
@@ -94,6 +99,44 @@ class TokowakaClient {
94
99
  return error;
95
100
  }
96
101
 
102
+ /**
103
+ * Updates the metaconfig with deployed endpoint paths
104
+ * @param {Object} metaconfig - Existing metaconfig object
105
+ * @param {Array<string>} deployedUrls - Array of successfully deployed URLs
106
+ * @param {string} baseUrl - Base URL for uploading metaconfig
107
+ * @returns {Promise<void>}
108
+ * @private
109
+ */
110
+ async #updateMetaconfigWithDeployedPaths(metaconfig, deployedUrls, baseUrl) {
111
+ if (!Array.isArray(deployedUrls) || deployedUrls.length === 0) {
112
+ return;
113
+ }
114
+
115
+ try {
116
+ // Initialize patches field if it doesn't exist
117
+ const updatedMetaconfig = {
118
+ ...metaconfig,
119
+ patches: { ...(metaconfig.patches || {}) },
120
+ };
121
+
122
+ // Extract normalized paths from deployed URLs and add to patches object
123
+ deployedUrls.forEach((url) => {
124
+ const urlObj = new URL(url);
125
+ const normalizedPath = normalizePath(urlObj.pathname);
126
+ updatedMetaconfig.patches[normalizedPath] = true;
127
+ });
128
+
129
+ await this.uploadMetaconfig(baseUrl, updatedMetaconfig);
130
+ this.log.info(`Updated metaconfig with ${deployedUrls.length} deployed endpoint(s)`);
131
+ } catch (error) {
132
+ this.log.error(`Failed to update metaconfig with deployed paths: ${error.message}`, error);
133
+ throw this.#createError(
134
+ `Failed to update metaconfig with deployed paths: ${error.message}`,
135
+ HTTP_INTERNAL_SERVER_ERROR,
136
+ );
137
+ }
138
+ }
139
+
97
140
  /**
98
141
  * Gets the list of CDN providers from environment configuration
99
142
  * Supports both single provider (string) and multiple providers (comma-separated string or array)
@@ -597,6 +640,9 @@ class TokowakaClient {
597
640
 
598
641
  this.log.info(`Uploaded Tokowaka configs for ${s3Paths.length} URLs`);
599
642
 
643
+ // Update metaconfig with deployed paths
644
+ await this.#updateMetaconfigWithDeployedPaths(metaconfig, deployedUrls, baseURL);
645
+
600
646
  // Invalidate CDN cache for all deployed URLs at once
601
647
  const cdnInvalidations = await this.invalidateCdnCache({ urls: deployedUrls });
602
648
 
@@ -1030,6 +1030,161 @@ describe('TokowakaClient', () => {
1030
1030
  expect(result.failedSuggestions[0].suggestion.getId()).to.equal('sugg-2');
1031
1031
  });
1032
1032
 
1033
+ it('should update metaconfig patches field with deployed endpoints', async () => {
1034
+ mockSuggestions = [
1035
+ {
1036
+ getId: () => 'sugg-1',
1037
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
1038
+ getData: () => ({
1039
+ url: 'https://example.com/page1',
1040
+ recommendedAction: 'Page 1 Heading',
1041
+ checkType: 'heading-empty',
1042
+ transformRules: {
1043
+ action: 'replace',
1044
+ selector: 'h1',
1045
+ },
1046
+ }),
1047
+ },
1048
+ {
1049
+ getId: () => 'sugg-2',
1050
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
1051
+ getData: () => ({
1052
+ url: 'https://example.com/page2',
1053
+ recommendedAction: 'Page 2 Heading',
1054
+ checkType: 'heading-empty',
1055
+ transformRules: {
1056
+ action: 'replace',
1057
+ selector: 'h1',
1058
+ },
1059
+ }),
1060
+ },
1061
+ ];
1062
+
1063
+ const result = await client.deploySuggestions(
1064
+ mockSite,
1065
+ mockOpportunity,
1066
+ mockSuggestions,
1067
+ );
1068
+
1069
+ expect(result.succeededSuggestions).to.have.length(2);
1070
+ // Verify uploadMetaconfig was called to update the metaconfig
1071
+ expect(client.uploadMetaconfig).to.have.been.called;
1072
+ // Check that the last call included the patches field
1073
+ const { lastCall } = client.uploadMetaconfig;
1074
+ expect(lastCall.args[1]).to.have.property('patches');
1075
+ expect(lastCall.args[1].patches).to.deep.equal({
1076
+ '/page1': true,
1077
+ '/page2': true,
1078
+ });
1079
+ });
1080
+
1081
+ it('should add to existing patches in metaconfig when deploying new endpoints', async () => {
1082
+ // Set up metaconfig with existing patches
1083
+ // Reset the stub to provide consistent behavior
1084
+ client.fetchMetaconfig.reset();
1085
+ client.fetchMetaconfig.resolves({
1086
+ siteId: 'site-123',
1087
+ prerender: true,
1088
+ patches: {
1089
+ '/existing-page': true,
1090
+ },
1091
+ });
1092
+
1093
+ mockSuggestions = [
1094
+ {
1095
+ getId: () => 'sugg-1',
1096
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
1097
+ getData: () => ({
1098
+ url: 'https://example.com/new-page',
1099
+ recommendedAction: 'New Heading',
1100
+ checkType: 'heading-empty',
1101
+ transformRules: {
1102
+ action: 'replace',
1103
+ selector: 'h1',
1104
+ },
1105
+ }),
1106
+ },
1107
+ ];
1108
+
1109
+ await client.deploySuggestions(
1110
+ mockSite,
1111
+ mockOpportunity,
1112
+ mockSuggestions,
1113
+ );
1114
+
1115
+ // Verify the updated metaconfig includes both existing and new patches
1116
+ const { lastCall } = client.uploadMetaconfig;
1117
+ expect(lastCall.args[1].patches).to.deep.equal({
1118
+ '/existing-page': true,
1119
+ '/new-page': true,
1120
+ });
1121
+ });
1122
+
1123
+ it('should throw error when metaconfig update fails', async () => {
1124
+ // Make uploadMetaconfig fail during the update
1125
+ client.uploadMetaconfig.rejects(new Error('S3 upload error'));
1126
+
1127
+ try {
1128
+ await client.deploySuggestions(
1129
+ mockSite,
1130
+ mockOpportunity,
1131
+ mockSuggestions,
1132
+ );
1133
+ expect.fail('Should have thrown error');
1134
+ } catch (error) {
1135
+ expect(error.message).to.include('Failed to update metaconfig with deployed paths');
1136
+ expect(error.status).to.equal(500);
1137
+ }
1138
+ });
1139
+
1140
+ it('should return early when no eligible suggestions to deploy', async () => {
1141
+ // All suggestions are ineligible
1142
+ mockSuggestions = [
1143
+ {
1144
+ getId: () => 'sugg-1',
1145
+ getData: () => ({
1146
+ url: 'https://example.com/page1',
1147
+ recommendedAction: 'New Heading',
1148
+ checkType: 'heading-missing', // Not eligible
1149
+ }),
1150
+ },
1151
+ ];
1152
+
1153
+ const result = await client.deploySuggestions(
1154
+ mockSite,
1155
+ mockOpportunity,
1156
+ mockSuggestions,
1157
+ );
1158
+
1159
+ // No suggestions deployed - returns early before metaconfig check
1160
+ expect(result.succeededSuggestions).to.have.length(0);
1161
+ expect(result.failedSuggestions).to.have.length(1);
1162
+ // fetchMetaconfig should not be called at all (returns before that point)
1163
+ expect(client.fetchMetaconfig).to.not.have.been.called;
1164
+ // uploadMetaconfig should not be called at all
1165
+ expect(client.uploadMetaconfig).to.not.have.been.called;
1166
+ });
1167
+
1168
+ it('should not update metaconfig when all URLs fail to generate configs', async () => {
1169
+ // Stub generateConfig to return null (no config generated)
1170
+ sinon.stub(client, 'generateConfig').returns(null);
1171
+
1172
+ const result = await client.deploySuggestions(
1173
+ mockSite,
1174
+ mockOpportunity,
1175
+ mockSuggestions,
1176
+ );
1177
+
1178
+ // Suggestions are marked as succeeded (eligible) but no configs uploaded
1179
+ expect(result.succeededSuggestions).to.have.length(2);
1180
+ expect(result.s3Paths).to.have.length(0); // No configs uploaded
1181
+ // fetchMetaconfig called once for initial check, but not for update
1182
+ // since no URLs were actually deployed (deployedUrls is empty)
1183
+ expect(client.fetchMetaconfig).to.have.been.calledOnce;
1184
+ // uploadMetaconfig should not be called at all
1185
+ expect(client.uploadMetaconfig).to.not.have.been.called;
1186
+ });
1187
+
1033
1188
  it('should skip URL when generateConfig returns no patches', async () => {
1034
1189
  // Stub mapper to return empty patches for the first call, normal for subsequent calls
1035
1190
  const mapper = client.mapperRegistry.getMapper('headings');