mcp-wordpress 1.1.7 → 1.2.2

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.
Files changed (255) hide show
  1. package/README.md +388 -66
  2. package/dist/cache/CacheInvalidation.d.ts +118 -0
  3. package/dist/cache/CacheInvalidation.d.ts.map +1 -0
  4. package/dist/cache/CacheInvalidation.js +349 -0
  5. package/dist/cache/CacheInvalidation.js.map +1 -0
  6. package/dist/cache/CacheManager.d.ts +143 -0
  7. package/dist/cache/CacheManager.d.ts.map +1 -0
  8. package/dist/cache/CacheManager.js +308 -0
  9. package/dist/cache/CacheManager.js.map +1 -0
  10. package/dist/cache/HttpCacheWrapper.d.ts +121 -0
  11. package/dist/cache/HttpCacheWrapper.d.ts.map +1 -0
  12. package/dist/cache/HttpCacheWrapper.js +280 -0
  13. package/dist/cache/HttpCacheWrapper.js.map +1 -0
  14. package/dist/cache/__tests__/CacheInvalidation.test.d.ts +5 -0
  15. package/dist/cache/__tests__/CacheInvalidation.test.d.ts.map +1 -0
  16. package/dist/cache/__tests__/CacheInvalidation.test.js +236 -0
  17. package/dist/cache/__tests__/CacheInvalidation.test.js.map +1 -0
  18. package/dist/cache/__tests__/CacheManager.test.d.ts +5 -0
  19. package/dist/cache/__tests__/CacheManager.test.d.ts.map +1 -0
  20. package/dist/cache/__tests__/CacheManager.test.js +233 -0
  21. package/dist/cache/__tests__/CacheManager.test.js.map +1 -0
  22. package/dist/cache/__tests__/CachedWordPressClient.test.d.ts +5 -0
  23. package/dist/cache/__tests__/CachedWordPressClient.test.d.ts.map +1 -0
  24. package/dist/cache/__tests__/CachedWordPressClient.test.js +228 -0
  25. package/dist/cache/__tests__/CachedWordPressClient.test.js.map +1 -0
  26. package/dist/cache/__tests__/HttpCacheWrapper.test.d.ts +5 -0
  27. package/dist/cache/__tests__/HttpCacheWrapper.test.d.ts.map +1 -0
  28. package/dist/cache/__tests__/HttpCacheWrapper.test.js +296 -0
  29. package/dist/cache/__tests__/HttpCacheWrapper.test.js.map +1 -0
  30. package/dist/cache/index.d.ts +12 -0
  31. package/dist/cache/index.d.ts.map +1 -0
  32. package/dist/cache/index.js +9 -0
  33. package/dist/cache/index.js.map +1 -0
  34. package/dist/client/CachedWordPressClient.d.ts +160 -0
  35. package/dist/client/CachedWordPressClient.d.ts.map +1 -0
  36. package/dist/client/CachedWordPressClient.js +338 -0
  37. package/dist/client/CachedWordPressClient.js.map +1 -0
  38. package/dist/client/WordPressClient.d.ts +81 -0
  39. package/dist/client/WordPressClient.d.ts.map +1 -0
  40. package/dist/client/WordPressClient.js +354 -0
  41. package/dist/client/WordPressClient.js.map +1 -0
  42. package/dist/config/ConfigurationSchema.d.ts +281 -0
  43. package/dist/config/ConfigurationSchema.d.ts.map +1 -0
  44. package/dist/config/ConfigurationSchema.js +205 -0
  45. package/dist/config/ConfigurationSchema.js.map +1 -0
  46. package/dist/config/ServerConfiguration.d.ts +38 -0
  47. package/dist/config/ServerConfiguration.d.ts.map +1 -0
  48. package/dist/config/ServerConfiguration.js +158 -0
  49. package/dist/config/ServerConfiguration.js.map +1 -0
  50. package/dist/docs/DocumentationGenerator.d.ts +184 -0
  51. package/dist/docs/DocumentationGenerator.d.ts.map +1 -0
  52. package/dist/docs/DocumentationGenerator.js +735 -0
  53. package/dist/docs/DocumentationGenerator.js.map +1 -0
  54. package/dist/docs/MarkdownFormatter.d.ts +84 -0
  55. package/dist/docs/MarkdownFormatter.d.ts.map +1 -0
  56. package/dist/docs/MarkdownFormatter.js +448 -0
  57. package/dist/docs/MarkdownFormatter.js.map +1 -0
  58. package/dist/docs/index.d.ts +8 -0
  59. package/dist/docs/index.d.ts.map +1 -0
  60. package/dist/docs/index.js +7 -0
  61. package/dist/docs/index.js.map +1 -0
  62. package/dist/index.d.ts +1 -4
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +12 -212
  65. package/dist/index.js.map +1 -1
  66. package/dist/performance/AnomalyDetector.d.ts +63 -0
  67. package/dist/performance/AnomalyDetector.d.ts.map +1 -0
  68. package/dist/performance/AnomalyDetector.js +222 -0
  69. package/dist/performance/AnomalyDetector.js.map +1 -0
  70. package/dist/performance/BenchmarkAnalyzer.d.ts +67 -0
  71. package/dist/performance/BenchmarkAnalyzer.d.ts.map +1 -0
  72. package/dist/performance/BenchmarkAnalyzer.js +301 -0
  73. package/dist/performance/BenchmarkAnalyzer.js.map +1 -0
  74. package/dist/performance/MetricsCollector.d.ts +139 -0
  75. package/dist/performance/MetricsCollector.d.ts.map +1 -0
  76. package/dist/performance/MetricsCollector.js +320 -0
  77. package/dist/performance/MetricsCollector.js.map +1 -0
  78. package/dist/performance/PerformanceAnalytics.d.ts +162 -0
  79. package/dist/performance/PerformanceAnalytics.d.ts.map +1 -0
  80. package/dist/performance/PerformanceAnalytics.js +554 -0
  81. package/dist/performance/PerformanceAnalytics.js.map +1 -0
  82. package/dist/performance/PerformanceMonitor.d.ts +202 -0
  83. package/dist/performance/PerformanceMonitor.d.ts.map +1 -0
  84. package/dist/performance/PerformanceMonitor.js +478 -0
  85. package/dist/performance/PerformanceMonitor.js.map +1 -0
  86. package/dist/performance/TrendAnalyzer.d.ts +69 -0
  87. package/dist/performance/TrendAnalyzer.d.ts.map +1 -0
  88. package/dist/performance/TrendAnalyzer.js +203 -0
  89. package/dist/performance/TrendAnalyzer.js.map +1 -0
  90. package/dist/performance/index.d.ts +11 -0
  91. package/dist/performance/index.d.ts.map +1 -0
  92. package/dist/performance/index.js +8 -0
  93. package/dist/performance/index.js.map +1 -0
  94. package/dist/security/InputValidator.d.ts +215 -0
  95. package/dist/security/InputValidator.d.ts.map +1 -0
  96. package/dist/security/InputValidator.js +278 -0
  97. package/dist/security/InputValidator.js.map +1 -0
  98. package/dist/security/SecurityConfig.d.ts +129 -0
  99. package/dist/security/SecurityConfig.d.ts.map +1 -0
  100. package/dist/security/SecurityConfig.js +262 -0
  101. package/dist/security/SecurityConfig.js.map +1 -0
  102. package/dist/server/ConnectionTester.d.ts +24 -0
  103. package/dist/server/ConnectionTester.d.ts.map +1 -0
  104. package/dist/server/ConnectionTester.js +61 -0
  105. package/dist/server/ConnectionTester.js.map +1 -0
  106. package/dist/server/ToolRegistry.d.ts +46 -0
  107. package/dist/server/ToolRegistry.d.ts.map +1 -0
  108. package/dist/server/ToolRegistry.js +148 -0
  109. package/dist/server/ToolRegistry.js.map +1 -0
  110. package/dist/tools/BaseToolClass.d.ts +76 -0
  111. package/dist/tools/BaseToolClass.d.ts.map +1 -0
  112. package/dist/tools/BaseToolClass.js +104 -0
  113. package/dist/tools/BaseToolClass.js.map +1 -0
  114. package/dist/tools/BaseToolManager.d.ts +26 -0
  115. package/dist/tools/BaseToolManager.d.ts.map +1 -0
  116. package/dist/tools/BaseToolManager.js +56 -0
  117. package/dist/tools/BaseToolManager.js.map +1 -0
  118. package/dist/tools/base.d.ts +37 -0
  119. package/dist/tools/base.d.ts.map +1 -0
  120. package/dist/tools/base.js +60 -0
  121. package/dist/tools/base.js.map +1 -0
  122. package/dist/tools/cache.d.ts +260 -0
  123. package/dist/tools/cache.d.ts.map +1 -0
  124. package/dist/tools/cache.js +237 -0
  125. package/dist/tools/cache.js.map +1 -0
  126. package/dist/tools/index.d.ts +2 -0
  127. package/dist/tools/index.d.ts.map +1 -1
  128. package/dist/tools/index.js +2 -0
  129. package/dist/tools/index.js.map +1 -1
  130. package/dist/tools/performance.d.ts +63 -0
  131. package/dist/tools/performance.d.ts.map +1 -0
  132. package/dist/tools/performance.js +865 -0
  133. package/dist/tools/performance.js.map +1 -0
  134. package/dist/types/client.d.ts +1 -0
  135. package/dist/types/client.d.ts.map +1 -1
  136. package/dist/types/client.js.map +1 -1
  137. package/dist/utils/toolWrapper.d.ts +4 -0
  138. package/dist/utils/toolWrapper.d.ts.map +1 -1
  139. package/dist/utils/toolWrapper.js +11 -0
  140. package/dist/utils/toolWrapper.js.map +1 -1
  141. package/dist/utils/validation.d.ts +68 -0
  142. package/dist/utils/validation.d.ts.map +1 -0
  143. package/dist/utils/validation.js +185 -0
  144. package/dist/utils/validation.js.map +1 -0
  145. package/docs/CACHING.md +340 -0
  146. package/docs/DOCKER.md +451 -0
  147. package/docs/PERFORMANCE_MONITORING.md +471 -0
  148. package/docs/SECURITY_TESTING.md +393 -0
  149. package/docs/api/README.md +200 -0
  150. package/docs/api/categories/auth.md +40 -0
  151. package/docs/api/categories/cache.md +41 -0
  152. package/docs/api/categories/comment.md +44 -0
  153. package/docs/api/categories/media.md +43 -0
  154. package/docs/api/categories/page.md +43 -0
  155. package/docs/api/categories/performance.md +44 -0
  156. package/docs/api/categories/post.md +43 -0
  157. package/docs/api/categories/site.md +43 -0
  158. package/docs/api/categories/taxonomy.md +47 -0
  159. package/docs/api/categories/user.md +43 -0
  160. package/docs/api/openapi.json +3305 -0
  161. package/docs/api/summary.json +12 -0
  162. package/docs/api/tools/wp_approve_comment.md +98 -0
  163. package/docs/api/tools/wp_cache_clear.md +120 -0
  164. package/docs/api/tools/wp_cache_info.md +119 -0
  165. package/docs/api/tools/wp_cache_stats.md +119 -0
  166. package/docs/api/tools/wp_cache_warm.md +119 -0
  167. package/docs/api/tools/wp_create_application_password.md +102 -0
  168. package/docs/api/tools/wp_create_category.md +102 -0
  169. package/docs/api/tools/wp_create_comment.md +128 -0
  170. package/docs/api/tools/wp_create_page.md +135 -0
  171. package/docs/api/tools/wp_create_post.md +147 -0
  172. package/docs/api/tools/wp_create_tag.md +101 -0
  173. package/docs/api/tools/wp_create_user.md +135 -0
  174. package/docs/api/tools/wp_delete_application_password.md +101 -0
  175. package/docs/api/tools/wp_delete_category.md +100 -0
  176. package/docs/api/tools/wp_delete_comment.md +101 -0
  177. package/docs/api/tools/wp_delete_media.md +108 -0
  178. package/docs/api/tools/wp_delete_page.md +108 -0
  179. package/docs/api/tools/wp_delete_post.md +117 -0
  180. package/docs/api/tools/wp_delete_tag.md +100 -0
  181. package/docs/api/tools/wp_delete_user.md +108 -0
  182. package/docs/api/tools/wp_get_application_passwords.md +103 -0
  183. package/docs/api/tools/wp_get_auth_status.md +101 -0
  184. package/docs/api/tools/wp_get_category.md +103 -0
  185. package/docs/api/tools/wp_get_comment.md +103 -0
  186. package/docs/api/tools/wp_get_current_user.md +101 -0
  187. package/docs/api/tools/wp_get_media.md +103 -0
  188. package/docs/api/tools/wp_get_page.md +103 -0
  189. package/docs/api/tools/wp_get_page_revisions.md +103 -0
  190. package/docs/api/tools/wp_get_post.md +112 -0
  191. package/docs/api/tools/wp_get_post_revisions.md +103 -0
  192. package/docs/api/tools/wp_get_site_settings.md +108 -0
  193. package/docs/api/tools/wp_get_tag.md +103 -0
  194. package/docs/api/tools/wp_get_user.md +103 -0
  195. package/docs/api/tools/wp_list_categories.md +111 -0
  196. package/docs/api/tools/wp_list_comments.md +111 -0
  197. package/docs/api/tools/wp_list_media.md +145 -0
  198. package/docs/api/tools/wp_list_pages.md +145 -0
  199. package/docs/api/tools/wp_list_posts.md +156 -0
  200. package/docs/api/tools/wp_list_tags.md +110 -0
  201. package/docs/api/tools/wp_list_users.md +111 -0
  202. package/docs/api/tools/wp_performance_alerts.md +162 -0
  203. package/docs/api/tools/wp_performance_benchmark.md +160 -0
  204. package/docs/api/tools/wp_performance_export.md +162 -0
  205. package/docs/api/tools/wp_performance_history.md +161 -0
  206. package/docs/api/tools/wp_performance_optimize.md +162 -0
  207. package/docs/api/tools/wp_performance_stats.md +160 -0
  208. package/docs/api/tools/wp_search_site.md +99 -0
  209. package/docs/api/tools/wp_spam_comment.md +98 -0
  210. package/docs/api/tools/wp_switch_auth_method.md +122 -0
  211. package/docs/api/tools/wp_test_auth.md +96 -0
  212. package/docs/api/tools/wp_update_category.md +102 -0
  213. package/docs/api/tools/wp_update_comment.md +127 -0
  214. package/docs/api/tools/wp_update_media.md +129 -0
  215. package/docs/api/tools/wp_update_page.md +135 -0
  216. package/docs/api/tools/wp_update_post.md +144 -0
  217. package/docs/api/tools/wp_update_site_settings.md +127 -0
  218. package/docs/api/tools/wp_update_tag.md +102 -0
  219. package/docs/api/tools/wp_update_user.md +134 -0
  220. package/docs/api/tools/wp_upload_media.md +131 -0
  221. package/docs/api/types/WordPressPost.md +39 -0
  222. package/docs/contract-testing.md +183 -0
  223. package/docs/developer/NPM_AUTH_SETUP.md +3 -3
  224. package/docs/wordpress-rest-api-authentication-troubleshooting.md +218 -0
  225. package/package.json +84 -64
  226. package/src/cache/CacheInvalidation.ts +421 -0
  227. package/src/cache/CacheManager.ts +391 -0
  228. package/src/cache/HttpCacheWrapper.ts +372 -0
  229. package/src/cache/__tests__/CacheInvalidation.test.ts +299 -0
  230. package/src/cache/__tests__/CacheManager.test.ts +300 -0
  231. package/src/cache/__tests__/CachedWordPressClient.test.ts +304 -0
  232. package/src/cache/__tests__/HttpCacheWrapper.test.ts +359 -0
  233. package/src/cache/index.ts +26 -0
  234. package/src/client/CachedWordPressClient.ts +442 -0
  235. package/src/config/ConfigurationSchema.ts +246 -0
  236. package/src/config/ServerConfiguration.ts +215 -0
  237. package/src/docs/DocumentationGenerator.ts +952 -0
  238. package/src/docs/MarkdownFormatter.ts +494 -0
  239. package/src/docs/index.ts +21 -0
  240. package/src/index.ts +14 -274
  241. package/src/performance/MetricsCollector.ts +447 -0
  242. package/src/performance/PerformanceAnalytics.ts +762 -0
  243. package/src/performance/PerformanceMonitor.ts +649 -0
  244. package/src/performance/index.ts +28 -0
  245. package/src/security/InputValidator.ts +319 -0
  246. package/src/security/SecurityConfig.ts +301 -0
  247. package/src/server/ConnectionTester.ts +74 -0
  248. package/src/server/ToolRegistry.ts +194 -0
  249. package/src/tools/BaseToolManager.ts +66 -0
  250. package/src/tools/cache.ts +259 -0
  251. package/src/tools/index.ts +2 -0
  252. package/src/tools/performance.ts +948 -0
  253. package/src/types/client.ts +1 -0
  254. package/src/utils/toolWrapper.ts +11 -0
  255. package/src/utils/validation.ts +259 -0
@@ -0,0 +1,372 @@
1
+ /**
2
+ * HTTP-level caching wrapper with ETags and Cache-Control headers
3
+ * Implements WordPress REST API caching best practices
4
+ */
5
+
6
+ import { CacheManager, CachePresets } from './CacheManager.js';
7
+ import * as crypto from 'crypto';
8
+
9
+ export interface HttpCacheOptions {
10
+ ttl?: number;
11
+ cacheControl?: string;
12
+ varyHeaders?: string[];
13
+ private?: boolean;
14
+ revalidate?: boolean;
15
+ }
16
+
17
+ export interface CachedResponse {
18
+ data: any;
19
+ status: number;
20
+ headers: Record<string, string>;
21
+ etag?: string;
22
+ lastModified?: string;
23
+ cacheControl?: string;
24
+ }
25
+
26
+ export interface RequestOptions {
27
+ method: string;
28
+ url: string;
29
+ headers?: Record<string, string>;
30
+ params?: any;
31
+ data?: any;
32
+ }
33
+
34
+ /**
35
+ * HTTP caching wrapper that adds intelligent caching to HTTP requests
36
+ */
37
+ export class HttpCacheWrapper {
38
+ constructor(
39
+ private cacheManager: CacheManager,
40
+ private siteId: string
41
+ ) {}
42
+
43
+ /**
44
+ * Execute request with intelligent caching
45
+ */
46
+ async request<T = any>(
47
+ requestFn: () => Promise<{ data: T; status: number; headers: Record<string, string> }>,
48
+ options: RequestOptions,
49
+ cacheOptions?: HttpCacheOptions
50
+ ): Promise<{ data: T; status: number; headers: Record<string, string>; cached?: boolean }> {
51
+ // Only cache GET requests
52
+ if (options.method.toUpperCase() !== 'GET') {
53
+ return await requestFn();
54
+ }
55
+
56
+ const cacheKey = this.generateCacheKey(options);
57
+ const cachedEntry = this.cacheManager.getEntry(cacheKey);
58
+
59
+ // Check for conditional request support
60
+ if (cachedEntry && this.cacheManager.supportsConditionalRequest(cacheKey)) {
61
+ const conditionalHeaders = this.cacheManager.getConditionalHeaders(cacheKey);
62
+
63
+ // Add conditional headers to request
64
+ const requestWithHeaders = {
65
+ ...options,
66
+ headers: {
67
+ ...options.headers,
68
+ ...conditionalHeaders
69
+ }
70
+ };
71
+
72
+ try {
73
+ const response = await this.executeRequestWithHeaders(requestFn, requestWithHeaders);
74
+
75
+ // 304 Not Modified - return cached data
76
+ if (response.status === 304) {
77
+ return {
78
+ data: cachedEntry.value.data,
79
+ status: 200,
80
+ headers: cachedEntry.value.headers,
81
+ cached: true
82
+ };
83
+ }
84
+
85
+ // Content changed - update cache
86
+ return await this.cacheAndReturn(response, cacheKey, cacheOptions);
87
+
88
+ } catch (error) {
89
+ // If conditional request fails, try without conditions
90
+ console.warn('Conditional request failed, falling back to regular request:', error);
91
+ }
92
+ }
93
+
94
+ // Check for valid cached response
95
+ const cached = this.cacheManager.get<CachedResponse>(cacheKey);
96
+ if (cached) {
97
+ return {
98
+ data: cached.data,
99
+ status: cached.status,
100
+ headers: cached.headers,
101
+ cached: true
102
+ };
103
+ }
104
+
105
+ // Execute fresh request
106
+ const response = await requestFn();
107
+ return await this.cacheAndReturn(response, cacheKey, cacheOptions);
108
+ }
109
+
110
+ /**
111
+ * Invalidate cache for specific endpoint
112
+ */
113
+ invalidate(endpoint: string, params?: any): void {
114
+ const cacheKey = this.cacheManager.generateKey(this.siteId, endpoint, params);
115
+ this.cacheManager.delete(cacheKey);
116
+ }
117
+
118
+ /**
119
+ * Invalidate cache entries matching pattern
120
+ */
121
+ invalidatePattern(pattern: string): number {
122
+ const regex = new RegExp(`${this.siteId}:${pattern}`);
123
+ return this.cacheManager.clearPattern(regex);
124
+ }
125
+
126
+ /**
127
+ * Invalidate all cache for this site
128
+ */
129
+ invalidateAll(): number {
130
+ return this.cacheManager.clearSite(this.siteId);
131
+ }
132
+
133
+ /**
134
+ * Pre-warm cache with data
135
+ */
136
+ warm<T>(endpoint: string, data: T, params?: any, cacheOptions?: HttpCacheOptions): void {
137
+ const cacheKey = this.cacheManager.generateKey(this.siteId, endpoint, params);
138
+ const ttl = cacheOptions?.ttl || this.getDefaultTTL(endpoint);
139
+
140
+ const cachedResponse: CachedResponse = {
141
+ data,
142
+ status: 200,
143
+ headers: this.generateCacheHeaders(cacheOptions, endpoint),
144
+ etag: this.generateETag(data),
145
+ lastModified: new Date().toUTCString(),
146
+ cacheControl: cacheOptions?.cacheControl || this.getDefaultCacheControl(endpoint)
147
+ };
148
+
149
+ this.cacheManager.set(
150
+ cacheKey,
151
+ cachedResponse,
152
+ ttl,
153
+ cachedResponse.etag,
154
+ cachedResponse.lastModified
155
+ );
156
+ }
157
+
158
+ /**
159
+ * Get cache statistics for this site
160
+ */
161
+ getStats() {
162
+ return this.cacheManager.getStats();
163
+ }
164
+
165
+ /**
166
+ * Generate cache key for request
167
+ */
168
+ private generateCacheKey(options: RequestOptions): string {
169
+ const endpoint = this.extractEndpoint(options.url);
170
+ return this.cacheManager.generateKey(this.siteId, endpoint, {
171
+ ...options.params,
172
+ // Include relevant headers that affect response
173
+ ...this.extractCacheableHeaders(options.headers)
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Extract endpoint from full URL
179
+ */
180
+ private extractEndpoint(url: string): string {
181
+ // Extract the path after /wp-json/wp/v2/
182
+ const match = url.match(/\/wp-json\/wp\/v2\/(.+?)(?:\?|$)/);
183
+ return match ? match[1] : url;
184
+ }
185
+
186
+ /**
187
+ * Extract headers that affect caching
188
+ */
189
+ private extractCacheableHeaders(headers?: Record<string, string>): Record<string, string> {
190
+ if (!headers) return {};
191
+
192
+ const cacheableHeaders: Record<string, string> = {};
193
+ const relevantHeaders = ['accept', 'accept-language', 'authorization'];
194
+
195
+ for (const header of relevantHeaders) {
196
+ if (headers[header]) {
197
+ cacheableHeaders[header] = headers[header];
198
+ }
199
+ }
200
+
201
+ return cacheableHeaders;
202
+ }
203
+
204
+ /**
205
+ * Execute request with modified headers
206
+ */
207
+ private async executeRequestWithHeaders(
208
+ requestFn: () => Promise<{ data: any; status: number; headers: Record<string, string> }>,
209
+ options: RequestOptions
210
+ ) {
211
+ // This is a simplified approach - in practice, you'd need to modify the actual request
212
+ // The actual implementation would depend on your HTTP client (axios, fetch, etc.)
213
+ return await requestFn();
214
+ }
215
+
216
+ /**
217
+ * Cache response and return with cache metadata
218
+ */
219
+ private async cacheAndReturn<T>(
220
+ response: { data: T; status: number; headers: Record<string, string> },
221
+ cacheKey: string,
222
+ cacheOptions?: HttpCacheOptions
223
+ ): Promise<{ data: T; status: number; headers: Record<string, string>; cached?: boolean }> {
224
+
225
+ // Don't cache error responses (unless specifically configured)
226
+ if (response.status >= 400) {
227
+ return response;
228
+ }
229
+
230
+ const endpoint = this.extractEndpointFromKey(cacheKey);
231
+ const ttl = cacheOptions?.ttl || this.getDefaultTTL(endpoint);
232
+
233
+ // Generate ETags and cache headers
234
+ const etag = this.generateETag(response.data);
235
+ const lastModified = new Date().toUTCString();
236
+ const cacheControl = cacheOptions?.cacheControl || this.getDefaultCacheControl(endpoint);
237
+
238
+ const cachedResponse: CachedResponse = {
239
+ data: response.data,
240
+ status: response.status,
241
+ headers: {
242
+ ...response.headers,
243
+ 'etag': etag,
244
+ 'last-modified': lastModified,
245
+ 'cache-control': cacheControl
246
+ },
247
+ etag,
248
+ lastModified,
249
+ cacheControl
250
+ };
251
+
252
+ // Store in cache
253
+ this.cacheManager.set(
254
+ cacheKey,
255
+ cachedResponse,
256
+ ttl,
257
+ etag,
258
+ lastModified
259
+ );
260
+
261
+ return {
262
+ data: response.data,
263
+ status: response.status,
264
+ headers: cachedResponse.headers,
265
+ cached: false
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Generate ETag for response data
271
+ */
272
+ private generateETag(data: any): string {
273
+ const hash = crypto
274
+ .createHash('md5')
275
+ .update(JSON.stringify(data))
276
+ .digest('hex');
277
+ return `"${hash}"`;
278
+ }
279
+
280
+ /**
281
+ * Get default TTL based on endpoint type
282
+ */
283
+ private getDefaultTTL(endpoint: string): number {
284
+ // Static data endpoints
285
+ if (this.isStaticEndpoint(endpoint)) {
286
+ return CachePresets.STATIC.ttl;
287
+ }
288
+
289
+ // Semi-static data endpoints
290
+ if (this.isSemiStaticEndpoint(endpoint)) {
291
+ return CachePresets.SEMI_STATIC.ttl;
292
+ }
293
+
294
+ // Session/auth endpoints
295
+ if (this.isSessionEndpoint(endpoint)) {
296
+ return CachePresets.SESSION.ttl;
297
+ }
298
+
299
+ // Default to dynamic for posts, comments, etc.
300
+ return CachePresets.DYNAMIC.ttl;
301
+ }
302
+
303
+ /**
304
+ * Get default Cache-Control header based on endpoint
305
+ */
306
+ private getDefaultCacheControl(endpoint: string): string {
307
+ if (this.isStaticEndpoint(endpoint)) {
308
+ return CachePresets.STATIC.cacheControl;
309
+ }
310
+
311
+ if (this.isSemiStaticEndpoint(endpoint)) {
312
+ return CachePresets.SEMI_STATIC.cacheControl;
313
+ }
314
+
315
+ if (this.isSessionEndpoint(endpoint)) {
316
+ return CachePresets.SESSION.cacheControl;
317
+ }
318
+
319
+ return CachePresets.DYNAMIC.cacheControl;
320
+ }
321
+
322
+ /**
323
+ * Generate cache headers
324
+ */
325
+ private generateCacheHeaders(options?: HttpCacheOptions, endpoint?: string): Record<string, string> {
326
+ const headers: Record<string, string> = {};
327
+
328
+ if (options?.cacheControl) {
329
+ headers['cache-control'] = options.cacheControl;
330
+ } else if (endpoint) {
331
+ headers['cache-control'] = this.getDefaultCacheControl(endpoint);
332
+ }
333
+
334
+ if (options?.varyHeaders?.length) {
335
+ headers['vary'] = options.varyHeaders.join(', ');
336
+ }
337
+
338
+ return headers;
339
+ }
340
+
341
+ /**
342
+ * Check if endpoint contains static data
343
+ */
344
+ private isStaticEndpoint(endpoint: string): boolean {
345
+ const staticEndpoints = ['settings', 'types', 'statuses'];
346
+ return staticEndpoints.some(pattern => endpoint.includes(pattern));
347
+ }
348
+
349
+ /**
350
+ * Check if endpoint contains semi-static data
351
+ */
352
+ private isSemiStaticEndpoint(endpoint: string): boolean {
353
+ const semiStaticEndpoints = ['categories', 'tags', 'users', 'taxonomies'];
354
+ return semiStaticEndpoints.some(pattern => endpoint.includes(pattern));
355
+ }
356
+
357
+ /**
358
+ * Check if endpoint is session-related
359
+ */
360
+ private isSessionEndpoint(endpoint: string): boolean {
361
+ const sessionEndpoints = ['users/me', 'application-passwords'];
362
+ return sessionEndpoints.some(pattern => endpoint.includes(pattern));
363
+ }
364
+
365
+ /**
366
+ * Extract endpoint from cache key
367
+ */
368
+ private extractEndpointFromKey(cacheKey: string): string {
369
+ const parts = cacheKey.split(':');
370
+ return parts[1] || '';
371
+ }
372
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Tests for CacheInvalidation
3
+ */
4
+
5
+ import { CacheManager } from '../CacheManager.js';
6
+ import { HttpCacheWrapper } from '../HttpCacheWrapper.js';
7
+ import { CacheInvalidation, WordPressCachePatterns } from '../CacheInvalidation.js';
8
+
9
+ describe('CacheInvalidation', () => {
10
+ let cacheManager: CacheManager;
11
+ let httpCache: HttpCacheWrapper;
12
+ let cacheInvalidation: CacheInvalidation;
13
+
14
+ beforeEach(() => {
15
+ cacheManager = new CacheManager({
16
+ maxSize: 100,
17
+ defaultTTL: 10000,
18
+ enableLRU: true,
19
+ enableStats: true
20
+ });
21
+
22
+ httpCache = new HttpCacheWrapper(cacheManager, 'test-site');
23
+ cacheInvalidation = new CacheInvalidation(httpCache);
24
+ });
25
+
26
+ afterEach(() => {
27
+ cacheManager.clear();
28
+ });
29
+
30
+ describe('Default Invalidation Rules', () => {
31
+ test('should have default rules for posts', () => {
32
+ const rules = cacheInvalidation.getRules();
33
+
34
+ expect(rules.posts).toBeDefined();
35
+ expect(rules.posts.length).toBeGreaterThan(0);
36
+
37
+ const createRule = rules.posts.find(r => r.trigger === 'create');
38
+ expect(createRule).toBeDefined();
39
+ expect(createRule?.patterns).toContain('posts');
40
+ expect(createRule?.immediate).toBe(true);
41
+ expect(createRule?.cascade).toBe(true);
42
+ });
43
+
44
+ test('should have default rules for categories', () => {
45
+ const rules = cacheInvalidation.getRules();
46
+
47
+ expect(rules.categories).toBeDefined();
48
+
49
+ const updateRule = rules.categories.find(r => r.trigger === 'update');
50
+ expect(updateRule).toBeDefined();
51
+ expect(updateRule?.patterns).toContain('categories/\\d+');
52
+ expect(updateRule?.cascade).toBe(true);
53
+ });
54
+
55
+ test('should have default rules for users', () => {
56
+ const rules = cacheInvalidation.getRules();
57
+
58
+ expect(rules.users).toBeDefined();
59
+
60
+ const deleteRule = rules.users.find(r => r.trigger === 'delete');
61
+ expect(deleteRule).toBeDefined();
62
+ expect(deleteRule?.patterns).toContain('users');
63
+ });
64
+ });
65
+
66
+ describe('Custom Invalidation Rules', () => {
67
+ test('should register custom invalidation rules', () => {
68
+ const customRule = {
69
+ trigger: 'update' as const,
70
+ patterns: ['custom-endpoint.*'],
71
+ immediate: true
72
+ };
73
+
74
+ cacheInvalidation.registerRule('custom', customRule);
75
+
76
+ const rules = cacheInvalidation.getRules();
77
+ expect(rules.custom).toBeDefined();
78
+ expect(rules.custom).toContain(customRule);
79
+ });
80
+
81
+ test('should support multiple rules per resource', () => {
82
+ const rule1 = {
83
+ trigger: 'create' as const,
84
+ patterns: ['test.*'],
85
+ immediate: true
86
+ };
87
+
88
+ const rule2 = {
89
+ trigger: 'update' as const,
90
+ patterns: ['test/\\d+'],
91
+ immediate: false
92
+ };
93
+
94
+ cacheInvalidation.registerRule('test', rule1);
95
+ cacheInvalidation.registerRule('test', rule2);
96
+
97
+ const rules = cacheInvalidation.getRules();
98
+ expect(rules.test).toHaveLength(2);
99
+ expect(rules.test).toContain(rule1);
100
+ expect(rules.test).toContain(rule2);
101
+ });
102
+ });
103
+
104
+ describe('Event Processing', () => {
105
+ test('should process invalidation events', async () => {
106
+ // Pre-populate cache with some test data
107
+ httpCache.warm('posts', [{ id: 1 }, { id: 2 }]);
108
+ httpCache.warm('posts/1', { id: 1, title: 'Test Post' });
109
+ httpCache.warm('categories', [{ id: 1 }]);
110
+
111
+ expect(cacheManager.getStats().totalSize).toBe(3);
112
+
113
+ // Trigger post creation event
114
+ await cacheInvalidation.trigger({
115
+ type: 'create',
116
+ resource: 'posts',
117
+ id: 3,
118
+ siteId: 'test-site',
119
+ timestamp: Date.now()
120
+ });
121
+
122
+ // Should invalidate posts listings but not specific post
123
+ const stats = cacheManager.getStats();
124
+ expect(stats.totalSize).toBeLessThan(3);
125
+ });
126
+
127
+ test('should process events in queue order', async () => {
128
+ const processedEvents: string[] = [];
129
+
130
+ // Mock the invalidation process to track order
131
+ const originalInvalidatePattern = httpCache.invalidatePattern;
132
+ httpCache.invalidatePattern = jest.fn().mockImplementation((pattern: string) => {
133
+ processedEvents.push(pattern);
134
+ return originalInvalidatePattern.call(httpCache, pattern);
135
+ });
136
+
137
+ // Queue multiple events
138
+ await Promise.all([
139
+ cacheInvalidation.trigger({
140
+ type: 'create',
141
+ resource: 'posts',
142
+ siteId: 'test-site',
143
+ timestamp: Date.now()
144
+ }),
145
+ cacheInvalidation.trigger({
146
+ type: 'update',
147
+ resource: 'categories',
148
+ siteId: 'test-site',
149
+ timestamp: Date.now()
150
+ })
151
+ ]);
152
+
153
+ expect(processedEvents.length).toBeGreaterThan(0);
154
+ });
155
+
156
+ test('should handle resource invalidation by type', async () => {
157
+ httpCache.warm('posts', [{ id: 1 }]);
158
+ httpCache.warm('posts/1', { id: 1 });
159
+
160
+ expect(cacheManager.getStats().totalSize).toBe(2);
161
+
162
+ await cacheInvalidation.invalidateResource('posts', 1, 'update');
163
+
164
+ // Should clear specific post and related caches
165
+ const stats = cacheManager.getStats();
166
+ expect(stats.totalSize).toBeLessThan(2);
167
+ });
168
+ });
169
+
170
+ describe('Pattern Matching', () => {
171
+ test('should replace placeholders in patterns', async () => {
172
+ // Pre-populate cache
173
+ httpCache.warm('posts/123', { id: 123 });
174
+ httpCache.warm('posts/456', { id: 456 });
175
+ httpCache.warm('pages/123', { id: 123 });
176
+
177
+ expect(cacheManager.getStats().totalSize).toBe(3);
178
+
179
+ // Trigger event with specific ID
180
+ await cacheInvalidation.trigger({
181
+ type: 'update',
182
+ resource: 'posts',
183
+ id: 123,
184
+ siteId: 'test-site',
185
+ timestamp: Date.now()
186
+ });
187
+
188
+ // Should only invalidate posts/123, not posts/456 or pages/123
189
+ const remainingKeys: string[] = [];
190
+ for (const [key] of (cacheManager as any).cache.entries()) {
191
+ remainingKeys.push(key);
192
+ }
193
+
194
+ expect(remainingKeys).not.toContain(expect.stringContaining('posts'));
195
+ });
196
+ });
197
+
198
+ describe('Statistics', () => {
199
+ test('should track invalidation statistics', () => {
200
+ const stats = cacheInvalidation.getStats();
201
+
202
+ expect(stats).toHaveProperty('queueSize');
203
+ expect(stats).toHaveProperty('rulesCount');
204
+ expect(stats).toHaveProperty('processing');
205
+
206
+ expect(typeof stats.queueSize).toBe('number');
207
+ expect(typeof stats.rulesCount).toBe('number');
208
+ expect(typeof stats.processing).toBe('boolean');
209
+ expect(stats.rulesCount).toBeGreaterThan(0);
210
+ });
211
+
212
+ test('should clear rules', () => {
213
+ const initialStats = cacheInvalidation.getStats();
214
+ expect(initialStats.rulesCount).toBeGreaterThan(0);
215
+
216
+ cacheInvalidation.clearRules();
217
+
218
+ const clearedStats = cacheInvalidation.getStats();
219
+ expect(clearedStats.rulesCount).toBe(0);
220
+ });
221
+ });
222
+ });
223
+
224
+ describe('WordPressCachePatterns', () => {
225
+ let cacheManager: CacheManager;
226
+ let httpCache: HttpCacheWrapper;
227
+
228
+ beforeEach(() => {
229
+ cacheManager = new CacheManager({
230
+ maxSize: 100,
231
+ defaultTTL: 10000,
232
+ enableLRU: true,
233
+ enableStats: true
234
+ });
235
+
236
+ httpCache = new HttpCacheWrapper(cacheManager, 'test-site');
237
+ });
238
+
239
+ afterEach(() => {
240
+ cacheManager.clear();
241
+ });
242
+
243
+ test('should invalidate content-related caches', () => {
244
+ // Pre-populate cache
245
+ httpCache.warm('posts', []);
246
+ httpCache.warm('pages', []);
247
+ httpCache.warm('comments', []);
248
+ httpCache.warm('categories', []);
249
+
250
+ expect(cacheManager.getStats().totalSize).toBe(4);
251
+
252
+ const invalidated = WordPressCachePatterns.invalidateContent(httpCache);
253
+
254
+ expect(invalidated).toBeGreaterThan(0);
255
+ // Should invalidate posts, pages, comments but not categories
256
+ expect(cacheManager.getStats().totalSize).toBeLessThan(4);
257
+ });
258
+
259
+ test('should invalidate taxonomy-related caches', () => {
260
+ httpCache.warm('categories', []);
261
+ httpCache.warm('tags', []);
262
+ httpCache.warm('posts', []);
263
+
264
+ expect(cacheManager.getStats().totalSize).toBe(3);
265
+
266
+ const invalidated = WordPressCachePatterns.invalidateTaxonomies(httpCache);
267
+
268
+ expect(invalidated).toBeGreaterThan(0);
269
+ // Should invalidate categories and tags but not posts
270
+ expect(cacheManager.getStats().totalSize).toBeLessThan(3);
271
+ });
272
+
273
+ test('should invalidate user-related caches', () => {
274
+ httpCache.warm('users', []);
275
+ httpCache.warm('users/me', {});
276
+ httpCache.warm('posts', []);
277
+
278
+ expect(cacheManager.getStats().totalSize).toBe(3);
279
+
280
+ const invalidated = WordPressCachePatterns.invalidateUsers(httpCache);
281
+
282
+ expect(invalidated).toBeGreaterThan(0);
283
+ // Should invalidate user caches but not posts
284
+ expect(cacheManager.getStats().totalSize).toBeLessThan(3);
285
+ });
286
+
287
+ test('should invalidate all caches', () => {
288
+ httpCache.warm('posts', []);
289
+ httpCache.warm('categories', []);
290
+ httpCache.warm('users', []);
291
+
292
+ expect(cacheManager.getStats().totalSize).toBe(3);
293
+
294
+ const invalidated = WordPressCachePatterns.invalidateAll(httpCache);
295
+
296
+ expect(invalidated).toBe(3);
297
+ expect(cacheManager.getStats().totalSize).toBe(0);
298
+ });
299
+ });